From 6efecf3e684cb83e097bcc8cf91a399719299fee Mon Sep 17 00:00:00 2001 From: Edgars Beigarts Date: Fri, 7 Aug 2026 02:29:11 +0300 Subject: [PATCH] Return 401 challenge for unauthenticated /mcp requests An /mcp request without a Bearer token built the MCP server with a nil server_context. MCP::ServerContext wraps it and delegates unknown methods via method_missing, so tools/list still returned 200 while any actual tool call failed with an opaque JSON-RPC -32603: undefined method 'imap_config' for an instance of MCP::ServerContext Reject the request up front with a 401 and the WWW-Authenticate resource_metadata pointer, so MCP clients re-run the OAuth flow instead. Extracted the response builder into unauthorized_response, reused by the existing JwtService::Error rescue. Note: bare OPTIONS /mcp preflights are now also rejected. The app has no CORS handling today, so browser clients were already broken; adding one will need to bypass this check. Co-Authored-By: Claude Opus 5 (1M context) --- Gemfile.lock | 2 +- lib/mail_mcp/app.rb | 12 ++++++++---- spec/mail_mcp/app_spec.rb | 10 ++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f211f34..f8ac30b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - mail_mcp (1.0.0) + mail_mcp (1.0.1) aws-sdk-s3 (~> 1.0) base64 (~> 0.2) json-jwt (~> 1.16) diff --git a/lib/mail_mcp/app.rb b/lib/mail_mcp/app.rb index c9fb59a..c5b20b2 100644 --- a/lib/mail_mcp/app.rb +++ b/lib/mail_mcp/app.rb @@ -144,7 +144,7 @@ class App < Sinatra::Base def resolve_mcp_context auth = request.env["HTTP_AUTHORIZATION"] - return [nil, nil] unless auth&.start_with?("Bearer ") + return [nil, unauthorized_response("missing bearer token")] unless auth&.start_with?("Bearer ") creds = JwtService.verify(auth[7..]) context = CredentialContext.new( @@ -161,12 +161,16 @@ def resolve_mcp_context ) [context, nil] rescue JwtService::Error => e - error = [ + [nil, unauthorized_response(e.message)] + end + + # 401 challenge that tells an MCP client where to run the OAuth flow. + def unauthorized_response(description) + [ 401, { "Content-Type" => "application/json", "WWW-Authenticate" => mcp_www_authenticate }, - [JSON.generate({ error: "invalid_token", error_description: e.message })] + [JSON.generate({ error: "invalid_token", error_description: description })] ] - [nil, error] end def exchange_code(params, client_id) diff --git a/spec/mail_mcp/app_spec.rb b/spec/mail_mcp/app_spec.rb index 80df609..85b947e 100644 --- a/spec/mail_mcp/app_spec.rb +++ b/spec/mail_mcp/app_spec.rb @@ -216,6 +216,16 @@ def mcp_request(method, params = {}, id: 1) body = JSON.parse(last_response.body) expect(body["error"]).to eq("invalid_token") end + + it "returns a 401 challenge when the Authorization header is missing" do + post "/mcp", mcp_request("tools/list"), mcp_headers.except("HTTP_AUTHORIZATION") + + expect(last_response.status).to eq(401) + expect(last_response.headers["WWW-Authenticate"]).to include("resource_metadata=") + body = JSON.parse(last_response.body) + expect(body["error"]).to eq("invalid_token") + expect(body["error_description"]).to eq("missing bearer token") + end end describe "GET /health" do