@esankhan3/anvil-agent-core 0.1.2 → 0.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.
Files changed (122) hide show
  1. package/dist/__tests__/fetch-pool.integration.test.d.ts +15 -0
  2. package/dist/__tests__/fetch-pool.integration.test.d.ts.map +1 -0
  3. package/dist/__tests__/fetch-pool.integration.test.js +109 -0
  4. package/dist/__tests__/fetch-pool.integration.test.js.map +1 -0
  5. package/dist/__tests__/fetch-pool.test.d.ts +15 -0
  6. package/dist/__tests__/fetch-pool.test.d.ts.map +1 -0
  7. package/dist/__tests__/fetch-pool.test.js +115 -0
  8. package/dist/__tests__/fetch-pool.test.js.map +1 -0
  9. package/dist/__tests__/mcp-merged-executor.test.d.ts +2 -0
  10. package/dist/__tests__/mcp-merged-executor.test.d.ts.map +1 -0
  11. package/dist/__tests__/mcp-merged-executor.test.js +221 -0
  12. package/dist/__tests__/mcp-merged-executor.test.js.map +1 -0
  13. package/dist/__tests__/mcp.test.js +21 -15
  14. package/dist/__tests__/mcp.test.js.map +1 -1
  15. package/dist/__tests__/model-catalog.test.d.ts +11 -0
  16. package/dist/__tests__/model-catalog.test.d.ts.map +1 -0
  17. package/dist/__tests__/model-catalog.test.js +102 -0
  18. package/dist/__tests__/model-catalog.test.js.map +1 -0
  19. package/dist/__tests__/model-tier-resolver.test.d.ts +19 -0
  20. package/dist/__tests__/model-tier-resolver.test.d.ts.map +1 -0
  21. package/dist/__tests__/model-tier-resolver.test.js +97 -0
  22. package/dist/__tests__/model-tier-resolver.test.js.map +1 -0
  23. package/dist/adk-adapter.d.ts.map +1 -1
  24. package/dist/adk-adapter.js +11 -0
  25. package/dist/adk-adapter.js.map +1 -1
  26. package/dist/adk-anthropic-llm.d.ts.map +1 -1
  27. package/dist/adk-anthropic-llm.js +23 -10
  28. package/dist/adk-anthropic-llm.js.map +1 -1
  29. package/dist/agent/session/__tests__/spawn-telemetry.test.js +39 -4
  30. package/dist/agent/session/__tests__/spawn-telemetry.test.js.map +1 -1
  31. package/dist/agent/session/adapter.d.ts +44 -0
  32. package/dist/agent/session/adapter.d.ts.map +1 -1
  33. package/dist/agent/session/adapter.js +1 -0
  34. package/dist/agent/session/adapter.js.map +1 -1
  35. package/dist/agent/session/default-adapter-factory.d.ts.map +1 -1
  36. package/dist/agent/session/default-adapter-factory.js +66 -2
  37. package/dist/agent/session/default-adapter-factory.js.map +1 -1
  38. package/dist/agent/session/language-model-bridge.d.ts.map +1 -1
  39. package/dist/agent/session/language-model-bridge.js +59 -2
  40. package/dist/agent/session/language-model-bridge.js.map +1 -1
  41. package/dist/agent/session/session.d.ts +16 -0
  42. package/dist/agent/session/session.d.ts.map +1 -1
  43. package/dist/agent/session/session.js +86 -2
  44. package/dist/agent/session/session.js.map +1 -1
  45. package/dist/agent/session/types.d.ts +12 -0
  46. package/dist/agent/session/types.d.ts.map +1 -1
  47. package/dist/agent/session/types.js.map +1 -1
  48. package/dist/fetch-pool.d.ts +47 -0
  49. package/dist/fetch-pool.d.ts.map +1 -0
  50. package/dist/fetch-pool.js +132 -0
  51. package/dist/fetch-pool.js.map +1 -0
  52. package/dist/gemini-adapter.d.ts.map +1 -1
  53. package/dist/gemini-adapter.js +30 -6
  54. package/dist/gemini-adapter.js.map +1 -1
  55. package/dist/gemini-cli-adapter.d.ts.map +1 -1
  56. package/dist/gemini-cli-adapter.js +10 -0
  57. package/dist/gemini-cli-adapter.js.map +1 -1
  58. package/dist/index.d.ts +6 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +7 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mcp/client.d.ts +64 -18
  63. package/dist/mcp/client.d.ts.map +1 -1
  64. package/dist/mcp/client.js +190 -26
  65. package/dist/mcp/client.js.map +1 -1
  66. package/dist/mcp/index.d.ts +5 -2
  67. package/dist/mcp/index.d.ts.map +1 -1
  68. package/dist/mcp/index.js +3 -8
  69. package/dist/mcp/index.js.map +1 -1
  70. package/dist/mcp/merged-executor.d.ts +74 -0
  71. package/dist/mcp/merged-executor.d.ts.map +1 -0
  72. package/dist/mcp/merged-executor.js +198 -0
  73. package/dist/mcp/merged-executor.js.map +1 -0
  74. package/dist/mcp/pool.d.ts +71 -0
  75. package/dist/mcp/pool.d.ts.map +1 -0
  76. package/dist/mcp/pool.js +171 -0
  77. package/dist/mcp/pool.js.map +1 -0
  78. package/dist/mcp/tool-merger.d.ts +8 -5
  79. package/dist/mcp/tool-merger.d.ts.map +1 -1
  80. package/dist/mcp/tool-merger.js +8 -16
  81. package/dist/mcp/tool-merger.js.map +1 -1
  82. package/dist/model-catalog.d.ts +36 -0
  83. package/dist/model-catalog.d.ts.map +1 -0
  84. package/dist/model-catalog.js +102 -0
  85. package/dist/model-catalog.js.map +1 -0
  86. package/dist/model-tier-resolver.d.ts +61 -0
  87. package/dist/model-tier-resolver.d.ts.map +1 -0
  88. package/dist/model-tier-resolver.js +170 -0
  89. package/dist/model-tier-resolver.js.map +1 -0
  90. package/dist/ollama-adapter.d.ts.map +1 -1
  91. package/dist/ollama-adapter.js +33 -8
  92. package/dist/ollama-adapter.js.map +1 -1
  93. package/dist/opencode-adapter.d.ts +11 -0
  94. package/dist/opencode-adapter.d.ts.map +1 -1
  95. package/dist/opencode-adapter.js +13 -0
  96. package/dist/opencode-adapter.js.map +1 -1
  97. package/dist/openrouter-adapter.d.ts +15 -0
  98. package/dist/openrouter-adapter.d.ts.map +1 -1
  99. package/dist/openrouter-adapter.js +76 -19
  100. package/dist/openrouter-adapter.js.map +1 -1
  101. package/dist/provider-liveness.d.ts +5 -0
  102. package/dist/provider-liveness.d.ts.map +1 -1
  103. package/dist/provider-liveness.js +28 -21
  104. package/dist/provider-liveness.js.map +1 -1
  105. package/dist/registry.d.ts +1 -0
  106. package/dist/registry.d.ts.map +1 -1
  107. package/dist/registry.js +1 -1
  108. package/dist/registry.js.map +1 -1
  109. package/dist/router/local-executor.d.ts.map +1 -1
  110. package/dist/router/local-executor.js +19 -6
  111. package/dist/router/local-executor.js.map +1 -1
  112. package/dist/single-shot.d.ts.map +1 -1
  113. package/dist/single-shot.js +26 -1
  114. package/dist/single-shot.js.map +1 -1
  115. package/dist/tools/builtin.d.ts +19 -0
  116. package/dist/tools/builtin.d.ts.map +1 -1
  117. package/dist/tools/builtin.js +51 -0
  118. package/dist/tools/builtin.js.map +1 -1
  119. package/dist/upstream-error.d.ts.map +1 -1
  120. package/dist/upstream-error.js +11 -1
  121. package/dist/upstream-error.js.map +1 -1
  122. package/package.json +3 -2
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Integration test — proves the heal-on-failure fix actually works.
3
+ *
4
+ * Without the fix, a poisoned pool fails every subsequent fetch with the
5
+ * same TypeError until the process restarts. With the fix:
6
+ * - call 1 succeeds against a live server
7
+ * - server dies → call 2 fails (TypeError: fetch failed)
8
+ * - recycleFetchPoolOnFailure() runs
9
+ * - server comes back → call 3 succeeds against the healed pool
10
+ *
11
+ * This is the test the plan in docs/FETCH-POOL-MANAGEMENT-PLAN.md §6.2
12
+ * specifies as the canary for the entire fix.
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=fetch-pool.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-pool.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Integration test — proves the heal-on-failure fix actually works.
3
+ *
4
+ * Without the fix, a poisoned pool fails every subsequent fetch with the
5
+ * same TypeError until the process restarts. With the fix:
6
+ * - call 1 succeeds against a live server
7
+ * - server dies → call 2 fails (TypeError: fetch failed)
8
+ * - recycleFetchPoolOnFailure() runs
9
+ * - server comes back → call 3 succeeds against the healed pool
10
+ *
11
+ * This is the test the plan in docs/FETCH-POOL-MANAGEMENT-PLAN.md §6.2
12
+ * specifies as the canary for the entire fix.
13
+ */
14
+ import { describe, it, before, after, beforeEach } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import http from 'node:http';
17
+ import { getFetchPool, recycleFetchPoolOnFailure, getPoolMetrics, resetAllPools, } from '../fetch-pool.js';
18
+ describe('fetch-pool integration — heal-on-failure end-to-end', () => {
19
+ let server = null;
20
+ let port = 0;
21
+ async function startServer() {
22
+ server = http.createServer((req, res) => {
23
+ res.writeHead(200, { 'Content-Type': 'application/json' });
24
+ res.end(JSON.stringify({ ok: true, path: req.url }));
25
+ });
26
+ await new Promise((resolve) => {
27
+ server.listen(port, '127.0.0.1', () => {
28
+ port = server.address().port;
29
+ resolve();
30
+ });
31
+ });
32
+ }
33
+ async function stopServer() {
34
+ if (!server)
35
+ return;
36
+ await new Promise((resolve) => {
37
+ server.close(() => resolve());
38
+ // Force-close any sockets the pool may be holding so the close()
39
+ // callback fires promptly.
40
+ server.closeAllConnections?.();
41
+ });
42
+ server = null;
43
+ }
44
+ before(async () => {
45
+ await startServer();
46
+ });
47
+ beforeEach(async () => {
48
+ await resetAllPools();
49
+ });
50
+ after(async () => {
51
+ await stopServer();
52
+ await resetAllPools();
53
+ });
54
+ it('healed pool succeeds after a network failure poisons the old pool', async () => {
55
+ const url = `http://127.0.0.1:${port}/v1/messages`;
56
+ // 1. First call succeeds against a live server.
57
+ const r1 = await fetch(url, {
58
+ // @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
59
+ dispatcher: getFetchPool('anthropic'),
60
+ });
61
+ assert.strictEqual(r1.status, 200);
62
+ await r1.text();
63
+ // 2. Kill the server. The pool now holds zombie sockets pointed at a
64
+ // dead listener — every subsequent fetch through the SAME dispatcher
65
+ // fails with `TypeError: fetch failed`.
66
+ await stopServer();
67
+ let caught = null;
68
+ try {
69
+ await fetch(url, {
70
+ // @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
71
+ dispatcher: getFetchPool('anthropic'),
72
+ });
73
+ }
74
+ catch (err) {
75
+ caught = err;
76
+ }
77
+ assert.ok(caught instanceof Error, 'expected fetch to throw with server down');
78
+ assert.match(caught.message + ' ' + (caught.cause instanceof Error ? caught.cause.message : ''), /fetch failed|ECONNREFUSED|ECONNRESET|socket/i);
79
+ // 3. Recycle the pool — this is what the adapter's catch block does
80
+ // in production.
81
+ await recycleFetchPoolOnFailure('anthropic', caught);
82
+ const metrics = getPoolMetrics().find((m) => m.provider === 'anthropic');
83
+ assert.strictEqual(metrics?.recycleCount, 1, 'pool should have been recycled');
84
+ // 4. Bring the server back on the same port.
85
+ await startServer();
86
+ // 5. The next fetch must succeed. Without the recycle, the same zombie
87
+ // socket would be re-used and this would fail again.
88
+ const r3 = await fetch(url, {
89
+ // @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
90
+ dispatcher: getFetchPool('anthropic'),
91
+ });
92
+ assert.strictEqual(r3.status, 200, 'healed pool must serve the next request');
93
+ const body = (await r3.json());
94
+ assert.strictEqual(body.ok, true);
95
+ });
96
+ it('one provider failing does not recycle a sibling provider', async () => {
97
+ // Touch both pools so they exist.
98
+ getFetchPool('anthropic');
99
+ getFetchPool('gemini');
100
+ // Poison only one.
101
+ await recycleFetchPoolOnFailure('anthropic', new TypeError('fetch failed'));
102
+ const metrics = getPoolMetrics();
103
+ const anthropic = metrics.find((m) => m.provider === 'anthropic');
104
+ const gemini = metrics.find((m) => m.provider === 'gemini');
105
+ assert.strictEqual(anthropic?.recycleCount, 1);
106
+ assert.strictEqual(gemini?.recycleCount, 0, 'sibling provider must be untouched');
107
+ });
108
+ });
109
+ //# sourceMappingURL=fetch-pool.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-pool.integration.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.integration.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Unit tests for fetch-pool — per-provider dispatcher with heal-on-failure.
3
+ *
4
+ * Covers the eight contracts from FETCH-POOL-MANAGEMENT-PLAN.md §6.1:
5
+ * a) Lazy construction returns a stable instance per provider
6
+ * b) Per-provider isolation — different providers get different agents
7
+ * c) Bounded options applied at construction
8
+ * d) Recycle on poisoned-error pattern swaps the agent
9
+ * e) No recycle on non-poisoned errors (e.g. 400, 401, 503)
10
+ * f) Concurrent recycles coalesce to one bump
11
+ * g) Metrics shape is well-formed
12
+ * h) resetAllPools clears state cleanly
13
+ */
14
+ export {};
15
+ //# sourceMappingURL=fetch-pool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-pool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Unit tests for fetch-pool — per-provider dispatcher with heal-on-failure.
3
+ *
4
+ * Covers the eight contracts from FETCH-POOL-MANAGEMENT-PLAN.md §6.1:
5
+ * a) Lazy construction returns a stable instance per provider
6
+ * b) Per-provider isolation — different providers get different agents
7
+ * c) Bounded options applied at construction
8
+ * d) Recycle on poisoned-error pattern swaps the agent
9
+ * e) No recycle on non-poisoned errors (e.g. 400, 401, 503)
10
+ * f) Concurrent recycles coalesce to one bump
11
+ * g) Metrics shape is well-formed
12
+ * h) resetAllPools clears state cleanly
13
+ */
14
+ import { describe, it, beforeEach } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { Agent } from 'undici';
17
+ import { getFetchPool, recycleFetchPoolOnFailure, getPoolMetrics, resetAllPools, } from '../fetch-pool.js';
18
+ describe('fetch-pool', () => {
19
+ beforeEach(async () => {
20
+ await resetAllPools();
21
+ });
22
+ it('a) lazy construction returns the same Agent on repeat calls', () => {
23
+ const a1 = getFetchPool('anthropic');
24
+ const a2 = getFetchPool('anthropic');
25
+ assert.ok(a1 instanceof Agent);
26
+ assert.strictEqual(a1, a2, 'second call should return same instance');
27
+ });
28
+ it('b) per-provider isolation — separate Agents per provider', () => {
29
+ const anthropic = getFetchPool('anthropic');
30
+ const gemini = getFetchPool('gemini');
31
+ const opencode = getFetchPool('opencode');
32
+ assert.notStrictEqual(anthropic, gemini);
33
+ assert.notStrictEqual(anthropic, opencode);
34
+ assert.notStrictEqual(gemini, opencode);
35
+ });
36
+ it('c) constructed Agent is bounded — close() resolves without hanging', async () => {
37
+ // Indirect bounded-keep-alive test: a fresh pool with no in-flight work
38
+ // closes synchronously-ish. If construction picked up unbounded defaults,
39
+ // tests for d/f would intermittently hang on the close() inside recycle.
40
+ const agent = getFetchPool('openai');
41
+ const closed = agent.close();
42
+ await Promise.race([
43
+ closed,
44
+ new Promise((_, reject) => setTimeout(() => reject(new Error('close hung beyond 500ms')), 500)),
45
+ ]);
46
+ });
47
+ it('d) recycle swaps the Agent when the error matches a poisoned pattern', async () => {
48
+ const before = getFetchPool('opencode');
49
+ await recycleFetchPoolOnFailure('opencode', new TypeError('fetch failed'));
50
+ const after = getFetchPool('opencode');
51
+ assert.notStrictEqual(after, before, 'agent should have been swapped');
52
+ const metrics = getPoolMetrics().find((m) => m.provider === 'opencode');
53
+ assert.ok(metrics);
54
+ assert.strictEqual(metrics.recycleCount, 1);
55
+ assert.ok(metrics.lastRecycleAt);
56
+ assert.match(metrics.lastRecycleReason ?? '', /fetch failed/);
57
+ });
58
+ it('e) does not recycle on non-poisoned errors (real upstream status)', async () => {
59
+ const before = getFetchPool('gemini');
60
+ await recycleFetchPoolOnFailure('gemini', new Error('400 Bad Request'));
61
+ await recycleFetchPoolOnFailure('gemini', new Error('503 Service Unavailable'));
62
+ await recycleFetchPoolOnFailure('gemini', new Error('quota exceeded'));
63
+ const after = getFetchPool('gemini');
64
+ assert.strictEqual(after, before, 'agent must not change on non-network errors');
65
+ const metrics = getPoolMetrics().find((m) => m.provider === 'gemini');
66
+ assert.strictEqual(metrics?.recycleCount, 0);
67
+ });
68
+ it('f) concurrent recycles coalesce — recycleCount increments once', async () => {
69
+ getFetchPool('ollama');
70
+ const err = new TypeError('fetch failed');
71
+ const all = await Promise.all(Array.from({ length: 10 }, () => recycleFetchPoolOnFailure('ollama', err)));
72
+ assert.strictEqual(all.length, 10);
73
+ const metrics = getPoolMetrics().find((m) => m.provider === 'ollama');
74
+ assert.strictEqual(metrics?.recycleCount, 1, 'must coalesce to one recycle');
75
+ });
76
+ it('g) getPoolMetrics returns well-shaped entries', async () => {
77
+ getFetchPool('anthropic');
78
+ getFetchPool('openrouter');
79
+ const metrics = getPoolMetrics();
80
+ assert.ok(Array.isArray(metrics));
81
+ assert.strictEqual(metrics.length, 2);
82
+ for (const m of metrics) {
83
+ assert.ok(['anthropic', 'openrouter'].includes(m.provider));
84
+ assert.strictEqual(typeof m.createdAt, 'string');
85
+ assert.match(m.createdAt, /\dT\d/, 'ISO timestamp');
86
+ assert.strictEqual(typeof m.recycleCount, 'number');
87
+ assert.strictEqual(typeof m.active, 'boolean');
88
+ assert.strictEqual(m.active, true, 'idle pool is active');
89
+ }
90
+ });
91
+ it('h) resetAllPools drops all state', async () => {
92
+ getFetchPool('anthropic');
93
+ getFetchPool('gemini');
94
+ assert.strictEqual(getPoolMetrics().length, 2);
95
+ await resetAllPools();
96
+ assert.strictEqual(getPoolMetrics().length, 0);
97
+ // Subsequent get reconstructs cleanly
98
+ const fresh = getFetchPool('anthropic');
99
+ assert.ok(fresh instanceof Agent);
100
+ assert.strictEqual(getPoolMetrics().length, 1);
101
+ });
102
+ it('recycle reads ECONNRESET from err.cause', async () => {
103
+ const before = getFetchPool('anthropic');
104
+ // Node's fetch wraps the underlying socket error in `.cause`.
105
+ const err = new TypeError('fetch failed', {
106
+ cause: new Error('read ECONNRESET'),
107
+ });
108
+ await recycleFetchPoolOnFailure('anthropic', err);
109
+ const after = getFetchPool('anthropic');
110
+ assert.notStrictEqual(after, before);
111
+ const m = getPoolMetrics().find((p) => p.provider === 'anthropic');
112
+ assert.strictEqual(m?.recycleCount, 1);
113
+ });
114
+ });
115
+ //# sourceMappingURL=fetch-pool.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-pool.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mcp-merged-executor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-merged-executor.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mcp-merged-executor.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,221 @@
1
+ /**
2
+ * End-to-end tests for `MergedToolExecutor` driving the official MCP TS SDK
3
+ * over an in-process transport. Validates:
4
+ * - tool merging (builtin + MCP)
5
+ * - exact-name + per-server glob allowlists
6
+ * - destructive-tool guard under glob
7
+ * - dispatch routing builtin vs MCP
8
+ * - flattening of `content[]` array results
9
+ * - cancellation propagation
10
+ */
11
+ import { describe, it } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, rmSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
18
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
19
+ import { McpAgentClient } from '../mcp/client.js';
20
+ import { McpClientPool } from '../mcp/pool.js';
21
+ import { MergedToolExecutor } from '../mcp/merged-executor.js';
22
+ import { BuiltinToolExecutor } from '../tools/builtin.js';
23
+ /**
24
+ * Spin up an MCP server in-process and return a connected `McpAgentClient`
25
+ * wired through `InMemoryTransport`. The official SDK handles the protocol;
26
+ * the test only cares about the tool surface.
27
+ */
28
+ async function makeInMemoryMcpClient(opts) {
29
+ const server = new Server({ name: `test-${opts.name}`, version: '0.0.1' }, { capabilities: { tools: {} } });
30
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
31
+ tools: opts.tools.map((t) => ({
32
+ name: t.name,
33
+ description: t.description,
34
+ inputSchema: t.inputSchema ?? { type: 'object' },
35
+ ...(t.annotations ? { annotations: t.annotations } : {}),
36
+ })),
37
+ }));
38
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
39
+ const tool = opts.tools.find((t) => t.name === req.params.name);
40
+ if (!tool) {
41
+ return {
42
+ isError: true,
43
+ content: [{ type: 'text', text: `unknown tool ${req.params.name}` }],
44
+ };
45
+ }
46
+ const result = tool.onCall?.(req.params.arguments ?? {}) ?? {
47
+ content: [{ type: 'text', text: 'ok' }],
48
+ };
49
+ return result;
50
+ });
51
+ const [clientT, serverT] = InMemoryTransport.createLinkedPair();
52
+ await server.connect(serverT);
53
+ // Construct a client and inject the linked transport, bypassing connect().
54
+ const client = new McpAgentClient({ name: opts.name, transport: 'stdio', command: 'noop' });
55
+ // Test seam: swap the SDK Client's transport for our in-memory one.
56
+ // The McpAgentClient holds the SDK Client in a private field; we
57
+ // bypass the type with a controlled cast.
58
+ await client.client.connect(clientT);
59
+ client.connected = true;
60
+ return client;
61
+ }
62
+ function workdir() {
63
+ const dir = mkdtempSync(join(tmpdir(), 'merged-exec-'));
64
+ return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
65
+ }
66
+ // ── Tests ───────────────────────────────────────────────────────────────
67
+ describe('MergedToolExecutor — schema merging', () => {
68
+ it('merges builtin + MCP tools and namespaces MCP entries as mcp__<server>__<tool>', async () => {
69
+ const client = await makeInMemoryMcpClient({
70
+ name: 'gh',
71
+ tools: [{ name: 'list_issues', description: 'list', annotations: { readOnlyHint: true } }],
72
+ });
73
+ const pool = new McpClientPool({ servers: [client.config] });
74
+ // Bypass pool.discoverTools' loader by stuffing the client directly.
75
+ pool.clients.set('gh', client);
76
+ const builtin = new BuiltinToolExecutor({
77
+ allowedTools: ['read_file', 'mcp__gh__list_issues'],
78
+ });
79
+ const exec = new MergedToolExecutor({
80
+ builtin,
81
+ pool,
82
+ allowedTools: ['read_file', 'mcp__gh__list_issues'],
83
+ });
84
+ await exec.prime();
85
+ const names = exec.listSchemas().map((s) => s.name).sort();
86
+ assert.deepEqual(names, ['mcp__gh__list_issues', 'read_file']);
87
+ await client.close();
88
+ await pool.close();
89
+ });
90
+ it('per-server glob allows read-only MCP tools but hides destructive ones', async () => {
91
+ const client = await makeInMemoryMcpClient({
92
+ name: 'fs',
93
+ tools: [
94
+ { name: 'read', description: 'read', annotations: { readOnlyHint: true } },
95
+ { name: 'delete', description: 'delete', annotations: { destructiveHint: true } },
96
+ ],
97
+ });
98
+ const pool = new McpClientPool({ servers: [client.config] });
99
+ pool.clients.set('fs', client);
100
+ const builtin = new BuiltinToolExecutor({ allowedTools: ['read_file', 'mcp__fs__*'] });
101
+ const exec = new MergedToolExecutor({
102
+ builtin,
103
+ pool,
104
+ allowedTools: ['read_file', 'mcp__fs__*'],
105
+ });
106
+ await exec.prime();
107
+ const names = exec.listSchemas().map((s) => s.name).sort();
108
+ assert.ok(names.includes('mcp__fs__read'), 'glob admits read-only MCP tool');
109
+ assert.ok(!names.includes('mcp__fs__delete'), 'glob hides destructive MCP tool');
110
+ await client.close();
111
+ await pool.close();
112
+ });
113
+ it('exact-name allowance admits destructive MCP tools', async () => {
114
+ const client = await makeInMemoryMcpClient({
115
+ name: 'fs',
116
+ tools: [{ name: 'delete', description: 'd', annotations: { destructiveHint: true } }],
117
+ });
118
+ const pool = new McpClientPool({ servers: [client.config] });
119
+ pool.clients.set('fs', client);
120
+ const exec = new MergedToolExecutor({
121
+ builtin: new BuiltinToolExecutor({ allowedTools: ['mcp__fs__delete'] }),
122
+ pool,
123
+ allowedTools: ['mcp__fs__delete'],
124
+ });
125
+ await exec.prime();
126
+ const names = exec.listSchemas().map((s) => s.name);
127
+ assert.deepEqual(names, ['mcp__fs__delete']);
128
+ await client.close();
129
+ await pool.close();
130
+ });
131
+ });
132
+ describe('MergedToolExecutor — dispatch', () => {
133
+ it('routes mcp__* calls to the pool and flattens text content[] into a string', async () => {
134
+ const client = await makeInMemoryMcpClient({
135
+ name: 'echo',
136
+ tools: [{
137
+ name: 'say',
138
+ description: 'echo',
139
+ onCall: (args) => ({
140
+ content: [
141
+ { type: 'text', text: `you said: ${args.msg ?? '?'}` },
142
+ { type: 'text', text: 'and a second line' },
143
+ ],
144
+ }),
145
+ }],
146
+ });
147
+ const pool = new McpClientPool({ servers: [client.config] });
148
+ pool.clients.set('echo', client);
149
+ const exec = new MergedToolExecutor({
150
+ builtin: new BuiltinToolExecutor({ allowedTools: ['mcp__echo__say'] }),
151
+ pool,
152
+ allowedTools: ['mcp__echo__say'],
153
+ });
154
+ await exec.prime();
155
+ const { dir, cleanup } = workdir();
156
+ try {
157
+ const ctrl = new AbortController();
158
+ const result = await exec.execute({ id: '1', name: 'mcp__echo__say', arguments: { msg: 'hi' } }, { workingDir: dir, abortSignal: ctrl.signal });
159
+ assert.equal(result.isError, false);
160
+ assert.match(result.content, /you said: hi/);
161
+ assert.match(result.content, /second line/);
162
+ }
163
+ finally {
164
+ cleanup();
165
+ await client.close();
166
+ await pool.close();
167
+ }
168
+ });
169
+ it('routes builtin names to BuiltinToolExecutor untouched', async () => {
170
+ const pool = new McpClientPool({ servers: [] });
171
+ const exec = new MergedToolExecutor({
172
+ builtin: new BuiltinToolExecutor({ allowedTools: ['list'] }),
173
+ pool,
174
+ allowedTools: ['list'],
175
+ });
176
+ const { dir, cleanup } = workdir();
177
+ try {
178
+ const ctrl = new AbortController();
179
+ const result = await exec.execute({ id: '1', name: 'list', arguments: {} }, { workingDir: dir, abortSignal: ctrl.signal });
180
+ assert.equal(result.isError, false);
181
+ }
182
+ finally {
183
+ cleanup();
184
+ await pool.close();
185
+ }
186
+ });
187
+ it('rejects disallowed MCP names at exec with isError=true', async () => {
188
+ const pool = new McpClientPool({ servers: [] });
189
+ const exec = new MergedToolExecutor({
190
+ builtin: new BuiltinToolExecutor({ allowedTools: ['read_file'] }),
191
+ pool,
192
+ allowedTools: ['read_file'],
193
+ });
194
+ const { dir, cleanup } = workdir();
195
+ try {
196
+ const ctrl = new AbortController();
197
+ const result = await exec.execute({ id: '1', name: 'mcp__rogue__do_anything', arguments: {} }, { workingDir: dir, abortSignal: ctrl.signal });
198
+ assert.equal(result.isError, true);
199
+ assert.match(result.content, /not permitted/);
200
+ }
201
+ finally {
202
+ cleanup();
203
+ await pool.close();
204
+ }
205
+ });
206
+ });
207
+ describe('McpClientPool — failure isolation', () => {
208
+ it('records connect failures in `failures` without crashing the pool', async () => {
209
+ // Construct a pool with one bogus server config that will fail at connect.
210
+ const pool = new McpClientPool({
211
+ servers: [{ name: 'broken', transport: 'stdio', command: '/no/such/command' }],
212
+ connectTimeoutMs: 1000,
213
+ });
214
+ const tools = await pool.discoverTools();
215
+ assert.deepEqual(tools, []);
216
+ assert.equal(pool.failures.length, 1);
217
+ assert.equal(pool.failures[0].server, 'broken');
218
+ await pool.close();
219
+ });
220
+ });
221
+ //# sourceMappingURL=mcp-merged-executor.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-merged-executor.test.js","sourceRoot":"","sources":["../../src/__tests__/mcp-merged-executor.test.ts"],"names":[],"mappings":"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"}
@@ -270,7 +270,13 @@ class FakeMcpClient {
270
270
  async listTools() {
271
271
  if (this.throwOnList)
272
272
  throw new Error('boom');
273
- return this.tools;
273
+ return this.tools.map((t) => ({
274
+ name: t.name,
275
+ bareName: t.name.includes('__') ? t.name.split('__').slice(2).join('__') : t.name,
276
+ serverName: this.config.name,
277
+ schema: t,
278
+ annotations: {},
279
+ }));
274
280
  }
275
281
  }
