@aiwerk/mcp-bridge 1.0.0 → 1.0.2

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 (63) hide show
  1. package/dist/bin/mcp-bridge.d.ts +2 -0
  2. package/dist/bin/mcp-bridge.js +320 -0
  3. package/dist/src/config.d.ts +19 -0
  4. package/dist/src/config.js +145 -0
  5. package/{src/index.ts → dist/src/index.d.ts} +1 -30
  6. package/dist/src/index.js +21 -0
  7. package/dist/src/mcp-router.d.ts +65 -0
  8. package/dist/src/mcp-router.js +271 -0
  9. package/dist/src/protocol.d.ts +4 -0
  10. package/dist/src/protocol.js +58 -0
  11. package/dist/src/schema-convert.d.ts +11 -0
  12. package/dist/src/schema-convert.js +150 -0
  13. package/dist/src/standalone-server.d.ts +30 -0
  14. package/dist/src/standalone-server.js +312 -0
  15. package/dist/src/tool-naming.d.ts +3 -0
  16. package/dist/src/tool-naming.js +38 -0
  17. package/dist/src/transport-base.d.ts +76 -0
  18. package/dist/src/transport-base.js +163 -0
  19. package/dist/src/transport-sse.d.ts +16 -0
  20. package/dist/src/transport-sse.js +207 -0
  21. package/dist/src/transport-stdio.d.ts +20 -0
  22. package/dist/src/transport-stdio.js +281 -0
  23. package/dist/src/transport-streamable-http.d.ts +11 -0
  24. package/dist/src/transport-streamable-http.js +164 -0
  25. package/dist/src/types.d.ts +72 -0
  26. package/dist/src/types.js +4 -0
  27. package/dist/src/update-checker.d.ts +25 -0
  28. package/dist/src/update-checker.js +132 -0
  29. package/package.json +19 -4
  30. package/scripts/install-server.ps1 +25 -58
  31. package/scripts/install-server.sh +37 -90
  32. package/servers/apify/README.md +6 -6
  33. package/servers/github/README.md +6 -6
  34. package/servers/google-maps/README.md +6 -6
  35. package/servers/hetzner/README.md +6 -6
  36. package/servers/hostinger/README.md +6 -6
  37. package/servers/linear/README.md +6 -6
  38. package/servers/miro/README.md +6 -6
  39. package/servers/notion/README.md +6 -6
  40. package/servers/stripe/README.md +6 -6
  41. package/servers/tavily/README.md +6 -6
  42. package/servers/todoist/README.md +6 -6
  43. package/servers/wise/README.md +6 -6
  44. package/bin/mcp-bridge.js +0 -9
  45. package/bin/mcp-bridge.ts +0 -335
  46. package/src/config.ts +0 -168
  47. package/src/mcp-router.ts +0 -366
  48. package/src/protocol.ts +0 -69
  49. package/src/schema-convert.ts +0 -178
  50. package/src/standalone-server.ts +0 -385
  51. package/src/tool-naming.ts +0 -51
  52. package/src/transport-base.ts +0 -199
  53. package/src/transport-sse.ts +0 -230
  54. package/src/transport-stdio.ts +0 -312
  55. package/src/transport-streamable-http.ts +0 -188
  56. package/src/types.ts +0 -88
  57. package/src/update-checker.ts +0 -155
  58. package/tests/collision.test.ts +0 -60
  59. package/tests/env-resolve.test.ts +0 -68
  60. package/tests/mcp-router.test.ts +0 -301
  61. package/tests/schema-convert.test.ts +0 -70
  62. package/tests/transport-base.test.ts +0 -214
  63. package/tsconfig.json +0 -15
