@cyanmycelium/mcp-broker 0.4.0 → 1.2.0

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 (151) 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 +8 -3
  4. package/.mcp-broker.example/config.json +86 -0
  5. package/README.md +122 -3
  6. package/dist/bin.d.ts +0 -1
  7. package/dist/bin.js +180 -208
  8. package/dist/bin.js.map +1 -1
  9. package/dist/chunk-FTDKH2C4.js +3670 -0
  10. package/dist/chunk-FTDKH2C4.js.map +1 -0
  11. package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
  12. package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
  13. package/dist/index.d.ts +1790 -18
  14. package/dist/index.js +2 -14
  15. package/dist/index.js.map +1 -1
  16. package/package.json +14 -8
  17. package/scripts/copy-assets.mjs +16 -10
  18. package/scripts/gen-cert.mjs +5 -5
  19. package/scripts/pack-mcpb.mjs +5 -5
  20. package/scripts/sign-bundle.mjs +6 -6
  21. package/src/auth/auth.config.ts +98 -0
  22. package/src/auth/auth.types.ts +120 -0
  23. package/src/auth/http.auth.ts +128 -0
  24. package/src/auth/index.ts +34 -0
  25. package/src/auth/jwt.validator.ts +63 -0
  26. package/src/auth/provider.auth.ts +114 -0
  27. package/src/auth/resource.metadata.ts +34 -0
  28. package/src/authorization/audit.ts +35 -0
  29. package/src/authorization/capability.classifier.ts +114 -0
  30. package/src/authorization/index.ts +37 -0
  31. package/src/authorization/policy.engine.ts +212 -0
  32. package/src/authorization/policy.types.ts +105 -0
  33. package/src/authorization/resource.path.ts +126 -0
  34. package/src/authorization/runtime.ts +56 -0
  35. package/src/authorization/slot.resource.ts +50 -0
  36. package/src/authorization/subject.mapper.ts +81 -0
  37. package/src/bin.ts +90 -7
  38. package/src/broker/adapters/broker.adapter.info.ts +3 -3
  39. package/src/broker/adapters/broker.adapter.providers.ts +3 -3
  40. package/src/broker/aggregate/aggregate.catalog.ts +49 -21
  41. package/src/broker/aggregate/aggregate.server.ts +149 -19
  42. package/src/broker/aggregate/provider.client.session.ts +22 -22
  43. package/src/broker/behaviors/broker.behavior.info.ts +4 -4
  44. package/src/broker/behaviors/broker.behavior.providers.ts +6 -6
  45. package/src/broker/broker.context.ts +10 -4
  46. package/src/broker/broker.grammars.ts +16 -13
  47. package/src/broker/broker.server.ts +12 -9
  48. package/src/broker/grammars/claude/en.json +1 -1
  49. package/src/broker/grammars/claude/fr.json +1 -1
  50. package/src/broker/index.ts +9 -9
  51. package/src/config.ts +81 -8
  52. package/src/index.ts +121 -20
  53. package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +24 -21
  54. package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
  55. package/src/remote.transports.ts +11 -8
  56. package/src/remote.upstream.ts +11 -8
  57. package/src/stdio.upstream.ts +9 -6
  58. package/src/upstream.ts +6 -3
  59. package/src/ws/ws.interfaces.ts +357 -0
  60. package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +112 -9
  61. package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +646 -457
  62. package/web/README.md +94 -0
  63. package/web/assets/logo.png +0 -0
  64. package/web/broker-self-mcp.html +338 -0
  65. package/web/css/styles.css +580 -0
  66. package/web/demos/DemoPlaceholder.html +258 -0
  67. package/web/demos/broker-explorer/css/app.css +393 -0
  68. package/web/demos/broker-explorer/index.html +94 -0
  69. package/web/demos/broker-explorer/js/app.js +271 -0
  70. package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
  71. package/web/demos/oauth-lab/README.md +94 -0
  72. package/web/demos/oauth-lab/config.json +117 -0
  73. package/web/demos/oauth-lab/css/app.css +1097 -0
  74. package/web/demos/oauth-lab/index.html +323 -0
  75. package/web/demos/oauth-lab/js/app.js +654 -0
  76. package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
  77. package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
  78. package/web/demos/oauth-lab/server/smoke-test.mjs +308 -0
  79. package/web/demos/oauth-lab/server/start.mjs +106 -0
  80. package/web/demos/provider-tunnel/css/app.css +384 -0
  81. package/web/demos/provider-tunnel/index.html +99 -0
  82. package/web/demos/provider-tunnel/js/app.js +226 -0
  83. package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
  84. package/web/index.html +558 -0
  85. package/web/js/lib/broker-tunnel.js +173 -0
  86. package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
  87. package/dist/broker/adapters/broker.adapter.info.js +0 -43
  88. package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
  89. package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
  90. package/dist/broker/adapters/broker.adapter.providers.js +0 -61
  91. package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
  92. package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -54
  93. package/dist/broker/aggregate/aggregate.catalog.js +0 -105
  94. package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
  95. package/dist/broker/aggregate/aggregate.server.d.ts +0 -47
  96. package/dist/broker/aggregate/aggregate.server.js +0 -151
  97. package/dist/broker/aggregate/aggregate.server.js.map +0 -1
  98. package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
  99. package/dist/broker/aggregate/provider.client.session.js +0 -140
  100. package/dist/broker/aggregate/provider.client.session.js.map +0 -1
  101. package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
  102. package/dist/broker/behaviors/broker.behavior.info.js +0 -41
  103. package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
  104. package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
  105. package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
  106. package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
  107. package/dist/broker/broker.context.d.ts +0 -59
  108. package/dist/broker/broker.context.js +0 -2
  109. package/dist/broker/broker.context.js.map +0 -1
  110. package/dist/broker/broker.grammars.d.ts +0 -130
  111. package/dist/broker/broker.grammars.js +0 -229
  112. package/dist/broker/broker.grammars.js.map +0 -1
  113. package/dist/broker/broker.server.d.ts +0 -66
  114. package/dist/broker/broker.server.js +0 -73
  115. package/dist/broker/broker.server.js.map +0 -1
  116. package/dist/broker/index.d.ts +0 -9
  117. package/dist/broker/index.js +0 -7
  118. package/dist/broker/index.js.map +0 -1
  119. package/dist/config.d.ts +0 -136
  120. package/dist/config.js +0 -61
  121. package/dist/config.js.map +0 -1
  122. package/dist/mcpb.loader.d.ts +0 -24
  123. package/dist/mcpb.loader.js +0 -161
  124. package/dist/mcpb.loader.js.map +0 -1
  125. package/dist/mcpb.unzip.d.ts +0 -6
  126. package/dist/mcpb.unzip.js +0 -95
  127. package/dist/mcpb.unzip.js.map +0 -1
  128. package/dist/remote.transports.d.ts +0 -16
  129. package/dist/remote.transports.js +0 -297
  130. package/dist/remote.transports.js.map +0 -1
  131. package/dist/remote.upstream.d.ts +0 -36
  132. package/dist/remote.upstream.js +0 -52
  133. package/dist/remote.upstream.js.map +0 -1
  134. package/dist/stdio.upstream.d.ts +0 -45
  135. package/dist/stdio.upstream.js +0 -85
  136. package/dist/stdio.upstream.js.map +0 -1
  137. package/dist/upstream.d.ts +0 -33
  138. package/dist/upstream.js +0 -2
  139. package/dist/upstream.js.map +0 -1
  140. package/dist/version.d.ts +0 -2
  141. package/dist/version.js +0 -9
  142. package/dist/version.js.map +0 -1
  143. package/dist/ws.tunnel.builder.d.ts +0 -139
  144. package/dist/ws.tunnel.builder.js +0 -205
  145. package/dist/ws.tunnel.builder.js.map +0 -1
  146. package/dist/ws.tunnel.d.ts +0 -373
  147. package/dist/ws.tunnel.js +0 -1090
  148. package/dist/ws.tunnel.js.map +0 -1
  149. /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
  150. /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
  151. /package/dist/{broker/grammars → grammars}/default/zh.json +0 -0
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { McpGrammar } from "@cyanmycelium/mcp-core";
5
5
 