276
282
  function asClient(c) {
@@ -282,13 +288,13 @@ describe('buildAgentToolset', () => {
282
288
  { name: 'fs.read', description: 'read', inputSchema: {} },
283
289
  ];
284
290
  const ghClient = new FakeMcpClient('github', [
285
- { name: 'github/list_issues', description: 'list', inputSchema: {} },
291
+ { name: 'mcp__github__list_issues', description: 'list', inputSchema: {} },
286
292
  ]);
287
293
  const ts = await buildAgentToolset(builtIn, [asClient(ghClient)]);
288
294
  assert.equal(ts.tools.length, 2);
289
- assert.deepEqual(ts.tools.map((t) => t.name).sort(), ['fs.read', 'github/list_issues']);
295
+ assert.deepEqual(ts.tools.map((t) => t.name).sort(), ['fs.read', 'mcp__github__list_issues']);
290
296
  assert.equal(ts.mcpDispatch.size, 1);
291
- assert.equal(ts.mcpDispatch.get('github/list_issues'), asClient(ghClient));
297
+ assert.equal(ts.mcpDispatch.get('mcp__github__list_issues'), asClient(ghClient));
292
298
  assert.equal(ts.mcpDispatch.has('fs.read'), false);
293
299
  });
294
300
  it('drops collisions with built-ins (warns)', async () => {
@@ -297,22 +303,22 @@ describe('buildAgentToolset', () => {
297
303
  ];
298
304
  const dup = new FakeMcpClient('weird', [
299
305
  { name: 'fs.read', description: 'mcp dup', inputSchema: {} },
300
- { name: 'weird/ok', description: 'fine', inputSchema: {} },
306
+ { name: 'mcp__weird__ok', description: 'fine', inputSchema: {} },
301
307
  ]);
302
308
  const { result, captured } = await captureStderrAsync(() => buildAgentToolset(builtIn, [asClient(dup)]));
303
309
  assert.equal(result.tools.length, 2);
304
310
  assert.equal(result.mcpDispatch.has('fs.read'), false);
305
- assert.equal(result.mcpDispatch.has('weird/ok'), true);
311
+ assert.equal(result.mcpDispatch.has('mcp__weird__ok'), true);
306
312
  assert.ok(captured.includes('collision "fs.read"'));
307
313
  });
308
314
  it('isolates listTools failures: one bad server does not kill others', async () => {
309
315
  const good = new FakeMcpClient('good', [
310
- { name: 'good/ping', description: 'p', inputSchema: {} },
316
+ { name: 'mcp__good__ping', description: 'p', inputSchema: {} },
311
317
  ]);
312
318
  const bad = new FakeMcpClient('bad', [], /* throwOnList */ true);
313
319
  const { result, captured } = await captureStderrAsync(() => buildAgentToolset([], [asClient(bad), asClient(good)]));
314
320
  assert.equal(result.tools.length, 1);
315
- assert.equal(result.tools[0].name, 'good/ping');
321
+ assert.equal(result.tools[0].name, 'mcp__good__ping');
316
322
  assert.ok(captured.includes('"bad" listTools failed'));
317
323
  });
318
324
  });
