keycardai-mcp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 598ccfb7138983e92a8e17d1b139689b313a9e7b2ebc4c0e422a47c9730f73a8
4
- data.tar.gz: 5f0828148a2565f81fa33574639418aeae0c0f64540cbda9e3e20cf7749070fe
3
+ metadata.gz: '0880d377eb3029f73175e4ba00d78bca2c6bbdaf0c7ace5a8012efc44416033e'
4
+ data.tar.gz: 73ba72fadb3d1f534847f171f46fed3c5db52744521f45658e9c1960979e4f21
5
5
  SHA512:
6
- metadata.gz: 38a310adbe756ab5f33bd3989235cdaa6873cb89b5b99e18e342f56d881d032ed461c2209988f45b0952d77243631c997f401e60f85245a0a045fad031c1a8e3
7
- data.tar.gz: 63256d53e222d04096c844e371721341c44891e42bdd1dd3202ad405d216908500f658f8380a294a19236231fd639895ae25c028d710a7c1f1ddc9e46b27a6e6
6
+ metadata.gz: daf1d161bb279219b0c7abc1c2bb801fb6f7e77507b64309425722bb6449e8ca76b2191115a9571b3db1cfd50155e7ae3ec85994ab5bac2659906520d68b814a
7
+ data.tar.gz: 104a2890048bfbda6935b39339946a82722e6c918dccda8b6f699aba27e8cea98bd243eae8b9d76a37e25f68b8a116dfab58c5f14f251bd033436a32582b5b01
data/CHANGELOG.md CHANGED
@@ -13,3 +13,50 @@ verification with RFC 6750 challenges, OAuth metadata endpoints (RFC 9728
13
13
  protected-resource with path insertion, an RFC 8414 authorization-server proxy,
14
14
  and JWKS), and an AuthProvider whose `grant` middleware exchanges the caller's
15
15
  token per resource into an AccessContext. Wraps no MCP SDK.
