@cyanmycelium/mcp-broker 0.1.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 (78) hide show
  1. package/.mcp-broker.example/README.md +53 -0
  2. package/.mcp-broker.example/config.json +32 -0
  3. package/.mcp-broker.example/grammars/claude/fr.json +7 -0
  4. package/LICENSE +201 -0
  5. package/README.md +253 -0
  6. package/dist/bin.d.ts +2 -0
  7. package/dist/bin.js +208 -0
  8. package/dist/bin.js.map +1 -0
  9. package/dist/broker/adapters/broker.adapter.info.d.ts +16 -0
  10. package/dist/broker/adapters/broker.adapter.info.js +43 -0
  11. package/dist/broker/adapters/broker.adapter.info.js.map +1 -0
  12. package/dist/broker/adapters/broker.adapter.providers.d.ts +18 -0
  13. package/dist/broker/adapters/broker.adapter.providers.js +61 -0
  14. package/dist/broker/adapters/broker.adapter.providers.js.map +1 -0
  15. package/dist/broker/behaviors/broker.behavior.info.d.ts +15 -0
  16. package/dist/broker/behaviors/broker.behavior.info.js +41 -0
  17. package/dist/broker/behaviors/broker.behavior.info.js.map +1 -0
  18. package/dist/broker/behaviors/broker.behavior.providers.d.ts +19 -0
  19. package/dist/broker/behaviors/broker.behavior.providers.js +69 -0
  20. package/dist/broker/behaviors/broker.behavior.providers.js.map +1 -0
  21. package/dist/broker/broker.context.d.ts +59 -0
  22. package/dist/broker/broker.context.js +2 -0
  23. package/dist/broker/broker.context.js.map +1 -0
  24. package/dist/broker/broker.grammars.d.ts +166 -0
  25. package/dist/broker/broker.grammars.js +258 -0
  26. package/dist/broker/broker.grammars.js.map +1 -0
  27. package/dist/broker/broker.server.d.ts +64 -0
  28. package/dist/broker/broker.server.js +111 -0
  29. package/dist/broker/broker.server.js.map +1 -0
  30. package/dist/broker/grammars/claude/en.json +16 -0
  31. package/dist/broker/grammars/claude/fr.json +16 -0
  32. package/dist/broker/grammars/default/en.json +32 -0
  33. package/dist/broker/grammars/default/fr.json +32 -0
  34. package/dist/broker/grammars/default/zh.json +32 -0
  35. package/dist/broker/index.d.ts +9 -0
  36. package/dist/broker/index.js +7 -0
  37. package/dist/broker/index.js.map +1 -0
  38. package/dist/config.d.ts +101 -0
  39. package/dist/config.js +61 -0
  40. package/dist/config.js.map +1 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.js +11 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/stdio.upstream.d.ts +42 -0
  45. package/dist/stdio.upstream.js +85 -0
  46. package/dist/stdio.upstream.js.map +1 -0
  47. package/dist/version.d.ts +2 -0
  48. package/dist/version.js +9 -0
  49. package/dist/version.js.map +1 -0
  50. package/dist/ws.tunnel.builder.d.ts +133 -0
  51. package/dist/ws.tunnel.builder.js +197 -0
  52. package/dist/ws.tunnel.builder.js.map +1 -0
  53. package/dist/ws.tunnel.d.ts +310 -0
  54. package/dist/ws.tunnel.js +971 -0
  55. package/dist/ws.tunnel.js.map +1 -0
  56. package/package.json +86 -0
  57. package/scripts/copy-assets.mjs +34 -0
  58. package/scripts/gen-cert.mjs +94 -0
  59. package/src/bin.ts +231 -0
  60. package/src/broker/adapters/broker.adapter.info.ts +46 -0
  61. package/src/broker/adapters/broker.adapter.providers.ts +67 -0
  62. package/src/broker/behaviors/broker.behavior.info.ts +46 -0
  63. package/src/broker/behaviors/broker.behavior.providers.ts +82 -0
  64. package/src/broker/broker.context.ts +75 -0
  65. package/src/broker/broker.grammars.ts +336 -0
  66. package/src/broker/broker.server.ts +168 -0
  67. package/src/broker/grammars/claude/en.json +16 -0
  68. package/src/broker/grammars/claude/fr.json +16 -0
  69. package/src/broker/grammars/default/en.json +32 -0
  70. package/src/broker/grammars/default/fr.json +32 -0
  71. package/src/broker/grammars/default/zh.json +32 -0
  72. package/src/broker/index.ts +24 -0
  73. package/src/config.ts +155 -0
  74. package/src/index.ts +26 -0
  75. package/src/stdio.upstream.ts +114 -0
  76. package/src/version.ts +10 -0
  77. package/src/ws.tunnel.builder.ts +214 -0
  78. package/src/ws.tunnel.ts +1269 -0