6
6
  // ---------------------------------------------------------------------------
7
- // Open string types — extensibility by convention
7
+ // Open string types, extensibility by convention
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  /**
@@ -27,7 +27,7 @@ export type BrokerLocale = string;
27
27
  * the defaults emitted by `grammarResolverFromOptions`: `claude`, `gpt`,
28
28
  * `mistral`, `copilot`, plus the universal `default`. Custom families
29
29
  * are supported by passing a custom `agents` map in
30
- * `StartBrokerServerOptions.grammarResolverOptions`.
30
+ * `IStartBrokerServerOptions.grammarResolverOptions`.
31
31
  */
32
32
  export type BrokerUserAgent = string;
33
33
 
@@ -40,8 +40,8 @@ export type BrokerUserAgent = string;
40
40
  * matrix the broker registers on disk.
41
41
  *
42
42
  * Pattern:
43
- * - `"<userAgent>:<locale>"` (no version) — e.g. `"claude:fr"`, `"default:en"`
44
- * - `"<userAgent>:<locale>@<version>"` (versioned) — e.g. `"claude:fr@v2"`
43
+ * - `"<userAgent>:<locale>"` (no version), e.g. `"claude:fr"`, `"default:en"`
44
+ * - `"<userAgent>:<locale>@<version>"` (versioned), e.g. `"claude:fr@v2"`
45
45
  *
46
46
  * The colon separator is reserved for the `<ua>:<locale>` composition; the
47
47
  * `@` separator is reserved for the optional version suffix. Neither