16
+
17
+ ## 0.1.2-keycardai-mcp (2026-10-12)
18
+
19
+
20
+ - fix(keycardai-mcp): RequireBearerAuth answers 503 when the zone's signing keys can't be fetched (SDK-20) (#44)
21
+ - RequireBearerAuth answers 503 {"error":"temporarily_unavailable"} with no challenge when verification fails with JWKSFetchError, JWKSDiscoveryError, HTTPError, or NetworkError, because the token was never judged. A kid absent from a JWKS that loaded, and any other invalid token, stays 401 invalid_token.
22
+
23
+ ## 0.1.1-keycardai-mcp (2026-09-16)
24
+
25
+
26
+ - fix(keycardai-mcp): retire the 2025-03-26 metadata rewrites (SDK-3) (#37)
27
+ - MetadataApp no longer rewrites authorization_servers to the request origin
28
+ for MCP-Protocol-Version: 2025-03-26 and no longer appends resource=<origin>
29
+ to the proxied authorization_endpoint. Both documents match keycard-sdk-spec
30
+ oauth-metadata-endpoints spec-version 2.
31
+ - Co-authored-by: devin-ai-keycard <devin-ai@keycard.ai>
32
+ Co-authored-by: Larry Osakwe <larry@keycard.ai>
33
+
34
+ ## 0.1.0-keycardai-mcp (2026-08-19)
35
+
36
+
37
+ - feat(keycardai-mcp): Rack bearer middleware, metadata endpoints, AuthProvider grant (#8)
38
+ - Phase 2: the MCP server integration at the Rack seam, implementing
39
+ specs/server-bearer-auth/{bearer-token-verification-middleware,
40
+ oauth-metadata-endpoints,route-level-auth-gating}.md and
41
+ specs/delegated-access/grant-decorator.md:
42
+ - - RequireBearerAuth: fail-closed Rack middleware; 401 RFC 6750
43
+ challenge with the RFC 9728 resource_metadata URL, 400 on malformed
44
+ headers, 401 invalid_token on non-bearer schemes (TS/Go position),
45
+ 403 insufficient_scope on missing required_scopes (all-of; gating is
46
+ this middleware re-applied per route, the TS/Go idiom), verified
47
+ AccessToken in the Rack env, construction error on a nil verifier
48
+ - MetadataApp: RFC 9728 protected-resource metadata with path
49
+ insertion, the 2025-03-26 MCP-version shim, RFC 8414 AS proxy with
50
+ the resource=<origin> rewrite (replace, not append), optional
51
+ /.well-known/jwks.json, permissive CORS + preflight, 502 on upstream
52
+ failure
53
+ - AuthProvider: zone + credential holder; token_verifier with
54
+ multi-zone issuers inferred from a self-describing credential;
55
+ grant(resources, user_identifier:, request_scopes:) middleware that
56
+ fails fast 401 without a verified token, never aborts on per-resource
57
+ errors, and merges stacked grants into one AccessContext;
58
+ exchange_tokens + exchange_tokens_for_zone
59
+ - Env accessors Keycardai::MCP.auth_info / .access_context
60
+ - No MCP SDK dependency, per the plan: compatibility with the official
61
+ mcp gem is proven by the phase 4 example server. Conformance suites map
62
+ to all four spec Testing tables (28 examples).
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  Keycard integration for MCP servers in Ruby, attached at the Rack seam.
4
4
 
5
- > **Preview.** APIs may change between minor versions while the surface settles.
6
5
  > Conformance against the cross-SDK contract is tracked in the
7
6
  > [conformance report](https://github.com/keycardai/ruby-sdk/blob/main/docs/conformance-report.md).
8
7
 
@@ -32,8 +31,11 @@ is proven by the example server in the repo's `examples/` directory.
32
31
  require "keycardai/mcp"
33
32
 
34
33
  zone_url = ENV.fetch("KEYCARD_URL")
34
+ # This server's registered Resource identifier. Leaving audiences: unset
35
+ # accepts tokens minted for any resource in the zone and warns once at boot.
36
+ resource_id = ENV.fetch("KEYCARD_RESOURCE_ID")
35
37
 
36
- verifier = Keycardai::OAuth::TokenVerifier.new(issuers: zone_url)
38
+ verifier = Keycardai::OAuth::TokenVerifier.new(issuers: zone_url, audiences: resource_id)
37
39
  metadata = Keycardai::MCP::MetadataApp.new(
38
40
  issuer: zone_url,
39
41
  resource_name: "mcp-server-ruby",
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "uri"
4
-
5
3
  module Keycardai
6
4
  module MCP
7
5
  # Rack app serving the resource server's OAuth discovery surface
8
6
  # (RFC 9728 / RFC 8414): the protected-resource metadata (with path
9
7
  # insertion for sub-path mounts), a server-side proxy of the zone's
10
- # authorization-server metadata (with the resource= rewrite shim for
11
- # pre-RFC-8707 MCP clients), and optionally the server's own public JWKS.
8
+ # authorization-server metadata (returned unmodified), and optionally
9
+ # the server's own public JWKS.
12
10
  # Responses carry permissive CORS so browser clients can read them.
13
11
  #
14
12
  # run Keycardai::MCP::MetadataApp.new(issuer: zone_url, scopes_supported: ["mcp:tools"])
@@ -16,7 +14,6 @@ module Keycardai
16
14
  PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource"
17
15
  AUTHORIZATION_SERVER_PATH = "/.well-known/oauth-authorization-server"
18
16
  JWKS_PATH = "/.well-known/jwks.json"
19
- LEGACY_MCP_PROTOCOL_VERSION = "2025-03-26"
20
17
 
21
18
  # @param issuer [String] the zone's issuer URL
22
19
  # @param scopes_supported [Array<String>, nil]
@@ -46,7 +43,7 @@ module Keycardai
46
43
  when %r{\A#{Regexp.escape(PROTECTED_RESOURCE_PATH)}(/.*)?\z}
47
44
  protected_resource_response(env, Regexp.last_match(1).to_s)
48
45
  when AUTHORIZATION_SERVER_PATH
49
- authorization_server_response(env)
46
+ authorization_server_response
50
47
  when JWKS_PATH
51
48
  jwks_response
52
49
  else
@@ -68,10 +65,9 @@ module Keycardai
68
65
  # origin + /mcp.
69
66
  def protected_resource_response(env, resource_path)
70
67
  origin = RackSupport.origin(env)
71
- legacy = env["HTTP_MCP_PROTOCOL_VERSION"] == LEGACY_MCP_PROTOCOL_VERSION
72
68
  document = {
73
69
  "resource" => "#{origin}#{resource_path}",
74
- "authorization_servers" => [legacy ? origin : @issuer],
70
+ "authorization_servers" => [@issuer],
75
71
  "bearer_methods_supported" => ["header"],
76
72
  "scopes_supported" => @scopes_supported,
77
73
  "resource_name" => @resource_name,
@@ -81,32 +77,17 @@ module Keycardai
81
77
  RackSupport.json_response(document)
82
78
  end
83
79
 
84
- # RFC 8414 proxy of the zone's metadata, rewriting the
85
- # authorization_endpoint to carry resource=<origin> (replacing any
86
- # resource parameter already present in the upstream value).
87
- def authorization_server_response(env)
80
+ # RFC 8414 proxy of the zone's metadata, returned unmodified. MCP
81
+ # 2025-06-18 clients send the RFC 8707 resource parameter themselves.
82
+ def authorization_server_response
88
83
  metadata = Keycardai::OAuth.fetch_authorization_server_metadata(
89
84
  @issuer, http_client: @http_client, timeout: @timeout
90
85
  )
91
- document = metadata.raw.dup
92
- if document["authorization_endpoint"]
93
- document["authorization_endpoint"] =
94
- with_resource_param(document["authorization_endpoint"], RackSupport.origin(env))
95
- end
96
- RackSupport.json_response(document)
86
+ RackSupport.json_response(metadata.raw)
97
87
  rescue Keycardai::Error
98
88
  RackSupport.json_response({ "error" => "bad_gateway" }, status: 502)
99
89
  end
100
90
 
101
- def with_resource_param(endpoint, resource)
102
- uri = URI(endpoint)
103
- params = URI.decode_www_form(uri.query.to_s)
104
- params.delete_if { |pair| pair.first == "resource" }
105
- params << ["resource", resource]
106
- uri.query = URI.encode_www_form(params)
107
- uri.to_s
108
- end
109
-
110
91
  def jwks_response
111
92
  return RackSupport.json_response({ "error" => "not_found" }, status: 404) unless @public_jwks
112
93
 
@@ -9,7 +9,7 @@ module Keycardai
9
9
  # the Rack env (Keycardai::MCP.auth_info).
10
10
  #
11
11
  # use Keycardai::MCP::RequireBearerAuth,
12
- # verifier: Keycardai::OAuth::TokenVerifier.new(issuers: zone_url),
12
+ # verifier: Keycardai::OAuth::TokenVerifier.new(issuers: zone_url, audiences: resource_id),
13
13
  # required_scopes: ["mcp:tools"]
14
14
  #
15
15
  # Route-level gating is this same middleware applied per route with a
@@ -49,6 +49,9 @@ module Keycardai
49
49
 
50
50
  def authorize(env, token)
51
51
  access_token, failure = verify(token)
52
+ if failure == :unavailable
53
+ return RackSupport.json_response({ "error" => "temporarily_unavailable" }, status: 503)
54
+ end
52
55
  return RackSupport.challenge_response(env, status: 401, error: "invalid_token", description: failure) if failure
53
56
 
54
57
  missing = @required_scopes - access_token.scopes
@@ -61,8 +64,21 @@ module Keycardai
61
64
  @app.call(env)
62
65
  end
63
66
 
67
+ # The zone's signing keys or its authorization-server metadata could not
68
+ # be fetched, so verification never completed. Answered 503 with no
69
+ # challenge: the token was not judged, and the client should retry.
70
+ # A kid absent from a JWKS that did load (JWKSKeyNotFoundError) is an
71
+ # invalid token and keeps the 401 path.
72
+ UNAVAILABLE_ERRORS = [
73
+ Keycardai::OAuth::JWKSFetchError, Keycardai::OAuth::JWKSDiscoveryError,
74
+ Keycardai::OAuth::HTTPError, Keycardai::OAuth::NetworkError
75
+ ].freeze
76
+ private_constant :UNAVAILABLE_ERRORS
77
+
64
78
  def verify(token)
65
79
  [@verifier.verify_token(token), nil]
80
+ rescue *UNAVAILABLE_ERRORS
81
+ [nil, :unavailable]
66
82
  rescue Keycardai::Error => e
67
83
  [nil, e.message]
68
84
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Keycardai
4
4
  module MCP
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycardai-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keycard