@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.
- package/.mcp-broker.example/CONFIGURATION-EN.md +861 -0
- package/.mcp-broker.example/CONFIGURATION-FR.md +871 -0
- package/.mcp-broker.example/README.md +8 -3
- package/.mcp-broker.example/config.json +86 -0
- package/README.md +122 -3
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +180 -208
- 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 -18
- package/dist/index.js +2 -14
- package/dist/index.js.map +1 -1
- package/package.json +14 -8
- 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 +98 -0
- package/src/auth/auth.types.ts +120 -0
- package/src/auth/http.auth.ts +128 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/jwt.validator.ts +63 -0
- package/src/auth/provider.auth.ts +114 -0
- package/src/auth/resource.metadata.ts +34 -0
- package/src/authorization/audit.ts +35 -0
- package/src/authorization/capability.classifier.ts +114 -0
- package/src/authorization/index.ts +37 -0
- package/src/authorization/policy.engine.ts +212 -0
- package/src/authorization/policy.types.ts +105 -0
- package/src/authorization/resource.path.ts +126 -0
- package/src/authorization/runtime.ts +56 -0
- package/src/authorization/slot.resource.ts +50 -0
- package/src/authorization/subject.mapper.ts +81 -0
- package/src/bin.ts +90 -7
- package/src/broker/adapters/broker.adapter.info.ts +3 -3
- package/src/broker/adapters/broker.adapter.providers.ts +3 -3
- package/src/broker/aggregate/aggregate.catalog.ts +49 -21
- package/src/broker/aggregate/aggregate.server.ts +149 -19
- package/src/broker/aggregate/provider.client.session.ts +22 -22
- package/src/broker/behaviors/broker.behavior.info.ts +4 -4
- package/src/broker/behaviors/broker.behavior.providers.ts +6 -6
- package/src/broker/broker.context.ts +10 -4
- package/src/broker/broker.grammars.ts +16 -13
- package/src/broker/broker.server.ts +12 -9
- 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 +81 -8
- package/src/index.ts +121 -20
- package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +24 -21
- package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
- package/src/remote.transports.ts +11 -8
- package/src/remote.upstream.ts +11 -8
- package/src/stdio.upstream.ts +9 -6
- package/src/upstream.ts +6 -3
- package/src/ws/ws.interfaces.ts +357 -0
- package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +112 -9
- package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +646 -457
- package/web/README.md +94 -0
- package/web/assets/logo.png +0 -0
- package/web/broker-self-mcp.html +338 -0
- package/web/css/styles.css +580 -0
- package/web/demos/DemoPlaceholder.html +258 -0
- package/web/demos/broker-explorer/css/app.css +393 -0
- package/web/demos/broker-explorer/index.html +94 -0
- package/web/demos/broker-explorer/js/app.js +271 -0
- package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
- package/web/demos/oauth-lab/README.md +94 -0
- package/web/demos/oauth-lab/config.json +117 -0
- package/web/demos/oauth-lab/css/app.css +1097 -0
- package/web/demos/oauth-lab/index.html +323 -0
- package/web/demos/oauth-lab/js/app.js +654 -0
- package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
- package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
- package/web/demos/oauth-lab/server/smoke-test.mjs +308 -0
- package/web/demos/oauth-lab/server/start.mjs +106 -0
- package/web/demos/provider-tunnel/css/app.css +384 -0
- package/web/demos/provider-tunnel/index.html +99 -0
- package/web/demos/provider-tunnel/js/app.js +226 -0
- package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
- package/web/index.html +558 -0
- package/web/js/lib/broker-tunnel.js +173 -0
- 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 -54
- package/dist/broker/aggregate/aggregate.catalog.js +0 -105
- package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
- package/dist/broker/aggregate/aggregate.server.d.ts +0 -47
- package/dist/broker/aggregate/aggregate.server.js +0 -151
- 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 -59
- 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 -130
- 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 -66
- 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 -136
- package/dist/config.js +0 -61
- package/dist/config.js.map +0 -1
- package/dist/mcpb.loader.d.ts +0 -24
- 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 -16
- package/dist/remote.transports.js +0 -297
- package/dist/remote.transports.js.map +0 -1
- package/dist/remote.upstream.d.ts +0 -36
- package/dist/remote.upstream.js +0 -52
- package/dist/remote.upstream.js.map +0 -1
- package/dist/stdio.upstream.d.ts +0 -45
- package/dist/stdio.upstream.js +0 -85
- package/dist/stdio.upstream.js.map +0 -1
- package/dist/upstream.d.ts +0 -33
- 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 -139
- package/dist/ws.tunnel.builder.js +0 -205
- package/dist/ws.tunnel.builder.js.map +0 -1
- package/dist/ws.tunnel.d.ts +0 -373
- package/dist/ws.tunnel.js +0 -1090
- 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,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AggregateCatalog
|
|
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
|
|
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
|
|
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
|
|
28
|
-
tools:
|
|
29
|
-
prompts:
|
|
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
|
|
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,
|
|
46
|
-
private _tools:
|
|
47
|
-
private _prompts:
|
|
48
|
-
private _toolRoutes = new Map<string,
|
|
49
|
-
private _promptRoutes = new Map<string,
|
|
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():
|
|
57
|
+
get tools(): ICatalogTool[] {
|
|
58
58
|
return this._tools;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** The merged, namespaced prompt list. */
|
|
62
|
-
get prompts():
|
|
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:
|
|
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):
|
|
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):
|
|
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:
|
|
89
|
-
const prompts:
|
|
90
|
-
const toolRoutes = new Map<string,
|
|
91
|
-
const promptRoutes = new Map<string,
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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) =>
|
|
20
|
+
export type InternalClientFactory = (providerName: string) => IInternalClient;
|
|
11
21
|
|
|
12
|
-
interface
|
|
22
|
+
interface IClientMessage {
|
|
13
23
|
id?: string | number | null;
|
|
14
24
|
method?: string;
|
|
15
25
|
params?: unknown;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
interface
|
|
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
|
-
/**
|
|
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
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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, {
|
|
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, {
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
import type {
|
|
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
|
|
11
|
+
interface IRpcOutcome {
|
|
12
12
|
result?: unknown;
|
|
13
13
|
error?: unknown;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
interface
|
|
17
|
-
resolve: (outcome:
|
|
16
|
+
interface IPendingRequest {
|
|
17
|
+
resolve: (outcome: IRpcOutcome) => void;
|
|
18
18
|
timer: ReturnType<typeof setTimeout>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
interface
|
|
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
|
|
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:
|
|
40
|
+
private readonly _client: IInternalClient;
|
|
41
41
|
private readonly _idPrefix: string;
|
|
42
|
-
private readonly _pending = new Map<string,
|
|
42
|
+
private readonly _pending = new Map<string, IPendingRequest>();
|
|
43
43
|
private _nextId = 0;
|
|
44
|
-
private _tools:
|
|
45
|
-
private _prompts:
|
|
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:
|
|
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():
|
|
66
|
+
get tools(): ICatalogTool[] {
|
|
67
67
|
return this._tools;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
get prompts():
|
|
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<
|
|
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<
|
|
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<
|
|
105
|
-
this._prompts = await this._listAll<
|
|
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<
|
|
129
|
-
return new Promise<
|
|
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:
|
|
145
|
+
let msg: IIncomingMessage;
|
|
146
146
|
try {
|
|
147
|
-
msg = JSON.parse(data) as
|
|
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
|
|
4
|
-
import { brokerBaselineResourceDescription, brokerBaselineResourceName, brokerBaselineToolDescription } from "../broker.grammars
|
|
5
|
-
import type {
|
|
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:
|
|
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
|
|
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
|
|
12
|
-
import type {
|
|
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
|
*/
|
|
23
23
|
export class BrokerProvidersBehavior extends McpBehavior {
|
|
24
24
|
public static readonly NAMESPACE = "broker_providers";
|
|
25
25
|
|
|
26
|
-
constructor(context:
|
|
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
|
|
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():
|
|
40
|
+
getProvidersInfo(): IBrokerProviderInfo[];
|
|
41
41
|
|
|
42
42
|
/** Snapshot of a single provider slot, or `undefined` if the name is unknown. */
|
|
43
|
-
getProviderInfo(name: string):
|
|
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
|
|
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;
|