@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
package/dist/index.js CHANGED
@@ -1,15 +1,3 @@
1
- export { WsTunnel } from "./ws.tunnel.js";
2
- export { WsTunnelBuilder } from "./ws.tunnel.builder.js";
3
- export { StdioUpstream } from "./stdio.upstream.js";
4
- export { RemoteUpstream } from "./remote.upstream.js";
5
- // `.mcpb` bundle loading — verifies + unpacks a bundle into a stdio upstream.
6
- export { loadMcpbBundle } from "./mcpb.loader.js";
7
- export { unzipMcpb } from "./mcpb.unzip.js";
8
- // Broker introspection — tier 1.
9
- export { BrokerInfoBehavior, BrokerProvidersBehavior, startBrokerServer, BROKER_PROVIDER_NAME } from "./broker/index.js";
10
- export { brokerGrammarKey, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerGrammar } from "./broker/index.js";
11
- export { VERSION, PACKAGE_NAME } from "./version.js";
12
- // JSON config file used by `bin.ts` at startup. Exported so a programmatic
13
- // embedder can re-use the same loader against a custom path.
14
- export { loadBrokerConfig, DEFAULT_CONFIG_FILENAME } from "./config.js";
1
+ export { AuthError, BROKER_PROVIDER_NAME, BrokerInfoBehavior, BrokerProvidersBehavior, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, HttpAuthGuard, JwtSubjectMapper, JwtTokenValidator, PACKAGE_NAME, RemoteUpstream, ResourcePath, ResourcePathPattern, SharedSecretProviderAuthenticator, StdioUpstream, SubjectMappingError, VERSION, WsTunnel, WsTunnelBuilder, authorizationWithEngine, brokerGrammarKey, buildJwtAuth, buildResourceMetadata, compileAuthorizationPolicy, hasAuthorizationPolicies, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerConfig, loadBrokerGrammar, loadMcpbBundle, normalizeProviderAuthentication, providerMayPublish, scopesOf, startBrokerServer, unzipMcpb, validateCapability } from './chunk-FTDKH2C4.js';
2
+ //# sourceMappingURL=index.js.map
15
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAItD,8EAA8E;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,iCAAiC;AACjC,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzH,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7H,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,2EAA2E;AAC3E,6DAA6D;AAC7D,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanmycelium/mcp-broker",
3
- "version": "0.4.0",
3
+ "version": "1.2.0",
4
4
  "description": "WebSocket-based Model Context Protocol broker. Aggregates multiple MCP providers behind a single endpoint with stdio, SSE, and Streamable HTTP client transports.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -8,7 +8,7 @@
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/pandaGaume/mcp-broker.git",
11
- "directory": "node"
11
+ "directory": "node/packages/broker"
12
12
  },
13
13
  "homepage": "https://github.com/pandaGaume/mcp-broker#readme",
14
14
  "bugs": {
@@ -36,6 +36,7 @@
36
36
  "dist",
37
37
  "src",
38
38
  "scripts",
39
+ "web",
39
40
  ".mcp-broker.example",
40
41
  "LICENSE",
41
42
  "README.md"
@@ -48,15 +49,17 @@
48
49
  }
49
50
  },
50
51
  "scripts": {
51
- "build": "npm run clean && npm run compile && npm run copy:assets",
52
- "clean": "rimraf dist tsconfig.build.tsbuildinfo",
53
- "compile": "tsc -b tsconfig.build.json",
52
+ "build": "tsup && npm run copy:assets",
53
+ "clean": "rimraf dist",
54
54
  "copy:assets": "node scripts/copy-assets.mjs",
55
- "watch": "tsc -b tsconfig.build.json -w",
55
+ "watch": "tsup --watch",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
57
  "start": "node dist/bin.js",
57
58
  "pack:mcpb": "npm run build && node scripts/pack-mcpb.mjs",
58
59
  "test": "vitest run",
59
60
  "test:watch": "vitest",
61
+ "demo:oauth": "npm run build && node web/demos/oauth-lab/server/start.mjs",
62
+ "demo:oauth:smoke": "node web/demos/oauth-lab/server/smoke-test.mjs",
60
63
  "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"",
61
64
  "lint:fix": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" --fix",
62
65
  "format": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
@@ -65,11 +68,13 @@
65
68
  "prepublishOnly": "npm run lint && npm run build && npm test"
66
69
  },
