@cyanmycelium/mcp-broker 0.3.0 → 0.4.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.
@@ -1,9 +1,8 @@
1
- import { McpGrammar, McpServerBuilder, LoopbackTransport } from "@cyanmycelium/mcp-core";
2
- import type { IMcpServer, IMessageTransport } from "@cyanmycelium/mcp-core";
1
+ import { McpServerBuilder, LoopbackTransport } from "@cyanmycelium/mcp-core";
2
+ import type { GrammarResolverOptions, IMcpServer, IMessageTransport } from "@cyanmycelium/mcp-core";
3
3
  import { BrokerInfoBehavior } from "./behaviors/broker.behavior.info.js";
4
4
  import { BrokerProvidersBehavior } from "./behaviors/broker.behavior.providers.js";
5
- import { brokerGrammarKey, defaultBrokerLocaleResolver, defaultBrokerUserAgentResolver, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom } from "./broker.grammars.js";
6
- import type { BrokerLocaleResolver, BrokerUserAgent, BrokerUserAgentResolver } from "./broker.grammars.js";
5
+ import { iterAvailableBrokerGrammars, iterBrokerGrammarsFrom } from "./broker.grammars.js";
7
6
  import type { BrokerContext } from "./broker.context.js";
8
7
 
9
8
  /**
@@ -22,32 +21,33 @@ export const BROKER_PROVIDER_NAME = "_broker";
22
21
  */
23
22
  export interface StartBrokerServerOptions {
24
23
  /**
25
- * Picks a locale (BCP-47 base language, by convention) for the current
26
- * session. Defaults to {@link defaultBrokerLocaleResolver} which reads
27
- * `MCP_BROKER_LOCALE` and keeps the ISO 639-1 prefix.
28
- */
29
- localeResolver?: BrokerLocaleResolver;
30
-
31
- /**
32
- * Picks a user-agent family for the connecting client. Defaults to
33
- * {@link defaultBrokerUserAgentResolver} which substring-matches
34
- * `clientInfo.name` against known LLM families.
35
- */
36
- userAgentResolver?: BrokerUserAgentResolver;
37
-
38
- /**
39
- * Source of the raw locale string fed to the locale resolver. Defaults
40
- * to `process.env.MCP_BROKER_LOCALE`. Override when the locale lives
41
- * somewhere else (config file, session metadata, HTTP header proxy, ...).
24
+ * Overrides for the built-in grammar resolver from `@cyanmycelium/mcp-core`.
25
+ *
26
+ * The broker installs sensible defaults: `localeSource` reads
27
+ * `process.env.MCP_BROKER_LOCALE`, the `agents` map uses the mcp-core
28
+ * defaults (`claude`, `gpt`, `mistral`, `copilot`, `default`), the
29
+ * narrowing chain is BCP-47-style, and `fallbackKey` is `default:en`
30
+ * so the baseline grammar always matches as last resort.
31
+ *
32
+ * Pass partial overrides here to inject a custom `localeSource` (e.g.
33
+ * pull from an HTTP header proxied by your transport), enable the
34
+ * `versionFrom` dimension, or extend the `agents` map with additional
35
+ * LLM families. Anything you omit keeps the broker default.
42
36
  */
43
- localeSource?: () => string | undefined;
37
+ grammarResolverOptions?: Partial<GrammarResolverOptions>;
44
38
 
45
39
  /**
46
40
  * Path to a user-supplied grammars directory whose `<userAgent>/<locale>.json`
47
- * files are merged **on top of** the packaged grammars. Local entries win
48
- * on conflicts; missing entries fall through to the packaged values.
41
+ * files are registered **in addition to** the packaged grammars.
42
+ *
43
+ * Both packaged and local entries are registered raw against the server
44
+ * via `withGrammar(brokerGrammarKey(ua, locale), grammar)`. The
45
+ * candidate-chain resolution implemented by `McpServer.initialize` in
46
+ * mcp-core@0.3.0 then walks the chain and merges the four layers
47
+ * (behavior, adapter, static, store) for the first matching key —
48
+ * the old hand-rolled pre-merge cascade is no longer needed.
49
49
  *
50
- * When `undefined` (default), only the packaged grammars are used.
50
+ * When `undefined` (default), only the packaged grammars are loaded.
51
51
  */
52
52
  localGrammarsDir?: string;
53
53
  }