@@ -348,21 +354,21 @@ describe('McpAgentClient routing', () => {
348
354
  c.client = fake;
349
355
  c.connected = true;
350
356
  }
351
- it('namespaces tools as `<server>/<tool>` in listTools()', async () => {
357
+ it('namespaces tools as `mcp__<server>__<tool>` in listTools()', async () => {
352
358
  const client = new McpAgentClient({ name: 'fs', transport: 'stdio', command: 'noop' });
353
359
  const fake = makeFakeSdkClient();
354
360
  injectFakeClient(client, fake);
355
361
  const tools = await client.listTools();
356
- assert.deepEqual(tools.map((t) => t.name), ['fs/read_file', 'fs/write_file']);
357
- // descriptions + inputSchema preserved, only name namespaced
358
- assert.equal(tools[0].description, 'read');
359
- assert.deepEqual(tools[0].inputSchema, { type: 'object' });
362
+ assert.deepEqual(tools.map((t) => t.name), ['mcp__fs__read_file', 'mcp__fs__write_file']);
363
+ // descriptions decorated with server tag + read-only heuristic; schema preserved
364
+ assert.match(tools[0].schema.description, /\(via fs MCP/);
365
+ assert.deepEqual(tools[0].schema.inputSchema, { type: 'object' });
360
366
  });
361
- it('callTool strips the `<server>/` prefix before forwarding to SDK', async () => {
367
+ it('callTool strips the namespacing prefix before forwarding to SDK', async () => {
362
368
  const client = new McpAgentClient({ name: 'gh', transport: 'stdio', command: 'noop' });
363
369
  const fake = makeFakeSdkClient();
364
370
  injectFakeClient(client, fake);
365
- await client.callTool('gh/list_issues', { repo: 'owner/repo' });
371
+ await client.callTool('mcp__gh__list_issues', { repo: 'owner/repo' });
366
372
  assert.equal(fake.calls.length, 1);
367
373
  assert.equal(fake.calls[0].params.name, 'list_issues');
368
374
  assert.deepEqual(fake.calls[0].params.arguments, { repo: 'owner/repo' });