@cyanmycelium/mcp-broker 1.0.1 → 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.
- package/.mcp-broker.example/README.md +3 -3
- package/README.md +6 -2
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +176 -253
- package/dist/bin.js.map +1 -1
- package/dist/chunk-FTDKH2C4.js +3670 -0
- package/dist/chunk-FTDKH2C4.js.map +1 -0
- package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
- package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
- package/dist/index.d.ts +1790 -22
- package/dist/index.js +2 -18
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/scripts/copy-assets.mjs +16 -10
- package/scripts/gen-cert.mjs +5 -5
- package/scripts/pack-mcpb.mjs +5 -5
- package/scripts/sign-bundle.mjs +6 -6
- package/src/auth/auth.config.ts +3 -3
- package/src/auth/auth.types.ts +34 -59
- package/src/auth/http.auth.ts +18 -22
- package/src/auth/index.ts +12 -12
- package/src/auth/jwt.validator.ts +3 -3
- package/src/auth/provider.auth.ts +1 -1
- package/src/auth/resource.metadata.ts +14 -21
- package/src/authorization/audit.ts +2 -2
- package/src/authorization/capability.classifier.ts +1 -1
- package/src/authorization/index.ts +13 -13
- package/src/authorization/policy.engine.ts +3 -3
- package/src/authorization/policy.types.ts +1 -1
- package/src/authorization/runtime.ts +5 -5
- package/src/authorization/slot.resource.ts +1 -1
- package/src/authorization/subject.mapper.ts +1 -1
- package/src/bin.ts +38 -8
- package/src/broker/adapters/broker.adapter.info.ts +1 -1
- package/src/broker/adapters/broker.adapter.providers.ts +1 -1
- package/src/broker/aggregate/aggregate.catalog.ts +1 -1
- package/src/broker/aggregate/aggregate.server.ts +7 -7
- package/src/broker/aggregate/provider.client.session.ts +2 -2
- package/src/broker/behaviors/broker.behavior.info.ts +3 -3
- package/src/broker/behaviors/broker.behavior.providers.ts +5 -5
- package/src/broker/broker.grammars.ts +9 -9
- package/src/broker/broker.server.ts +6 -6
- package/src/broker/grammars/claude/en.json +1 -1
- package/src/broker/grammars/claude/fr.json +1 -1
- package/src/broker/index.ts +9 -9
- package/src/config.ts +25 -3
- package/src/index.ts +24 -24
- package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +11 -11
- package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
- package/src/remote.transports.ts +3 -3
- package/src/remote.upstream.ts +3 -3
- package/src/stdio.upstream.ts +2 -2
- package/src/upstream.ts +2 -2
- package/src/ws/ws.interfaces.ts +357 -0
- package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +28 -5
- package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +231 -467
- package/web/README.md +3 -3
- package/web/broker-self-mcp.html +9 -9
- package/web/css/styles.css +1 -1
- package/web/demos/DemoPlaceholder.html +6 -6
- package/web/demos/broker-explorer/css/app.css +1 -1
- package/web/demos/broker-explorer/index.html +2 -2
- package/web/demos/broker-explorer/js/app.js +4 -4
- package/web/demos/broker-explorer/js/mcp-ws-client.js +2 -2
- package/web/demos/oauth-lab/README.md +26 -0
- package/web/demos/oauth-lab/config.json +1 -0
- package/web/demos/oauth-lab/js/app.js +40 -16
- package/web/demos/oauth-lab/server/smoke-test.mjs +57 -10
- package/web/demos/provider-tunnel/css/app.css +1 -1
- package/web/demos/provider-tunnel/index.html +3 -3
- package/web/demos/provider-tunnel/js/app.js +10 -10
- package/web/demos/provider-tunnel/js/toolbox-server.js +5 -5
- package/web/index.html +11 -11
- package/web/js/lib/broker-tunnel.js +12 -12
- package/dist/auth/auth.config.d.ts +0 -40
- package/dist/auth/auth.config.js +0 -62
- package/dist/auth/auth.config.js.map +0 -1
- package/dist/auth/auth.types.d.ts +0 -119
- package/dist/auth/auth.types.js +0 -38
- package/dist/auth/auth.types.js.map +0 -1
- package/dist/auth/http.auth.d.ts +0 -51
- package/dist/auth/http.auth.js +0 -121
- package/dist/auth/http.auth.js.map +0 -1
- package/dist/auth/index.d.ts +0 -16
- package/dist/auth/index.js +0 -12
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/jwt.validator.d.ts +0 -35
- package/dist/auth/jwt.validator.js +0 -46
- package/dist/auth/jwt.validator.js.map +0 -1
- package/dist/auth/provider.auth.d.ts +0 -51
- package/dist/auth/provider.auth.js +0 -80
- package/dist/auth/provider.auth.js.map +0 -1
- package/dist/auth/resource.metadata.d.ts +0 -32
- package/dist/auth/resource.metadata.js +0 -26
- package/dist/auth/resource.metadata.js.map +0 -1
- package/dist/authorization/audit.d.ts +0 -14
- package/dist/authorization/audit.js +0 -20
- package/dist/authorization/audit.js.map +0 -1
- package/dist/authorization/capability.classifier.d.ts +0 -30
- package/dist/authorization/capability.classifier.js +0 -85
- package/dist/authorization/capability.classifier.js.map +0 -1
- package/dist/authorization/index.d.ts +0 -13
- package/dist/authorization/index.js +0 -8
- package/dist/authorization/index.js.map +0 -1
- package/dist/authorization/policy.engine.d.ts +0 -11
- package/dist/authorization/policy.engine.js +0 -192
- package/dist/authorization/policy.engine.js.map +0 -1
- package/dist/authorization/policy.types.d.ts +0 -92
- package/dist/authorization/policy.types.js +0 -2
- package/dist/authorization/policy.types.js.map +0 -1
- package/dist/authorization/resource.path.d.ts +0 -27
- package/dist/authorization/resource.path.js +0 -122
- package/dist/authorization/resource.path.js.map +0 -1
- package/dist/authorization/runtime.d.ts +0 -22
- package/dist/authorization/runtime.js +0 -34
- package/dist/authorization/runtime.js.map +0 -1
- package/dist/authorization/slot.resource.d.ts +0 -12
- package/dist/authorization/slot.resource.js +0 -41
- package/dist/authorization/slot.resource.js.map +0 -1
- package/dist/authorization/subject.mapper.d.ts +0 -15
- package/dist/authorization/subject.mapper.js +0 -67
- package/dist/authorization/subject.mapper.js.map +0 -1
- package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
- package/dist/broker/adapters/broker.adapter.info.js +0 -43
- package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
- package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
- package/dist/broker/adapters/broker.adapter.providers.js +0 -61
- package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
- package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -66
- package/dist/broker/aggregate/aggregate.catalog.js +0 -119
- package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
- package/dist/broker/aggregate/aggregate.server.d.ts +0 -66
- package/dist/broker/aggregate/aggregate.server.js +0 -257
- package/dist/broker/aggregate/aggregate.server.js.map +0 -1
- package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
- package/dist/broker/aggregate/provider.client.session.js +0 -140
- package/dist/broker/aggregate/provider.client.session.js.map +0 -1
- package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
- package/dist/broker/behaviors/broker.behavior.info.js +0 -41
- package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
- package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
- package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
- package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
- package/dist/broker/broker.context.d.ts +0 -63
- package/dist/broker/broker.context.js +0 -2
- package/dist/broker/broker.context.js.map +0 -1
- package/dist/broker/broker.grammars.d.ts +0 -132
- package/dist/broker/broker.grammars.js +0 -229
- package/dist/broker/broker.grammars.js.map +0 -1
- package/dist/broker/broker.server.d.ts +0 -68
- package/dist/broker/broker.server.js +0 -73
- package/dist/broker/broker.server.js.map +0 -1
- package/dist/broker/index.d.ts +0 -9
- package/dist/broker/index.js +0 -7
- package/dist/broker/index.js.map +0 -1
- package/dist/config.d.ts +0 -182
- package/dist/config.js +0 -61
- package/dist/config.js.map +0 -1
- package/dist/mcpb.loader.d.ts +0 -26
- package/dist/mcpb.loader.js +0 -161
- package/dist/mcpb.loader.js.map +0 -1
- package/dist/mcpb.unzip.d.ts +0 -6
- package/dist/mcpb.unzip.js +0 -95
- package/dist/mcpb.unzip.js.map +0 -1
- package/dist/remote.transports.d.ts +0 -18
- package/dist/remote.transports.js +0 -297
- package/dist/remote.transports.js.map +0 -1
- package/dist/remote.upstream.d.ts +0 -38
- package/dist/remote.upstream.js +0 -52
- package/dist/remote.upstream.js.map +0 -1
- package/dist/stdio.upstream.d.ts +0 -47
- package/dist/stdio.upstream.js +0 -85
- package/dist/stdio.upstream.js.map +0 -1
- package/dist/upstream.d.ts +0 -35
- package/dist/upstream.js +0 -2
- package/dist/upstream.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -9
- package/dist/version.js.map +0 -1
- package/dist/ws.tunnel.builder.d.ts +0 -176
- package/dist/ws.tunnel.builder.js +0 -271
- package/dist/ws.tunnel.builder.js.map +0 -1
- package/dist/ws.tunnel.d.ts +0 -455
- package/dist/ws.tunnel.js +0 -1425
- package/dist/ws.tunnel.js.map +0 -1
- /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
- /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
- /package/dist/{broker/grammars → grammars}/default/zh.json +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IAuthorizationSubject, ISubjectMappingConfig } from "./policy.types
|
|
1
|
+
import type { IAuthorizationSubject, ISubjectMappingConfig } from "./policy.types";
|
|
2
2
|
|
|
3
3
|
export interface ISubjectMapper {
|
|
4
4
|
map(claims: Readonly<Record<string, unknown>>): IAuthorizationSubject;
|
package/src/bin.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - Env-var paths (`MCP_BROKER_*_DIR`, `MCP_BROKER_TLS_*`) are resolved
|
|
17
17
|
* against `process.cwd()`.
|
|
18
18
|
* - Config-file paths (`tls.cert`, `www.mounts[*].dir`, `stdioUpstreams[*]`,
|
|
19
|
-
* `mcpbBundles[*]`) are resolved against the **config file's directory
|
|
19
|
+
* `mcpbBundles[*]`) are resolved against the **config file's directory**: so a config in
|
|
20
20
|
* `./.mcp-broker/config.json` referring to `"certs/cert.pem"` points at
|
|
21
21
|
* `./.mcp-broker/certs/cert.pem`. The folder is self-contained.
|
|
22
22
|
*
|
|
@@ -48,13 +48,15 @@
|
|
|
48
48
|
* | | | provider (Claude Desktop bridge). |
|
|
49
49
|
* | MCP_BROKER_LOCALE | en | Locale used for tool descriptions on the `_broker` |
|
|
50
50
|
* | | | slot. ISO 639-1 base; regional variants accepted. |
|
|
51
|
+
* | MCP_BROKER_ALLOWED_ORIGINS | (none) | Comma-separated browser origins allowed on |
|
|
52
|
+
* | | | `/<slot>/mcp`. None means no browser origin passes. |
|
|
51
53
|
*/
|
|
52
54
|
import * as fs from "fs";
|
|
53
55
|
import * as path from "path";
|
|
54
56
|
import open from "open";
|
|
55
|
-
import { WsTunnelBuilder } from "./index
|
|
56
|
-
import { loadBrokerConfig } from "./config
|
|
57
|
-
import { loadMcpbBundle } from "./mcpb.loader
|
|
57
|
+
import { WsTunnelBuilder } from "./index";
|
|
58
|
+
import { loadBrokerConfig } from "./config";
|
|
59
|
+
import { loadMcpbBundle } from "./mcpb/mcpb.loader";
|
|
58
60
|
|
|
59
61
|
// ---------------------------------------------------------------------------
|
|
60
62
|
// Configuration
|
|
@@ -65,7 +67,7 @@ const cwd = process.cwd();
|
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Fills an env var from the config file when the env var is not already set.
|
|
68
|
-
* Used for **non-path** scalars
|
|
70
|
+
* Used for **non-path** scalars, path-bearing fields are read directly so
|
|
69
71
|
* they can be resolved against `baseDir` (config file's directory) instead
|
|
70
72
|
* of `cwd` (deploy environment).
|
|
71
73
|
*/
|
|
@@ -92,7 +94,7 @@ envFromConfig("MCP_BROKER_PROVIDER_SECRET", config.auth?.providerSecret);
|
|
|
92
94
|
|
|
93
95
|
const stdioProvider = process.env["MCP_BROKER_STDIO_PROVIDER"];
|
|
94
96
|
|
|
95
|
-
// In stdio mode stdout is reserved for JSON-RPC
|
|
97
|
+
// In stdio mode stdout is reserved for JSON-RPC, redirect all console output to stderr.
|
|
96
98
|
if (stdioProvider) {
|
|
97
99
|
const toStderr = (...args: unknown[]) => process.stderr.write(args.join(" ") + "\n");
|
|
98
100
|
console.log = toStderr;
|
|
@@ -125,6 +127,30 @@ if (protocolOverride === "https" && (!tlsCertPath || !tlsKeyPath)) {
|
|
|
125
127
|
|
|
126
128
|
const useTls = protocolOverride === "http" ? false : protocolOverride === "https" ? true : !!(tlsCertPath && tlsKeyPath);
|
|
127
129
|
|
|
130
|
+
// ── Allowed browser origins for /<slot>/mcp ──────────────────────────────────
|
|
131
|
+
// Absent leaves the endpoint closed to browsers, which is the safe default.
|
|
132
|
+
// The env var only carries the list form; a pattern needs the config file,
|
|
133
|
+
// since a regular expression cannot survive comma-splitting.
|
|
134
|
+
const envOrigins = process.env["MCP_BROKER_ALLOWED_ORIGINS"]
|
|
135
|
+
?.split(",")
|
|
136
|
+
.map((origin) => origin.trim())
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
|
|
139
|
+
let allowedOrigins: readonly string[] | RegExp | undefined;
|
|
140
|
+
if (envOrigins?.length) {
|
|
141
|
+
allowedOrigins = envOrigins;
|
|
142
|
+
} else if (Array.isArray(config.allowedOrigins)) {
|
|
143
|
+
allowedOrigins = config.allowedOrigins;
|
|
144
|
+
} else if (config.allowedOrigins) {
|
|
145
|
+
try {
|
|
146
|
+
allowedOrigins = new RegExp(config.allowedOrigins.pattern, config.allowedOrigins.flags);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// Falling back to the closed default rather than starting with a rule
|
|
149
|
+
// the operator believes is in force but which never compiled.
|
|
150
|
+
console.error(`[mcp-broker] Ignoring allowedOrigins.pattern: ${(err as Error).message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
128
154
|
// ── Env-var static mount shortcuts (relative to cwd) ─────────────────────────
|
|
129
155
|
const envWwwDir = process.env["MCP_BROKER_WWW_DIR"] ? path.resolve(cwd, process.env["MCP_BROKER_WWW_DIR"]) : null;
|
|
130
156
|
const envBundleDir = process.env["MCP_BROKER_BUNDLE_DIR"] ? path.resolve(cwd, process.env["MCP_BROKER_BUNDLE_DIR"]) : null;
|
|
@@ -149,6 +175,10 @@ async function main(): Promise<void> {
|
|
|
149
175
|
builder.withTlsFiles(tlsCertPath!, tlsKeyPath!);
|
|
150
176
|
}
|
|
151
177
|
|
|
178
|
+
if (allowedOrigins) {
|
|
179
|
+
builder.withAllowedOrigins(allowedOrigins);
|
|
180
|
+
}
|
|
181
|
+
|
|
152
182
|
// ── Static mounts ────────────────────────────────────────────────────────
|
|
153
183
|
// First the env-var shortcuts (relative to cwd), then the config `www.mounts`
|
|
154
184
|
// (relative to baseDir). Order matters: /bundle before / so the prefix router
|
|
@@ -254,7 +284,7 @@ async function main(): Promise<void> {
|
|
|
254
284
|
|
|
255
285
|
// ── Provider authentication (independent of client OAuth) ────────────────
|
|
256
286
|
// Requires every provider connecting to /provider/<slot> or /providers to
|
|
257
|
-
// present the shared secret
|
|
287
|
+
// present the shared secret, closes off slot occupation by strangers.
|
|
258
288
|
const providerSecret = process.env["MCP_BROKER_PROVIDER_SECRET"];
|
|
259
289
|
if (providerSecret) {
|
|
260
290
|
builder.withProviderSecret(providerSecret);
|
|
@@ -298,7 +328,7 @@ async function main(): Promise<void> {
|
|
|
298
328
|
|
|
299
329
|
// ── Signal handlers ─────────────────────────────────────────────────────
|
|
300
330
|
const shutdown = async (signal: string): Promise<void> => {
|
|
301
|
-
console.log(`\n⛔ ${signal} received
|
|
331
|
+
console.log(`\n⛔ ${signal} received, shutting down…`);
|
|
302
332
|
await tunnel.stop();
|
|
303
333
|
process.exit(0);
|
|
304
334
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpAdapterBase, McpToolResults } from "@cyanmycelium/mcp-core";
|
|
2
2
|
import type { McpResourceContent, McpToolResult } from "@cyanmycelium/mcp-core";
|
|
3
|
-
import type { IBrokerContext } from "../broker.context
|
|
3
|
+
import type { IBrokerContext } from "../broker.context";
|
|
4
4
|
|
|
5
5
|
/** URI of the static resource backing the `broker_info` snapshot. */
|
|
6
6
|
export const BROKER_INFO_URI = "broker://info";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpAdapterBase, McpToolResults } from "@cyanmycelium/mcp-core";
|
|
2
2
|
import type { McpResourceContent, McpToolResult } from "@cyanmycelium/mcp-core";
|
|
3
|
-
import type { IBrokerContext, IBrokerProviderInfo } from "../broker.context
|
|
3
|
+
import type { IBrokerContext, IBrokerProviderInfo } from "../broker.context";
|
|
4
4
|
|
|
5
5
|
/** URI of the static resource that lists every provider slot. */
|
|
6
6
|
export const PROVIDERS_URI = "broker://providers";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IMessageTransport } from "@cyanmycelium/mcp-core";
|
|
2
|
-
import type { IInternalClient } from "../../ws.
|
|
3
|
-
import type { AggregateScopeFilter, IPrincipal } from "../../auth/index
|
|
2
|
+
import type { IInternalClient } from "../../ws/ws.interfaces";
|
|
3
|
+
import type { AggregateScopeFilter, IPrincipal } from "../../auth/index";
|
|
4
4
|
import {
|
|
5
5
|
SubjectMappingError,
|
|
6
6
|
makeAuthorizationAuditEvent,
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
type IAuthorizationSubject,
|
|
10
10
|
type IMcpOperation,
|
|
11
11
|
type IPolicyAuthorization,
|
|
12
|
-
} from "../../authorization/index
|
|
13
|
-
import { AggregateCatalog } from "./aggregate.catalog
|
|
14
|
-
import { ProviderClientSession } from "./provider.client.session
|
|
12
|
+
} from "../../authorization/index";
|
|
13
|
+
import { AggregateCatalog } from "./aggregate.catalog";
|
|
14
|
+
import { ProviderClientSession } from "./provider.client.session";
|
|
15
15
|
|
|
16
16
|
/** MCP protocol version advertised by the aggregate server. */
|
|
17
17
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
@@ -232,7 +232,7 @@ export class AggregateServer implements IMessageTransport {
|
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
const id = msg.id;
|
|
235
|
-
if (id == null) return; // client notification
|
|
235
|
+
if (id == null) return; // client notification: nothing to answer
|
|
236
236
|
|
|
237
237
|
switch (msg.method) {
|
|
238
238
|
case "initialize":
|
|
@@ -275,7 +275,7 @@ export class AggregateServer implements IMessageTransport {
|
|
|
275
275
|
private async _route(id: string | number, params: unknown, kind: "tool" | "prompt", principal: IPrincipal | null): Promise<void> {
|
|
276
276
|
const p = (params ?? {}) as ICallParams;
|
|
277
277
|
const route = p.name ? (kind === "tool" ? this._catalog.resolveTool(p.name) : this._catalog.resolvePrompt(p.name)) : undefined;
|
|
278
|
-
// Treat a provider the caller may not see as if it did not exist
|
|
278
|
+
// Treat a provider the caller may not see as if it did not exist, do not
|
|
279
279
|
// leak its presence through a distinct "forbidden" error.
|
|
280
280
|
const operation: IMcpOperation | undefined = route
|
|
281
281
|
? kind === "tool"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { IInternalClient } from "../../ws.
|
|
2
|
-
import type { ICatalogTool, ICatalogPrompt } from "./aggregate.catalog
|
|
1
|
+
import type { IInternalClient } from "../../ws/ws.interfaces";
|
|
2
|
+
import type { ICatalogTool, ICatalogPrompt } from "./aggregate.catalog";
|
|
3
3
|
|
|
4
4
|
/** MCP protocol version the aggregate sessions negotiate with sub-providers. */
|
|
5
5
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { McpBehavior } from "@cyanmycelium/mcp-core";
|
|
2
2
|
import type { McpResource, McpTool } from "@cyanmycelium/mcp-core";
|
|
3
|
-
import { BROKER_INFO_URI, BrokerInfoAdapter } from "../adapters/broker.adapter.info
|
|
4
|
-
import { brokerBaselineResourceDescription, brokerBaselineResourceName, brokerBaselineToolDescription } from "../broker.grammars
|
|
5
|
-
import type { IBrokerContext } from "../broker.context
|
|
3
|
+
import { BROKER_INFO_URI, BrokerInfoAdapter } from "../adapters/broker.adapter.info";
|
|
4
|
+
import { brokerBaselineResourceDescription, brokerBaselineResourceName, brokerBaselineToolDescription } from "../broker.grammars";
|
|
5
|
+
import type { IBrokerContext } from "../broker.context";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Exposes basic broker identity (name, version, uptime, listening config) as
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpBehavior } from "@cyanmycelium/mcp-core";
|
|
2
2
|
import type { McpResource, McpResourceTemplate, McpTool } from "@cyanmycelium/mcp-core";
|
|
3
|
-
import { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "../adapters/broker.adapter.providers
|
|
3
|
+
import { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "../adapters/broker.adapter.providers";
|
|
4
4
|
import {
|
|
5
5
|
brokerBaselinePropertyDescription,
|
|
6
6
|
brokerBaselineResourceDescription,
|
|
@@ -8,15 +8,15 @@ import {
|
|
|
8
8
|
brokerBaselineResourceTemplateDescription,
|
|
9
9
|
brokerBaselineResourceTemplateName,
|
|
10
10
|
brokerBaselineToolDescription,
|
|
11
|
-
} from "../broker.grammars
|
|
12
|
-
import type { IBrokerContext } from "../broker.context
|
|
11
|
+
} from "../broker.grammars";
|
|
12
|
+
import type { IBrokerContext } from "../broker.context";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Exposes the broker's provider slots so an MCP agent can discover what is
|
|
16
16
|
* currently routable behind the broker, with two tools:
|
|
17
17
|
*
|
|
18
|
-
* - `providers_list
|
|
19
|
-
* - `provider_status({ name })
|
|
18
|
+
* - `providers_list`: every slot (including disconnected ones).
|
|
19
|
+
* - `provider_status({ name })`, detail on one slot.
|
|
20
20
|
*
|
|
21
21
|
* Plus matching resources at `broker://providers` and `broker://providers/<name>`.
|
|
22
22
|
*/
|
|
@@ -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
|
|
7
|
+
// Open string types, extensibility by convention
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -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)
|
|
44
|
-
* - `"<userAgent>:<locale>@<version>"` (versioned)
|
|
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
|
|
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,7 +132,7 @@ 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
|
|
135
|
+
* user-agents or locales, adding a new grammar is dropping a JSON file.
|
|
136
136
|
*/
|
|
137
137
|
export interface IBrokerGrammarEntry {
|
|
138
138
|
userAgent: BrokerUserAgent;
|
|
@@ -186,7 +186,7 @@ export function* iterBrokerGrammarsFrom(grammarsDir: string): Generator<IBrokerG
|
|
|
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
|
|
189
|
+
* directory, typically `.mcp-broker/grammars/` next to the config file.
|
|
190
190
|
*/
|
|
191
191
|
export function* iterAvailableBrokerGrammars(): Generator<IBrokerGrammarEntry> {
|
|
192
192
|
yield* iterBrokerGrammarsFrom(GRAMMARS_DIR);
|
|
@@ -206,20 +206,20 @@ export type BrokerGrammarEntry = IBrokerGrammarEntry;
|
|
|
206
206
|
* Conventionally this is `default:en`. Session-specific grammars selected by
|
|
207
207
|
* the resolver override individual entries on top of this baseline.
|
|
208
208
|
*
|
|
209
|
-
* Throws if the JSON resource is missing
|
|
209
|
+
* Throws if the JSON resource is missing: the broker behaviors cannot be
|
|
210
210
|
* built without baseline descriptions.
|
|
211
211
|
*/
|
|
212
212
|
export function brokerBaselineGrammar(): McpGrammar {
|
|
213
213
|
const g = loadBrokerGrammar("default", "en");
|
|
214
214
|
if (!g) {
|
|
215
|
-
throw new Error(`Required baseline broker grammar "default:en" is missing
|
|
215
|
+
throw new Error(`Required baseline broker grammar "default:en" is missing, expected at ${join(GRAMMARS_DIR, "default", "en.json")}.`);
|
|
216
216
|
}
|
|
217
217
|
return g;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
221
|
* Convenience accessor for a baseline tool description. Throws when the
|
|
222
|
-
* tool is not listed in the baseline grammar
|
|
222
|
+
* tool is not listed in the baseline grammar, i.e. the JSON file is missing
|
|
223
223
|
* an entry for a tool the code knows about.
|
|
224
224
|
*/
|
|
225
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
|
|
4
|
-
import { BrokerProvidersBehavior } from "./behaviors/broker.behavior.providers
|
|
5
|
-
import { iterAvailableBrokerGrammars, iterBrokerGrammarsFrom } from "./broker.grammars
|
|
6
|
-
import type { IBrokerContext } from "./broker.context
|
|
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
|
|
@@ -44,7 +44,7 @@ export interface IStartBrokerServerOptions {
|
|
|
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.
|
|
@@ -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
|
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
}
|
package/src/broker/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
|
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
|
|
21
|
-
export type { IBrokerGrammarEntry, BrokerGrammarEntry, BrokerLocale, BrokerUserAgent } from "./broker.grammars
|
|
22
|
-
export type { IBrokerContext, IBrokerProviderInfo, BrokerContext, BrokerProviderInfo, BrokerProviderTransport } from "./broker.context
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import type { IAuthorizationPolicyConfig } from "./authorization/index
|
|
3
|
+
import type { IAuthorizationPolicyConfig } from "./authorization/index";
|
|
4
4
|
|
|
5
5
|
export interface IBrokerAuthConfig extends IAuthorizationPolicyConfig {
|
|
6
6
|
/** Master switch. Absent/`false` keeps the broker unauthenticated. */
|
|
@@ -67,6 +67,28 @@ export interface IBrokerConfig {
|
|
|
67
67
|
/** Logical broker name reported by `broker_info`. */
|
|
68
68
|
brokerName?: string;
|
|
69
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
|
+
|
|
70
92
|
/**
|
|
71
93
|
* OAuth 2.1 resource-server authorization. When `enabled` is `true`, every
|
|
72
94
|
* HTTP client request to a slot must carry a valid `Authorization: Bearer`
|
|
@@ -184,12 +206,12 @@ export const LEGACY_CONFIG_FILENAME = "mcp-broker.config.json";
|
|
|
184
206
|
* 1. The `path` argument when provided (explicit override).
|
|
185
207
|
* 2. The `MCP_BROKER_CONFIG` env var.
|
|
186
208
|
* 3. `./.mcp-broker/config.json` relative to `process.cwd()`.
|
|
187
|
-
* 4. `./mcp-broker.config.json` relative to `process.cwd()` (legacy layout
|
|
209
|
+
* 4. `./mcp-broker.config.json` relative to `process.cwd()` (legacy layout ,
|
|
188
210
|
* a deprecation warning is written to stderr).
|
|
189
211
|
*
|
|
190
212
|
* When no file is found, returns the built-in empty config with
|
|
191
213
|
* `baseDir = process.cwd()`. On invalid JSON, logs a warning to stderr and
|
|
192
|
-
* returns the same empty config
|
|
214
|
+
* returns the same empty config, never throws.
|
|
193
215
|
*
|
|
194
216
|
* Paths inside the config file are intended to be resolved against
|
|
195
217
|
* {@link ILoadedBrokerConfig.baseDir} by the consumer.
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
export { WsTunnel } from "./ws.tunnel
|
|
2
|
-
export { WsTunnelBuilder } from "./ws.tunnel.builder
|
|
3
|
-
export type { IInternalClient, IWsTunnelOptions, IStaticMount, InternalClient, WsTunnelOptions, StaticMount } from "./ws.
|
|
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
|
|
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
|
|
11
|
-
export { loadMcpbBundle } from "./mcpb.loader
|
|
12
|
-
export type { IMcpbBundleConfig, McpbBundleConfig } from "./mcpb.loader
|
|
13
|
-
export { unzipMcpb } from "./mcpb.unzip
|
|
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
|
|
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
|
|
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
19
|
export type {
|
|
20
20
|
IBrokerContext,
|
|
21
21
|
IBrokerGrammarEntry,
|
|
@@ -26,9 +26,9 @@ export type {
|
|
|
26
26
|
BrokerProviderTransport,
|
|
27
27
|
BrokerLocale,
|
|
28
28
|
BrokerUserAgent,
|
|
29
|
-
} from "./broker/index
|
|
29
|
+
} from "./broker/index";
|
|
30
30
|
|
|
31
|
-
export { VERSION, PACKAGE_NAME } from "./version
|
|
31
|
+
export { VERSION, PACKAGE_NAME } from "./version";
|
|
32
32
|
|
|
33
33
|
// OAuth 2.1 resource-server authorization.
|
|
34
34
|
export {
|
|
@@ -41,7 +41,7 @@ export {
|
|
|
41
41
|
normalizeProviderAuthentication,
|
|
42
42
|
providerMayPublish,
|
|
43
43
|
SharedSecretProviderAuthenticator,
|
|
44
|
-
} from "./auth/index
|
|
44
|
+
} from "./auth/index";
|
|
45
45
|
export type {
|
|
46
46
|
IAccessTokenClaims,
|
|
47
47
|
ITokenValidator,
|
|
@@ -65,7 +65,7 @@ export type {
|
|
|
65
65
|
ProviderAuthenticationResult,
|
|
66
66
|
ProviderAuthenticatorReturn,
|
|
67
67
|
ProviderPrincipal,
|
|
68
|
-
} from "./auth/index
|
|
68
|
+
} from "./auth/index";
|
|
69
69
|
|
|
70
70
|
// Hierarchical, domain-neutral authorization.
|
|
71
71
|
export {
|
|
@@ -80,7 +80,7 @@ export {
|
|
|
80
80
|
compileAuthorizationPolicy,
|
|
81
81
|
hasAuthorizationPolicies,
|
|
82
82
|
validateCapability,
|
|
83
|
-
} from "./authorization/index
|
|
83
|
+
} from "./authorization/index";
|
|
84
84
|
export type {
|
|
85
85
|
IAuditContext,
|
|
86
86
|
IAuthorizationAuditConfig,
|
|
@@ -119,9 +119,9 @@ export type {
|
|
|
119
119
|
SlotResourceResolver,
|
|
120
120
|
SubjectMapper,
|
|
121
121
|
SubjectMappingConfig,
|
|
122
|
-
} from "./authorization/index
|
|
122
|
+
} from "./authorization/index";
|
|
123
123
|
|
|
124
124
|
// JSON config file used by `bin.ts` at startup. Exported so a programmatic
|
|
125
125
|
// embedder can re-use the same loader against a custom path.
|
|
126
|
-
export { loadBrokerConfig, DEFAULT_CONFIG_FILENAME } from "./config
|
|
127
|
-
export type { IBrokerAuthConfig, IBrokerConfig, ILoadedBrokerConfig, BrokerAuthConfig, BrokerConfig, LoadedBrokerConfig } from "./config
|
|
126
|
+
export { loadBrokerConfig, DEFAULT_CONFIG_FILENAME } from "./config";
|
|
127
|
+
export type { IBrokerAuthConfig, IBrokerConfig, ILoadedBrokerConfig, BrokerAuthConfig, BrokerConfig, LoadedBrokerConfig } from "./config";
|
|
@@ -5,14 +5,14 @@
|
|
|
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)
|
|
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
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
|
|
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,8 +22,8 @@ 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
|
|
26
|
-
import type { IStdioUpstreamConfig } from "
|
|
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
29
|
export interface IMcpbBundleConfig {
|
|
@@ -85,7 +85,7 @@ function resolvePlaceholder(key: string, dirname: string, userConfig: IMcpbBundl
|
|
|
85
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
|
|
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
|
});
|
|
@@ -122,7 +122,7 @@ export async function loadMcpbBundle(cfg: IMcpbBundleConfig, baseDir: string): P
|
|
|
122
122
|
["signature", signaturePath],
|
|
123
123
|
] as const) {
|
|
124
124
|
if (!existsSync(file)) {
|
|
125
|
-
console.error(`${tag}: ${label} file not found at ${file}
|
|
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: IMcpbBundleConfig, baseDir: string): P
|
|
|
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
|
|
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: IMcpbBundleConfig, baseDir: string): P
|
|
|
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
|
|
147
|
+
console.error(`${tag}: manifest.json missing inside the bundle, bundle refused.`);
|
|
148
148
|
return null;
|
|
149
149
|
}
|
|
150
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
|
|
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: IMcpbBundleConfig, baseDir: string): P
|
|
|
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
|
|
163
|
+
console.error(`${tag}: manifest mcp_config has no command, bundle refused.`);
|
|
164
164
|
return null;
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -180,7 +180,7 @@ export async function loadMcpbBundle(cfg: IMcpbBundleConfig, baseDir: string): P
|
|
|
180
180
|
aggregate: cfg.aggregate ?? true,
|
|
181
181
|
};
|
|
182
182
|
} catch (err) {
|
|
183
|
-
console.error(`${tag}: ${(err as Error).message}
|
|
183
|
+
console.error(`${tag}: ${(err as Error).message}, bundle refused.`);
|
|
184
184
|
return null;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
@@ -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
|
|
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
|
|
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";
|