@@ -0,0 +1,46 @@
1
+ import { McpBehavior } from "@cyanmycelium/mcp-core";
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";
6
+
7
+ /**
8
+ * Exposes basic broker identity (name, version, uptime, listening config) as
9
+ * one tool (`broker_info`) and one resource (`broker://info`).
10
+ *
11
+ * An MCP agent typically calls `broker_info` first to learn who it is talking to.
12
+ */
13
+ export class BrokerInfoBehavior extends McpBehavior {
14
+ public static readonly NAMESPACE = "broker";
15
+
16
+ constructor(context: BrokerContext) {
17
+ super(new BrokerInfoAdapter(context), {
18
+ namespace: BrokerInfoBehavior.NAMESPACE,
19
+ });
20
+ }
21
+
22
+ protected override _buildResources(): McpResource[] {
23
+ return [
24
+ {
25
+ uri: BROKER_INFO_URI,
26
+ name: brokerBaselineResourceName(BROKER_INFO_URI),
27
+ mimeType: "application/json",
28
+ description: brokerBaselineResourceDescription(BROKER_INFO_URI),
29
+ },
30
+ ];
31
+ }
32
+
33
+ protected override _buildTools(): McpTool[] {
34
+ return [
35
+ {
36
+ name: "broker_info",
37
+ description: brokerBaselineToolDescription("broker_info"),
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {},
41
+ additionalProperties: false,
42
+ },
43
+ },
44
+ ];
45
+ }
46
+ }
@@ -0,0 +1,82 @@
1
+ import { McpBehavior } from "@cyanmycelium/mcp-core";
2
+ import type { McpResource, McpResourceTemplate, McpTool } from "@cyanmycelium/mcp-core";
3
+ import { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "../adapters/broker.adapter.providers.js";
4
+ import {
5
+ brokerBaselinePropertyDescription,
6
+ brokerBaselineResourceDescription,
7
+ brokerBaselineResourceName,
8
+ brokerBaselineResourceTemplateDescription,
9
+ brokerBaselineResourceTemplateName,
10
+ brokerBaselineToolDescription,
11
+ } from "../broker.grammars.js";
12
+ import type { BrokerContext } from "../broker.context.js";
13
+
14
+ /**
15
+ * Exposes the broker's provider slots so an MCP agent can discover what is
16
+ * currently routable behind the broker, with two tools:
17
+ *
18
+ * - `providers_list` — every slot (including disconnected ones).
19
+ * - `provider_status({ name })` — detail on one slot.
20
+ *
21
+ * Plus matching resources at `broker://providers` and `broker://providers/<name>`.
22
+ */
23
+ export class BrokerProvidersBehavior extends McpBehavior {
24
+ public static readonly NAMESPACE = "broker_providers";
25
+
26
+ constructor(context: BrokerContext) {
27
+ super(new BrokerProvidersAdapter(context), {
28
+ namespace: BrokerProvidersBehavior.NAMESPACE,
29
+ });
30
+ }
31
+
32
+ protected override _buildResources(): McpResource[] {
33
+ return [
34
+ {
35
+ uri: PROVIDERS_URI,
36
+ name: brokerBaselineResourceName(PROVIDERS_URI),
37
+ mimeType: "application/json",
38
+ description: brokerBaselineResourceDescription(PROVIDERS_URI),
39
+ },
40
+ ];
41
+ }
42
+
43
+ protected override _buildTemplate(): McpResourceTemplate[] {
44
+ return [
45
+ {
46
+ uriTemplate: PROVIDER_URI_TEMPLATE,
47
+ name: brokerBaselineResourceTemplateName(PROVIDER_URI_TEMPLATE),
48
+ mimeType: "application/json",
49
+ description: brokerBaselineResourceTemplateDescription(PROVIDER_URI_TEMPLATE),
50
+ },
51
+ ];
52
+ }
53
+
54
+ protected override _buildTools(): McpTool[] {
55
+ return [
56
+ {
57
+ name: "providers_list",
58
+ description: brokerBaselineToolDescription("providers_list"),
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {},
62
+ additionalProperties: false,
63
+ },
64
+ },
65
+ {
66
+ name: "provider_status",
67
+ description: brokerBaselineToolDescription("provider_status"),
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ name: {
72
+ type: "string",
73
+ description: brokerBaselinePropertyDescription("provider_status", "name"),
74
+ },
75
+ },
76
+ required: ["name"],
77
+ additionalProperties: false,
78
+ },
79
+ },
80
+ ];
81
+ }
82
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Read-only view of the broker's runtime state, exposed to broker behaviors.
3
+ *
4
+ * Decouples the behaviors from the concrete `WsTunnel` class, making them
5
+ * unit-testable and reusable (e.g. a future .NET-backed context).
6
+ */
7
+ export interface BrokerContext {
8
+ /** Package version (from package.json). */
9
+ readonly version: string;
10
+
11
+ /** Logical broker name reported to MCP clients. */
12
+ readonly name: string;
13
+
14
+ /** Timestamp of the most recent successful `start()`, or `null` if never started. */
15
+ readonly startedAt: Date | null;
16
+
17
+ /** Seconds since `startedAt`, or `0` if not running. */
18
+ readonly uptimeSeconds: number;
19
+
20
+ /** Bind host. `undefined` means default (`0.0.0.0`). */
21
+ readonly host: string | undefined;
22
+
23
+ /** TCP port the broker is listening on. */
24
+ readonly port: number;
25
+
26
+ /** Whether TLS is active for the HTTP/WS server. */
27
+ readonly tls: boolean;
28
+
29
+ /** All configured URL paths, with defaults already substituted. */
30
+ readonly paths: {
31
+ provider: string;
32
+ providers: string;
33
+ client: string;
34
+ mcp: string;
35
+ sse: string;
36
+ messages: string;
37
+ };
38
+
39
+ /** Snapshot of every known provider slot, including disconnected ones. */
40
+ getProvidersInfo(): BrokerProviderInfo[];
41
+
42
+ /** Snapshot of a single provider slot, or `undefined` if the name is unknown. */
43
+ getProviderInfo(name: string): BrokerProviderInfo | undefined;
44
+ }
45
+
46
+ /**
47
+ * Transport kind currently feeding a provider slot.
48
+ *
49
+ * - `ws`: dedicated WebSocket provider (`/provider/<name>`).
50
+ * - `ws-multiplex`: multiplexed WebSocket envelope on `/providers`.
51
+ * - `stdio`: child process spawned at broker startup.
52
+ * - `loopback`: in-process transport (e.g. the broker exposing itself as `_broker`).
53
+ * - `none`: the slot was referenced by a client but no provider has attached yet.
54
+ */
55
+ export type BrokerProviderTransport = "ws" | "ws-multiplex" | "stdio" | "loopback" | "none";
56
+
57
+ export interface BrokerProviderInfo {
58
+ /** Slot name as advertised on `/<name>/...` endpoints. */
59
+ name: string;
60
+
61
+ /** Which transport is currently feeding the slot. */
62
+ transport: BrokerProviderTransport;
63
+
64
+ /** `true` iff the slot is reachable for routing right now. */
65
+ connected: boolean;
66
+
67
+ /** Number of raw-WebSocket MCP clients on this slot. */
68
+ clientCount: number;
69
+
70
+ /** Number of long-lived sessions (SSE + Streamable HTTP GET streams). */
71
+ sessionCount: number;
72
+
73
+ /** Number of in-flight JSON-RPC requests awaiting a response. */
74
+ pendingCount: number;
75
+ }
@@ -0,0 +1,336 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { McpClientInfo } from "@cyanmycelium/mcp-core";
5
+ import { McpGrammar } from "@cyanmycelium/mcp-core";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Open string types — extensibility by convention
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * Locale identifier used to look up a grammar JSON file under
13
+ * `<userAgent>/<locale>.json`. Open string: an application can introduce any
14
+ * value its custom resolver and JSON resources support.
15
+ *
16
+ * The {@link defaultBrokerLocaleResolver} returns the ISO 639-1 prefix of a
17
+ * BCP-47 tag (`"fr-CA"` → `"fr"`, `"zh-Hans"` → `"zh"`).
18
+ */
19
+ export type BrokerLocale = string;
20
+
21
+ /**
22
+ * User-agent family identifier used to look up a grammar JSON file under
23
+ * `<userAgent>/<locale>.json`. Open string. The {@link defaultBrokerUserAgentResolver}
24
+ * recognizes the common LLM families and returns `"default"` for everything else.
25
+ */
26
+ export type BrokerUserAgent = string;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Resolver function types
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Picks an **ordered fallback chain** of {@link BrokerLocale} values from a raw
34
+ * input. Typically the raw input is `process.env.MCP_BROKER_LOCALE`, but any
35
+ * string source works (HTTP header, session metadata, etc.).
36
+ *
37
+ * The returned array is consumed by the broker server most-specific-first, so
38
+ * the resolver controls the BCP-47 narrowing policy. The default resolver
39
+ * follows the standard `lang-region` → `lang` → `en` shape:
40
+ *
41
+ * ```
42
+ * raw = "fr-CA" → ["fr-ca", "fr", "en"]
43
+ * raw = "en-US" → ["en-us", "en"]
44
+ * raw = "zh" → ["zh", "en"]
45
+ * raw = "" → ["en"]
46
+ * ```
47
+ *
48
+ * A custom resolver may shape the chain however it wants — e.g. inject a
49
+ * project-specific dialect first, skip the bare language prefix, or pull
50
+ * candidates from a session config.
51
+ */
52
+ export type BrokerLocaleResolver = (raw: string | undefined) => BrokerLocale[];
53
+
54
+ /**
55
+ * Picks a {@link BrokerUserAgent} from the connecting client's identity. Called
56
+ * by the embedded broker `McpServer` once per session, during the MCP
57
+ * `initialize` handshake.
58
+ */
59
+ export type BrokerUserAgentResolver = (clientInfo: McpClientInfo | undefined) => BrokerUserAgent;
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Default resolvers
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Default locale resolver — emits the BCP-47 narrowing chain for a raw locale
67
+ * tag, from most specific to least specific, always ending with the universal
68
+ * `"en"` fallback.
69
+ *
70
+ * Steps for an input `raw`:
71
+ * 1. Lowercase the input.
72
+ * 2. Push it as the most-specific candidate (only if non-empty).
73
+ * 3. If it contains a `-` separator, push its bare language prefix next.
74
+ * 4. Always push `"en"` last as the universal fallback.
75
+ *
76
+ * The broker server tries each candidate in turn against `<userAgent>/<locale>.json`
77
+ * — so dropping a `claude/fr-ca.json` lets Canadian-French Claude clients
78
+ * pick up that specific dialect, while clients with `fr` or `fr-FR` fall back
79
+ * to `claude/fr.json` or `default/fr.json` automatically.
80
+ *
81
+ * Examples:
82
+ * - `"fr-CA"` → `["fr-ca", "fr", "en"]`
83
+ * - `"fr"` → `["fr", "en"]`
84
+ * - `"zh-CN"` → `["zh-cn", "zh", "en"]`
85
+ * - `"en-US"` → `["en-us", "en"]`
86
+ * - `""` / `undefined` → `["en"]`
87
+ */
88
+ export const defaultBrokerLocaleResolver: BrokerLocaleResolver = (raw) => {
89
+ const a = [];
90
+ if (raw) {
91
+ const sep = "-";
92
+ raw = raw.toLowerCase();
93
+ a.push(raw);
94
+ if (raw.indexOf(sep) !== -1) {
95
+ a.push(raw.split(sep)[0]);
96
+ }
97
+ }
98
+ a.push("en");
99
+ return a;
100
+ };
101
+
102
+ /**
103
+ * Default user-agent resolver — substring match on `clientInfo.name` against
104
+ * a list of known LLM family hints. Unknown clients fall through to
105
+ * `"default"` which is the universal baseline.
106
+ *
107
+ * This is intentionally a heuristic: MCP does not yet standardize an
108
+ * agent-family field in `clientInfo`. Override the resolver in the broker
109
+ * options if you need richer logic (header inspection, allow-list, etc.).
110
+ */
111
+ export const defaultBrokerUserAgentResolver: BrokerUserAgentResolver = (clientInfo) => {
112
+ const n = (clientInfo?.name ?? "").toLowerCase();
113
+ if (n.includes("claude")) return "claude";
114
+ if (n.includes("gpt") || n.includes("openai")) return "gpt";
115
+ if (n.includes("mistral")) return "mistral";
116
+ if (n.includes("copilot")) return "copilot";
117
+ return "default";
118
+ };
119
+
120
+ /** @deprecated Use {@link defaultBrokerLocaleResolver}. Kept as backward-compat alias. */
121
+ export const resolveBrokerLocale = defaultBrokerLocaleResolver;
122
+ /** @deprecated Use {@link defaultBrokerUserAgentResolver}. Kept as backward-compat alias. */
123
+ export const resolveBrokerUserAgent: (clientName: string | undefined) => BrokerUserAgent = (clientName) => defaultBrokerUserAgentResolver({ name: clientName ?? "", version: "" });
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Canonical grammar key
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Builds the canonical grammar key for the `(userAgent, locale)` matrix.
131
+ *
132
+ * Pattern: `"<userAgent>:<locale>"` — e.g. `"claude:fr"`, `"default:en"`.
133
+ * The colon separator is reserved for this composition and never appears in
134
+ * user-agent or locale identifiers.
135
+ */
136
+ export function brokerGrammarKey(userAgent: BrokerUserAgent, locale: BrokerLocale): string {
137
+ return `${userAgent}:${locale}`;
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // JSON resource loading
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Absolute path of the directory holding the grammar JSON resources.
146
+ *
147
+ * Layout (one folder per user-agent family, one JSON file per locale):
148
+ * ```
149
+ * <GRAMMARS_DIR>/
150
+ * ├── default/
151
+ * │ ├── en.json
152
+ * │ ├── fr.json
153
+ * │ └── zh.json
154
+ * └── claude/
155
+ * ├── en.json
156
+ * └── fr.json
157
+ * ```
158
+ *
159
+ * JSON files live alongside this module in `src/broker/grammars/` during
160
+ * development and are mirrored under `dist/broker/grammars/` at build time
161
+ * by `scripts/copy-assets.mjs`. Adding a new `(userAgent, locale)` pair is
162
+ * just a matter of dropping a new JSON file — no code change required.
163
+ */
164
+ const GRAMMARS_DIR = join(dirname(fileURLToPath(import.meta.url)), "grammars");
165
+
166
+ const _cache = new Map<string, McpGrammar>();
167
+
168
+ /**
169
+ * Loads and caches the grammar for a given `(userAgent, locale)` combination.
170
+ * Returns `undefined` (instead of throwing) when the resource file is missing,
171
+ * so the caller can implement a fallback chain.
172
+ */
173
+ export function loadBrokerGrammar(userAgent: BrokerUserAgent, locale: BrokerLocale): McpGrammar | undefined {
174
+ const key = brokerGrammarKey(userAgent, locale);
175
+ const cached = _cache.get(key);
176
+ if (cached) return cached;
177
+
178
+ const path = join(GRAMMARS_DIR, userAgent, `${locale}.json`);
179
+ if (!existsSync(path)) return undefined;
180
+
181
+ const raw = readFileSync(path, "utf-8");
182
+ const data = JSON.parse(raw);
183
+ const grammar = McpGrammar.fromJSON(data);
184
+ _cache.set(key, grammar);
185
+ return grammar;
186
+ }
187
+
188
+ /**
189
+ * Walks a grammars directory and yields every `(userAgent, locale)` pair
190
+ * found on disk. The directory must follow the layout
191
+ * `<dir>/<userAgent>/<locale>.json`.
192
+ *
193
+ * Used by the broker server at startup to bulk-register both the packaged
194
+ * grammars and any local overrides. No hard-coded list of supported
195
+ * user-agents or locales — adding a new grammar is dropping a JSON file.
196
+ */
197
+ export function* iterBrokerGrammarsFrom(grammarsDir: string): Generator<{
198
+ userAgent: BrokerUserAgent;
199
+ locale: BrokerLocale;
200
+ key: string;
201
+ grammar: McpGrammar;
202
+ }> {
203
+ if (!existsSync(grammarsDir)) return;
204
+
205
+ const userAgents = readdirSync(grammarsDir).sort();
206
+ for (const userAgent of userAgents) {
207
+ const uaDir = join(grammarsDir, userAgent);
208
+ if (!statSync(uaDir).isDirectory()) continue;
209
+
210
+ const files = readdirSync(uaDir).sort();
211
+ for (const file of files) {
212
+ if (!file.endsWith(".json")) continue;
213
+ const locale = file.slice(0, -".json".length);
214
+ const path = join(uaDir, file);
215
+ try {
216
+ const raw = readFileSync(path, "utf-8");
217
+ const data = JSON.parse(raw);
218
+ const grammar = McpGrammar.fromJSON(data);
219
+ yield { userAgent, locale, key: brokerGrammarKey(userAgent, locale), grammar };
220
+ } catch (err) {
221
+ process.stderr.write(`[mcp-broker] Failed to load grammar ${path}: ${(err as Error).message}\n`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Walks the **packaged** grammars directory (the one shipped with the
229
+ * mcp-broker package). Equivalent to `iterBrokerGrammarsFrom(<packaged-dir>)`.
230
+ *
231
+ * For local user overrides, see {@link iterBrokerGrammarsFrom} with a custom
232
+ * directory — typically `.mcp-broker/grammars/` next to the config file.
233
+ */
234
+ export function* iterAvailableBrokerGrammars(): Generator<{
235
+ userAgent: BrokerUserAgent;
236
+ locale: BrokerLocale;
237
+ key: string;
238
+ grammar: McpGrammar;
239
+ }> {
240
+ yield* iterBrokerGrammarsFrom(GRAMMARS_DIR);
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Baseline helpers (used by behaviors to source their inline descriptions)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /**
248
+ * Returns the baseline grammar used by the broker behaviors as their
249
+ * source-of-truth for inline tool / property descriptions.
250
+ *
251
+ * Conventionally this is `default:en`. Session-specific grammars selected by
252
+ * the resolver override individual entries on top of this baseline.
253
+ *
254
+ * Throws if the JSON resource is missing — the broker behaviors cannot be
255
+ * built without baseline descriptions.
256
+ */
257
+ export function brokerBaselineGrammar(): McpGrammar {
258
+ const g = loadBrokerGrammar("default", "en");
259
+ if (!g) {
260
+ throw new Error(`Required baseline broker grammar "default:en" is missing — expected at ${join(GRAMMARS_DIR, "default", "en.json")}.`);
261
+ }
262
+ return g;
263
+ }
264
+
265
+ /**
266
+ * Convenience accessor for a baseline tool description. Throws when the
267
+ * tool is not listed in the baseline grammar — i.e. the JSON file is missing
268
+ * an entry for a tool the code knows about.
269
+ */
270
+ export function brokerBaselineToolDescription(toolName: string): string {
271
+ const desc = brokerBaselineGrammar().getToolDescription(toolName);
272
+ if (!desc) {
273
+ throw new Error(`Missing baseline description for tool "${toolName}" in default/en.json.`);
274
+ }
275
+ return desc;
276
+ }
277
+
278
+ /**
279
+ * Convenience accessor for a baseline property description. Throws when the
280
+ * property is not listed under the tool in the baseline grammar.
281
+ */
282
+ export function brokerBaselinePropertyDescription(toolName: string, propertyName: string): string {
283
+ const desc = brokerBaselineGrammar().getPropertyDescription(toolName, propertyName);
284
+ if (!desc) {
285
+ throw new Error(`Missing baseline description for property "${propertyName}" of tool "${toolName}" in default/en.json.`);
286
+ }
287
+ return desc;
288
+ }
289
+
290
+ /**
291
+ * Convenience accessor for a baseline resource name. Throws when the resource
292
+ * URI has no entry in the baseline grammar.
293
+ */
294
+ export function brokerBaselineResourceName(uri: string): string {
295
+ const name = brokerBaselineGrammar().getResourceName(uri);
296
+ if (!name) {
297
+ throw new Error(`Missing baseline name for resource "${uri}" in default/en.json.`);
298
+ }
299
+ return name;
300
+ }
301
+
302
+ /**
303
+ * Convenience accessor for a baseline resource description. Throws when the
304
+ * resource URI has no entry in the baseline grammar.
305
+ */
306
+ export function brokerBaselineResourceDescription(uri: string): string {
307
+ const desc = brokerBaselineGrammar().getResourceDescription(uri);
308
+ if (!desc) {
309
+ throw new Error(`Missing baseline description for resource "${uri}" in default/en.json.`);
310
+ }
311
+ return desc;
312
+ }
313
+
314
+ /**
315
+ * Convenience accessor for a baseline resource template name. Throws when the
316
+ * template URI has no entry in the baseline grammar.
317
+ */
318
+ export function brokerBaselineResourceTemplateName(uriTemplate: string): string {
319
+ const name = brokerBaselineGrammar().getResourceTemplateName(uriTemplate);
320
+ if (!name) {
321
+ throw new Error(`Missing baseline name for resource template "${uriTemplate}" in default/en.json.`);
322
+ }
323
+ return name;
324
+ }
325
+
326
+ /**
327
+ * Convenience accessor for a baseline resource template description. Throws
328
+ * when the template URI has no entry in the baseline grammar.
329
+ */
330
+ export function brokerBaselineResourceTemplateDescription(uriTemplate: string): string {
331
+ const desc = brokerBaselineGrammar().getResourceTemplateDescription(uriTemplate);
332
+ if (!desc) {
333
+ throw new Error(`Missing baseline description for resource template "${uriTemplate}" in default/en.json.`);
334
+ }
335
+ return desc;
336
+ }