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