@cyanmycelium/mcp-broker 1.2.0 → 1.3.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 +300 -44
- package/.mcp-broker.example/CONFIGURATION-FR.md +313 -44
- package/.mcp-broker.example/README.md +73 -2
- package/.mcp-broker.example/config.json +18 -9
- package/.mcp-broker.example/config.stdio-bridge.json +16 -0
- package/README.md +407 -27
- package/dist/bin.js +215 -20
- package/dist/bin.js.map +1 -1
- package/dist/chunk-BZUZYXVA.js +5955 -0
- package/dist/chunk-BZUZYXVA.js.map +1 -0
- package/dist/grammars/claude/en.json +12 -0
- package/dist/grammars/claude/fr.json +12 -0
- package/dist/grammars/default/en.json +40 -0
- package/dist/grammars/default/fr.json +40 -0
- package/dist/grammars/default/zh.json +40 -0
- package/dist/index.d.ts +991 -25
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/auth/index.ts +3 -1
- package/src/auth/provider.auth.ts +126 -8
- package/src/authorization/policy.engine.ts +11 -2
- package/src/authorization/policy.types.ts +25 -1
- package/src/bin.ts +325 -28
- package/src/broker/adapters/broker.adapter.diagnose.ts +45 -0
- package/src/broker/adapters/broker.adapter.guide.ts +108 -0
- package/src/broker/aggregate/aggregate.server.ts +82 -15
- package/src/broker/aggregate/provider.client.session.ts +85 -11
- package/src/broker/behaviors/broker.behavior.diagnose.ts +47 -0
- package/src/broker/behaviors/broker.behavior.guide.ts +79 -0
- package/src/broker/broker.context.ts +65 -0
- package/src/broker/broker.diagnostics.ts +495 -0
- package/src/broker/broker.guides.ts +1029 -0
- package/src/broker/broker.server.ts +23 -7
- package/src/broker/broker.slots.ts +36 -0
- package/src/broker/grammars/claude/en.json +12 -0
- package/src/broker/grammars/claude/fr.json +12 -0
- package/src/broker/grammars/default/en.json +40 -0
- package/src/broker/grammars/default/fr.json +40 -0
- package/src/broker/grammars/default/zh.json +40 -0
- package/src/config.ts +191 -4
- package/src/index.ts +38 -3
- package/src/remote.transports.ts +127 -10
- package/src/remote.upstream.ts +4 -1
- package/src/ws/ws.interfaces.ts +148 -3
- package/src/ws/ws.tunnel.builder.ts +63 -1
- package/src/ws/ws.tunnel.ts +1150 -173
- package/web/README.md +31 -4
- package/dist/chunk-FTDKH2C4.js +0 -3670
- package/dist/chunk-FTDKH2C4.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AuthError, BROKER_PROVIDER_NAME, BrokerInfoBehavior, BrokerProvidersBehavior, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, HttpAuthGuard, JwtSubjectMapper, JwtTokenValidator, PACKAGE_NAME, RemoteUpstream, ResourcePath, ResourcePathPattern, SharedSecretProviderAuthenticator, StdioUpstream, SubjectMappingError, VERSION, WsTunnel, WsTunnelBuilder, authorizationWithEngine, brokerGrammarKey, buildJwtAuth, buildResourceMetadata, compileAuthorizationPolicy, hasAuthorizationPolicies, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerConfig, loadBrokerGrammar, loadMcpbBundle, normalizeProviderAuthentication, providerMayPublish, scopesOf, startBrokerServer, unzipMcpb, validateCapability } from './chunk-
|
|
1
|
+
export { AuthError, BROKER_AGGREGATE_NAME, BROKER_GUIDES, BROKER_GUIDE_MIME_TYPE, BROKER_GUIDE_TOPICS, BROKER_GUIDE_URI_PREFIX, BROKER_GUIDE_URI_TEMPLATE, BROKER_PROVIDER_NAME, BROKER_RESERVED_SLOTS, BrokerDiagnoseAdapter, BrokerDiagnoseBehavior, BrokerGuideAdapter, BrokerGuideBehavior, BrokerInfoBehavior, BrokerProvidersBehavior, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, HttpAuthGuard, JwtSubjectMapper, JwtTokenValidator, PACKAGE_NAME, RemoteUpstream, ResourcePath, ResourcePathPattern, SharedSecretProviderAuthenticator, StdioUpstream, SubjectMappingError, VERSION, WsTunnel, WsTunnelBuilder, authorizationWithEngine, brokerGrammarKey, brokerGuide, brokerGuideIndex, brokerGuideTopicFromUri, brokerGuideUri, buildJwtAuth, buildResourceMetadata, compileAuthorizationPolicy, compileProviderAllowedResources, diagnoseBroker, hasAuthorizationPolicies, isReservedBrokerSlot, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerConfig, loadBrokerGrammar, loadMcpbBundle, normalizeProviderAuthentication, providerMayPublish, providerPublishDecision, resolveOpenTarget, scopesOf, startBrokerServer, unzipMcpb, validateCapability } from './chunk-BZUZYXVA.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanmycelium/mcp-broker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "WebSocket-based Model Context Protocol broker. Aggregates multiple MCP providers behind a single endpoint with stdio, SSE, and Streamable HTTP client transports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -68,13 +68,13 @@
|
|
|
68
68
|
"prepublishOnly": "npm run lint && npm run build && npm test"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@cyanmycelium/mcp-core": "^0.
|
|
71
|
+
"@cyanmycelium/mcp-core": "^1.0.0",
|
|
72
72
|
"jose": "^6.2.3",
|
|
73
73
|
"open": "^11.0.0",
|
|
74
74
|
"ws": "^8.18.0"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@cyanmycelium/mcp-broker-provider": "^0.
|
|
77
|
+
"@cyanmycelium/mcp-broker-provider": "^0.2.0",
|
|
78
78
|
"@types/node": "^20.11.0",
|
|
79
79
|
"@types/ws": "^8.5.0",
|
|
80
80
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
package/src/auth/index.ts
CHANGED
|
@@ -23,12 +23,14 @@ export { HttpAuthGuard } from "./http.auth";
|
|
|
23
23
|
export { buildJwtAuth } from "./auth.config";
|
|
24
24
|
export type { IJwtAuthOptions, JwtAuthOptions } from "./auth.config";
|
|
25
25
|
export { SharedSecretProviderAuthenticator } from "./provider.auth";
|
|
26
|
-
export { normalizeProviderAuthentication, providerMayPublish } from "./provider.auth";
|
|
26
|
+
export { compileProviderAllowedResources, normalizeProviderAuthentication, providerMayPublish, providerPublishDecision } from "./provider.auth";
|
|
27
27
|
export type {
|
|
28
28
|
IProviderAuthenticator,
|
|
29
29
|
IProviderPrincipal,
|
|
30
|
+
IProviderPublishDecision,
|
|
30
31
|
ProviderAuthenticationResult,
|
|
31
32
|
ProviderAuthenticator,
|
|
32
33
|
ProviderAuthenticatorReturn,
|
|
33
34
|
ProviderPrincipal,
|
|
35
|
+
ProviderPublishDenialReason,
|
|
34
36
|
} from "./provider.auth";
|
|
@@ -40,16 +40,134 @@ export function normalizeProviderAuthentication(result: ProviderAuthenticatorRet
|
|
|
40
40
|
return result;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/** Wording shared by every "this pattern is not valid" diagnostic. */
|
|
44
|
+
const PATTERN_SYNTAX_HINT = 'Resource patterns are absolute and segment-based: "/enterprise/**", "/enterprise/*/line-3", "/enterprise/site-a/asset", or "**" for everything.';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compiled `allowedResources` patterns, keyed by the exact pattern string, with
|
|
48
|
+
* the compile failure cached alongside the successes.
|
|
49
|
+
*
|
|
50
|
+
* {@link providerMayPublish} runs on every provider WebSocket upgrade, and the
|
|
51
|
+
* previous per-call `ResourcePathPattern.parse` inside a `try` meant a single
|
|
52
|
+
* typo re-threw on every registration and was mapped to a bare `false`. Caching
|
|
53
|
+
* the outcome makes the parse happen once per distinct pattern and gives the
|
|
54
|
+
* failure a stable identity we can report exactly once.
|
|
55
|
+
*/
|
|
56
|
+
const compiledPatterns = new Map<string, ResourcePathPattern | Error>();
|
|
57
|
+
|
|
58
|
+
/** Malformed-pattern reports already emitted, so a wedged config logs once. */
|
|
59
|
+
const reportedMalformedPatterns = new Set<string>();
|
|
60
|
+
|
|
61
|
+
/** Parses one pattern, memoizing both the success and the failure. */
|
|
62
|
+
function compilePattern(pattern: string): ResourcePathPattern | Error {
|
|
63
|
+
const cached = compiledPatterns.get(pattern);
|
|
64
|
+
if (cached !== undefined) return cached;
|
|
65
|
+
let compiled: ResourcePathPattern | Error;
|
|
47
66
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
compiled = ResourcePathPattern.parse(pattern);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
compiled = error instanceof Error ? error : new Error(String(error));
|
|
70
|
+
}
|
|
71
|
+
compiledPatterns.set(pattern, compiled);
|
|
72
|
+
return compiled;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Why a provider was refused a slot. `undefined` when it was allowed. */
|
|
76
|
+
export type ProviderPublishDenialReason = "no-allowed-resources" | "out-of-namespace" | "malformed-pattern";
|
|
77
|
+
|
|
78
|
+
/** The outcome of a provider namespace check, with the cause when it denies. */
|
|
79
|
+
export interface IProviderPublishDecision {
|
|
80
|
+
readonly allowed: boolean;
|
|
81
|
+
/** Machine-readable cause, present only when `allowed` is `false`. */
|
|
82
|
+
readonly reason?: ProviderPublishDenialReason;
|
|
83
|
+
/** Operator-facing explanation that already names the fix. */
|
|
84
|
+
readonly detail?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Compiles a principal's `allowedResources`, throwing on the first malformed
|
|
89
|
+
* pattern with the offending pattern quoted in the message.
|
|
90
|
+
*
|
|
91
|
+
* Call this wherever a provider principal is *built* (config load, a custom
|
|
92
|
+
* authenticator's constructor) so a typo fails the broker at startup instead of
|
|
93
|
+
* silently 403-ing every provider on that principal forever.
|
|
94
|
+
* {@link providerPublishDecision} deliberately does not throw: it runs inside the
|
|
95
|
+
* WebSocket upgrade, where the only safe answer to "is this pattern valid?" is
|
|
96
|
+
* to deny loudly.
|
|
97
|
+
*/
|
|
98
|
+
export function compileProviderAllowedResources(allowed: readonly string[], label = "provider allowedResources"): readonly ResourcePathPattern[] {
|
|
99
|
+
return allowed.map((pattern) => {
|
|
100
|
+
const compiled = compilePattern(pattern);
|
|
101
|
+
if (compiled instanceof Error) {
|
|
102
|
+
throw new Error(`${label}: "${pattern}" is not a valid resource pattern: ${compiled.message} ${PATTERN_SYNTAX_HINT}`);
|
|
103
|
+
}
|
|
104
|
+
return compiled;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decides whether a provider principal may claim `resource`, and says why when
|
|
110
|
+
* it may not.
|
|
111
|
+
*
|
|
112
|
+
* Every denial here surfaces to the provider as a bare 403 at the WebSocket
|
|
113
|
+
* handshake, which a browser reports as a contentless error event. The reason is
|
|
114
|
+
* therefore the only thing an operator (or an agent wiring this up) has to work
|
|
115
|
+
* from, so it is returned to the caller for the registration log and, for the
|
|
116
|
+
* configuration-fault case, logged here as well.
|
|
117
|
+
*/
|
|
118
|
+
export function providerPublishDecision(principal: IProviderPrincipal, resource: ResourcePath): IProviderPublishDecision {
|
|
119
|
+
const allowed = principal.allowedResources;
|
|
120
|
+
if (allowed === undefined) return { allowed: true };
|
|
121
|
+
if (allowed.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
allowed: false,
|
|
124
|
+
reason: "no-allowed-resources",
|
|
125
|
+
detail:
|
|
126
|
+
`Provider principal "${principal.id}" has an empty allowedResources list, which forbids every slot. ` +
|
|
127
|
+
`Remove the key entirely to allow all slots, or list the namespaces this principal may publish, for example ["/enterprise/**"].`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const patterns: ResourcePathPattern[] = [];
|
|
132
|
+
const malformed: string[] = [];
|
|
133
|
+
for (const pattern of allowed) {
|
|
134
|
+
const compiled = compilePattern(pattern);
|
|
135
|
+
if (compiled instanceof Error) malformed.push(`"${pattern}" (${compiled.message})`);
|
|
136
|
+
else patterns.push(compiled);
|
|
52
137
|
}
|
|
138
|
+
|
|
139
|
+
if (malformed.length > 0) {
|
|
140
|
+
const detail =
|
|
141
|
+
`Provider principal "${principal.id}" carries ${malformed.length} malformed allowedResources pattern(s): ${malformed.join("; ")} ` +
|
|
142
|
+
`Every provider registration on this principal is refused with 403 until they are fixed, whatever slot it asks for. ${PATTERN_SYNTAX_HINT}`;
|
|
143
|
+
// Loud, but once per (principal, bad pattern set): this is a
|
|
144
|
+
// startup-class configuration fault the broker can never honor, and its
|
|
145
|
+
// request-time symptom is a 403 that names nothing.
|
|
146
|
+
const key = `${principal.id}|${malformed.join("|")}`;
|
|
147
|
+
if (!reportedMalformedPatterns.has(key)) {
|
|
148
|
+
reportedMalformedPatterns.add(key);
|
|
149
|
+
console.error(`[broker] provider auth: ${detail}`);
|
|
150
|
+
}
|
|
151
|
+
return { allowed: false, reason: "malformed-pattern", detail };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (patterns.some((pattern) => pattern.matches(resource))) return { allowed: true };
|
|
155
|
+
return {
|
|
156
|
+
allowed: false,
|
|
157
|
+
reason: "out-of-namespace",
|
|
158
|
+
detail:
|
|
159
|
+
`Provider principal "${principal.id}" may not publish resource "${resource.value}": it is outside allowedResources [${allowed.join(", ")}]. ` +
|
|
160
|
+
`Either connect the provider to a slot inside one of those namespaces, or widen allowedResources for this principal.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Boolean form of {@link providerPublishDecision}, kept for callers that only
|
|
166
|
+
* need the verdict. Prefer the decision form so the refusal can be logged with
|
|
167
|
+
* its cause.
|
|
168
|
+
*/
|
|
169
|
+
export function providerMayPublish(principal: IProviderPrincipal, resource: ResourcePath): boolean {
|
|
170
|
+
return providerPublishDecision(principal, resource).allowed;
|
|
53
171
|
}
|
|
54
172
|
|
|
55
173
|
/** Constant-time string comparison; `false` on any length mismatch. */
|
|
@@ -172,8 +172,17 @@ export class ConfigPolicyEngine implements IPolicyEngine {
|
|
|
172
172
|
if (!request.resource) return { allowed: false, reason: "invalid-resource" };
|
|
173
173
|
try {
|
|
174
174
|
validateCapability(request.capability, "request capability");
|
|
175
|
-
} catch {
|
|
176
|
-
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// Not a policy miss: the capability string itself is malformed, so no
|
|
177
|
+
// grant could ever match it. Reporting this as "no-matching-grant"
|
|
178
|
+
// pointed operators at their grants instead of at the mapping that
|
|
179
|
+
// produced the bad value.
|
|
180
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
181
|
+
console.error(
|
|
182
|
+
`[broker] authorization: capability "${String(request.capability)}" requested on resource "${request.resource.value}" is malformed, denying. ${detail} ` +
|
|
183
|
+
`Capability names look like "mcp.tools.call" or "broker.providers.read": fix the authorization.toolCapabilities / providerToolCapabilities entry that produced this value, or the caller that passed it.`
|
|
184
|
+
);
|
|
185
|
+
return { allowed: false, reason: "invalid-capability" };
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
const matchingDenies = new Set<string>();
|
|
@@ -13,7 +13,31 @@ export interface IAuthorizationRequest {
|
|
|
13
13
|
readonly tool?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Why a decision came out the way it did, as written to the audit log.
|
|
18
|
+
*
|
|
19
|
+
* The distinction between a policy outcome and a fault is load-bearing: the
|
|
20
|
+
* audit log is what an operator actually reads, and reporting a fault as
|
|
21
|
+
* `"no-matching-grant"` sends them off writing grants that can never help.
|
|
22
|
+
*
|
|
23
|
+
* - `explicit-deny` a deny policy matched.
|
|
24
|
+
* - `role-grant` an assignment matched; the only allowing reason.
|
|
25
|
+
* - `no-matching-grant` no policy matched. A genuine policy outcome.
|
|
26
|
+
* - `invalid-resource` the request carried no resource at all.
|
|
27
|
+
* - `unknown-resource` the slot maps to no configured resource path.
|
|
28
|
+
* - `invalid-capability` the requested capability string is malformed, so no
|
|
29
|
+
* grant could ever match it. A configuration fault.
|
|
30
|
+
* - `evaluation-error` evaluation threw. Nothing was decided; the request is
|
|
31
|
+
* denied because that is the safe answer. A fault.
|
|
32
|
+
*/
|
|
33
|
+
export type AuthorizationDecisionReason =
|
|
34
|
+
| "explicit-deny"
|
|
35
|
+
| "role-grant"
|
|
36
|
+
| "no-matching-grant"
|
|
37
|
+
| "invalid-resource"
|
|
38
|
+
| "unknown-resource"
|
|
39
|
+
| "invalid-capability"
|
|
40
|
+
| "evaluation-error";
|
|
17
41
|
|
|
18
42
|
export interface IAuthorizationDecision {
|
|
19
43
|
readonly allowed: boolean;
|