@cyanmycelium/mcp-broker 0.3.0 → 1.0.1

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.
Files changed (167) hide show
  1. package/.mcp-broker.example/CONFIGURATION-EN.md +861 -0
  2. package/.mcp-broker.example/CONFIGURATION-FR.md +871 -0
  3. package/.mcp-broker.example/README.md +5 -0
  4. package/.mcp-broker.example/config.json +86 -0
  5. package/README.md +116 -1
  6. package/dist/auth/auth.config.d.ts +40 -0
  7. package/dist/auth/auth.config.js +62 -0
  8. package/dist/auth/auth.config.js.map +1 -0
  9. package/dist/auth/auth.types.d.ts +119 -0
  10. package/dist/auth/auth.types.js +38 -0
  11. package/dist/auth/auth.types.js.map +1 -0
  12. package/dist/auth/http.auth.d.ts +51 -0
  13. package/dist/auth/http.auth.js +121 -0
  14. package/dist/auth/http.auth.js.map +1 -0
  15. package/dist/auth/index.d.ts +16 -0
  16. package/dist/auth/index.js +12 -0
  17. package/dist/auth/index.js.map +1 -0
  18. package/dist/auth/jwt.validator.d.ts +35 -0
  19. package/dist/auth/jwt.validator.js +46 -0
  20. package/dist/auth/jwt.validator.js.map +1 -0
  21. package/dist/auth/provider.auth.d.ts +51 -0
  22. package/dist/auth/provider.auth.js +80 -0
  23. package/dist/auth/provider.auth.js.map +1 -0
  24. package/dist/auth/resource.metadata.d.ts +32 -0
  25. package/dist/auth/resource.metadata.js +26 -0
  26. package/dist/auth/resource.metadata.js.map +1 -0
  27. package/dist/authorization/audit.d.ts +14 -0
  28. package/dist/authorization/audit.js +20 -0
  29. package/dist/authorization/audit.js.map +1 -0
  30. package/dist/authorization/capability.classifier.d.ts +30 -0
  31. package/dist/authorization/capability.classifier.js +85 -0
  32. package/dist/authorization/capability.classifier.js.map +1 -0
  33. package/dist/authorization/index.d.ts +13 -0
  34. package/dist/authorization/index.js +8 -0
  35. package/dist/authorization/index.js.map +1 -0
  36. package/dist/authorization/policy.engine.d.ts +11 -0
  37. package/dist/authorization/policy.engine.js +192 -0
  38. package/dist/authorization/policy.engine.js.map +1 -0
  39. package/dist/authorization/policy.types.d.ts +92 -0
  40. package/dist/authorization/policy.types.js +2 -0
  41. package/dist/authorization/policy.types.js.map +1 -0
  42. package/dist/authorization/resource.path.d.ts +27 -0
  43. package/dist/authorization/resource.path.js +122 -0
  44. package/dist/authorization/resource.path.js.map +1 -0
  45. package/dist/authorization/runtime.d.ts +22 -0
  46. package/dist/authorization/runtime.js +34 -0
  47. package/dist/authorization/runtime.js.map +1 -0
  48. package/dist/authorization/slot.resource.d.ts +12 -0
  49. package/dist/authorization/slot.resource.js +41 -0
  50. package/dist/authorization/slot.resource.js.map +1 -0
  51. package/dist/authorization/subject.mapper.d.ts +15 -0
  52. package/dist/authorization/subject.mapper.js +67 -0
  53. package/dist/authorization/subject.mapper.js.map +1 -0
  54. package/dist/bin.js +49 -0
  55. package/dist/bin.js.map +1 -1
  56. package/dist/broker/adapters/broker.adapter.info.d.ts +3 -3
  57. package/dist/broker/adapters/broker.adapter.info.js +1 -1
  58. package/dist/broker/adapters/broker.adapter.info.js.map +1 -1
  59. package/dist/broker/adapters/broker.adapter.providers.d.ts +2 -2
  60. package/dist/broker/adapters/broker.adapter.providers.js.map +1 -1
  61. package/dist/broker/aggregate/aggregate.catalog.d.ts +23 -11
  62. package/dist/broker/aggregate/aggregate.catalog.js +14 -0
  63. package/dist/broker/aggregate/aggregate.catalog.js.map +1 -1
  64. package/dist/broker/aggregate/aggregate.server.d.ts +22 -3
  65. package/dist/broker/aggregate/aggregate.server.js +115 -9
  66. package/dist/broker/aggregate/aggregate.server.js.map +1 -1
  67. package/dist/broker/aggregate/provider.client.session.d.ts +9 -9
  68. package/dist/broker/aggregate/provider.client.session.js +1 -1
  69. package/dist/broker/aggregate/provider.client.session.js.map +1 -1
  70. package/dist/broker/behaviors/broker.behavior.info.d.ts +2 -2
  71. package/dist/broker/behaviors/broker.behavior.info.js.map +1 -1
  72. package/dist/broker/behaviors/broker.behavior.providers.d.ts +2 -2
  73. package/dist/broker/behaviors/broker.behavior.providers.js.map +1 -1
  74. package/dist/broker/broker.context.d.ts +8 -4
  75. package/dist/broker/broker.grammars.d.ts +52 -86
  76. package/dist/broker/broker.grammars.js +55 -84
  77. package/dist/broker/broker.grammars.js.map +1 -1
  78. package/dist/broker/broker.server.d.ts +28 -24
  79. package/dist/broker/broker.server.js +21 -70
  80. package/dist/broker/broker.server.js.map +1 -1
  81. package/dist/broker/index.d.ts +4 -4
  82. package/dist/broker/index.js +1 -1
  83. package/dist/broker/index.js.map +1 -1
  84. package/dist/config.d.ts +51 -5
  85. package/dist/config.js +1 -1
  86. package/dist/config.js.map +1 -1
  87. package/dist/index.d.ts +13 -9
  88. package/dist/index.js +5 -1
  89. package/dist/index.js.map +1 -1
  90. package/dist/mcpb.loader.d.ts +6 -4
  91. package/dist/mcpb.loader.js +3 -3
  92. package/dist/mcpb.loader.js.map +1 -1
  93. package/dist/remote.transports.d.ts +4 -2
  94. package/dist/remote.transports.js.map +1 -1
  95. package/dist/remote.upstream.d.ts +6 -4
  96. package/dist/remote.upstream.js.map +1 -1
  97. package/dist/stdio.upstream.d.ts +6 -4
  98. package/dist/stdio.upstream.js.map +1 -1
  99. package/dist/upstream.d.ts +3 -1
  100. package/dist/ws.tunnel.builder.d.ts +41 -4
  101. package/dist/ws.tunnel.builder.js +66 -0
  102. package/dist/ws.tunnel.builder.js.map +1 -1
  103. package/dist/ws.tunnel.d.ts +116 -36
  104. package/dist/ws.tunnel.js +384 -51
  105. package/dist/ws.tunnel.js.map +1 -1
  106. package/package.json +7 -3
  107. package/src/auth/auth.config.ts +98 -0
  108. package/src/auth/auth.types.ts +145 -0
  109. package/src/auth/http.auth.ts +132 -0
  110. package/src/auth/index.ts +34 -0
  111. package/src/auth/jwt.validator.ts +63 -0
  112. package/src/auth/provider.auth.ts +114 -0
  113. package/src/auth/resource.metadata.ts +41 -0
  114. package/src/authorization/audit.ts +35 -0
  115. package/src/authorization/capability.classifier.ts +114 -0
  116. package/src/authorization/index.ts +37 -0
  117. package/src/authorization/policy.engine.ts +212 -0
  118. package/src/authorization/policy.types.ts +105 -0
  119. package/src/authorization/resource.path.ts +126 -0
  120. package/src/authorization/runtime.ts +56 -0
  121. package/src/authorization/slot.resource.ts +50 -0
  122. package/src/authorization/subject.mapper.ts +81 -0
  123. package/src/bin.ts +53 -0
  124. package/src/broker/adapters/broker.adapter.info.ts +3 -3
  125. package/src/broker/adapters/broker.adapter.providers.ts +3 -3
  126. package/src/broker/aggregate/aggregate.catalog.ts +48 -20
  127. package/src/broker/aggregate/aggregate.server.ts +146 -16
  128. package/src/broker/aggregate/provider.client.session.ts +22 -22
  129. package/src/broker/behaviors/broker.behavior.info.ts +2 -2
  130. package/src/broker/behaviors/broker.behavior.providers.ts +2 -2
  131. package/src/broker/broker.context.ts +10 -4
  132. package/src/broker/broker.grammars.ts +77 -122
  133. package/src/broker/broker.server.ts +51 -101
  134. package/src/broker/index.ts +5 -7
  135. package/src/config.ts +57 -6
  136. package/src/index.ts +109 -16
  137. package/src/mcpb.loader.ts +14 -11
  138. package/src/remote.transports.ts +8 -5
  139. package/src/remote.upstream.ts +10 -7
  140. package/src/stdio.upstream.ts +8 -5
  141. package/src/upstream.ts +4 -1
  142. package/src/ws.tunnel.builder.ts +89 -9
  143. package/src/ws.tunnel.ts +521 -102
  144. package/web/README.md +94 -0
  145. package/web/assets/logo.png +0 -0
  146. package/web/broker-self-mcp.html +338 -0
  147. package/web/css/styles.css +580 -0
  148. package/web/demos/DemoPlaceholder.html +258 -0
  149. package/web/demos/broker-explorer/css/app.css +393 -0
  150. package/web/demos/broker-explorer/index.html +94 -0
  151. package/web/demos/broker-explorer/js/app.js +271 -0
  152. package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
  153. package/web/demos/oauth-lab/README.md +68 -0
  154. package/web/demos/oauth-lab/config.json +116 -0
  155. package/web/demos/oauth-lab/css/app.css +1097 -0
  156. package/web/demos/oauth-lab/index.html +323 -0
  157. package/web/demos/oauth-lab/js/app.js +630 -0
  158. package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
  159. package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
  160. package/web/demos/oauth-lab/server/smoke-test.mjs +261 -0
  161. package/web/demos/oauth-lab/server/start.mjs +106 -0
  162. package/web/demos/provider-tunnel/css/app.css +384 -0
  163. package/web/demos/provider-tunnel/index.html +99 -0
  164. package/web/demos/provider-tunnel/js/app.js +226 -0
  165. package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
  166. package/web/index.html +558 -0
  167. package/web/js/lib/broker-tunnel.js +173 -0