@@ -1,155 +0,0 @@
1
- import { execSync, exec as execCb } from "child_process";
2
- import { Logger } from "./types.js";
3
- import { PACKAGE_VERSION } from "./protocol.js";
4
-
5
- export interface UpdateInfo {
6
- currentVersion: string;
7
- latestVersion: string;
8
- updateAvailable: boolean;
9
- updateCommand: string;
10
- }
11
-
12
- const PACKAGE_NAME = "@aiwerk/mcp-bridge";
13
-
14
- let cachedUpdateInfo: UpdateInfo | null = null;
15
- let noticeDelivered = false;
16
-
17
- /**
18
- * Check npm registry for a newer version. Non-blocking, best-effort.
19
- * Caches result for the lifetime of the process.
20
- */
21
- export async function checkForUpdate(logger: Logger): Promise<UpdateInfo> {
22
- if (cachedUpdateInfo) return cachedUpdateInfo;
23
-
24
- const current = PACKAGE_VERSION;
25
- const updateCmd = `npm update -g ${PACKAGE_NAME}`;
26
-
27
- try {
28
- const latest = await npmViewVersion(logger);
29
- const updateAvailable = latest !== current && isNewer(latest, current);
30
-
31
- cachedUpdateInfo = {
32
- currentVersion: current,
33
- latestVersion: latest,
34
- updateAvailable,
35
- updateCommand: updateCmd,
36
- };
37
-
38
- if (updateAvailable) {
39
- logger.info(`[mcp-bridge] Update available: ${current} → ${latest}`);
40
- } else {
41
- logger.info(`[mcp-bridge] Version ${current} is up to date`);
42
- }
43
- } catch (err) {
44
- logger.warn(`[mcp-bridge] Version check failed: ${err instanceof Error ? err.message : err}`);
45
- cachedUpdateInfo = {
46
- currentVersion: current,
47
- latestVersion: current,
48
- updateAvailable: false,
49
- updateCommand: updateCmd,
50
- };
51
- }
52
-
53
- return cachedUpdateInfo;
54
- }
55
-
56
- /**
57
- * Build the notice string to inject into the first tool response.
58
- * Returns empty string if no update or already delivered.
59
- */
60
- export function getUpdateNotice(): string {
61
- if (noticeDelivered || !cachedUpdateInfo?.updateAvailable) return "";
62
- noticeDelivered = true;
63
- return (
64
- `\n\n---\nUpdate available: ${cachedUpdateInfo.currentVersion} → ${cachedUpdateInfo.latestVersion}\n` +
65
- `Run: ${cachedUpdateInfo.updateCommand}`
66
- );
67
- }
68
-
69
- /**
70
- * Reset the notice flag (for testing).
71
- */
72
- export function resetNoticeFlag(): void {
73
- noticeDelivered = false;
74
- }
75
-
76
- /**
77
- * Execute the actual npm update. Returns a result message.
78
- */
79
- export async function runUpdate(logger: Logger): Promise<string> {
80
- const info = cachedUpdateInfo ?? await checkForUpdate(logger);
81
-
82
- if (!info.updateAvailable) {
83
- return `MCP Bridge is already up to date (v${info.currentVersion}).`;
84
- }
85
-
86
- logger.info(`[mcp-bridge] Running update: ${info.updateCommand}`);
87
-
88
- try {
89
- const output = await execAsync(info.updateCommand, 60_000);
90
- // Invalidate cache so next check re-fetches
91
- cachedUpdateInfo = null;
92
- noticeDelivered = false;
93
-
94
- // Verify new version
95
- const newVersion = npmViewVersionSync(logger);
96
- return (
97
- `MCP Bridge updated: ${info.currentVersion} → ${newVersion}\n` +
98
- `A restart is needed to load the new version.\n\n` +
99
- `npm output:\n${output.trim()}`
100
- );
101
- } catch (err) {
102
- const msg = err instanceof Error ? err.message : String(err);
103
- logger.error(`[mcp-bridge] Update failed: ${msg}`);
104
- return (
105
- `Update failed. You can try manually:\n` +
106
- `${info.updateCommand}\n\nError: ${msg}`
107
- );
108
- }
109
- }
110
-
111
- // --- helpers ---
112
-
113
- function npmViewVersion(_logger: Logger): Promise<string> {
114
- return new Promise((resolve, reject) => {
115
- const timeout = setTimeout(() => reject(new Error("npm view timed out")), 10_000);
116
- execCb(`npm view ${PACKAGE_NAME} version`, { encoding: "utf-8" }, (err, stdout) => {
117
- clearTimeout(timeout);
118
- if (err) return reject(err);
119
- const ver = (stdout ?? "").trim();
120
- if (!ver) return reject(new Error("empty version from npm"));
121
- resolve(ver);
122
- });
123
- });
124
- }
125
-
126
- function npmViewVersionSync(_logger: Logger): string {
127
- try {
128
- return execSync(`npm view ${PACKAGE_NAME} version`, { encoding: "utf-8", timeout: 10_000 }).trim();
129
- } catch {
130
- return "unknown";
131
- }
132
- }
133
-
134
- function execAsync(cmd: string, timeoutMs: number): Promise<string> {
135
- return new Promise((resolve, reject) => {
136
- const timeout = setTimeout(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)), timeoutMs);
137
- execCb(cmd, { encoding: "utf-8", timeout: timeoutMs }, (err, stdout, stderr) => {
138
- clearTimeout(timeout);
139
- if (err) return reject(new Error(`${err.message}\n${stderr ?? ""}`));
140
- resolve(stdout ?? "");
141
- });
142
- });
143
- }
144
-
145
- function isNewer(latest: string, current: string): boolean {
146
- const l = latest.split(".").map(Number);
147
- const c = current.split(".").map(Number);
148
- for (let i = 0; i < Math.max(l.length, c.length); i++) {
149
- const lv = l[i] ?? 0;
150
- const cv = c[i] ?? 0;
151
- if (lv > cv) return true;
152
- if (lv < cv) return false;
153
- }
154
- return false;
155
- }
@@ -1,60 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { pickRegisteredToolName } from "../src/tool-naming.ts";
4
-
5
- test("auto mode: collision causes second server tool to be prefixed", () => {
6
- const globalNames = new Set<string>();
7
-
8
- // "auto" (default) — no prefix unless collision
9
- const first = pickRegisteredToolName("alpha", "search", "auto", new Set<string>(), globalNames);
10
- globalNames.add(first);
11
-
12
- const second = pickRegisteredToolName("beta", "search", "auto", new Set<string>(), globalNames);
13
-
14
- assert.equal(first, "search");
15
- assert.equal(second, "beta_search");
16
- });
17
-
18
- test("auto mode: no collision means no prefix", () => {
19
- const globalNames = new Set<string>();
20
-
21
- const first = pickRegisteredToolName("alpha", "search", "auto", new Set<string>(), globalNames);
22
- globalNames.add(first);
23
-
24
- const second = pickRegisteredToolName("beta", "list", "auto", new Set<string>(), globalNames);
25
-
26
- assert.equal(first, "search");
27
- assert.equal(second, "list");
28
- });
29
-
30
- test("auto mode: undefined defaults to auto", () => {
31
- const globalNames = new Set<string>();
32
-
33
- const first = pickRegisteredToolName("alpha", "search", undefined, new Set<string>(), globalNames);
34
- globalNames.add(first);
35
-
36
- const second = pickRegisteredToolName("beta", "search", undefined, new Set<string>(), globalNames);
37
-
38
- assert.equal(first, "search");
39
- assert.equal(second, "beta_search");
40
- });
41
-
42
- test("true mode: always prefixes", () => {
43
- const globalNames = new Set<string>();
44
-
45
- const name = pickRegisteredToolName("alpha", "search", true, new Set<string>(), globalNames);
46
-
47
- assert.equal(name, "alpha_search");
48
- });
49
-
50
- test("false mode: never prefixes, uses suffix on collision", () => {
51
- const globalNames = new Set<string>();
52
-
53
- const first = pickRegisteredToolName("alpha", "search", false, new Set<string>(), globalNames);
54
- globalNames.add(first);
55
-
56
- const second = pickRegisteredToolName("beta", "search", false, new Set<string>(), globalNames);
57
-
58
- assert.equal(first, "search");
59
- assert.equal(second, "search_2");
60
- });
@@ -1,68 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { resolveEnvVars, resolveEnvRecord, resolveArgs } from "../src/transport-base.ts";
4
-
5
- test("resolveEnvRecord throws when env var is missing", () => {
6
- assert.throws(
7
- () => resolveEnvRecord({ TOKEN: "${MISSING_TEST_ENV}" }, "env key"),
8
- /Missing required environment variable/
9
- );
10
- });
11
-
12
- test("resolveArgs resolves env vars in args", () => {
13
- const env = { MY_TOKEN: "secret123" };
14
- const result = resolveArgs(["--token", "${MY_TOKEN}", "--verbose"], env);
15
- assert.deepStrictEqual(result, ["--token", "secret123", "--verbose"]);
16
- });
17
-
18
- test("resolveArgs throws when env var is missing in args", () => {
19
- assert.throws(
20
- () => resolveArgs(["--token", "${MISSING_TEST_ENV}"], {}),
21
- /Missing required environment variable/
22
- );
23
- });
24
-
25
- test("resolveArgs passes through args without variables", () => {
26
- const result = resolveArgs(["-y", "@llmindset/mcp-miro", "--verbose"], {});
27
- assert.deepStrictEqual(result, ["-y", "@llmindset/mcp-miro", "--verbose"]);
28
- });
29
-
30
- test("resolveEnvRecord resolves headers with env vars", () => {
31
- process.env.__TEST_MCP_TOKEN = "test-secret-456";
32
- try {
33
- const result = resolveEnvRecord(
34
- { Authorization: "Bearer ${__TEST_MCP_TOKEN}" },
35
- "header"
36
- );
37
- assert.deepStrictEqual(result, { Authorization: "Bearer test-secret-456" });
38
- } finally {
39
- delete process.env.__TEST_MCP_TOKEN;
40
- }
41
- });
42
-
43
- test("resolveEnvRecord throws for missing header env var", () => {
44
- assert.throws(
45
- () => resolveEnvRecord({ Authorization: "Bearer ${MISSING_TEST_ENV}" }, "header"),
46
- /Missing required environment variable/
47
- );
48
- });
49
-
50
- test("resolveEnvVars resolves single value", () => {
51
- process.env.__TEST_MCP_SINGLE = "hello";
52
- try {
53
- const result = resolveEnvVars("prefix-${__TEST_MCP_SINGLE}-suffix", "test");
54
- assert.equal(result, "prefix-hello-suffix");
55
- } finally {
56
- delete process.env.__TEST_MCP_SINGLE;
57
- }
58
- });
59
-
60
- test("resolveEnvVars uses extraEnv before process.env", () => {
61
- process.env.__TEST_MCP_PRIO = "from-process";
62
- try {
63
- const result = resolveEnvVars("${__TEST_MCP_PRIO}", "test", { __TEST_MCP_PRIO: "from-extra" });
64
- assert.equal(result, "from-extra");
65
- } finally {
66
- delete process.env.__TEST_MCP_PRIO;
67
- }
68
- });
@@ -1,301 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { McpRouter } from "../src/mcp-router.ts";
4
- import type { McpRequest, McpResponse, McpServerConfig, McpTransport, McpTool } from "../src/types.ts";
5
-
6
- type Behavior = {
7
- tools: McpTool[];
8
- callResult?: any;
9
- callError?: { code: number; message: string };
10
- connectError?: Error;
11
- };
12
-
13
- class MockTransport implements McpTransport {
14
- static behaviors = new Map<string, Behavior>();
15
- static instances = new Map<string, MockTransport>();
16
-
17
- static reset(): void {
18
- this.behaviors.clear();
19
- this.instances.clear();
20
- }
21
-
22
- connected = false;
23
- requests: McpRequest[] = [];
24
- connectCount = 0;
25
- disconnectCount = 0;
26
- private readonly key: string;
27
-
28
- constructor(config: McpServerConfig, _clientConfig?: any, _logger?: any, _onReconnected?: () => Promise<void>) {
29
- this.key = config.url || config.command || "default";
30
- MockTransport.instances.set(this.key, this);
31
- }
32
-
33
- async connect(): Promise<void> {
34
- this.connectCount += 1;
35
- const behavior = MockTransport.behaviors.get(this.key);
36
- if (behavior?.connectError) {
37
- throw behavior.connectError;
38
- }
39
- this.connected = true;
40
- }
41
-
42
- async disconnect(): Promise<void> {
43
- this.disconnectCount += 1;
44
- this.connected = false;
45
- }
46
-
47
- async sendRequest(request: McpRequest): Promise<McpResponse> {
48
- this.requests.push(request);
49
- const behavior = MockTransport.behaviors.get(this.key);
50
-
51
- if (request.method === "initialize") {
52
- return { jsonrpc: "2.0", id: 1, result: {} };
53
- }
54
-
55
- if (request.method === "tools/list") {
56
- return { jsonrpc: "2.0", id: 2, result: { tools: behavior?.tools || [] } };
57
- }
58
-
59
- if (request.method === "tools/call") {
60
- if (behavior?.callError) {
61
- return {
62
- jsonrpc: "2.0",
63
- id: 3,
64
- error: {
65
- code: behavior.callError.code,
66
- message: behavior.callError.message
67
- }
68
- };
69
- }
70
- return { jsonrpc: "2.0", id: 3, result: behavior?.callResult || { ok: true } };
71
- }
72
-
73
- return { jsonrpc: "2.0", id: 4, result: {} };
74
- }
75
-
76
- async sendNotification(_notification: any): Promise<void> {
77
- return;
78
- }
79
-
80
- isConnected(): boolean {
81
- return this.connected;
82
- }
83
- }
84
-
85
- function makeLogger() {
86
- return {
87
- info: () => {},
88
- warn: () => {},
89
- error: () => {},
90
- debug: () => {}
91
- };
92
- }
93
-
94
- function makeRouter(
95
- servers: Record<string, McpServerConfig>,
96
- overrides: { routerIdleTimeoutMs?: number; routerMaxConcurrent?: number } = {}
97
- ): McpRouter {
98
- return new McpRouter(
99
- servers,
100
- {
101
- servers,
102
- routerIdleTimeoutMs: overrides.routerIdleTimeoutMs,
103
- routerMaxConcurrent: overrides.routerMaxConcurrent
104
- },
105
- makeLogger(),
106
- {
107
- sse: MockTransport,
108
- stdio: MockTransport,
109
- streamableHttp: MockTransport
110
- }
111
- );
112
- }
113
-
114
- test.beforeEach(() => {
115
- MockTransport.reset();
116
- });
117
-
118
- test("dispatch returns unknown_server error for missing server", async () => {
119
- const router = makeRouter({
120
- alpha: { transport: "sse", url: "mock://alpha" }
121
- });
122
-
123
- const result = await router.dispatch("missing", "list");
124
- assert.equal("error" in result ? result.error : "", "unknown_server");
125
- });
126
-
127
- test("dispatch action=list returns cached tool list", async () => {
128
- const server = { transport: "sse" as const, url: "mock://cache" };
129
- MockTransport.behaviors.set("mock://cache", {
130
- tools: [
131
- {
132
- name: "create_server",
133
- description: "Create server",
134
- inputSchema: {
135
- type: "object",
136
- properties: { name: { type: "string" } },
137
- required: ["name"]
138
- }
139
- }
140
- ]
141
- });
142
-
143
- const router = makeRouter({ cache: server });
144
- const first = await router.dispatch("cache", "list");
145
- const second = await router.dispatch("cache", "list");
146
-
147
- assert.equal("error" in first, false);
148
- assert.equal("error" in second, false);
149
- if (!("error" in first) && first.action === "list") {
150
- assert.deepEqual(first.tools, [
151
- {
152
- name: "create_server",
153
- description: "Create server",
154
- requiredParams: ["name"]
155
- }
156
- ]);
157
- }
158
-
159
- const instance = MockTransport.instances.get("mock://cache");
160
- assert.ok(instance);
161
- const listCalls = instance!.requests.filter((req) => req.method === "tools/list");
162
- assert.equal(listCalls.length, 1);
163
- });
164
-
165
- test("dispatch action=call proxies to transport", async () => {
166
- MockTransport.behaviors.set("mock://call", {
167
- tools: [{ name: "list_servers", description: "List", inputSchema: { type: "object" } }],
168
- callResult: { servers: [{ id: "1" }] }
169
- });
170
-
171
- const router = makeRouter({
172
- call: { transport: "sse", url: "mock://call" }
173
- });
174
-
175
- const result = await router.dispatch("call", "call", "list_servers", { region: "eu-central" });
176
- assert.equal("error" in result, false);
177
- if (!("error" in result) && result.action === "call") {
178
- assert.deepEqual(result.result, { servers: [{ id: "1" }] });
179
- }
180
-
181
- const instance = MockTransport.instances.get("mock://call");
182
- assert.ok(instance);
183
- const callRequest = instance!.requests.find((req) => req.method === "tools/call");
184
- assert.ok(callRequest);
185
- assert.deepEqual(callRequest!.params, {
186
- name: "list_servers",
187
- arguments: { region: "eu-central" }
188
- });
189
- });
190
-
191
- test("evicts least recently used connection when max concurrent exceeded", async () => {
192
- const servers = {
193
- a: { transport: "sse" as const, url: "mock://a" },
194
- b: { transport: "sse" as const, url: "mock://b" },
195
- c: { transport: "sse" as const, url: "mock://c" }
196
- };
197
-
198
- for (const key of Object.keys(servers)) {
199
- MockTransport.behaviors.set(`mock://${key}`, {
200
- tools: [{ name: "ping", description: "Ping", inputSchema: { type: "object" } }]
201
- });
202
- }
203
-
204
- const router = makeRouter(servers, { routerMaxConcurrent: 2, routerIdleTimeoutMs: 60_000 });
205
-
206
- await router.dispatch("a", "list");
207
- await new Promise((resolve) => setTimeout(resolve, 5));
208
- await router.dispatch("b", "list");
209
- await new Promise((resolve) => setTimeout(resolve, 5));
210
- await router.dispatch("c", "list");
211
-
212
- assert.equal(MockTransport.instances.get("mock://a")?.disconnectCount, 1);
213
- assert.equal(MockTransport.instances.get("mock://b")?.isConnected(), true);
214
- assert.equal(MockTransport.instances.get("mock://c")?.isConnected(), true);
215
- });
216
-
217
- test("disconnects idle connection after timeout", async () => {
218
- MockTransport.behaviors.set("mock://idle", {
219
- tools: [{ name: "ping", description: "Ping", inputSchema: { type: "object" } }]
220
- });
221
-
222
- const router = makeRouter(
223
- { idle: { transport: "sse", url: "mock://idle" } },
224
- { routerIdleTimeoutMs: 25, routerMaxConcurrent: 5 }
225
- );
226
-
227
- await router.dispatch("idle", "list");
228
- await new Promise((resolve) => setTimeout(resolve, 80));
229
-
230
- const instance = MockTransport.instances.get("mock://idle");
231
- assert.ok(instance);
232
- assert.equal(instance!.disconnectCount >= 1, true);
233
- assert.equal(instance!.isConnected(), false);
234
- });
235
-
236
- test("generateDescription includes configured servers", () => {
237
- const description = McpRouter.generateDescription({
238
- hetzner: { transport: "sse", url: "mock://h" },
239
- github: { transport: "sse", url: "mock://g" }
240
- });
241
-
242
- assert.match(description, /hetzner/);
243
- assert.match(description, /github/);
244
- assert.match(description, /action='list'/);
245
- assert.match(description, /action='call'/);
246
- assert.match(description, /action='refresh'/);
247
- });
248
-
249
- test("status action returns all servers with connection state", async () => {
250
- const servers = {
251
- alpha: { transport: "stdio" as const, command: "node", args: ["fake.js"] },
252
- beta: { transport: "sse" as const, url: "http://localhost:9999/sse" }
253
- };
254
- const router = makeRouter(servers);
255
-
256
- const result = await router.dispatch(undefined, "status");
257
- assert.equal("action" in result && result.action, "status");
258
- if ("servers" in result) {
259
- assert.equal(result.servers.length, 2);
260
- const alpha = result.servers.find(s => s.name === "alpha");
261
- assert.ok(alpha);
262
- assert.equal(alpha!.status, "disconnected");
263
- assert.equal(alpha!.tools, 0);
264
- assert.equal(alpha!.transport, "stdio");
265
- const beta = result.servers.find(s => s.name === "beta");
266
- assert.ok(beta);
267
- assert.equal(beta!.status, "disconnected");
268
- assert.equal(beta!.transport, "sse");
269
- }
270
- });
271
-
272
- test("status action shows connected server after list", async () => {
273
- const servers = {
274
- alpha: { transport: "stdio" as const, command: "node", args: ["fake.js"] }
275
- };
276
- MockTransport.behaviors.set("node", {
277
- tools: [
278
- { name: "tool_a", description: "A", inputSchema: { type: "object" } },
279
- { name: "tool_b", description: "B", inputSchema: { type: "object" } }
280
- ]
281
- });
282
- const router = makeRouter(servers);
283
-
284
- // List triggers connection + tool fetch
285
- await router.dispatch("alpha", "list");
286
-
287
- const result = await router.dispatch(undefined, "status");
288
- if ("servers" in result) {
289
- const alpha = result.servers.find(s => s.name === "alpha");
290
- assert.ok(alpha);
291
- assert.equal(alpha!.status, "connected");
292
- assert.equal(alpha!.tools, 2);
293
- }
294
- });
295
-
296
- test("generateDescription includes status action", () => {
297
- const description = McpRouter.generateDescription({
298
- test: { transport: "stdio", command: "node", args: [] }
299
- });
300
- assert.match(description, /action='status'/);
301
- });
@@ -1,70 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- convertJsonSchemaToTypeBox,
5
- createToolParameters,
6
- setTypeBoxLoader
7
- } from "../src/schema-convert.ts";
8
-
9
- test("converts string schema", async () => {
10
- const schema = await convertJsonSchemaToTypeBox({ type: "string", minLength: 1 });
11
- assert.equal(schema.type, "string");
12
- assert.equal(schema.minLength, 1);
13
- });
14
-
15
- test("converts number schema", async () => {
16
- const schema = await convertJsonSchemaToTypeBox({ type: "number", minimum: 2, maximum: 5 });
17
- assert.equal(schema.type, "number");
18
- assert.equal(schema.minimum, 2);
19
- assert.equal(schema.maximum, 5);
20
- });
21
-
22
- test("converts object schema", async () => {
23
- const schema = await convertJsonSchemaToTypeBox({
24
- type: "object",
25
- properties: {
26
- name: { type: "string" },
27
- age: { type: "number" }
28
- },
29
- required: ["name"]
30
- });
31
-
32
- assert.equal(schema.type, "object");
33
- assert.ok(schema.properties.name);
34
- assert.ok(schema.properties.age);
35
- });
36
-
37
- test("converts array schema", async () => {
38
- const schema = await convertJsonSchemaToTypeBox({ type: "array", items: { type: "string" } });
39
- assert.equal(schema.type, "array");
40
- assert.equal(schema.items.type, "string");
41
- });
42
-
43
- test("converts anyOf schema", async () => {
44
- const schema = await convertJsonSchemaToTypeBox({
45
- anyOf: [{ type: "string" }, { type: "number" }]
46
- });
47
-
48
- assert.ok(Array.isArray(schema.anyOf));
49
- assert.equal(schema.anyOf.length, 2);
50
- assert.equal(schema.anyOf[0].type, "string");
51
- assert.equal(schema.anyOf[1].type, "number");
52
- });
53
-
54
- test("falls back when TypeBox is missing", async () => {
55
- // Inject a loader that simulates missing TypeBox
56
- setTypeBoxLoader(async () => null);
57
-
58
- try {
59
- const schema = await convertJsonSchemaToTypeBox({ type: "string" });
60
- const params = await createToolParameters({ type: "object" });
61
-
62
- // convertJsonSchemaToTypeBox fallback: empty schema {}
63
- assert.deepStrictEqual(schema, {});
64
- // createToolParameters fallback: returns raw inputSchema as-is
65
- assert.deepStrictEqual(params, { type: "object" });
66
- } finally {
67
- // Restore default loader
68
- setTypeBoxLoader(null);
69
- }
70
- });