67
70
  "dependencies": {
68
- "@cyanmycelium/mcp-core": "^0.3.0",
71
+ "@cyanmycelium/mcp-core": "^0.7.0",
72
+ "jose": "^6.2.3",
69
73
  "open": "^11.0.0",
70
74
  "ws": "^8.18.0"
71
75
  },
72
76
  "devDependencies": {
77
+ "@cyanmycelium/mcp-broker-provider": "^0.1.0",
73
78
  "@types/node": "^20.11.0",
74
79
  "@types/ws": "^8.5.0",
75
80
  "@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -81,7 +86,8 @@
81
86
  "rimraf": "~6.0.1",
82
87
  "selfsigned": "^2.4.1",
83
88
  "tslib": "^2.8.1",
89
+ "tsup": "^8.5.1",
84
90
  "typescript": "^5.4.0",
85
- "vitest": "^1.6.0"
91
+ "vitest": "^4.1.9"
86
92
  }
87
93
  }
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Copies non-TypeScript assets from `src/` to `dist/`.
3
3
  *
4
- * tsc does not copy non-`.ts` files (e.g. `.json`, `.proto`, `.txt`) to the
5
- * output directory. This script walks the known asset directories and mirrors
6
- * them under `dist/`, preserving their relative layout so runtime
7
- * `fileURLToPath(import.meta.url)`-based resolution keeps working.
4
+ * A bundler emits no `.json`, and it also **flattens the module tree**: code
5
+ * that lived in `src/broker/` ends up in a bundle at the root of `dist/`. Any
6
+ * asset resolved at runtime from `fileURLToPath(import.meta.url)` must therefore
7
+ * be copied next to the bundle, not to a path mirroring the source layout.
8
+ *
9
+ * That is why each entry below is an explicit `from → to` pair rather than a
10
+ * single relative path: the two sides genuinely differ, and assuming otherwise
11
+ * builds a `dist` that passes every test (they run against `src`) and fails
12
+ * the moment the published artifact is executed.
8
13
  *
9
14
  * Usage (called from npm scripts):
10
15
  * node scripts/copy-assets.mjs
@@ -18,17 +23,18 @@ const here = dirname(fileURLToPath(import.meta.url));
18
23
  const root = join(here, "..");
19
24
 
20
25
  const assets = [
21
- // Broker grammar resources (loaded at runtime by broker.grammars.ts)
22
- "broker/grammars",
26
+ // Broker grammars, read at runtime by broker.grammars.ts relative to its own
27
+ // module. Bundled, that module sits at the root of `dist`.
28
+ { from: "broker/grammars", to: "grammars" },
23
29
  ];
24
30
 
