Skip to content

Commit e5d3b2c

Browse files
committed
Support CORS and Accept wildcard for browser-based MCP clients
## Summary When using browser-based MCP clients such as MCP Inspector, connections to the example HTTP server failed due to two issues: 1. CORS preflight (OPTIONS) requests returned 405 Method Not Allowed, causing browsers to block all cross-origin requests. 2. `Accept: */*` (commonly sent by browsers and fetch API) returned 406 Not Acceptable because the transport required the Accept header to explicitly list `application/json` and `text/event-stream`. Add `rack-cors` middleware to the example HTTP servers so that preflight requests are handled correctly and `Mcp-Session-Id` is exposed to browser clients. Also fix `validate_accept_header` in `StreamableHTTPTransport` to treat `*/*` as satisfying all required Accept types. Fixes #141. ## Repro Steps ### 1. Start the example server (Terminal 1) ```console $ bundle exec ruby examples/streamable_http_server.rb ``` ### 2. Start MCP Inspector (Terminal 2) ```console $ npx @modelcontextprotocol/inspector ``` ### 3. Open the Inspector Web UI (http://localhost:6274) in a browser - Set Transport Type to "Streamable HTTP" - Set URL to http://localhost:9393 - Disable the Authorization header toggle (the example server does not require authentication) - Click "Connect" Before this change, the connection fails due to CORS or 406 errors. After this change, the connection succeeds and tools are listed. These steps have been added to examples/README.md.
1 parent 5d8f1bd commit e5d3b2c

File tree

6 files changed

+97
-0
lines changed

6 files changed

+97
-0
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ gem "rubocop-rake", require: false
1111
gem "rubocop-shopify", ">= 2.18", require: false if RUBY_VERSION >= "3.1"
1212

1313
gem "puma", ">= 5.0.0"
14+
gem "rack-cors"
1415
gem "rackup", ">= 2.1.0"
1516

1617
gem "activesupport"

examples/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,31 @@ The client will:
121121
- Provide an interactive menu to trigger notifications
122122
- Display all received SSE events in real-time
123123

124+
### Testing with MCP Inspector
125+
126+
[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a browser-based tool for testing and debugging MCP servers.
127+
128+
1. Start the server:
129+
130+
```console
131+
$ ruby examples/streamable_http_server.rb
132+
```
133+
134+
2. Start Inspector in another terminal:
135+
136+
```console
137+
$ npx @modelcontextprotocol/inspector
138+
```
139+
140+
3. Open `http://localhost:6274` in a browser:
141+
142+
- Set Transport Type to "Streamable HTTP"
143+
- Set URL to `http://localhost:9393`
144+
- Disable the Authorization header toggle (the example server does not require authentication)
145+
- Click "Connect"
146+
147+
Once connected, you can list tools, call them, and see SSE notifications in the Inspector UI.
148+
124149
### Testing SSE with cURL
125150

126151
You can also test SSE functionality manually using cURL:

examples/http_server.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
44
require "mcp"
5+
require "rack/cors"
56
require "rackup"
67
require "json"
78
require "logger"
@@ -142,6 +143,20 @@ def template(args, server_context:)
142143

143144
# Wrap the app with Rack middleware
144145
rack_app = Rack::Builder.new do
146+
# Enable CORS to allow browser-based MCP clients (e.g., MCP Inspector)
147+
# WARNING: origins("*") allows all origins. Restrict this in production.
148+
use(Rack::Cors) do
149+
allow do
150+
origins("*")
151+
resource(
152+
"*",
153+
headers: :any,
154+
methods: [:get, :post, :delete, :options],
155+
expose: ["Mcp-Session-Id"],
156+
)
157+
end
158+
end
159+
145160
# Use CommonLogger for standard HTTP request logging
146161
use(Rack::CommonLogger, Logger.new($stdout))
147162

examples/streamable_http_server.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
44
require "mcp"
5+
require "rack/cors"
56
require "rackup"
67
require "json"
78
require "logger"
@@ -129,6 +130,20 @@ def call(message:, delay: 0)
129130

130131
# Build the Rack application with middleware
131132
rack_app = Rack::Builder.new do
133+
# Enable CORS to allow browser-based MCP clients (e.g., MCP Inspector)
134+
# WARNING: origins("*") allows all origins. Restrict this in production.
135+
use(Rack::Cors) do
136+
allow do
137+
origins("*")
138+
resource(
139+
"*",
140+
headers: :any,
141+
methods: [:get, :post, :delete, :options],
142+
expose: ["Mcp-Session-Id"],
143+
)
144+
end
145+
end
146+
132147
use(Rack::CommonLogger, Logger.new($stdout))
133148
use(Rack::ShowExceptions)
134149
run(app)

lib/mcp/server/transports/streamable_http_transport.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ def validate_accept_header(request, required_types)
193193
return not_acceptable_response(required_types) unless accept_header
194194

195195
accepted_types = parse_accept_header(accept_header)
196+
return if accepted_types.include?("*/*")
197+
196198
missing_types = required_types - accepted_types
197199
return not_acceptable_response(required_types) unless missing_types.empty?
198200

test/mcp/server/transports/streamable_http_transport_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,21 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase
855855
assert_equal 200, response[0]
856856
end
857857

858+
test "POST request with Accept: */* succeeds" do
859+
request = create_rack_request_without_accept(
860+
"POST",
861+
"/",
862+
{
863+
"CONTENT_TYPE" => "application/json",
864+
"HTTP_ACCEPT" => "*/*",
865+
},
866+
{ jsonrpc: "2.0", method: "initialize", id: "123" }.to_json,
867+
)
868+
869+
response = @transport.handle_request(request)
870+
assert_equal 200, response[0]
871+
end
872+
858873
test "GET request without Accept header returns 406" do
859874
init_request = create_rack_request(
860875
"POST",
@@ -928,6 +943,30 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase
928943
assert_equal "text/event-stream", response[1]["Content-Type"]
929944
end
930945

946+
test "GET request with Accept: */* succeeds" do
947+
init_request = create_rack_request(
948+
"POST",
949+
"/",
950+
{ "CONTENT_TYPE" => "application/json" },
951+
{ jsonrpc: "2.0", method: "initialize", id: "123" }.to_json,
952+
)
953+
init_response = @transport.handle_request(init_request)
954+
session_id = init_response[1]["Mcp-Session-Id"]
955+
956+
request = create_rack_request_without_accept(
957+
"GET",
958+
"/",
959+
{
960+
"HTTP_MCP_SESSION_ID" => session_id,
961+
"HTTP_ACCEPT" => "*/*",
962+
},
963+
)
964+
965+
response = @transport.handle_request(request)
966+
assert_equal 200, response[0]
967+
assert_equal "text/event-stream", response[1]["Content-Type"]
968+
end
969+
931970
test "stateless mode allows requests without session IDs, responding with no session ID" do
932971
stateless_transport = StreamableHTTPTransport.new(@server, stateless: true)
933972

0 commit comments

Comments
 (0)