@@ -94,7 +94,7 @@ export function parseBrokerGrammarStem(stem: string): { locale: BrokerLocale; ve
94
94
  * JSON files live alongside this module in `src/broker/grammars/` during
95
95
  * development and are mirrored under `dist/broker/grammars/` at build time
96
96
  * by `scripts/copy-assets.mjs`. Adding a new `(userAgent, locale)` pair is
97
- * just a matter of dropping a new JSON file — no code change required.
97
+ * just a matter of dropping a new JSON file: no code change required.
98
98
  */
99
99
  const GRAMMARS_DIR = join(dirname(fileURLToPath(import.meta.url)), "grammars");
100
100
 
@@ -132,9 +132,9 @@ export function loadBrokerGrammar(userAgent: BrokerUserAgent, locale: BrokerLoca
132
132
  *
133
133
  * Used by the broker server at startup to bulk-register both the packaged
134
134
  * grammars and any local overrides. No hard-coded list of supported
135
- * user-agents or locales — adding a new grammar is dropping a JSON file.
135
+ * user-agents or locales, adding a new grammar is dropping a JSON file.
136
136
  */
137
- export interface BrokerGrammarEntry {
137
+ export interface IBrokerGrammarEntry {
138
138
  userAgent: BrokerUserAgent;
139
139
  locale: BrokerLocale;
140
140
  /** Set only for filenames carrying an `@<version>` suffix. */
@@ -144,7 +144,7 @@ export interface BrokerGrammarEntry {
144
144
  grammar: McpGrammar;
145
145
  }
146
146
 
147
- export function* iterBrokerGrammarsFrom(grammarsDir: string): Generator<BrokerGrammarEntry> {
147
+ export function* iterBrokerGrammarsFrom(grammarsDir: string): Generator<IBrokerGrammarEntry> {
148
148
  if (!existsSync(grammarsDir)) return;
149
149
 
150
150
  const userAgents = readdirSync(grammarsDir).sort();
@@ -186,12 +186,15 @@ export function* iterBrokerGrammarsFrom(grammarsDir: string): Generator<BrokerGr
186
186
  * mcp-broker package). Equivalent to `iterBrokerGrammarsFrom(<packaged-dir>)`.
187
187
  *
188
188
  * For local user overrides, see {@link iterBrokerGrammarsFrom} with a custom
189
- * directory — typically `.mcp-broker/grammars/` next to the config file.
189
+ * directory, typically `.mcp-broker/grammars/` next to the config file.
190
190
  */
191
- export function* iterAvailableBrokerGrammars(): Generator<BrokerGrammarEntry> {
191
+ export function* iterAvailableBrokerGrammars(): Generator<IBrokerGrammarEntry> {
192
192
  yield* iterBrokerGrammarsFrom(GRAMMARS_DIR);
193
193
  }
194
194
 
195
+ /** @deprecated Use {@link IBrokerGrammarEntry}. */
196
+ export type BrokerGrammarEntry = IBrokerGrammarEntry;
197
+
195
198
  // ---------------------------------------------------------------------------
196
199
  // Baseline helpers (used by behaviors to source their inline descriptions)
197
200
  // ---------------------------------------------------------------------------
@@ -203,20 +206,20 @@ export function* iterAvailableBrokerGrammars(): Generator<BrokerGrammarEntry> {
203
206
  * Conventionally this is `default:en`. Session-specific grammars selected by
204
207
  * the resolver override individual entries on top of this baseline.
205
208
  *
206
- * Throws if the JSON resource is missing — the broker behaviors cannot be
209
+ * Throws if the JSON resource is missing: the broker behaviors cannot be
207
210
  * built without baseline descriptions.
208
211
  */
209
212
  export function brokerBaselineGrammar(): McpGrammar {
210
213
  const g = loadBrokerGrammar("default", "en");
211
214
  if (!g) {
212
- throw new Error(`Required baseline broker grammar "default:en" is missing — expected at ${join(GRAMMARS_DIR, "default", "en.json")}.`);
215
+ throw new Error(`Required baseline broker grammar "default:en" is missing, expected at ${join(GRAMMARS_DIR, "default", "en.json")}.`);
213
216
  }
214
217
  return g;
215
218
  }
216
219
 
217
220
  /**
218
221
  * Convenience accessor for a baseline tool description. Throws when the
219
- * tool is not listed in the baseline grammar — i.e. the JSON file is missing
222
+ * tool is not listed in the baseline grammar, i.e. the JSON file is missing
220
223
  * an entry for a tool the code knows about.
221
224
  */
222
225
  export function brokerBaselineToolDescription(toolName: string): string {
@@ -1,9 +1,9 @@
1
1
  import { McpServerBuilder, LoopbackTransport } from "@cyanmycelium/mcp-core";
2
2
  import type { GrammarResolverOptions, IMcpServer, IMessageTransport } from "@cyanmycelium/mcp-core";
3
- import { BrokerInfoBehavior } from "./behaviors/broker.behavior.info.js";
4
- import { BrokerProvidersBehavior } from "./behaviors/broker.behavior.providers.js";
5
- import { iterAvailableBrokerGrammars, iterBrokerGrammarsFrom } from "./broker.grammars.js";
6
- import type { BrokerContext } from "./broker.context.js";
3
+ import { BrokerInfoBehavior } from "./behaviors/broker.behavior.info";
4
+ import { BrokerProvidersBehavior } from "./behaviors/broker.behavior.providers";
5
+ import { iterAvailableBrokerGrammars, iterBrokerGrammarsFrom } from "./broker.grammars";
6
+ import type { IBrokerContext } from "./broker.context";
7
7
 
8
8
  /**
9
9
  * Reserved provider slot name under which the broker exposes itself as an MCP
@@ -19,7 +19,7 @@ export const BROKER_PROVIDER_NAME = "_broker";
19
19
  * replace either resolver with custom logic without touching mcp-broker
20
20
  * internals.
21
21
  */
22
- export interface StartBrokerServerOptions {
22
+ export interface IStartBrokerServerOptions {
23
23
  /**
24
24
  * Overrides for the built-in grammar resolver from `@cyanmycelium/mcp-core`.
25
25
  *
@@ -44,7 +44,7 @@ export interface StartBrokerServerOptions {
44
44
  * via `withGrammar(brokerGrammarKey(ua, locale), grammar)`. The
45
45
  * candidate-chain resolution implemented by `McpServer.initialize` in
46
46
  * mcp-core@0.3.0 then walks the chain and merges the four layers
47
- * (behavior, adapter, static, store) for the first matching key —
47
+ * (behavior, adapter, static, store) for the first matching key ,
48
48
  * the old hand-rolled pre-merge cascade is no longer needed.
49
49
  *
50
50
  * When `undefined` (default), only the packaged grammars are loaded.
@@ -69,8 +69,8 @@ export interface StartBrokerServerOptions {
69
69
  * loopback transport to attach to the tunnel.
70
70
  */
71
71
  export async function startBrokerServer(
72
- context: BrokerContext,
73
- options: StartBrokerServerOptions = {}
72
+ context: IBrokerContext,
73
+ options: IStartBrokerServerOptions = {}
74
74
  ): Promise<{
75
75
  server: IMcpServer;
76
76
  clientTransport: IMessageTransport;
@@ -93,7 +93,7 @@ export async function startBrokerServer(
93
93
  // Register every `(userAgent, locale)` JSON found on disk as a raw
94
94
  // grammar layer. The candidate-chain resolution in
95
95
  // McpServer.initialize (mcp-core@0.3.0) walks the resolver's chain
96
- // and merges all matching layers — so partial user-agent files no
96
+ // and merges all matching layers: so partial user-agent files no
97
97
  // longer need to be pre-merged with the default-locale baseline at
98
98
  // boot. Local overrides come after packaged entries; identical keys
99
99
  // get overlaid via the registry's last-write-wins.
@@ -124,3 +124,6 @@ export async function startBrokerServer(
124
124
 
125
125
  return { server, clientTransport: clientEnd };
126
126
  }
127
+
128
+ /** @deprecated Use {@link IStartBrokerServerOptions}. */
129
+ export type StartBrokerServerOptions = IStartBrokerServerOptions;
@@ -7,7 +7,7 @@
7
7
  "description": "List every provider slot on this broker, connected or not. Each entry tells you the transport kind, current client count, and how many requests are in flight. Use this to discover what you can route to."
8
8
  },
9
9
  "provider_status": {
10
- "description": "Get the full status of one provider slot by name. Returns an error if the slot does not exist — start with providers_list if you are not sure of the name.",
10
+ "description": "Get the full status of one provider slot by name. Returns an error if the slot does not exist, start with providers_list if you are not sure of the name.",
11
11
  "properties": {
12
12
  "name": "The exact provider slot name. Case-sensitive. Use providers_list to discover valid values."
13
13
  }
@@ -7,7 +7,7 @@
7
7
  "description": "Liste tous les emplacements de providers de ce broker, connectés ou non. Chaque entrée indique le type de transport, le nombre de clients actuels et le nombre de requêtes en cours. Utilise-le pour découvrir vers qui tu peux router."
8
8
  },
9
9
  "provider_status": {
10
- "description": "Récupère le statut complet d'un emplacement de provider par son nom. Retourne une erreur si l'emplacement n'existe pas — commence par providers_list si tu n'es pas sûr du nom.",
10
+ "description": "Récupère le statut complet d'un emplacement de provider par son nom. Retourne une erreur si l'emplacement n'existe pas, commence par providers_list si tu n'es pas sûr du nom.",
11
11
  "properties": {
12
12
  "name": "Le nom exact de l'emplacement provider. Sensible à la casse. Utilise providers_list pour découvrir les valeurs valides."
13
13
  }
@@ -1,9 +1,9 @@
1
- export { BrokerInfoBehavior } from "./behaviors/broker.behavior.info.js";
2
- export { BrokerProvidersBehavior } from "./behaviors/broker.behavior.providers.js";
3
- export { BrokerInfoAdapter, BROKER_INFO_URI } from "./adapters/broker.adapter.info.js";
4
- export { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "./adapters/broker.adapter.providers.js";
5
- export { startBrokerServer, BROKER_PROVIDER_NAME } from "./broker.server.js";
6
- export type { StartBrokerServerOptions } from "./broker.server.js";
1
+ export { BrokerInfoBehavior } from "./behaviors/broker.behavior.info";
2
+ export { BrokerProvidersBehavior } from "./behaviors/broker.behavior.providers";
3
+ export { BrokerInfoAdapter, BROKER_INFO_URI } from "./adapters/broker.adapter.info";
4
+ export { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "./adapters/broker.adapter.providers";
5
+ export { startBrokerServer, BROKER_PROVIDER_NAME } from "./broker.server";
6
+ export type { IStartBrokerServerOptions, StartBrokerServerOptions } from "./broker.server";
7
7
  export {
8
8
  brokerBaselineGrammar,
9
9
  brokerBaselinePropertyDescription,
@@ -17,6 +17,6 @@ export {
17
17
  iterBrokerGrammarsFrom,
18
18
  loadBrokerGrammar,
19
19
  parseBrokerGrammarStem,
20
- } from "./broker.grammars.js";
21
- export type { BrokerGrammarEntry, BrokerLocale, BrokerUserAgent } from "./broker.grammars.js";
22
- export type { BrokerContext, BrokerProviderInfo, BrokerProviderTransport } from "./broker.context.js";
20
+ } from "./broker.grammars";
21
+ export type { IBrokerGrammarEntry, BrokerGrammarEntry, BrokerLocale, BrokerUserAgent } from "./broker.grammars";
22
+ export type { IBrokerContext, IBrokerProviderInfo, BrokerContext, BrokerProviderInfo, BrokerProviderTransport } from "./broker.context";
package/src/config.ts CHANGED
@@ -1,5 +1,35 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
+ import type { IAuthorizationPolicyConfig } from "./authorization/index";
4
+
5
+ export interface IBrokerAuthConfig extends IAuthorizationPolicyConfig {
6
+ /** Master switch. Absent/`false` keeps the broker unauthenticated. */
7
+ enabled?: boolean;
8
+ /** Public origin the broker is reached at (e.g. `https://mcp.example.com`). */
9
+ publicBaseUrl?: string;
10
+ /** Authorization server issuer URL(s) advertised in the metadata. */
11
+ authorizationServers?: string[];
12
+ /** URL of the authorization server's JWKS document. */
13
+ jwks?: string;
14
+ /** Expected token issuer. Defaults to the sole `authorizationServers` entry. */
15
+ issuer?: string;
16
+ /** Scopes advertised in the metadata `scopes_supported`. */
17
+ scopesSupported?: string[];
18
+ /** Baseline scope(s) required to reach any slot. */
19
+ requiredScopes?: string[];
20
+ /** Per-slot required-scope overrides (e.g. an admin scope for `_broker`). */
21
+ perSlotScopes?: Record<string, string[]>;
22
+ /**
23
+ * Per-provider scope requirements for the `_all` aggregate. Deprecated in
24
+ * favor of hierarchical policy assignments.
25
+ */
26
+ providerScopes?: Record<string, string[]>;
27
+ /**
28
+ * Shared secret every provider must present to occupy a slot. Independent
29
+ * of client authorization.
30
+ */
31
+ providerSecret?: string;
32
+ }
3
33
 
4
34
  /**
5
35
  * Shape of the optional JSON config file consumed by `bin.ts` at startup.
@@ -18,7 +48,7 @@ import { dirname, resolve } from "node:path";
18
48
  * }
19
49
  * ```
20
50
  */
21
- export interface BrokerConfig {
51
+ export interface IBrokerConfig {
22
52
  /** TCP port. Maps to `MCP_BROKER_PORT`. */
23
53
  port?: number;
24
54
 
@@ -37,6 +67,40 @@ export interface BrokerConfig {
37
67
  /** Logical broker name reported by `broker_info`. */
38
68
  brokerName?: string;
39
69
 
70
+ /**
71
+ * Browser origins allowed to reach `/<slot>/mcp`.
72
+ *
73
+ * Absent means no browser origin is accepted: a request carrying an
74
+ * `Origin` header gets `403`, one carrying none (Claude Desktop, MCP
75
+ * Inspector, any server-side SDK) always passes. The MCP specification asks
76
+ * for this check, since without it any web page loaded in a browser that
77
+ * can reach the broker could drive it.
78
+ *
79
+ * An array lists origins matched **exactly**. An object supplies a regular
80
+ * expression tested against the whole `Origin` header. Also maps to
81
+ * `MCP_BROKER_ALLOWED_ORIGINS` (comma-separated list form only; a pattern
82
+ * needs the config file).
83
+ *
84
+ * @example
85
+ * ```json
86
+ * { "allowedOrigins": ["https://app.example.com", "http://localhost:5173"] }
87
+ * { "allowedOrigins": { "pattern": "^https://[a-z0-9-]+\\.example\\.com$" } }
88
+ * ```
89
+ */
90
+ allowedOrigins?: string[] | { pattern: string; flags?: string };
91
+
92
+ /**
93
+ * OAuth 2.1 resource-server authorization. When `enabled` is `true`, every
94
+ * HTTP client request to a slot must carry a valid `Authorization: Bearer`
95
+ * token issued for that slot, and the broker publishes Protected Resource
96
+ * Metadata (RFC 9728). Absent/`false` ⇒ no authentication (trusted-network
97
+ * mode, the historical behavior).
98
+ *
99
+ * Scalars also map to env vars (which win): `MCP_BROKER_AUTH_ENABLED`,
100
+ * `MCP_BROKER_PUBLIC_BASE_URL`, `MCP_BROKER_JWKS`, `MCP_BROKER_ISSUER`.
101
+ */
102
+ auth?: IBrokerAuthConfig;
103
+
40
104
  /** URL paths (override the defaults). */
41
105
  paths?: {
42
106
  provider?: string;
@@ -119,8 +183,8 @@ export interface BrokerConfig {
119
183
  * (the directory containing the config file when one was found, otherwise
120
184
  * `process.cwd()`).
121
185
  */
122
- export interface LoadedBrokerConfig {
123
- config: BrokerConfig;
186
+ export interface ILoadedBrokerConfig {
187
+ config: IBrokerConfig;
124
188
  baseDir: string;
125
189
  /** Absolute path of the config file that was loaded, or `null` if none. */
126
190
  sourcePath: string | null;
@@ -142,17 +206,17 @@ export const LEGACY_CONFIG_FILENAME = "mcp-broker.config.json";
142
206
  * 1. The `path` argument when provided (explicit override).
143
207
  * 2. The `MCP_BROKER_CONFIG` env var.
144
208
  * 3. `./.mcp-broker/config.json` relative to `process.cwd()`.
145
- * 4. `./mcp-broker.config.json` relative to `process.cwd()` (legacy layout —
209
+ * 4. `./mcp-broker.config.json` relative to `process.cwd()` (legacy layout ,
146
210
  * a deprecation warning is written to stderr).
147
211
  *
148
212
  * When no file is found, returns the built-in empty config with
149
213
  * `baseDir = process.cwd()`. On invalid JSON, logs a warning to stderr and
150
- * returns the same empty config — never throws.
214
+ * returns the same empty config, never throws.
151
215
  *
152
216
  * Paths inside the config file are intended to be resolved against
153
- * {@link LoadedBrokerConfig.baseDir} by the consumer.
217
+ * {@link ILoadedBrokerConfig.baseDir} by the consumer.
154
218
  */
155
- export function loadBrokerConfig(path?: string): LoadedBrokerConfig {
219
+ export function loadBrokerConfig(path?: string): ILoadedBrokerConfig {
156
220
  const cwd = process.cwd();
157
221
  const envPath = process.env["MCP_BROKER_CONFIG"];
158
222
 
@@ -183,10 +247,19 @@ export function loadBrokerConfig(path?: string): LoadedBrokerConfig {
183
247
 
184
248
  try {
185
249
  const raw = readFileSync(sourcePath, "utf-8");
186
- const config = JSON.parse(raw) as BrokerConfig;
250
+ const config = JSON.parse(raw) as IBrokerConfig;
187
251
  return { config, baseDir, sourcePath };
188
252
  } catch (err) {
189
253
  process.stderr.write(`[mcp-broker] Failed to parse config file at ${sourcePath}: ${(err as Error).message}\n`);
190
254
  return { config: {}, baseDir: cwd, sourcePath: null };
191
255
  }
192
256
  }
257
+
258
+ /** @deprecated Use {@link IBrokerAuthConfig}. */
259
+ export type BrokerAuthConfig = IBrokerAuthConfig;
260
+
261
+ /** @deprecated Use {@link IBrokerConfig}. */
262
+ export type BrokerConfig = IBrokerConfig;
263
+
264
+ /** @deprecated Use {@link ILoadedBrokerConfig}. */
265
+ export type LoadedBrokerConfig = ILoadedBrokerConfig;
package/src/index.ts CHANGED
@@ -1,26 +1,127 @@
1
- export { WsTunnel } from "./ws.tunnel.js";
2
- export { WsTunnelBuilder } from "./ws.tunnel.builder.js";
3
- export type { WsTunnelOptions, StaticMount } from "./ws.tunnel.js";
4
- export { StdioUpstream } from "./stdio.upstream.js";
5
- export type { StdioUpstreamConfig } from "./stdio.upstream.js";
6
- export { RemoteUpstream } from "./remote.upstream.js";
7
- export type { RemoteUpstreamConfig } from "./remote.upstream.js";
8
- export type { Upstream } from "./upstream.js";
1
+ export { WsTunnel } from "./ws/ws.tunnel";
2
+ export { WsTunnelBuilder } from "./ws/ws.tunnel.builder";
3
+ export type { AllowedOrigins, IInternalClient, IWsTunnelOptions, IStaticMount, InternalClient, WsTunnelOptions, StaticMount } from "./ws/ws.interfaces";
4
+ export { StdioUpstream } from "./stdio.upstream";
5
+ export type { IStdioUpstreamConfig, StdioUpstreamConfig } from "./stdio.upstream";
6
+ export { RemoteUpstream } from "./remote.upstream";
7
+ export type { IRemoteUpstreamConfig, RemoteUpstreamConfig } from "./remote.upstream";
8
+ export type { IUpstream, Upstream } from "./upstream";
9
9
 
10
- // `.mcpb` bundle loading — verifies + unpacks a bundle into a stdio upstream.
11
- export { loadMcpbBundle } from "./mcpb.loader.js";
12
- export type { McpbBundleConfig } from "./mcpb.loader.js";
13
- export { unzipMcpb } from "./mcpb.unzip.js";
10
+ // `.mcpb` bundle loading, verifies + unpacks a bundle into a stdio upstream.
11
+ export { loadMcpbBundle } from "./mcpb/mcpb.loader";
12
+ export type { IMcpbBundleConfig, McpbBundleConfig } from "./mcpb/mcpb.loader";
13
+ export { unzipMcpb } from "./mcpb/mcpb.unzip";
14
14
 
15
- // Broker introspection — tier 1.
16
- export { BrokerInfoBehavior, BrokerProvidersBehavior, startBrokerServer, BROKER_PROVIDER_NAME } from "./broker/index.js";
17
- export type { StartBrokerServerOptions } from "./broker/index.js";
18
- export { brokerGrammarKey, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerGrammar } from "./broker/index.js";
19
- export type { BrokerContext, BrokerProviderInfo, BrokerProviderTransport, BrokerLocale, BrokerUserAgent } from "./broker/index.js";
15
+ // Broker introspection, tier 1.
16
+ export { BrokerInfoBehavior, BrokerProvidersBehavior, startBrokerServer, BROKER_PROVIDER_NAME } from "./broker/index";
17
+ export type { IStartBrokerServerOptions, StartBrokerServerOptions } from "./broker/index";
18
+ export { brokerGrammarKey, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerGrammar } from "./broker/index";
19
+ export type {
20
+ IBrokerContext,
21
+ IBrokerGrammarEntry,
22
+ IBrokerProviderInfo,
23
+ BrokerContext,
24
+ BrokerGrammarEntry,
25
+ BrokerProviderInfo,
26
+ BrokerProviderTransport,
27
+ BrokerLocale,
28
+ BrokerUserAgent,
29
+ } from "./broker/index";
20
30
 
21
- export { VERSION, PACKAGE_NAME } from "./version.js";
31
+ export { VERSION, PACKAGE_NAME } from "./version";
32
+
33
+ // OAuth 2.1 resource-server authorization.
34
+ export {
35
+ AuthError,
36
+ scopesOf,
37
+ JwtTokenValidator,
38
+ buildResourceMetadata,
39
+ HttpAuthGuard,
40
+ buildJwtAuth,
41
+ normalizeProviderAuthentication,
42
+ providerMayPublish,
43
+ SharedSecretProviderAuthenticator,
44
+ } from "./auth/index";
45
+ export type {
46
+ IAccessTokenClaims,
47
+ ITokenValidator,
48
+ IPrincipal,
49
+ IResolvedAuth,
50
+ IJwtValidatorOptions,
51
+ IProtectedResourceMetadata,
52
+ IJwtAuthOptions,
53
+ IProviderAuthenticator,
54
+ IProviderPrincipal,
55
+ AccessTokenClaims,
56
+ TokenValidator,
57
+ AuthErrorCode,
58
+ Principal,
59
+ ResolvedAuth,
60
+ JwtValidatorOptions,
61
+ ProtectedResourceMetadata,
62
+ JwtAuthOptions,
63
+ ProviderAuthenticator,
64
+ AggregateScopeFilter,
65
+ ProviderAuthenticationResult,
66
+ ProviderAuthenticatorReturn,
67
+ ProviderPrincipal,
68
+ } from "./auth/index";
69
+
70
+ // Hierarchical, domain-neutral authorization.
71
+ export {
72
+ ConfigPolicyEngine,
73
+ ConfiguredCapabilityClassifier,
74
+ DefaultSlotResourceResolver,
75
+ JwtSubjectMapper,
76
+ ResourcePath,
77
+ ResourcePathPattern,
78
+ SubjectMappingError,
79
+ authorizationWithEngine,
80
+ compileAuthorizationPolicy,
81
+ hasAuthorizationPolicies,
82
+ validateCapability,
83
+ } from "./authorization/index";
84
+ export type {
85
+ IAuditContext,
86
+ IAuthorizationAuditConfig,
87
+ IAuthorizationAuditEvent,
88
+ IAuthorizationDecision,
89
+ IAuthorizationPolicyConfig,
90
+ IAuthorizationRequest,
91
+ IAuthorizationSubject,
92
+ ICapabilityClassifier,
93
+ IClassifiedCapability,
94
+ IDenyPolicy,
95
+ IMcpOperation,
96
+ IPolicyAssignment,
97
+ IPolicyAuthorization,
98
+ IPolicyEngine,
99
+ IRoleDefinition,
100
+ ISlotResourceResolver,
101
+ ISubjectMapper,
102
+ ISubjectMappingConfig,
103
+ AuditContext,
104
+ AuthorizationAuditConfig,
105
+ AuthorizationAuditEvent,
106
+ AuthorizationDecision,
107
+ AuthorizationDecisionReason,
108
+ AuthorizationPolicyConfig,
109
+ AuthorizationRequest,
110
+ AuthorizationSubject,
111
+ CapabilityClassifier,
112
+ ClassifiedCapability,
113
+ DenyPolicy,
114
+ McpOperation,
115
+ PolicyAssignment,
116
+ PolicyAuthorization,
117
+ PolicyEngine,
118
+ RoleDefinition,
119
+ SlotResourceResolver,
120
+ SubjectMapper,
121
+ SubjectMappingConfig,
122
+ } from "./authorization/index";
22
123
 
23
124
  // JSON config file used by `bin.ts` at startup. Exported so a programmatic
24
125
  // embedder can re-use the same loader against a custom path.
25
- export { loadBrokerConfig, DEFAULT_CONFIG_FILENAME } from "./config.js";
26
- export type { BrokerConfig } from "./config.js";
126
+ export { loadBrokerConfig, DEFAULT_CONFIG_FILENAME } from "./config";
127
+ export type { IBrokerAuthConfig, IBrokerConfig, ILoadedBrokerConfig, BrokerAuthConfig, BrokerConfig, LoadedBrokerConfig } from "./config";
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Loads a local `.mcpb` bundle into a {@link StdioUpstreamConfig}.
2
+ * Loads a local `.mcpb` bundle into an {@link IStdioUpstreamConfig}.
3
3
  *
4
4
  * A `.mcpb` bundle is a ZIP holding a `manifest.json` whose `server.mcp_config`
5
5
  * describes a stdio MCP server process. The broker:
6
6
  *
7
7
  * 1. verifies a **detached signature** of the `.mcpb` file against a trusted
8
- * public key (PEM) — integrity *and* provenance, using `node:crypto` only;
8
+ * public key (PEM), integrity *and* provenance, using `node:crypto` only;
9
9
  * 2. unpacks the archive;
10
10
  * 3. reads the manifest, expands the `mcp_config` placeholders, and produces a
11
- * `StdioUpstreamConfig` that the existing upstream wiring spawns.
11
+ * `IStdioUpstreamConfig` that the existing upstream wiring spawns.
12
12
  *
13
13
  * The broker stays compatible with the `.mcpb` format without depending on the
14
14
  * `@anthropic-ai/mcpb` package: the bundle's *own* (native PKCS#7) signature is
15
- * never parsed — the detached signature layer is the broker's trust anchor.
15
+ * never parsed: the detached signature layer is the broker's trust anchor.
16
16
  *
17
17
  * Any failure (missing files, bad signature, malformed manifest, missing
18
18
  * `user_config` value) is logged and yields `null`: the bundle is refused and
@@ -22,11 +22,11 @@ import { createPublicKey, verify, X509Certificate, type KeyObject } from "node:c
22
22
  import { existsSync, readFileSync, rmSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { join, resolve, sep } from "node:path";
25
- import { unzipMcpb } from "./mcpb.unzip.js";
26
- import type { StdioUpstreamConfig } from "./stdio.upstream.js";
25
+ import { unzipMcpb } from "./mcpb.unzip";
26
+ import type { IStdioUpstreamConfig } from "../stdio.upstream";
27
27
 
28
28
  /** A `.mcpb` bundle entry from the broker config file. */
29
- export interface McpbBundleConfig {
29
+ export interface IMcpbBundleConfig {
30
30
  /** Provider slot name the bundle is bound to. */
31
31
  name: string;
32
32
  /** Path to the `.mcpb` file. */
@@ -41,7 +41,7 @@ export interface McpbBundleConfig {
41
41
  aggregate?: boolean;
42
42
  }
43
43
 
44
- interface McpConfig {
44
+ interface IMcpConfig {
45
45
  command?: string;
46
46
  args?: string[];
47
47
  env?: Record<string, string>;
@@ -65,7 +65,7 @@ function verifyDetachedSignature(data: Buffer, signature: Buffer, publicKey: Key
65
65
  }
66
66
 
67
67
  /** Resolves a single `${...}` placeholder key, or `undefined` when unknown. */
68
- function resolvePlaceholder(key: string, dirname: string, userConfig: McpbBundleConfig["userConfig"]): string | number | boolean | Array<string | number> | undefined {
68
+ function resolvePlaceholder(key: string, dirname: string, userConfig: IMcpbBundleConfig["userConfig"]): string | number | boolean | Array<string | number> | undefined {
69
69
  if (key === "__dirname") return dirname;
70
70
  if (key === "HOME") return homedir();
71
71
  if (key === "DESKTOP") return join(homedir(), "Desktop");
@@ -82,17 +82,17 @@ function resolvePlaceholder(key: string, dirname: string, userConfig: McpbBundle
82
82
  }
83
83
 
84
84
  /** Expands placeholders in a scalar string (command, env value). */
85
- function expandScalar(input: string, dirname: string, userConfig: McpbBundleConfig["userConfig"]): string {
85
+ function expandScalar(input: string, dirname: string, userConfig: IMcpbBundleConfig["userConfig"]): string {
86
86
  return input.replace(/\$\{([^}]+)\}/g, (match, key: string) => {
87
87
  const value = resolvePlaceholder(key, dirname, userConfig);
88
- if (value === undefined) return match; // unknown placeholder — leave verbatim
88
+ if (value === undefined) return match; // unknown placeholder, leave verbatim
89
89
  if (Array.isArray(value)) throw new Error(`placeholder "\${${key}}" is multi-valued and cannot be used here`);
90
90
  return String(value);
91
91
  });
92
92
  }
93
93
 
94
94
  /** Expands one manifest argument; a standalone multi-valued placeholder spreads. */
95
- function expandArg(arg: string, dirname: string, userConfig: McpbBundleConfig["userConfig"]): string[] {
95
+ function expandArg(arg: string, dirname: string, userConfig: IMcpbBundleConfig["userConfig"]): string[] {
96
96
  const standalone = /^\$\{(user_config\.[^}]+)\}$/.exec(arg);
97
97
  if (standalone) {
98
98
  const value = resolvePlaceholder(standalone[1], dirname, userConfig);
@@ -103,13 +103,13 @@ function expandArg(arg: string, dirname: string, userConfig: McpbBundleConfig["u
103
103
  }
104
104
 
105
105
  /**
106
- * Verifies, unpacks and resolves a `.mcpb` bundle into a `StdioUpstreamConfig`.
106
+ * Verifies, unpacks and resolves a `.mcpb` bundle into an `IStdioUpstreamConfig`.
107
107
  *
108
108
  * @param cfg The bundle entry from the broker config.
109
109
  * @param baseDir Directory the bundle paths are resolved against.
110
110
  * @returns A ready upstream config, or `null` when the bundle is refused.
111
111
  */
112
- export async function loadMcpbBundle(cfg: McpbBundleConfig, baseDir: string): Promise<StdioUpstreamConfig | null> {
112
+ export async function loadMcpbBundle(cfg: IMcpbBundleConfig, baseDir: string): Promise<IStdioUpstreamConfig | null> {
113
113
  const tag = `[mcp-broker] mcpb bundle "${cfg.name}"`;
114
114
  try {
115
115
  const mcpbPath = resolve(baseDir, cfg.path);
@@ -122,7 +122,7 @@ export async function loadMcpbBundle(cfg: McpbBundleConfig, baseDir: string): Pr
122
122
  ["signature", signaturePath],
123
123
  ] as const) {
124
124
  if (!existsSync(file)) {
125
- console.error(`${tag}: ${label} file not found at ${file} — bundle refused.`);
125
+ console.error(`${tag}: ${label} file not found at ${file}, bundle refused.`);
126
126
  return null;
127
127
  }
128
128
  }
@@ -132,7 +132,7 @@ export async function loadMcpbBundle(cfg: McpbBundleConfig, baseDir: string): Pr
132
132
  const signatureBytes = readFileSync(signaturePath);
133
133
  const publicKey = loadPublicKey(readFileSync(publicKeyPath, "utf8"));
134
134
  if (!verifyDetachedSignature(bundleBytes, signatureBytes, publicKey)) {
135
- console.error(`${tag}: detached signature is invalid for the configured public key — bundle refused.`);
135
+ console.error(`${tag}: detached signature is invalid for the configured public key, bundle refused.`);
136
136
  return null;
137
137
  }
138
138
 
@@ -144,13 +144,13 @@ export async function loadMcpbBundle(cfg: McpbBundleConfig, baseDir: string): Pr
144
144
  // ── Manifest → mcp_config ───────────────────────────────────────────
145
145
  const manifestPath = join(outputDir, "manifest.json");
146
146
  if (!existsSync(manifestPath)) {
147
- console.error(`${tag}: manifest.json missing inside the bundle — bundle refused.`);
147
+ console.error(`${tag}: manifest.json missing inside the bundle, bundle refused.`);
148
148
  return null;
149
149
  }
150
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { server?: { mcp_config?: McpConfig } };
150
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as { server?: { mcp_config?: IMcpConfig } };
151
151
  const mcpConfig = manifest.server?.mcp_config;
152
152
  if (!mcpConfig) {
153
- console.error(`${tag}: manifest has no server.mcp_config — bundle refused.`);
153
+ console.error(`${tag}: manifest has no server.mcp_config, bundle refused.`);
154
154
  return null;
155
155
  }
156
156
 
@@ -160,7 +160,7 @@ export async function loadMcpbBundle(cfg: McpbBundleConfig, baseDir: string): Pr
160
160
  const rawArgs = override?.args ?? mcpConfig.args ?? [];
161
161
  const rawEnv = { ...mcpConfig.env, ...override?.env };
162
162
  if (!command) {
163
- console.error(`${tag}: manifest mcp_config has no command — bundle refused.`);
163
+ console.error(`${tag}: manifest mcp_config has no command, bundle refused.`);
164
164
  return null;
165
165
  }
166
166
 
@@ -180,7 +180,10 @@ export async function loadMcpbBundle(cfg: McpbBundleConfig, baseDir: string): Pr
180
180
  aggregate: cfg.aggregate ?? true,
181
181
  };
182
182
  } catch (err) {
183
- console.error(`${tag}: ${(err as Error).message} — bundle refused.`);
183
+ console.error(`${tag}: ${(err as Error).message}, bundle refused.`);
184
184
  return null;
185
185
  }
186
186
  }
187
+
188
+ /** @deprecated Use {@link IMcpbBundleConfig}. */
189
+ export type McpbBundleConfig = IMcpbBundleConfig;
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * A `.mcpb` bundle is an ordinary ZIP archive. The broker deliberately avoids
5
5
  * the `@anthropic-ai/mcpb` package so it stays compatible with the format
6
- * without being coupled to Anthropic's tooling — hence this small reader.
6
+ * without being coupled to Anthropic's tooling, hence this small reader.
7
7
  *
8
8
  * Supports the two compression methods used in practice: stored (0) and
9
9
  * deflate (8). ZIP64 archives are rejected with a clear error (`.mcpb` bundles
10
10
  * are small and never need it). Any trailing bytes after the End Of Central
11
- * Directory record — e.g. a native PKCS#7 signature block — are ignored, since
11
+ * Directory record (e.g. a native PKCS#7 signature block) are ignored, since
12
12
  * extraction is driven entirely by the central directory.
13
13
  */
14
14
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";