25
- for (const rel of assets) {
26
- const src = join(root, "src", rel);
27
- const dst = join(root, "dist", rel);
31
+ for (const { from, to } of assets) {
32
+ const src = join(root, "src", from);
33
+ const dst = join(root, "dist", to);
28
34
  if (!existsSync(src)) {
29
35
  console.warn(`[copy-assets] skipping missing source: ${src}`);
30
36
  continue;
31
37
  }
32
38
  cpSync(src, dst, { recursive: true });
33
- console.log(`[copy-assets] copied ${rel}`);
39
+ console.log(`[copy-assets] copied ${from} -> dist/${to}`);
34
40
  }
@@ -2,12 +2,12 @@
2
2
  * Generates a self-signed TLS certificate for local development.
3
3
  *
4
4
  * Usage (from the `node/` directory):
5
- * npm run gen-cert — writes to ../certs/ (repo root)
6
- * npm run gen-cert -- --out <dir> — writes to a custom directory
5
+ * npm run gen-cert , writes to ../certs/ (repo root)
6
+ * npm run gen-cert -- --out <dir> , writes to a custom directory
7
7
  *
8
8
  * Outputs:
9
- * <outDir>/cert.pem — TLS certificate (pass as MCP_BROKER_TLS_CERT)
10
- * <outDir>/key.pem — Private key (pass as MCP_BROKER_TLS_KEY)
9
+ * <outDir>/cert.pem , TLS certificate (pass as MCP_BROKER_TLS_CERT)
10
+ * <outDir>/key.pem , Private key (pass as MCP_BROKER_TLS_KEY)
11
11
  *
12
12
  * The certificate covers: localhost, 127.0.0.1, ::1
13
13
  * Validity: 365 days | Key: RSA 2048-bit
@@ -20,7 +20,7 @@ import { mkdirSync, writeFileSync } from "fs";
20
20
  import { resolve, join } from "path";
21
21
  import { createRequire } from "module";
22
22
 
23
- // selfsigned is a CJS module — use createRequire for a clean import from ESM.
23
+ // selfsigned is a CJS module, use createRequire for a clean import from ESM.
24
24
  const require = createRequire(import.meta.url);
25
25
  const selfsigned = require("selfsigned");
26
26
 
@@ -12,7 +12,7 @@
12
12
  * ├── node_modules/ ← npm install --omit=dev (the host does not install)
13
13
  * └── dist/ ← compiled broker
14
14
  *
15
- * Requires `dist/` to exist — run `npm run build` first (the `pack:mcpb`
15
+ * Requires `dist/` to exist, run `npm run build` first (the `pack:mcpb`
16
16
  * npm script chains it).
17
17
  *
18
18
  * Usage (from node/):
@@ -35,13 +35,13 @@ function run(command, args, cwd) {
35
35
  }
36
36
  }
37
37
 
38
- // ── Read package metadata — single source of truth for version + deps ───────
38
+ // ── Read package metadata, single source of truth for version + deps ───────
39
39
  const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
40
40
  const { version, dependencies } = pkg;
41
41
 
42
42
  // ── Preconditions ───────────────────────────────────────────────────────────
43
43
  if (!existsSync(join(root, "dist", "bin.js"))) {
44
- console.error("[pack-mcpb] dist/bin.js not found — run `npm run build` first.");
44
+ console.error("[pack-mcpb] dist/bin.js not found, run `npm run build` first.");
45
45
  process.exit(1);
46
46
  }
47
47
 
@@ -55,7 +55,7 @@ mkdirSync(stage, { recursive: true });
55
55
  cpSync(join(root, "dist"), join(stage, "dist"), { recursive: true });
56
56
  console.log("[pack-mcpb] copied dist/");
57
57
 
58
- // ── Production package.json — drives the bundled npm install ────────────────
58
+ // ── Production package.json, drives the bundled npm install ────────────────
59
59
  // type=module is required so Node treats the compiled .js as ESM.
60
60
  writeFileSync(
61
61
  join(stage, "package.json"),
@@ -81,4 +81,4 @@ console.log("[pack-mcpb] running mcpb pack …");
81
81
  run("npx", ["--yes", "@anthropic-ai/mcpb@2", "pack", stage, output]);
82
82
 
83
83
  const sizeMb = (statSync(output).size / 1024 / 1024).toFixed(2);
84
- console.log(`[pack-mcpb] done — ${output} (${sizeMb} MB)`);
84
+ console.log(`[pack-mcpb] done, ${output} (${sizeMb} MB)`);
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Signs `.mcpb` bundles for the broker's bundle loader — built-ins only.
2
+ * Signs `.mcpb` bundles for the broker's bundle loader, built-ins only.
3
3
  *
4
4
  * The broker verifies a **detached signature** of a `.mcpb` file against a
5
5
  * trusted public key (see `src/mcpb.loader.ts`). This script produces that
6
- * signature using `node:crypto` only — no dependency on OpenSSL or the
6
+ * signature using `node:crypto` only: no dependency on OpenSSL or the
7
7
  * `@anthropic-ai/mcpb` CLI.
8
8
  *
9
9
  * Usage (from the `node/` directory):
10
10
  *
11
11
  * node scripts/sign-bundle.mjs keygen [outDir]
12
12
  * Generates an Ed25519 key pair:
13
- * <outDir>/mcpb-signing.key.pem — private key (keep secret)
14
- * <outDir>/mcpb-signing.pub.pem — public key (point `publicKey` at this)
13
+ * <outDir>/mcpb-signing.key.pem , private key (keep secret)
14
+ * <outDir>/mcpb-signing.pub.pem , public key (point `publicKey` at this)
15
15
  * outDir defaults to the current directory.
16
16
  *
17
17
  * node scripts/sign-bundle.mjs sign <bundle.mcpb> <privateKey.pem> [signaturePath]
18
18
  * Writes the detached signature. signaturePath defaults to
19
- * `<bundle.mcpb>.sig` — the path the broker looks for by default.
19
+ * `<bundle.mcpb>.sig`: the path the broker looks for by default.
20
20
  */
21
21
  import { generateKeyPairSync, createPrivateKey, sign } from "node:crypto";
22
22
  import { readFileSync, writeFileSync } from "node:fs";
@@ -57,5 +57,5 @@ if (command === "keygen") {
57
57
  writeFileSync(sigPath, signature);
58
58
  console.log(`[sign-bundle] signature (${keyType}) → ${sigPath}`);
59
59
  } else {
60
- fail('unknown command — use "keygen" or "sign". See the file header for usage.');
60
+ fail('unknown command, use "keygen" or "sign". See the file header for usage.');
61
61
  }
@@ -0,0 +1,98 @@
1
+ import { JwtTokenValidator } from "./jwt.validator";
2
+ import type { AggregateScopeFilter, IResolvedAuth } from "./auth.types";
3
+ import { compileAuthorizationPolicy, DefaultSlotResourceResolver, hasAuthorizationPolicies, type IAuthorizationPolicyConfig } from "../authorization/index";
4
+
5
+ /**
6
+ * High-level options for the default JWT/JWKS resource-server setup. Mirrors the
7
+ * `auth` block of the broker JSON config and is turned into a fully
8
+ * {@link IResolvedAuth} (with a {@link JwtTokenValidator}) by {@link buildJwtAuth}.
9
+ */
10
+ export interface IJwtAuthOptions extends IAuthorizationPolicyConfig {
11
+ /** Public origin the broker is reached at (e.g. `https://mcp.example.com`). */
12
+ publicBaseUrl: string;
13
+ /** Authorization server issuer URL(s) advertised in the PRM. At least one. */
14
+ authorizationServers: string[];
15
+ /** URL of the authorization server's JWKS document. */
16
+ jwksUri: string;
17
+ /** Expected token issuer(s). Defaults to the sole authorization server. */
18
+ issuer?: string | string[];
19
+ /** Scopes advertised in the PRM `scopes_supported`. */
20
+ scopesSupported?: string[];
21
+ /** Baseline scope(s) required to reach any slot. */
22
+ requiredScopes?: string[];
23
+ /** Per-slot required-scope overrides (e.g. an admin scope for `_broker`). */
24
+ perSlotScopes?: Record<string, string[]>;
25
+ /**
26
+ * Per-provider scope requirements for the `_all` aggregate. A caller sees a
27
+ * provider in `_all` only if it holds at least one of the listed scopes.
28
+ * Providers not listed here stay visible to every authenticated caller.
29
+ * Turned into an {@link AggregateScopeFilter} automatically.
30
+ */
31
+ providerScopes?: Record<string, string[]>;
32
+ /** Leeway in seconds for token `exp`/`nbf` checks. */
33
+ clockToleranceSec?: number;
34
+ }
35
+
36
+ /** Builds the default aggregate filter from a per-provider scope map. */
37
+ function makeProviderScopeFilter(providerScopes: Record<string, string[]>): AggregateScopeFilter {
38
+ return (principal, providerName) => {
39
+ const required = providerScopes[providerName];
40
+ if (!required || required.length === 0) return true; // unlisted ⇒ visible to all
41
+ return required.some((scope) => principal.scopes.has(scope));
42
+ };
43
+ }
44
+
45
+ /** Removes a single trailing slash so resource URIs concatenate cleanly. */
46
+ function stripTrailingSlash(url: string): string {
47
+ return url.endsWith("/") ? url.slice(0, -1) : url;
48
+ }
49
+
50
+ /**
51
+ * Builds an {@link IResolvedAuth} backed by a {@link JwtTokenValidator}. Validates
52
+ * the required inputs up front and throws a descriptive error on misconfig, so
53
+ * an operator sees the problem at boot rather than as opaque `401`s later.
54
+ */
55
+ export function buildJwtAuth(options: IJwtAuthOptions): IResolvedAuth {
56
+ if (!options.publicBaseUrl) {
57
+ throw new Error("auth: publicBaseUrl is required.");
58
+ }
59
+ if (!options.authorizationServers || options.authorizationServers.length === 0) {
60
+ throw new Error("auth: at least one authorizationServers entry is required.");
61
+ }
62
+ if (!options.jwksUri) {
63
+ throw new Error("auth: jwksUri is required.");
64
+ }
65
+
66
+ const publicBaseUrl = stripTrailingSlash(options.publicBaseUrl);
67
+ const issuer = options.issuer ?? (options.authorizationServers.length === 1 ? options.authorizationServers[0] : undefined);
68
+
69
+ const validator = new JwtTokenValidator({
70
+ jwksUri: options.jwksUri,
71
+ issuer,
72
+ clockToleranceSec: options.clockToleranceSec,
73
+ });
74
+
75
+ const resolved: IResolvedAuth = {
76
+ publicBaseUrl,
77
+ authorizationServers: options.authorizationServers,
78
+ validator,
79
+ };
80
+ if (options.scopesSupported) resolved.scopesSupported = options.scopesSupported;
81
+ if (options.requiredScopes) resolved.requiredScopes = options.requiredScopes;
82
+ if (options.perSlotScopes) resolved.perSlotScopes = options.perSlotScopes;
83
+ if (options.providerScopes && Object.keys(options.providerScopes).length > 0) {
84
+ console.warn("[mcp-broker] auth.providerScopes is deprecated; migrate to roles and hierarchical assignments.");
85
+ resolved.aggregateScopeFilter = makeProviderScopeFilter(options.providerScopes);
86
+ }
87
+ if (options.slotResources && Object.keys(options.slotResources).length > 0) {
88
+ resolved.slotResourceResolver = new DefaultSlotResourceResolver(options.slotResources);
89
+ }
90
+ if (hasAuthorizationPolicies(options)) {
91
+ resolved.authorization = compileAuthorizationPolicy(options);
92
+ resolved.slotResourceResolver = resolved.authorization.slotResourceResolver;
93
+ }
94
+ return resolved;
95
+ }
96
+
97
+ /** @deprecated Use {@link IJwtAuthOptions}. */
98
+ export type JwtAuthOptions = IJwtAuthOptions;
@@ -0,0 +1,120 @@
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
+ * Everything the MCP specification itself defines comes from `mcp-core` and is
11
+ * re-exported here under the broker's historical names: the claim shape, the
12
+ * validator seam, the error carrying a challenge. What stays broker-owned is
13
+ * what the spec does not define, namely a multi-slot resource server whose
14
+ * principals feed a hierarchical policy engine.
15
+ */
16
+ import { McpAuthError, scopesOf as mcpScopesOf, type IAccessTokenClaims, type IMcpPrincipal, type ITokenValidator } from "@cyanmycelium/mcp-core";
17
+ import type { IAuthorizationSubject, IPolicyAuthorization, ISlotResourceResolver } from "../authorization/index";
18
+
19
+ export type { IAccessTokenClaims, ITokenValidator } from "@cyanmycelium/mcp-core";
20
+
21
+ /** OAuth 2.1 error codes the broker emits in `WWW-Authenticate` challenges. */
22
+ export type AuthErrorCode = import("@cyanmycelium/mcp-core").McpAuthErrorCode;
23
+
24
+ /**
25
+ * A thrown authorization failure carrying the HTTP status and OAuth error code
26
+ * the enforcement point should surface. `scope` is set for `insufficient_scope`
27
+ * challenges to advertise the scope(s) the resource requires.
28
+ *
29
+ * The broker's own name for `mcp-core`'s `McpAuthError`, kept so `instanceof`
30
+ * checks and the published API survive the consolidation. There is one class at
31
+ * runtime, so an error thrown by `mcp-core` is caught by the broker and the
32
+ * other way round.
33
+ */
34
+ export const AuthError = McpAuthError;
35
+ export type AuthError = McpAuthError;
36
+
37
+ /**
38
+ * The authenticated caller, produced once a token passes validation and the
39
+ * required scopes are satisfied. Threaded through the request so downstream
40
+ * components (e.g. the `_all` aggregate) can make scope-aware decisions.
41
+ *
42
+ * Extends `mcp-core`'s principal with the subject the policy engine reasons
43
+ * about, which the specification says nothing about and which therefore stays
44
+ * here.
45
+ */
46
+ export interface IPrincipal extends IMcpPrincipal {
47
+ readonly claims: IAccessTokenClaims;
48
+ readonly scopes: ReadonlySet<string>;
49
+ /** Subjects derived exclusively from validated token claims. */
50
+ readonly subject?: IAuthorizationSubject;
51
+ }
52
+
53
+ /**
54
+ * Decides, per authenticated caller, whether a given provider is visible in the
55
+ * `_all` aggregate. This is the content-confidentiality enforcement point: a
56
+ * client only sees (and can call) tools/prompts from providers it is authorized
57
+ * for. Return `true` to include the provider for this principal.
58
+ */
59
+ export type AggregateScopeFilter = (principal: IPrincipal, providerName: string) => boolean;
60
+
61
+ /**
62
+ * A fully resolved authorization configuration, ready for the enforcement
63
+ * layer. Built either from JSON config (via `buildJwtAuth`) or supplied
64
+ * directly by an embedder with a custom {@link ITokenValidator}.
65
+ */
66
+ export interface IResolvedAuth {
67
+ /**
68
+ * Public origin the broker is reached at, used to build canonical resource
69
+ * URIs and metadata URLs. No trailing slash (e.g. `https://mcp.example.com`).
70
+ */
71
+ publicBaseUrl: string;
72
+ /** One or more authorization server issuer URLs, advertised in the PRM. */
73
+ authorizationServers: string[];
74
+ /** Optional list of scopes advertised in the PRM `scopes_supported`. */
75
+ scopesSupported?: string[];
76
+ /** The token validator (JWKS-backed by default). */
77
+ validator: ITokenValidator;
78
+ /** Baseline scope(s) required to reach any slot. Empty ⇒ any valid token. */
79
+ requiredScopes?: string[];
80
+ /** Per-slot required-scope overrides (e.g. an admin scope for `_broker`). */
81
+ perSlotScopes?: Record<string, string[]>;
82
+ /**
83
+ * Per-caller filter for the `_all` aggregate. When set, a client's view of
84
+ * `_all` is narrowed to the providers this returns `true` for. When absent,
85
+ * every authenticated caller sees the full aggregate.
86
+ */
87
+ aggregateScopeFilter?: AggregateScopeFilter;
88
+ /** Compiled hierarchical policy runtime, absent for legacy OAuth behavior. */
89
+ authorization?: IPolicyAuthorization;
90
+ /** Optional resolver usable for provider namespace checks without policies. */
91
+ slotResourceResolver?: ISlotResourceResolver;
92
+ }
93
+
94
+ /**
95
+ * Extracts the effective set of granted scopes from token claims.
96
+ *
97
+ * A deliberate superset of `mcp-core`'s: OAuth 2.1 only defines the
98
+ * space-delimited `scope` string, which is what the spec-level helper reads,
99
+ * but several authorization servers emit an array-form `scopes` claim instead.
100
+ * Dropping that here would silently strip every scope for those deployments, so
101
+ * the two are unioned. The `scope` half is parsed by `mcp-core` rather than
102
+ * re-implemented.
103
+ */
104
+ export function scopesOf(claims: IAccessTokenClaims): Set<string> {
105
+ const set = new Set<string>(mcpScopesOf(claims));
106
+ const arrayForm: unknown = claims["scopes"];
107
+ if (Array.isArray(arrayForm)) {
108
+ for (const scope of arrayForm) if (typeof scope === "string" && scope) set.add(scope);
109
+ }
110
+ return set;
111
+ }
112
+
113
+ /** @deprecated Use {@link IAccessTokenClaims}. */
114
+ export type AccessTokenClaims = IAccessTokenClaims;
115
+ /** @deprecated Use {@link ITokenValidator}. */
116
+ export type TokenValidator = ITokenValidator;
117
+ /** @deprecated Use {@link IPrincipal}. */
118
+ export type Principal = IPrincipal;
119
+ /** @deprecated Use {@link IResolvedAuth}. */
120
+ export type ResolvedAuth = IResolvedAuth;
@@ -0,0 +1,128 @@
1
+ import type { IncomingMessage, ServerResponse } from "http";
2
+ import { bearerToken, buildChallengeHeader, PROTECTED_RESOURCE_METADATA_PREFIX } from "@cyanmycelium/mcp-core";
3
+ import { AuthError, scopesOf, type IPrincipal, type IResolvedAuth } from "./auth.types";
4
+ import { buildResourceMetadata, type IProtectedResourceMetadata } from "./resource.metadata";
5
+ import { SubjectMappingError } from "../authorization/index";
6
+
7
+ /**
8
+ * Well-known prefix under which per-slot Protected Resource Metadata is served.
9
+ *
10
+ * Carries a trailing slash because every use here splits a slot off it, while
11
+ * `mcp-core` exports the bare RFC 9728 prefix.
12
+ */
13
+ const PRM_PREFIX = `${PROTECTED_RESOURCE_METADATA_PREFIX}/`;
14
+
15
+ /**
16
+ * The HTTP enforcement point for the resource-server layer. Wraps a
17
+ * {@link IResolvedAuth} plus the configured `/mcp` path suffix and turns it into
18
+ * the three operations the transport needs: serve Protected Resource Metadata,
19
+ * authorize a request, and write an RFC 9728 `401`/`403` challenge.
20
+ *
21
+ * A slot's **canonical resource URI** is `<publicBaseUrl>/<slot>/<mcp>` and is
22
+ * used uniformly across all of that slot's HTTP endpoints (`/mcp`, `/sse`,
23
+ * `/messages`) so a single token audience covers the whole slot.
24
+ */
25
+ export class HttpAuthGuard {
26
+ private readonly _auth: IResolvedAuth;
27
+ /** The `/mcp` suffix without a leading slash, e.g. `"mcp"`. */
28
+ private readonly _mcpSuffix: string;
29
+
30
+ constructor(auth: IResolvedAuth, mcpSuffix: string) {
31
+ this._auth = auth;
32
+ this._mcpSuffix = mcpSuffix.replace(/^\//, "");
33
+ }
34
+
35
+ /** Canonical resource identifier for a slot (RFC 8707 §2). */
36
+ resourceFor(slot: string): string {
37
+ return `${this._auth.publicBaseUrl}/${encodeURIComponent(slot)}/${this._mcpSuffix}`;
38
+ }
39
+
40
+ /** RFC 9728 metadata URL for a slot, advertised in the `401` challenge. */
41
+ metadataUrlFor(slot: string): string {
42
+ return `${this._auth.publicBaseUrl}${PRM_PREFIX}${encodeURIComponent(slot)}/${this._mcpSuffix}`;
43
+ }
44
+
45
+ /** Required scope(s) for a slot: per-slot override, else the baseline. */
46
+ requiredScopesFor(slot: string): string[] {
47
+ return this._auth.perSlotScopes?.[slot] ?? this._auth.requiredScopes ?? [];
48
+ }
49
+
50
+ /** The RFC 9728 metadata document for a slot. */
51
+ metadataFor(slot: string): IProtectedResourceMetadata {
52
+ return buildResourceMetadata({
53
+ resource: this.resourceFor(slot),
54
+ authorizationServers: this._auth.authorizationServers,
55
+ scopesSupported: this._auth.scopesSupported,
56
+ });
57
+ }
58
+
59
+ /**
60
+ * If `rawUrl` is a Protected Resource Metadata request
61
+ * (`/.well-known/oauth-protected-resource/<slot>/<mcp>`), returns the slot;
62
+ * otherwise `null`.
63
+ */
64
+ matchMetadataRequest(rawUrl: string): string | null {
65
+ if (!rawUrl.startsWith(PRM_PREFIX)) return null;
66
+ const parts = rawUrl.slice(PRM_PREFIX.length).split("/").filter(Boolean);
67
+ if (parts.length !== 2 || parts[1] !== this._mcpSuffix) return null;
68
+ return decodeURIComponent(parts[0]);
69
+ }
70
+
71
+ /**
72
+ * Validates the request's bearer token against the slot's canonical resource
73
+ * and enforces the slot's required scopes. Resolves to the {@link IPrincipal}
74
+ * on success; rejects with an {@link AuthError} (`401` missing/invalid token,
75
+ * `403` insufficient scope) otherwise.
76
+ */
77
+ async authorize(req: IncomingMessage, slot: string): Promise<IPrincipal> {
78
+ const token = bearerToken(req.headers["authorization"]);
79
+ if (!token) {
80
+ throw new AuthError(401, "invalid_token", "Missing bearer token");
81
+ }
82
+
83
+ const claims = await this._auth.validator.validate(token, this.resourceFor(slot));
84
+ const scopes = scopesOf(claims);
85
+
86
+ const required = this.requiredScopesFor(slot);
87
+ const missing = required.filter((s) => !scopes.has(s));
88
+ if (missing.length > 0) {
89
+ throw new AuthError(403, "insufficient_scope", `Missing required scope(s): ${missing.join(" ")}`, required.join(" "));
90
+ }
91
+
92
+ let subject: IPrincipal["subject"];
93
+ try {
94
+ subject = this._auth.authorization?.subjectMapper.map(claims);
95
+ } catch (error) {
96
+ if (error instanceof SubjectMappingError) {
97
+ throw new AuthError(403, "insufficient_scope", "Malformed configured JWT subject claim");
98
+ }
99
+ throw error;
100
+ }
101
+
102
+ return subject ? { claims, scopes, subject } : { claims, scopes };
103
+ }
104
+
105
+ /**
106
+ * Builds the RFC 9728 §5.1 `WWW-Authenticate` header value for a challenge.
107
+ * Always points the client at the slot's metadata URL so it can discover the
108
+ * authorization server and retry. Shared by the HTTP and WebSocket paths.
109
+ */
110
+ challengeHeader(slot: string, err: AuthError): string {
111
+ return buildChallengeHeader({
112
+ resourceMetadata: this.metadataUrlFor(slot),
113
+ error: err.code,
114
+ errorDescription: err.description,
115
+ scope: err.scope,
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Writes an RFC 9728 §5.1 `WWW-Authenticate` challenge and the matching
121
+ * status to an HTTP response.
122
+ */
123
+ writeChallenge(res: ServerResponse, slot: string, err: AuthError): void {
124
+ res.setHeader("WWW-Authenticate", this.challengeHeader(slot, err));
125
+ res.writeHead(err.status, { "Content-Type": "application/json; charset=utf-8" });
126
+ res.end(JSON.stringify({ error: err.code, error_description: err.description }));
127
+ }
128
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Barrel for the broker's OAuth 2.1 resource-server layer. See the individual
3
+ * modules for details; the transport wires these up in `ws.tunnel.ts`.
4
+ */
5
+ export { AuthError, scopesOf } from "./auth.types";
6
+ export type {
7
+ IAccessTokenClaims,
8
+ ITokenValidator,
9
+ AuthErrorCode,
10
+ IPrincipal,
11
+ IResolvedAuth,
12
+ AggregateScopeFilter,
13
+ AccessTokenClaims,
14
+ TokenValidator,
15
+ Principal,
16
+ ResolvedAuth,
17
+ } from "./auth.types";
18
+ export { JwtTokenValidator } from "./jwt.validator";
19
+ export type { IJwtValidatorOptions, JwtValidatorOptions } from "./jwt.validator";
20
+ export { buildResourceMetadata } from "./resource.metadata";
21
+ export type { IProtectedResourceMetadata, ProtectedResourceMetadata } from "./resource.metadata";
22
+ export { HttpAuthGuard } from "./http.auth";
23
+ export { buildJwtAuth } from "./auth.config";
24
+ export type { IJwtAuthOptions, JwtAuthOptions } from "./auth.config";
25
+ export { SharedSecretProviderAuthenticator } from "./provider.auth";
26
+ export { normalizeProviderAuthentication, providerMayPublish } from "./provider.auth";
27
+ export type {
28
+ IProviderAuthenticator,
29
+ IProviderPrincipal,
30
+ ProviderAuthenticationResult,
31
+ ProviderAuthenticator,
32
+ ProviderAuthenticatorReturn,
33
+ ProviderPrincipal,
34
+ } from "./provider.auth";