@@ -7,6 +7,11 @@ broker, then adapt to your needs:
7
7
  cp -r .mcp-broker.example .mcp-broker
8
8
  ```
9
9
 
10
+ Property-by-property educational guides:
11
+
12
+ - [English](CONFIGURATION-EN.md)
13
+ - [Français](CONFIGURATION-FR.md)
14
+
10
15
  ## Layout
11
16
 
12
17
  ```
@@ -22,6 +22,92 @@
22
22
  ]
23
23
  },
24
24
 
25
+ "auth": {
26
+ "enabled": true,
27
+ "publicBaseUrl": "https://mcp.factory.local",
28
+ "authorizationServers": [
29
+ "https://identity.factory.local"
30
+ ],
31
+ "jwks": "https://identity.factory.local/.well-known/jwks.json",
32
+ "requiredScopes": ["mcp:call"],
33
+ "perSlotScopes": {
34
+ "_broker": ["broker:admin"]
35
+ },
36
+ "subjectMapping": {
37
+ "userClaim": "sub",
38
+ "groupClaims": ["groups"],
39
+ "clientClaim": "client_id"
40
+ },
41
+ "roles": {
42
+ "viewer": {
43
+ "capabilities": [
44
+ "mcp.resources.read",
45
+ "mcp.tools.list",
46
+ "mcp.prompts.read"
47
+ ]
48
+ },
49
+ "maintenance": {
50
+ "inherits": ["viewer"],
51
+ "capabilities": [
52
+ "mcp.tools.call",
53
+ "mcp.tools.diagnose",
54
+ "mcp.tools.configure-analysis"
55
+ ]
56
+ },
57
+ "operator": {
58
+ "inherits": ["viewer"],
59
+ "capabilities": ["mcp.tools.operate"]
60
+ },
61
+ "administrator": {
62
+ "capabilities": ["*"]
63
+ }
64
+ },
65
+ "assignments": [
66
+ {
67
+ "id": "maintenance-area-a",
68
+ "subject": "group:maintenance-area-a",
69
+ "role": "maintenance",
70
+ "resource": "/enterprise-a/site-paris/area-a/**"
71
+ },
72
+ {
73
+ "id": "energy-team",
74
+ "subject": "group:energy-team",
75
+ "role": "viewer",
76
+ "resource": "/enterprise-a/site-paris/**"
77
+ }
78
+ ],
79
+ "denies": [
80
+ {
81
+ "id": "protect-critical-furnace",
82
+ "subject": "group:maintenance-area-a",
83
+ "capabilities": [
84
+ "mcp.tools.configure-analysis",
85
+ "mcp.tools.operate"
86
+ ],
87
+ "resource": "/enterprise-a/site-paris/area-a/line-2/cell-4/critical-furnace"
88
+ }
89
+ ],
90
+ "slotResources": {
91
+ "spoony-00452": "/enterprise-a/site-paris/area-a/line-3/cell-2/motor-7",
92
+ "site-energy": "/enterprise-a/site-paris"
93
+ },
94
+ "toolCapabilities": {
95
+ "get_electrical_state": "mcp.resources.read",
96
+ "diagnose_motor": "mcp.tools.diagnose",
97
+ "reset_baseline": "mcp.tools.configure-analysis",
98
+ "start_motor": "mcp.tools.operate"
99
+ },
100
+ "providerToolCapabilities": {
101
+ "/enterprise-a/site-paris/area-a/**": {
102
+ "start_motor": "mcp.tools.operate"
103
+ }
104
+ },
105
+ "audit": {
106
+ "logAllowed": false
107
+ },
108
+ "providerSecret": "change-me"
109
+ },
110
+
25
111
  "stdioUpstreams": [
26
112
  {
27
113
  "name": "fs",
package/README.md CHANGED
@@ -96,6 +96,81 @@ Full reference (every field, defaults, recipes, grammar overrides): **[docs/conf
96
96
  | `MCP_BROKER_TLS_KEY` | (unset) | Path to a PEM private key. Enables HTTPS/WSS |
97
97
  | `MCP_BROKER_PROTOCOL` | auto | `http` forces plain, `https` forces TLS, unset auto-detects from cert+key |
98
98
  | `MCP_BROKER_STDIO_PROVIDER` | (unset) | When set, bridge stdin/stdout JSON-RPC to this provider (Claude Desktop integration) |
99
+ | `MCP_BROKER_AUTH_ENABLED` | (unset) | `1` to turn on the OAuth 2.1 resource server (requires the three below) |
100
+ | `MCP_BROKER_PUBLIC_BASE_URL` | (unset) | Public origin used to build canonical resource URIs, e.g. `https://mcp.example.com` |
101
+ | `MCP_BROKER_JWKS` | (unset) | Authorization server's JWKS URL, used to verify token signatures |
102
+ | `MCP_BROKER_ISSUER` | (unset) | Expected token issuer (defaults to the sole authorization server) |
103
+ | `MCP_BROKER_PROVIDER_SECRET` | (unset) | Shared secret every provider must present to occupy a slot |
104
+
105
+ ## Authorization (OAuth 2.1)
106
+
107
+ By default the broker performs **no** authentication. That is fine behind a
108
+ trusted network boundary, but do not expose it publicly as-is. Turn on the OAuth 2.1
109
+ resource server to require a bearer token on every client request, authenticate
110
+ providers, and filter the `_all` aggregate per caller.
111
+
112
+ Enabled via the `auth` config block (or env vars). Minimal `.mcp-broker/config.json`:
113
+
114
+ ```json
115
+ {
116
+ "auth": {
117
+ "enabled": true,
118
+ "publicBaseUrl": "https://mcp.example.com",
119
+ "authorizationServers": ["https://auth.example.com"],
120
+ "jwks": "https://auth.example.com/.well-known/jwks.json",
121
+ "requiredScopes": ["mcp:call"],
122
+ "perSlotScopes": { "_broker": ["broker:admin"] },
123
+ "subjectMapping": {
124
+ "userClaim": "sub",
125
+ "groupClaims": ["groups"],
126
+ "clientClaim": "client_id"
127
+ },
128
+ "roles": {
129
+ "viewer": {
130
+ "capabilities": ["mcp.tools.list", "mcp.resources.read"]
131
+ },
132
+ "maintenance": {
133
+ "inherits": ["viewer"],
134
+ "capabilities": ["mcp.tools.diagnose"]
135
+ }
136
+ },
137
+ "assignments": [
138
+ {
139
+ "subject": "group:maintenance-site-a",
140
+ "role": "maintenance",
141
+ "resource": "/enterprise/site-a/**"
142
+ }
143
+ ],
144
+ "slotResources": {
145
+ "motor-7": "/enterprise/site-a/area-a/line-3/cell-2/motor-7"
146
+ },
147
+ "toolCapabilities": {
148
+ "diagnose_motor": "mcp.tools.diagnose"
149
+ },
150
+ "providerSecret": "change-me"
151
+ }
152
+ }
153
+ ```
154
+
155
+ With this on:
156
+
157
+ - `POST /<slot>/mcp` (and `/sse`, `/messages`, `ws://…/<slot>`) require
158
+ `Authorization: Bearer <token>`; the token's audience must be
159
+ `https://mcp.example.com/<slot>/mcp`.
160
+ - Clients discover the authorization server via
161
+ `GET /.well-known/oauth-protected-resource/<slot>/mcp` (RFC 9728), advertised
162
+ in the `401` challenge.
163
+ - Providers must present `providerSecret` (via `X-Provider-Token` or
164
+ `Authorization: Bearer`) to connect to `/provider/<slot>` or `/providers`.
165
+ - `_all` only shows providers allowed by the caller's hierarchical policy.
166
+ - Explicit denies override inherited role grants.
167
+ - Structured provider principals can publish only inside their allowed resource
168
+ namespace.
169
+
170
+ Full model, flows, scopes, and endpoints: **[../docs/authorization.md](../docs/authorization.md)**.
171
+ Roles, resource paths, denies, and migration:
172
+ **[../docs/hierarchical-authorization.md](../docs/hierarchical-authorization.md)**.
173
+ Config field reference: **[docs/config.md](docs/config.md#auth-oauth-21-authorization)**.
99
174
 
100
175
  ## Programmatic API
101
176
 
@@ -111,12 +186,52 @@ const broker = new WsTunnelBuilder()
111
186
  .withStdioUpstream("my-server", "node", ["./my-server.js"])
112
187
  // Optional: serve a dev harness at /
113
188
  .withStaticMount("/", "/abs/path/to/www")
189
+ // Optional: enable the OAuth 2.1 resource server + provider auth
190
+ .withJwtAuth({
191
+ publicBaseUrl: "https://mcp.example.com",
192
+ authorizationServers: ["https://auth.example.com"],
193
+ jwksUri: "https://auth.example.com/.well-known/jwks.json",
194
+ requiredScopes: ["mcp:call"],
195
+ roles: {
196
+ viewer: { capabilities: ["mcp.tools.list"] },
197
+ },
198
+ assignments: [
199
+ {
200
+ subject: "group:operators",
201
+ role: "viewer",
202
+ resource: "/enterprise/site-a/**",
203
+ },
204
+ ],
205
+ subjectMapping: { groupClaims: ["groups"] },
206
+ })
207
+ .withProviderSecret(process.env.PROVIDER_SECRET!)
114
208
  .build();
115
209
 
116
210
  await broker.start();
117
211
  ```
118
212
 
119
- All builder methods are documented inline. The full options interface is `WsTunnelOptions`, also exported.
213
+ All builder methods are documented inline. The full options interface is
214
+ `IWsTunnelOptions`, also exported. Use `withAuthorizationPolicy` for a
215
+ standalone policy configuration, `withPolicyEngine` for a custom engine, and
216
+ `withSlotResourceResolver` for a custom namespace mapping. For a custom token
217
+ validator (for example RFC 7662 introspection) or provider authenticator, use
218
+ `withAuth(resolvedAuth)` or `withProviderAuth(authenticator)` with your own
219
+ `ITokenValidator` or `IProviderAuthenticator`.
220
+
221
+ ## Interactive OAuth demo
222
+
223
+ The bundled [OAuth Policy Lab](web/demos/oauth-lab/) runs a complete local
224
+ Authorization Code and PKCE flow with signed JWTs, JWKS validation,
225
+ audience-bound tokens, hierarchical policies, explicit denies, and a live
226
+ audit:
227
+
228
+ ```sh
229
+ npm run demo:oauth
230
+ ```
231
+
232
+ The page opens at `http://127.0.0.1:3001/demos/oauth-lab/`. See the
233
+ [demo README](web/demos/oauth-lab/README.md) for its identities, policy matrix,
234
+ and automated smoke test.
120
235
 
121
236
  ## TLS for local development
122
237
 
@@ -0,0 +1,40 @@
1
+ import type { IResolvedAuth } from "./auth.types.js";
2
+ import { type IAuthorizationPolicyConfig } from "../authorization/index.js";
3
+ /**
4
+ * High-level options for the default JWT/JWKS resource-server setup. Mirrors the
5
+ * `auth` block of the broker JSON config and is turned into a fully
6
+ * {@link IResolvedAuth} (with a {@link JwtTokenValidator}) by {@link buildJwtAuth}.
7
+ */
8
+ export interface IJwtAuthOptions extends IAuthorizationPolicyConfig {
9
+ /** Public origin the broker is reached at (e.g. `https://mcp.example.com`). */
10
+ publicBaseUrl: string;
11
+ /** Authorization server issuer URL(s) advertised in the PRM. At least one. */
12
+ authorizationServers: string[];
13
+ /** URL of the authorization server's JWKS document. */
14
+ jwksUri: string;
15
+ /** Expected token issuer(s). Defaults to the sole authorization server. */
16
+ issuer?: string | string[];
17
+ /** Scopes advertised in the PRM `scopes_supported`. */
18
+ scopesSupported?: string[];
19
+ /** Baseline scope(s) required to reach any slot. */
20
+ requiredScopes?: string[];
21
+ /** Per-slot required-scope overrides (e.g. an admin scope for `_broker`). */
22
+ perSlotScopes?: Record<string, string[]>;
23
+ /**
24
+ * Per-provider scope requirements for the `_all` aggregate. A caller sees a
25
+ * provider in `_all` only if it holds at least one of the listed scopes.
26
+ * Providers not listed here stay visible to every authenticated caller.
27
+ * Turned into an {@link AggregateScopeFilter} automatically.
28
+ */
29
+ providerScopes?: Record<string, string[]>;
30
+ /** Leeway in seconds for token `exp`/`nbf` checks. */
31
+ clockToleranceSec?: number;
32
+ }
33
+ /**
34
+ * Builds an {@link IResolvedAuth} backed by a {@link JwtTokenValidator}. Validates
35
+ * the required inputs up front and throws a descriptive error on misconfig, so
36
+ * an operator sees the problem at boot rather than as opaque `401`s later.
37
+ */
38
+ export declare function buildJwtAuth(options: IJwtAuthOptions): IResolvedAuth;
39
+ /** @deprecated Use {@link IJwtAuthOptions}. */
40
+ export type JwtAuthOptions = IJwtAuthOptions;
@@ -0,0 +1,62 @@
1
+ import { JwtTokenValidator } from "./jwt.validator.js";
2
+ import { compileAuthorizationPolicy, DefaultSlotResourceResolver, hasAuthorizationPolicies } from "../authorization/index.js";
3
+ /** Builds the default aggregate filter from a per-provider scope map. */
4
+ function makeProviderScopeFilter(providerScopes) {
5
+ return (principal, providerName) => {
6
+ const required = providerScopes[providerName];
7
+ if (!required || required.length === 0)
8
+ return true; // unlisted ⇒ visible to all
9
+ return required.some((scope) => principal.scopes.has(scope));
10
+ };
11
+ }
12
+ /** Removes a single trailing slash so resource URIs concatenate cleanly. */
13
+ function stripTrailingSlash(url) {
14
+ return url.endsWith("/") ? url.slice(0, -1) : url;
15
+ }
16
+ /**
17
+ * Builds an {@link IResolvedAuth} backed by a {@link JwtTokenValidator}. Validates
18
+ * the required inputs up front and throws a descriptive error on misconfig, so
19
+ * an operator sees the problem at boot rather than as opaque `401`s later.
20
+ */
21
+ export function buildJwtAuth(options) {
22
+ if (!options.publicBaseUrl) {
23
+ throw new Error("auth: publicBaseUrl is required.");
24
+ }
25
+ if (!options.authorizationServers || options.authorizationServers.length === 0) {
26
+ throw new Error("auth: at least one authorizationServers entry is required.");
27
+ }
28
+ if (!options.jwksUri) {
29
+ throw new Error("auth: jwksUri is required.");
30
+ }
31
+ const publicBaseUrl = stripTrailingSlash(options.publicBaseUrl);
32
+ const issuer = options.issuer ?? (options.authorizationServers.length === 1 ? options.authorizationServers[0] : undefined);
33
+ const validator = new JwtTokenValidator({
34
+ jwksUri: options.jwksUri,
35
+ issuer,
36
+ clockToleranceSec: options.clockToleranceSec,
37
+ });
38
+ const resolved = {
39
+ publicBaseUrl,
40
+ authorizationServers: options.authorizationServers,
41
+ validator,
42
+ };
43
+ if (options.scopesSupported)
44
+ resolved.scopesSupported = options.scopesSupported;
45
+ if (options.requiredScopes)
46
+ resolved.requiredScopes = options.requiredScopes;
47
+ if (options.perSlotScopes)
48
+ resolved.perSlotScopes = options.perSlotScopes;
49
+ if (options.providerScopes && Object.keys(options.providerScopes).length > 0) {
50
+ console.warn("[mcp-broker] auth.providerScopes is deprecated; migrate to roles and hierarchical assignments.");
51
+ resolved.aggregateScopeFilter = makeProviderScopeFilter(options.providerScopes);
52
+ }
53
+ if (options.slotResources && Object.keys(options.slotResources).length > 0) {
54
+ resolved.slotResourceResolver = new DefaultSlotResourceResolver(options.slotResources);
55
+ }
56
+ if (hasAuthorizationPolicies(options)) {
57
+ resolved.authorization = compileAuthorizationPolicy(options);
58
+ resolved.slotResourceResolver = resolved.authorization.slotResourceResolver;
59
+ }
60
+ return resolved;
61
+ }
62
+ //# sourceMappingURL=auth.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.config.js","sourceRoot":"","sources":["../../src/auth/auth.config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Core contracts for the broker's OAuth 2.1 **resource server** layer.
3
+ *
4
+ * The broker never acts as an authorization server: it only validates the
5
+ * access tokens minted by an external AS and advertises that AS through
6
+ * Protected Resource Metadata (RFC 9728). These types are the seam every other
7
+ * auth module builds on, so a host can drop in a custom {@link ITokenValidator}
8
+ * (e.g. RFC 7662 introspection) without touching the enforcement code.
9
+ */
10
+ import type { IAuthorizationSubject, IPolicyAuthorization, ISlotResourceResolver } from "../authorization/index.js";
11
+ /**
12
+ * The subset of OAuth 2.1 / RFC 9068 access-token claims a resource server
13
+ * cares about. `aud` carries the audience binding (RFC 8707), `scope` the
14
+ * space-delimited granted scopes; some authorization servers emit `scopes` as
15
+ * an array instead. Unknown claims are preserved via the index signature.
16
+ */
17
+ export interface IAccessTokenClaims {
18
+ /** Subject — the principal the token was issued for. */
19
+ sub?: string;
20
+ /** Intended audience(s). MUST include the slot's canonical resource URI. */
21
+ aud?: string | string[];
22
+ /** Issuer (the authorization server). */
23
+ iss?: string;
24
+ /** Expiry, seconds since the epoch. */
25
+ exp?: number;
26
+ /** Space-delimited granted scopes (RFC 8693). */
27
+ scope?: string;
28
+ /** Array-form granted scopes, emitted by some authorization servers. */
29
+ scopes?: string[];
30
+ [claim: string]: unknown;
31
+ }
32
+ /**
33
+ * Validates a bearer access token for a specific resource. Implementations MUST
34
+ * verify the token's signature/validity **and** that it was issued for
35
+ * `resource` (audience binding, RFC 8707). On any failure they MUST throw an
36
+ * {@link AuthError} with status `401` and code `invalid_token`; unexpected
37
+ * infrastructure failures (e.g. JWKS unreachable) should throw a plain error so
38
+ * the caller can surface a `500` rather than a misleading `401`.
39
+ */
40
+ export interface ITokenValidator {
41
+ validate(token: string, resource: string): Promise<IAccessTokenClaims>;
42
+ }
43
+ /** OAuth 2.1 error codes the broker emits in `WWW-Authenticate` challenges. */
44
+ export type AuthErrorCode = "invalid_token" | "insufficient_scope" | "invalid_request";
45
+ /**
46
+ * A thrown authorization failure carrying the HTTP status and OAuth error code
47
+ * the enforcement point should surface. `scope` is set for `insufficient_scope`
48
+ * challenges to advertise the scope(s) the resource requires.
49
+ */
50
+ export declare class AuthError extends Error {
51
+ readonly status: 401 | 403 | 400;
52
+ readonly code: AuthErrorCode;
53
+ readonly description?: string;
54
+ readonly scope?: string;
55
+ constructor(status: 401 | 403 | 400, code: AuthErrorCode, description?: string, scope?: string);
56
+ }
57
+ /**
58
+ * The authenticated caller, produced once a token passes validation and the
59
+ * required scopes are satisfied. Threaded through the request so downstream
60
+ * components (e.g. the `_all` aggregate) can make scope-aware decisions.
61
+ */
62
+ export interface IPrincipal {
63
+ readonly claims: IAccessTokenClaims;
64
+ readonly scopes: ReadonlySet<string>;
65
+ /** Subjects derived exclusively from validated token claims. */
66
+ readonly subject?: IAuthorizationSubject;
67
+ }
68
+ /**
69
+ * Decides, per authenticated caller, whether a given provider is visible in the
70
+ * `_all` aggregate. This is the content-confidentiality enforcement point: a
71
+ * client only sees (and can call) tools/prompts from providers it is authorized
72
+ * for. Return `true` to include the provider for this principal.
73
+ */
74
+ export type AggregateScopeFilter = (principal: IPrincipal, providerName: string) => boolean;
75
+ /**
76
+ * A fully resolved authorization configuration, ready for the enforcement
77
+ * layer. Built either from JSON config (via `buildJwtAuth`) or supplied
78
+ * directly by an embedder with a custom {@link ITokenValidator}.
79
+ */
80
+ export interface IResolvedAuth {
81
+ /**
82
+ * Public origin the broker is reached at, used to build canonical resource
83
+ * URIs and metadata URLs. No trailing slash (e.g. `https://mcp.example.com`).
84
+ */
85
+ publicBaseUrl: string;
86
+ /** One or more authorization server issuer URLs, advertised in the PRM. */
87
+ authorizationServers: string[];
88
+ /** Optional list of scopes advertised in the PRM `scopes_supported`. */
89
+ scopesSupported?: string[];
90
+ /** The token validator (JWKS-backed by default). */
91
+ validator: ITokenValidator;
92
+ /** Baseline scope(s) required to reach any slot. Empty ⇒ any valid token. */
93
+ requiredScopes?: string[];
94
+ /** Per-slot required-scope overrides (e.g. an admin scope for `_broker`). */
95
+ perSlotScopes?: Record<string, string[]>;
96
+ /**
97
+ * Per-caller filter for the `_all` aggregate. When set, a client's view of
98
+ * `_all` is narrowed to the providers this returns `true` for. When absent,
99
+ * every authenticated caller sees the full aggregate.
100
+ */
101
+ aggregateScopeFilter?: AggregateScopeFilter;
102
+ /** Compiled hierarchical policy runtime, absent for legacy OAuth behavior. */
103
+ authorization?: IPolicyAuthorization;
104
+ /** Optional resolver usable for provider namespace checks without policies. */
105
+ slotResourceResolver?: ISlotResourceResolver;
106
+ }
107
+ /**
108
+ * Extracts the effective set of granted scopes from token claims, unioning the
109
+ * space-delimited `scope` string and the array-form `scopes` claim.
110
+ */
111
+ export declare function scopesOf(claims: IAccessTokenClaims): Set<string>;
112
+ /** @deprecated Use {@link IAccessTokenClaims}. */
113
+ export type AccessTokenClaims = IAccessTokenClaims;
114
+ /** @deprecated Use {@link ITokenValidator}. */
115
+ export type TokenValidator = ITokenValidator;
116
+ /** @deprecated Use {@link IPrincipal}. */
117
+ export type Principal = IPrincipal;
118
+ /** @deprecated Use {@link IResolvedAuth}. */
119
+ export type ResolvedAuth = IResolvedAuth;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A thrown authorization failure carrying the HTTP status and OAuth error code
3
+ * the enforcement point should surface. `scope` is set for `insufficient_scope`
4
+ * challenges to advertise the scope(s) the resource requires.
5
+ */
6
+ export class AuthError extends Error {
7
+ status;
8
+ code;
9
+ description;
10
+ scope;
11
+ constructor(status, code, description, scope) {
12
+ super(description ?? code);
13
+ this.name = "AuthError";
14
+ this.status = status;
15
+ this.code = code;
16
+ this.description = description;
17
+ this.scope = scope;
18
+ }
19
+ }
20
+ /**
21
+ * Extracts the effective set of granted scopes from token claims, unioning the
22
+ * space-delimited `scope` string and the array-form `scopes` claim.
23
+ */
24
+ export function scopesOf(claims) {
25
+ const set = new Set();
26
+ if (typeof claims.scope === "string") {
27
+ for (const s of claims.scope.split(/\s+/))
28
+ if (s)
29
+ set.add(s);
30
+ }
31
+ if (Array.isArray(claims.scopes)) {
32
+ for (const s of claims.scopes)
33
+ if (typeof s === "string" && s)
34
+ set.add(s);
35
+ }
36
+ return set;
37
+ }
38
+ //# sourceMappingURL=auth.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.types.js","sourceRoot":"","sources":["../../src/auth/auth.types.ts"],"names":[],"mappings":"AAgDA;;;;GAIG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IACvB,MAAM,CAAkB;IACxB,IAAI,CAAgB;IACpB,WAAW,CAAU;IACrB,KAAK,CAAU;IAExB,YAAY,MAAuB,EAAE,IAAmB,EAAE,WAAoB,EAAE,KAAc;QAC1F,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAuDD;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,MAA0B;IAC/C,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM;YAAE,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { IncomingMessage, ServerResponse } from "http";
2
+ import { AuthError, type IPrincipal, type IResolvedAuth } from "./auth.types.js";
3
+ import { type IProtectedResourceMetadata } from "./resource.metadata.js";
4
+ /**
5
+ * The HTTP enforcement point for the resource-server layer. Wraps a
6
+ * {@link IResolvedAuth} plus the configured `/mcp` path suffix and turns it into
7
+ * the three operations the transport needs: serve Protected Resource Metadata,
8
+ * authorize a request, and write an RFC 9728 `401`/`403` challenge.
9
+ *
10
+ * A slot's **canonical resource URI** is `<publicBaseUrl>/<slot>/<mcp>` and is
11
+ * used uniformly across all of that slot's HTTP endpoints (`/mcp`, `/sse`,
12
+ * `/messages`) so a single token audience covers the whole slot.
13
+ */
14
+ export declare class HttpAuthGuard {
15
+ private readonly _auth;
16
+ /** The `/mcp` suffix without a leading slash, e.g. `"mcp"`. */
17
+ private readonly _mcpSuffix;
18
+ constructor(auth: IResolvedAuth, mcpSuffix: string);
19
+ /** Canonical resource identifier for a slot (RFC 8707 §2). */
20
+ resourceFor(slot: string): string;
21
+ /** RFC 9728 metadata URL for a slot, advertised in the `401` challenge. */
22
+ metadataUrlFor(slot: string): string;
23
+ /** Required scope(s) for a slot: per-slot override, else the baseline. */
24
+ requiredScopesFor(slot: string): string[];
25
+ /** The RFC 9728 metadata document for a slot. */
26
+ metadataFor(slot: string): IProtectedResourceMetadata;
27
+ /**
28
+ * If `rawUrl` is a Protected Resource Metadata request
29
+ * (`/.well-known/oauth-protected-resource/<slot>/<mcp>`), returns the slot;
30
+ * otherwise `null`.
31
+ */
32
+ matchMetadataRequest(rawUrl: string): string | null;
33
+ /**
34
+ * Validates the request's bearer token against the slot's canonical resource
35
+ * and enforces the slot's required scopes. Resolves to the {@link IPrincipal}
36
+ * on success; rejects with an {@link AuthError} (`401` missing/invalid token,
37
+ * `403` insufficient scope) otherwise.
38
+ */
39
+ authorize(req: IncomingMessage, slot: string): Promise<IPrincipal>;
40
+ /**
41
+ * Builds the RFC 9728 §5.1 `WWW-Authenticate` header value for a challenge.
42
+ * Always points the client at the slot's metadata URL so it can discover the
43
+ * authorization server and retry. Shared by the HTTP and WebSocket paths.
44
+ */
45
+ challengeHeader(slot: string, err: AuthError): string;
46
+ /**
47
+ * Writes an RFC 9728 §5.1 `WWW-Authenticate` challenge and the matching
48
+ * status to an HTTP response.
49
+ */
50
+ writeChallenge(res: ServerResponse, slot: string, err: AuthError): void;
51
+ }