@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * AggregateCatalog — merges the tool and prompt catalogs of several providers
2
+ * AggregateCatalog, merges the tool and prompt catalogs of several providers
3
3
  * into a single namespaced view, and routes aggregated names back to their
4
4
  * origin provider.
5
5
  *
@@ -9,14 +9,14 @@
9
9
  */
10
10
 
11
11
  /** Minimal shape of an MCP tool entry. Extra fields are preserved as-is. */
12
- export interface CatalogTool {
12
+ export interface ICatalogTool {
13
13
  name: string;
14
14
  description?: string;
15
15
  [key: string]: unknown;
16
16
  }
17
17
 
18
18
  /** Minimal shape of an MCP prompt entry. Extra fields are preserved as-is. */
19
- export interface CatalogPrompt {
19
+ export interface ICatalogPrompt {
20
20
  name: string;
21
21
  title?: string;
22
22
  description?: string;
@@ -24,13 +24,13 @@ export interface CatalogPrompt {
24
24
  }
25
25
 
26
26
  /** One provider's raw contribution to the aggregate. */
27
- export interface ProviderEntry {
28
- tools: CatalogTool[];
29
- prompts: CatalogPrompt[];
27
+ export interface IProviderEntry {
28
+ tools: ICatalogTool[];
29
+ prompts: ICatalogPrompt[];
30
30
  }
31
31
 
32
32
  /** Where an aggregated name routes back to. */
33
- export interface Route {
33
+ export interface IRoute {
34
34
  provider: string;
35
35
  original: string;
36
36
  }
@@ -42,11 +42,11 @@ const SEPARATOR = "-";
42
42
  const MAX_TOOL_NAME_LENGTH = 64;
43
43
 
44
44
  export class AggregateCatalog {
45
- private readonly _providers = new Map<string, ProviderEntry>();
46
- private _tools: CatalogTool[] = [];
47
- private _prompts: CatalogPrompt[] = [];
48
- private _toolRoutes = new Map<string, Route>();
49
- private _promptRoutes = new Map<string, Route>();
45
+ private readonly _providers = new Map<string, IProviderEntry>();
46
+ private _tools: ICatalogTool[] = [];
47
+ private _prompts: ICatalogPrompt[] = [];
48
+ private _toolRoutes = new Map<string, IRoute>();
49
+ private _promptRoutes = new Map<string, IRoute>();
50
50
 
51
51
  /** Number of providers currently contributing to the aggregate. */
52
52
  get providerCount(): number {
@@ -54,17 +54,17 @@ export class AggregateCatalog {
54
54
  }
55
55
 
56
56
  /** The merged, namespaced tool list. */
57
- get tools(): CatalogTool[] {
57
+ get tools(): ICatalogTool[] {
58
58
  return this._tools;
59
59
  }
60
60
 
61
61
  /** The merged, namespaced prompt list. */
62
- get prompts(): CatalogPrompt[] {
62
+ get prompts(): ICatalogPrompt[] {
63
63
  return this._prompts;
64
64
  }
65
65
 
66
66
  /** Adds or replaces a provider's contribution, then rebuilds the merged view. */
67
- setProvider(provider: string, entry: ProviderEntry): void {
67
+ setProvider(provider: string, entry: IProviderEntry): void {
68
68
  this._providers.set(provider, entry);
69
69
  this._rebuild();
70
70
  }
@@ -74,21 +74,37 @@ export class AggregateCatalog {
74
74
  if (this._providers.delete(provider)) this._rebuild();
75
75
  }
76
76
 
77
+ /** The merged tool list restricted to providers `allow` returns `true` for. */
78
+ toolsFor(allow: (provider: string) => boolean): ICatalogTool[] {
79
+ return this._tools.filter((tool) => {
80
+ const route = this._toolRoutes.get(tool.name);
81
+ return route ? allow(route.provider) : false;
82
+ });
83
+ }
84
+
85
+ /** The merged prompt list restricted to providers `allow` returns `true` for. */
86
+ promptsFor(allow: (provider: string) => boolean): ICatalogPrompt[] {
87
+ return this._prompts.filter((prompt) => {
88
+ const route = this._promptRoutes.get(prompt.name);
89
+ return route ? allow(route.provider) : false;
90
+ });
91
+ }
92
+
77
93
  /** Resolves an aggregated tool name to its origin, or `undefined` if unknown. */
78
- resolveTool(name: string): Route | undefined {
94
+ resolveTool(name: string): IRoute | undefined {
79
95
  return this._toolRoutes.get(name);
80
96
  }
81
97
 
82
98
  /** Resolves an aggregated prompt name to its origin, or `undefined` if unknown. */
83
- resolvePrompt(name: string): Route | undefined {
99
+ resolvePrompt(name: string): IRoute | undefined {
84
100
  return this._promptRoutes.get(name);
85
101
  }
86
102
 
87
103
  private _rebuild(): void {
88
- const tools: CatalogTool[] = [];
89
- const prompts: CatalogPrompt[] = [];
90
- const toolRoutes = new Map<string, Route>();
91
- const promptRoutes = new Map<string, Route>();
104
+ const tools: ICatalogTool[] = [];
105
+ const prompts: ICatalogPrompt[] = [];
106
+ const toolRoutes = new Map<string, IRoute>();
107
+ const promptRoutes = new Map<string, IRoute>();
92
108
 
93
109
  for (const [provider, entry] of this._providers) {
94
110
  for (const tool of entry.tools) {
@@ -110,6 +126,18 @@ export class AggregateCatalog {
110
126
  }
111
127
  }
112
128
 
129
+ /** @deprecated Use {@link ICatalogTool}. */
130
+ export type CatalogTool = ICatalogTool;
131
+
132
+ /** @deprecated Use {@link ICatalogPrompt}. */
133
+ export type CatalogPrompt = ICatalogPrompt;
134
+
135
+ /** @deprecated Use {@link IProviderEntry}. */
136
+ export type ProviderEntry = IProviderEntry;
137
+
138
+ /** @deprecated Use {@link IRoute}. */
139
+ export type Route = IRoute;
140
+
113
141
  function prefixed(provider: string, original: string): string {
114
142
  return `${provider}${SEPARATOR}${original}`;
115
143
  }
@@ -1,21 +1,31 @@
1
1
  import type { IMessageTransport } from "@cyanmycelium/mcp-core";
2
- import type { InternalClient } from "../../ws.tunnel.js";
3
- import { AggregateCatalog } from "./aggregate.catalog.js";
4
- import { ProviderClientSession } from "./provider.client.session.js";
2
+ import type { IInternalClient } from "../../ws/ws.interfaces";
3
+ import type { AggregateScopeFilter, IPrincipal } from "../../auth/index";
4
+ import {
5
+ SubjectMappingError,
6
+ makeAuthorizationAuditEvent,
7
+ writeAuthorizationAuditEvent,
8
+ type IAuthorizationDecision,
9
+ type IAuthorizationSubject,
10
+ type IMcpOperation,
11
+ type IPolicyAuthorization,
12
+ } from "../../authorization/index";
13
+ import { AggregateCatalog } from "./aggregate.catalog";
14
+ import { ProviderClientSession } from "./provider.client.session";
5
15
 
6
16
  /** MCP protocol version advertised by the aggregate server. */
7
17
  const PROTOCOL_VERSION = "2024-11-05";
8
18
 
9
19
  /** Opens an in-process client to a named provider slot. Supplied by WsTunnel. */
10
- export type InternalClientFactory = (providerName: string) => InternalClient;
20
+ export type InternalClientFactory = (providerName: string) => IInternalClient;
11
21
 
12
- interface ClientMessage {
22
+ interface IClientMessage {
13
23
  id?: string | number | null;
14
24
  method?: string;
15
25
  params?: unknown;
16
26
  }
17
27
 
18
- interface CallParams {
28
+ interface ICallParams {
19
29
  name?: string;
20
30
  arguments?: Record<string, unknown>;
21
31
  }
@@ -37,6 +47,10 @@ export class AggregateServer implements IMessageTransport {
37
47
  private readonly _sessions = new Map<string, ProviderClientSession>();
38
48
  private readonly _openClient: InternalClientFactory;
39
49
  private _running = false;
50
+ /** Per-caller provider visibility filter; `null` ⇒ every caller sees all. */
51
+ private _scopeFilter: AggregateScopeFilter | null = null;
52
+ /** Provider-aware hierarchical authorization, absent in legacy mode. */
53
+ private _authorization: IPolicyAuthorization | null = null;
40
54
 
41
55
  onMessage: ((data: string) => void) | null = null;
42
56
  onOpen: (() => void) | null = null;
@@ -61,9 +75,27 @@ export class AggregateServer implements IMessageTransport {
61
75
  this._running = true;
62
76
  }
63
77
 
64
- /** WsTunnel hands a client request for the `_all` slot here. */
78
+ /** Sets the per-caller provider visibility filter. `null` disables filtering. */
79
+ setScopeFilter(filter: AggregateScopeFilter | null): void {
80
+ this._scopeFilter = filter;
81
+ }
82
+
83
+ /** Sets the compiled hierarchical policy runtime. */
84
+ setPolicyAuthorization(authorization: IPolicyAuthorization | null): void {
85
+ this._authorization = authorization;
86
+ }
87
+
88
+ /** WsTunnel hands a client request for the `_all` slot here (no principal). */
65
89
  send(data: string): void {
66
- void this._handleClientMessage(data);
90
+ void this._handleClientMessage(data, null);
91
+ }
92
+
93
+ /**
94
+ * Like {@link send}, but carries the authenticated caller so the aggregate
95
+ * narrows the catalog and routing to the providers the caller may see.
96
+ */
97
+ sendAs(data: string, principal: IPrincipal | null): void {
98
+ void this._handleClientMessage(data, principal);
67
99
  }
68
100
 
69
101
  /** Closes every provider session and the aggregate transport. */
@@ -109,15 +141,98 @@ export class AggregateServer implements IMessageTransport {
109
141
  this._emitListChanged();
110
142
  }
111
143
 
112
- private async _handleClientMessage(data: string): Promise<void> {
113
- let msg: ClientMessage;
144
+ private _subjectFor(principal: IPrincipal | null): IAuthorizationSubject {
145
+ if (principal?.subject) return principal.subject;
146
+ if (!principal) return { ids: [] };
147
+ try {
148
+ return (
149
+ this._authorization?.subjectMapper.map(principal.claims) ?? {
150
+ ids: [],
151
+ claims: principal.claims,
152
+ }
153
+ );
154
+ } catch (error) {
155
+ if (!(error instanceof SubjectMappingError)) {
156
+ console.error("[broker] aggregate authorization subject mapping failed.");
157
+ }
158
+ return { ids: [], claims: principal.claims };
159
+ }
160
+ }
161
+
162
+ private _scopeAllows(principal: IPrincipal | null, provider: string): boolean {
163
+ const filter = this._scopeFilter;
164
+ return !filter || !principal || filter(principal, provider);
165
+ }
166
+
167
+ private _policyAllows(principal: IPrincipal | null, provider: string, operation: IMcpOperation): boolean {
168
+ const authorization = this._authorization;
169
+ if (!authorization) return true;
170
+
171
+ const subject = this._subjectFor(principal);
172
+ const resource = authorization.slotResourceResolver.resolve(provider);
173
+ if (!resource) {
174
+ const decision: IAuthorizationDecision = { allowed: false, reason: "unknown-resource" };
175
+ writeAuthorizationAuditEvent(makeAuthorizationAuditEvent({ subject, slot: AggregateServer.SLOT, provider }, decision));
176
+ return false;
177
+ }
178
+
114
179
  try {
115
- msg = JSON.parse(data) as ClientMessage;
180
+ const classified = authorization.capabilityClassifier.classify(operation, resource, provider);
181
+ if (!classified) return true;
182
+ const decision = authorization.engine.authorize({
183
+ subject,
184
+ capability: classified.capability,
185
+ resource,
186
+ provider,
187
+ tool: classified.tool,
188
+ });
189
+ if (!decision.allowed || authorization.audit.logAllowed) {
190
+ writeAuthorizationAuditEvent(
191
+ makeAuthorizationAuditEvent(
192
+ {
193
+ subject,
194
+ slot: AggregateServer.SLOT,
195
+ resource,
196
+ capability: classified.capability,
197
+ provider,
198
+ tool: classified.tool,
199
+ },
200
+ decision
201
+ )
202
+ );
203
+ }
204
+ return decision.allowed;
205
+ } catch {
206
+ console.error("[broker] aggregate policy evaluation failed.");
207
+ const decision: IAuthorizationDecision = { allowed: false, reason: "no-matching-grant" };
208
+ writeAuthorizationAuditEvent(
209
+ makeAuthorizationAuditEvent(
210
+ {
211
+ subject,
212
+ slot: AggregateServer.SLOT,
213
+ resource,
214
+ provider,
215
+ },
216
+ decision
217
+ )
218
+ );
219
+ return false;
220
+ }
221
+ }
222
+
223
+ private _allows(principal: IPrincipal | null, provider: string, operation: IMcpOperation): boolean {
224
+ return this._scopeAllows(principal, provider) && this._policyAllows(principal, provider, operation);
225
+ }
226
+
227
+ private async _handleClientMessage(data: string, principal: IPrincipal | null): Promise<void> {
228
+ let msg: IClientMessage;
229
+ try {
230
+ msg = JSON.parse(data) as IClientMessage;
116
231
  } catch {
117
232
  return;
118
233
  }
119
234
  const id = msg.id;
120
- if (id == null) return; // client notification — nothing to answer
235
+ if (id == null) return; // client notification: nothing to answer
121
236
 
122
237
  switch (msg.method) {
123
238
  case "initialize":
@@ -133,26 +248,41 @@ export class AggregateServer implements IMessageTransport {
133
248
  this._reply(id, { result: {} });
134
249
  break;
135
250
  case "tools/list":
136
- this._reply(id, { result: { tools: this._catalog.tools } });
251
+ this._reply(id, {
252
+ result: {
253
+ tools: this._catalog.toolsFor((provider) => this._allows(principal, provider, { method: "tools/list" })),
254
+ },
255
+ });
137
256
  break;
138
257
  case "prompts/list":
139
- this._reply(id, { result: { prompts: this._catalog.prompts } });
258
+ this._reply(id, {
259
+ result: {
260
+ prompts: this._catalog.promptsFor((provider) => this._allows(principal, provider, { method: "prompts/list" })),
261
+ },
262
+ });
140
263
  break;
141
264
  case "tools/call":
142
- await this._route(id, msg.params, "tool");
265
+ await this._route(id, msg.params, "tool", principal);
143
266
  break;
144
267
  case "prompts/get":
145
- await this._route(id, msg.params, "prompt");
268
+ await this._route(id, msg.params, "prompt", principal);
146
269
  break;
147
270
  default:
148
271
  this._reply(id, { error: { code: -32601, message: `Method not found: ${msg.method ?? "(none)"}` } });
149
272
  }
150
273
  }
151
274
 
152
- private async _route(id: string | number, params: unknown, kind: "tool" | "prompt"): Promise<void> {
153
- const p = (params ?? {}) as CallParams;
275
+ private async _route(id: string | number, params: unknown, kind: "tool" | "prompt", principal: IPrincipal | null): Promise<void> {
276
+ const p = (params ?? {}) as ICallParams;
154
277
  const route = p.name ? (kind === "tool" ? this._catalog.resolveTool(p.name) : this._catalog.resolvePrompt(p.name)) : undefined;
155
- if (!route) {
278
+ // Treat a provider the caller may not see as if it did not exist, do not
279
+ // leak its presence through a distinct "forbidden" error.
280
+ const operation: IMcpOperation | undefined = route
281
+ ? kind === "tool"
282
+ ? { method: "tools/call", params: { name: route.original } }
283
+ : { method: "prompts/get", params: { name: route.original } }
284
+ : undefined;
285
+ if (!route || !operation || !this._allows(principal, route.provider, operation)) {
156
286
  this._reply(id, { error: { code: -32602, message: `Unknown aggregated ${kind}: ${p.name ?? "(none)"}` } });
157
287
  return;
158
288
  }
@@ -1,5 +1,5 @@
1
- import type { InternalClient } from "../../ws.tunnel.js";
2
- import type { CatalogTool, CatalogPrompt } from "./aggregate.catalog.js";
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";
@@ -8,17 +8,17 @@ const PROTOCOL_VERSION = "2024-11-05";
8
8
  const REQUEST_TIMEOUT_MS = 30_000;
9
9
 
10
10
  /** Outcome of a JSON-RPC request: exactly one of `result` / `error` is set. */
11
- interface RpcOutcome {
11
+ interface IRpcOutcome {
12
12
  result?: unknown;
13
13
  error?: unknown;
14
14
  }
15
15
 
16
- interface PendingRequest {
17
- resolve: (outcome: RpcOutcome) => void;
16
+ interface IPendingRequest {
17
+ resolve: (outcome: IRpcOutcome) => void;
18
18
  timer: ReturnType<typeof setTimeout>;
19
19
  }
20
20
 
21
- interface IncomingMessage {
21
+ interface IIncomingMessage {
22
22
  id?: string | number | null;
23
23
  method?: string;
24
24
  result?: unknown;
@@ -27,7 +27,7 @@ interface IncomingMessage {
27
27
 
28
28
  /**
29
29
  * A hand-rolled JSON-RPC client session to one aggregated provider, running
30
- * over an in-process {@link InternalClient}.
30
+ * over an in-process {@link IInternalClient}.
31
31
  *
32
32
  * Performs the MCP `initialize` handshake, caches the provider's `tools/list`
33
33
  * and `prompts/list`, and re-fetches them when the provider emits a
@@ -37,12 +37,12 @@ interface IncomingMessage {
37
37
  export class ProviderClientSession {
38
38
  readonly provider: string;
39
39
 
40
- private readonly _client: InternalClient;
40
+ private readonly _client: IInternalClient;
41
41
  private readonly _idPrefix: string;
42
- private readonly _pending = new Map<string, PendingRequest>();
42
+ private readonly _pending = new Map<string, IPendingRequest>();
43
43
  private _nextId = 0;
44
- private _tools: CatalogTool[] = [];
45
- private _prompts: CatalogPrompt[] = [];
44
+ private _tools: ICatalogTool[] = [];
45
+ private _prompts: ICatalogPrompt[] = [];
46
46
  private _closed = false;
47
47
 
48
48
  /** Fires after the cached catalog changes (initial load or `list_changed`). */
@@ -51,7 +51,7 @@ export class ProviderClientSession {
51
51
  /** Fires when the underlying provider slot disconnects. */
52
52
  onClosed: (() => void) | null = null;
53
53
 
54
- constructor(provider: string, client: InternalClient) {
54
+ constructor(provider: string, client: IInternalClient) {
55
55
  this.provider = provider;
56
56
  this._client = client;
57
57
  this._idPrefix = `agg-${provider}-`;
@@ -63,11 +63,11 @@ export class ProviderClientSession {
63
63
  };
64
64
  }
65
65
 
66
- get tools(): CatalogTool[] {
66
+ get tools(): ICatalogTool[] {
67
67
  return this._tools;
68
68
  }
69
69
 
70
- get prompts(): CatalogPrompt[] {
70
+ get prompts(): ICatalogPrompt[] {
71
71
  return this._prompts;
72
72
  }
73
73
 
@@ -83,12 +83,12 @@ export class ProviderClientSession {
83
83
  }
84
84
 
85
85
  /** Forwards a `tools/call` to the provider, relaying the raw outcome. */
86
- callTool(name: string, args: Record<string, unknown>): Promise<RpcOutcome> {
86
+ callTool(name: string, args: Record<string, unknown>): Promise<IRpcOutcome> {
87
87
  return this._request("tools/call", { name, arguments: args });
88
88
  }
89
89
 
90
90
  /** Forwards a `prompts/get` to the provider, relaying the raw outcome. */
91
- getPrompt(name: string, args: Record<string, unknown>): Promise<RpcOutcome> {
91
+ getPrompt(name: string, args: Record<string, unknown>): Promise<IRpcOutcome> {
92
92
  return this._request("prompts/get", { name, arguments: args });
93
93
  }
94
94
 
@@ -101,8 +101,8 @@ export class ProviderClientSession {
101
101
  }
102
102
 
103
103
  private async _refresh(): Promise<void> {
104
- this._tools = await this._listAll<CatalogTool>("tools/list", "tools");
105
- this._prompts = await this._listAll<CatalogPrompt>("prompts/list", "prompts");
104
+ this._tools = await this._listAll<ICatalogTool>("tools/list", "tools");
105
+ this._prompts = await this._listAll<ICatalogPrompt>("prompts/list", "prompts");
106
106
  this.onCatalogChanged?.();
107
107
  }
108
108
 
@@ -125,8 +125,8 @@ export class ProviderClientSession {
125
125
  return items;
126
126
  }
127
127
 
128
- private _request(method: string, params: unknown): Promise<RpcOutcome> {
129
- return new Promise<RpcOutcome>((resolve) => {
128
+ private _request(method: string, params: unknown): Promise<IRpcOutcome> {
129
+ return new Promise<IRpcOutcome>((resolve) => {
130
130
  if (this._closed) {
131
131
  resolve({ error: { code: -32000, message: "session closed" } });
132
132
  return;
@@ -142,9 +142,9 @@ export class ProviderClientSession {
142
142
  }
143
143
 
144
144
  private _handleMessage(data: string): void {
145
- let msg: IncomingMessage;
145
+ let msg: IIncomingMessage;
146
146
  try {
147
- msg = JSON.parse(data) as IncomingMessage;
147
+ msg = JSON.parse(data) as IIncomingMessage;
148
148
  } catch {
149
149
  return;
150
150
  }
@@ -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.js";
4
- import { brokerBaselineResourceDescription, brokerBaselineResourceName, brokerBaselineToolDescription } from "../broker.grammars.js";
5
- import type { BrokerContext } from "../broker.context.js";
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
@@ -13,7 +13,7 @@ import type { BrokerContext } from "../broker.context.js";
13
13
  export class BrokerInfoBehavior extends McpBehavior {
14
14
  public static readonly NAMESPACE = "broker";
15
15
 
16
- constructor(context: BrokerContext) {
16
+ constructor(context: IBrokerContext) {
17
17
  super(new BrokerInfoAdapter(context), {
18
18
  namespace: BrokerInfoBehavior.NAMESPACE,
19
19
  });
@@ -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.js";
3
+ import { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "../adapters/broker.adapter.providers";
4
4
  import {
5
5
  brokerBaselinePropertyDescription,
6
6
  brokerBaselineResourceDescription,
@@ -8,22 +8,22 @@ import {
8
8
  brokerBaselineResourceTemplateDescription,
9
9
  brokerBaselineResourceTemplateName,
10
10
  brokerBaselineToolDescription,
11
- } from "../broker.grammars.js";
12
- import type { BrokerContext } from "../broker.context.js";
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` — every slot (including disconnected ones).
19
- * - `provider_status({ name })` — detail on one slot.
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
  */
23
23
  export class BrokerProvidersBehavior extends McpBehavior {
24
24
  public static readonly NAMESPACE = "broker_providers";
25
25
 
26
- constructor(context: BrokerContext) {
26
+ constructor(context: IBrokerContext) {
27
27
  super(new BrokerProvidersAdapter(context), {
28
28
  namespace: BrokerProvidersBehavior.NAMESPACE,
29
29
  });
@@ -4,7 +4,7 @@
4
4
  * Decouples the behaviors from the concrete `WsTunnel` class, making them
5
5
  * unit-testable and reusable (e.g. a future .NET-backed context).
6
6
  */
7
- export interface BrokerContext {
7
+ export interface IBrokerContext {
8
8
  /** Package version (from package.json). */
9
9
  readonly version: string;
10
10
 
@@ -37,10 +37,10 @@ export interface BrokerContext {
37
37
  };
38
38
 
39
39
  /** Snapshot of every known provider slot, including disconnected ones. */
40
- getProvidersInfo(): BrokerProviderInfo[];
40
+ getProvidersInfo(): IBrokerProviderInfo[];
41
41
 
42
42
  /** Snapshot of a single provider slot, or `undefined` if the name is unknown. */
43
- getProviderInfo(name: string): BrokerProviderInfo | undefined;
43
+ getProviderInfo(name: string): IBrokerProviderInfo | undefined;
44
44
  }
45
45
 
46
46
  /**
@@ -54,7 +54,7 @@ export interface BrokerContext {
54
54
  */
55
55
  export type BrokerProviderTransport = "ws" | "ws-multiplex" | "stdio" | "loopback" | "none";
56
56
 
57
- export interface BrokerProviderInfo {
57
+ export interface IBrokerProviderInfo {
58
58
  /** Slot name as advertised on `/<name>/...` endpoints. */
59
59
  name: string;
60
60
 
@@ -73,3 +73,9 @@ export interface BrokerProviderInfo {
73
73
  /** Number of in-flight JSON-RPC requests awaiting a response. */
74
74
  pendingCount: number;
75
75
  }
76
+
77
+ /** @deprecated Use {@link IBrokerContext}. */
78
+ export type BrokerContext = IBrokerContext;
79
+
80
+ /** @deprecated Use {@link IBrokerProviderInfo}. */
81
+ export type BrokerProviderInfo = IBrokerProviderInfo;