@@ -75,10 +75,6 @@ export async function startBrokerServer(
75
75
  server: IMcpServer;
76
76
  clientTransport: IMessageTransport;
77
77
  }> {
78
- const localeResolver = options.localeResolver ?? defaultBrokerLocaleResolver;
79
- const userAgentResolver = options.userAgentResolver ?? defaultBrokerUserAgentResolver;
80
- const localeSource = options.localeSource ?? (() => process.env["MCP_BROKER_LOCALE"]);
81
-
82
78
  const [serverEnd, clientEnd] = LoopbackTransport.createPair();
83
79
 
84
80
  // Without an initializer, McpServerBuilder reports `version: "0.0.0"` in the
@@ -94,79 +90,30 @@ export async function startBrokerServer(
94
90
  })
95
91
  .register(new BrokerInfoBehavior(context), new BrokerProvidersBehavior(context));
96
92
 
97
- // Discover every grammar by scanning two directories:
98
- // 1. The packaged grammars shipped with the broker.
99
- // 2. Optionally, a user-supplied directory whose entries are merged
100
- // ON TOP of the packaged ones (local wins on conflicts).
101
- //
102
- // Then build the (userAgent × locale) matrix. Within each user-agent,
103
- // the grammar cascades on top of "default:<locale>" so per-user-agent
104
- // files can stay partial.
105
- const defaults = new Map<string, McpGrammar>(); // locale → grammar
106
- const overrides = new Map<BrokerUserAgent, Map<string, McpGrammar>>(); // userAgent → locale → grammar
107
-
108
- function ingest(entry: { userAgent: BrokerUserAgent; locale: string; grammar: McpGrammar }, isOverride: boolean): void {
109
- if (entry.userAgent === "default") {
110
- const existing = defaults.get(entry.locale);
111
- const merged = existing && isOverride ? McpGrammar.merge(existing, entry.grammar) : entry.grammar;
112
- defaults.set(entry.locale, merged);
113
- } else {
114
- let byLocale = overrides.get(entry.userAgent);
115
- if (!byLocale) {
116
- byLocale = new Map();
117
- overrides.set(entry.userAgent, byLocale);
118
- }
119
- const existing = byLocale.get(entry.locale);
120
- const merged = existing && isOverride ? McpGrammar.merge(existing, entry.grammar) : entry.grammar;
121
- byLocale.set(entry.locale, merged);
122
- }
93
+ // Register every `(userAgent, locale)` JSON found on disk as a raw
94
+ // grammar layer. The candidate-chain resolution in
95
+ // McpServer.initialize (mcp-core@0.3.0) walks the resolver's chain
96
+ // and merges all matching layers — so partial user-agent files no
97
+ // longer need to be pre-merged with the default-locale baseline at
98
+ // boot. Local overrides come after packaged entries; identical keys
99
+ // get overlaid via the registry's last-write-wins.
100
+ for (const entry of iterAvailableBrokerGrammars()) {
101
+ builder.withGrammar(entry.key, entry.grammar);
123
102
  }
124
-
125
- for (const entry of iterAvailableBrokerGrammars()) ingest(entry, false);
126
103
  if (options.localGrammarsDir) {
127
- for (const entry of iterBrokerGrammarsFrom(options.localGrammarsDir)) ingest(entry, true);
128
- }
129
-
130
- const availableKeys = new Set<string>();
131
-
132
- // Register every "default:<locale>" as-is.
133
- for (const [locale, grammar] of defaults) {
134
- const key = brokerGrammarKey("default", locale);
135
- builder.withGrammar(key, grammar);
136
- availableKeys.add(key);
137
- }
138
-
139
- // Register every "<userAgent>:<locale>" as merge(default:<locale>, ua:<locale>).
140
- // The user-agent grammar wins per entry; missing entries cascade from default.
141
- for (const [agent, byLocale] of overrides) {
142
- for (const [locale, uaGrammar] of byLocale) {
143
- const baseline = defaults.get(locale);
144
- const merged = baseline ? McpGrammar.merge(baseline, uaGrammar) : uaGrammar;
145
- const key = brokerGrammarKey(agent, locale);
146
- builder.withGrammar(key, merged);
147
- availableKeys.add(key);
104
+ for (const entry of iterBrokerGrammarsFrom(options.localGrammarsDir)) {
105
+ builder.withGrammar(entry.key, entry.grammar);
148
106
  }
149
107
  }
150
108
 
151
- builder.withGrammarResolver((clientInfo) => {
152
- // localeResolver returns the full fallback chain (most specific first),
153
- // ending with the universal "en". The user-agent axis is the broker's
154
- // own concern: per locale, prefer the user-agent-specific grammar, then
155
- // fall back to the "default" agent.
156
- const locales = localeResolver(localeSource());
157
- const userAgent = userAgentResolver(clientInfo);
158
- const userAgents = userAgent === "default" ? ["default"] : [userAgent, "default"];
159
-
160
- for (const locale of locales) {
161
- for (const ua of userAgents) {
162
- const key = brokerGrammarKey(ua, locale);
163
- if (availableKeys.has(key)) return key;
164
- }
165
- }
166
-
167
- // None of the candidates is on disk → return an empty key. McpServer
168
- // then falls back to the behavior's baseline descriptions (English).
169
- return "";
109
+ // Wire the resolver via mcp-core's declarative helper. The broker
110
+ // ships sensible defaults (locale from MCP_BROKER_LOCALE env, agents
111
+ // from mcp-core's catalogue, BCP-47 narrowing, default:en fallback);
112
+ // anything the host application sets via `grammarResolverOptions`
113
+ // takes precedence.
114
+ builder.withGrammarResolver({
115
+ localeSource: () => process.env["MCP_BROKER_LOCALE"],
116
+ ...(options.grammarResolverOptions ?? {}),
170
117
  });
171
118
 
172
119
  // No reconnect policy → the McpServer does not attempt to reopen the loopback
@@ -13,12 +13,10 @@ export {
13
13
  brokerBaselineResourceTemplateName,
14
14
  brokerBaselineToolDescription,
15
15
  brokerGrammarKey,
16
- defaultBrokerLocaleResolver,
17
- defaultBrokerUserAgentResolver,
18
16
  iterAvailableBrokerGrammars,
17
+ iterBrokerGrammarsFrom,
19
18
  loadBrokerGrammar,
20
- resolveBrokerLocale,
21
- resolveBrokerUserAgent,
19
+ parseBrokerGrammarStem,
22
20
  } from "./broker.grammars.js";
23
- export type { BrokerLocale, BrokerLocaleResolver, BrokerUserAgent, BrokerUserAgentResolver } from "./broker.grammars.js";
21
+ export type { BrokerGrammarEntry, BrokerLocale, BrokerUserAgent } from "./broker.grammars.js";
24
22
  export type { BrokerContext, BrokerProviderInfo, BrokerProviderTransport } from "./broker.context.js";
package/src/index.ts CHANGED
@@ -15,16 +15,8 @@ export { unzipMcpb } from "./mcpb.unzip.js";
15
15
  // Broker introspection — tier 1.
16
16
  export { BrokerInfoBehavior, BrokerProvidersBehavior, startBrokerServer, BROKER_PROVIDER_NAME } from "./broker/index.js";
17
17
  export type { StartBrokerServerOptions } from "./broker/index.js";
18
- export {
19
- brokerGrammarKey,
20
- defaultBrokerLocaleResolver,
21
- defaultBrokerUserAgentResolver,
22
- iterAvailableBrokerGrammars,
23
- loadBrokerGrammar,
24
- resolveBrokerLocale,
25
- resolveBrokerUserAgent,
26
- } from "./broker/index.js";
27
- export type { BrokerContext, BrokerProviderInfo, BrokerProviderTransport, BrokerLocale, BrokerLocaleResolver, BrokerUserAgent, BrokerUserAgentResolver } from "./broker/index.js";
18
+ export { brokerGrammarKey, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerGrammar } from "./broker/index.js";
19
+ export type { BrokerContext, BrokerProviderInfo, BrokerProviderTransport, BrokerLocale, BrokerUserAgent } from "./broker/index.js";
28
20
 
29
21
  export { VERSION, PACKAGE_NAME } from "./version.js";
30
22
 
package/src/ws.tunnel.ts CHANGED
@@ -5,12 +5,12 @@ import * as nodePath from "path";
5
5
  import { randomUUID } from "crypto";
6
6
  import type { IncomingMessage, ServerResponse } from "http";
7
7
  import { WebSocket, WebSocketServer } from "ws";
8
- import type { IMessageTransport, IMcpServer } from "@cyanmycelium/mcp-core";
8
+ import type { GrammarResolverOptions, IMessageTransport, IMcpServer } from "@cyanmycelium/mcp-core";
9
9
  import { StdioUpstream, type StdioUpstreamConfig } from "./stdio.upstream.js";
10
10
  import { RemoteUpstream, type RemoteUpstreamConfig } from "./remote.upstream.js";
11
11
  import type { Upstream } from "./upstream.js";
12
12
  import { startBrokerServer, BROKER_PROVIDER_NAME } from "./broker/index.js";
13
- import type { BrokerContext, BrokerLocaleResolver, BrokerProviderInfo, BrokerProviderTransport, BrokerUserAgentResolver } from "./broker/index.js";
13
+ import type { BrokerContext, BrokerProviderInfo, BrokerProviderTransport } from "./broker/index.js";
14
14
  import { AggregateServer } from "./broker/aggregate/aggregate.server.js";
15
15
  import { VERSION, PACKAGE_NAME } from "./version.js";
16
16
 
@@ -257,30 +257,26 @@ export interface WsTunnelOptions {
257
257
  brokerName?: string;
258
258
 
259
259
  /**
260
- * Custom resolver picking the grammar locale for the embedded broker
261
- * server. Defaults to `defaultBrokerLocaleResolver` (keeps the ISO 639-1
262
- * prefix of a BCP-47 tag read from `MCP_BROKER_LOCALE`).
263
- */
264
- brokerLocaleResolver?: BrokerLocaleResolver;
265
-
266
- /**
267
- * Custom resolver mapping a connecting client's identity to a user-agent
268
- * family. Defaults to `defaultBrokerUserAgentResolver` (substring match on
269
- * `clientInfo.name` against known LLM families).
270
- */
271
- brokerUserAgentResolver?: BrokerUserAgentResolver;
272
-
273
- /**
274
- * Custom source of the raw locale string fed to the locale resolver.
275
- * Defaults to `() => process.env.MCP_BROKER_LOCALE`. Override when the
276
- * locale should come from a config file, HTTP header, etc.
260
+ * Overrides for the embedded broker server's grammar resolver — passed
261
+ * straight through to `mcp-core`'s `grammarResolverFromOptions`. The
262
+ * broker installs its own default `localeSource` (reads
263
+ * `process.env.MCP_BROKER_LOCALE`); anything you set here wins.
264
+ *
265
+ * Use this to inject a custom `localeSource` (e.g. read from an HTTP
266
+ * header proxied by your transport), enable the optional `versionFrom`
267
+ * dimension, or extend the `agents` map with additional LLM families.
277
268
  */
278
- brokerLocaleSource?: () => string | undefined;
269
+ brokerGrammarResolverOptions?: Partial<GrammarResolverOptions>;
279
270
 
280
271
  /**
281
272
  * Path to a user-supplied grammars directory whose `<userAgent>/<locale>.json`
282
- * files are merged **on top of** the packaged grammars used by the embedded
283
- * broker server. Typically pointed at `.mcp-broker/grammars/`.
273
+ * files are registered alongside the packaged grammars used by the
274
+ * embedded broker server. Typically pointed at `.mcp-broker/grammars/`.
275
+ *
276
+ * The candidate-chain resolution in `McpServer.initialize`
277
+ * (mcp-core@0.3.0) handles cascade across user-agent and locale
278
+ * dimensions, so partial files no longer need to be pre-merged with a
279
+ * baseline.
284
280
  */
285
281
  brokerLocalGrammarsDir?: string;
286
282
  }
@@ -662,9 +658,7 @@ export class WsTunnel implements BrokerContext {
662
658
  private async _maybeStartBrokerServer(): Promise<void> {
663
659
  if (this._options.enableBrokerProvider === false) return;
664
660
  const { server, clientTransport } = await startBrokerServer(this, {
665
- localeResolver: this._options.brokerLocaleResolver,
666
- userAgentResolver: this._options.brokerUserAgentResolver,
667
- localeSource: this._options.brokerLocaleSource,
661
+ grammarResolverOptions: this._options.brokerGrammarResolverOptions,
668
662
  localGrammarsDir: this._options.brokerLocalGrammarsDir,
669
663
  });
670
664
  this._brokerServer = server;