@a5c-ai/tool-mux 5.0.1-staging.016f0b0e8119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/__tests__/dispatch.test.d.ts +2 -0
- package/dist/__tests__/dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch.test.js +209 -0
- package/dist/__tests__/dispatch.test.js.map +1 -0
- package/dist/__tests__/hooks.test.d.ts +2 -0
- package/dist/__tests__/hooks.test.d.ts.map +1 -0
- package/dist/__tests__/hooks.test.js +100 -0
- package/dist/__tests__/hooks.test.js.map +1 -0
- package/dist/__tests__/mcp-bridge.test.d.ts +2 -0
- package/dist/__tests__/mcp-bridge.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-bridge.test.js +129 -0
- package/dist/__tests__/mcp-bridge.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +162 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/schema-translation.test.d.ts +2 -0
- package/dist/__tests__/schema-translation.test.d.ts.map +1 -0
- package/dist/__tests__/schema-translation.test.js +126 -0
- package/dist/__tests__/schema-translation.test.js.map +1 -0
- package/dist/dispatch.d.ts +40 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +117 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/hooks.d.ts +42 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +25 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-bridge.d.ts +54 -0
- package/dist/mcp-bridge.d.ts.map +1 -0
- package/dist/mcp-bridge.js +77 -0
- package/dist/mcp-bridge.js.map +1 -0
- package/dist/registry.d.ts +40 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +83 -0
- package/dist/registry.js.map +1 -0
- package/dist/schema-translation.d.ts +38 -0
- package/dist/schema-translation.d.ts.map +1 -0
- package/dist/schema-translation.js +61 -0
- package/dist/schema-translation.js.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @a5c-ai/tool-mux
|
|
2
|
+
|
|
3
|
+
Tool lifecycle layer for the babysitter monorepo. Provides:
|
|
4
|
+
|
|
5
|
+
- **ToolRegistry** — in-memory, Map-backed registry for tool descriptors and servers
|
|
6
|
+
- **ToolDispatcher** — policy-driven resolution of tool-to-server mapping with glob matching and before/after hook integration
|
|
7
|
+
- **Schema translation** — adapters between `ToolDescriptor` and `NormalizedToolDefinition` (transport-mux), plus `translateTools` for provider-specific wire formats
|
|
8
|
+
- **ToolHookBridge** — interface (and no-op implementation) bridging tool dispatch to hooks-mux PreToolUse/PostToolUse lifecycle
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @a5c-ai/tool-mux
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import {
|
|
20
|
+
ToolRegistry,
|
|
21
|
+
ToolDispatcher,
|
|
22
|
+
NoopToolHookBridge,
|
|
23
|
+
toToolDescriptor,
|
|
24
|
+
translateTools,
|
|
25
|
+
} from '@a5c-ai/tool-mux';
|
|
26
|
+
|
|
27
|
+
// 1. Build a registry
|
|
28
|
+
const registry = new ToolRegistry();
|
|
29
|
+
registry.register({
|
|
30
|
+
name: 'read_file',
|
|
31
|
+
source: 'builtin',
|
|
32
|
+
description: 'Read a file from disk',
|
|
33
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } } },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 2. Create a dispatcher with a policy
|
|
37
|
+
const dispatcher = new ToolDispatcher({
|
|
38
|
+
registry,
|
|
39
|
+
policy: {
|
|
40
|
+
rules: [{ match: 'mcp_*', server: 'mcp-server-1', priority: 10 }],
|
|
41
|
+
defaultServer: 'local',
|
|
42
|
+
},
|
|
43
|
+
hooks: new NoopToolHookBridge(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 3. Dispatch a tool call
|
|
47
|
+
const result = await dispatcher.dispatch(
|
|
48
|
+
{ toolName: 'read_file', input: { path: '/tmp/test.txt' } },
|
|
49
|
+
async (tool, ctx) => {
|
|
50
|
+
// your executor logic here
|
|
51
|
+
return { content: '...' };
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// 4. Translate tool schemas for a specific provider
|
|
56
|
+
const anthropicTools = translateTools(registry.list(), 'anthropic');
|
|
57
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dispatch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ToolDispatcher } from '../dispatch.js';
|
|
3
|
+
import { ToolRegistry } from '../registry.js';
|
|
4
|
+
/* -------------------------------------------------------------------------- */
|
|
5
|
+
/* Helpers */
|
|
6
|
+
/* -------------------------------------------------------------------------- */
|
|
7
|
+
function makeTool(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
name: 'test_tool',
|
|
10
|
+
description: 'A test tool',
|
|
11
|
+
parameters: { type: 'object' },
|
|
12
|
+
source: 'builtin',
|
|
13
|
+
...overrides,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function makeContext(overrides = {}) {
|
|
17
|
+
return {
|
|
18
|
+
toolName: 'test_tool',
|
|
19
|
+
input: {},
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const noopExecutor = async (_tool, _ctx) => 'executed';
|
|
24
|
+
/* ========================================================================== */
|
|
25
|
+
/* ToolDispatcher */
|
|
26
|
+
/* ========================================================================== */
|
|
27
|
+
describe('ToolDispatcher', () => {
|
|
28
|
+
let registry;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
registry = new ToolRegistry();
|
|
31
|
+
});
|
|
32
|
+
/* ---------------------------------------------------------------------- */
|
|
33
|
+
/* resolveServer */
|
|
34
|
+
/* ---------------------------------------------------------------------- */
|
|
35
|
+
describe('resolveServer', () => {
|
|
36
|
+
it('resolves with exact match rule', () => {
|
|
37
|
+
registry.register(makeTool({ name: 'file_read', server: 'default' }));
|
|
38
|
+
const dispatcher = new ToolDispatcher({
|
|
39
|
+
registry,
|
|
40
|
+
policy: {
|
|
41
|
+
rules: [{ match: 'file_read', server: 'file-server', priority: 10 }],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
expect(dispatcher.resolveServer('file_read')).toBe('file-server');
|
|
45
|
+
});
|
|
46
|
+
it('resolves with glob pattern (wildcard)', () => {
|
|
47
|
+
registry.register(makeTool({ name: 'file_read' }));
|
|
48
|
+
const dispatcher = new ToolDispatcher({
|
|
49
|
+
registry,
|
|
50
|
+
policy: {
|
|
51
|
+
rules: [{ match: 'file_*', server: 'file-server' }],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
expect(dispatcher.resolveServer('file_read')).toBe('file-server');
|
|
55
|
+
expect(dispatcher.resolveServer('file_write')).toBe('file-server');
|
|
56
|
+
});
|
|
57
|
+
it('resolves with single-char glob pattern (?)', () => {
|
|
58
|
+
const dispatcher = new ToolDispatcher({
|
|
59
|
+
registry,
|
|
60
|
+
policy: {
|
|
61
|
+
rules: [{ match: 'tool_?', server: 'single-char-server' }],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
expect(dispatcher.resolveServer('tool_a')).toBe('single-char-server');
|
|
65
|
+
expect(dispatcher.resolveServer('tool_ab')).toBeUndefined(); // two chars, no match
|
|
66
|
+
});
|
|
67
|
+
it('falls back to descriptor server when no rules match', () => {
|
|
68
|
+
registry.register(makeTool({ name: 'my_tool', server: 'desc-server' }));
|
|
69
|
+
const dispatcher = new ToolDispatcher({
|
|
70
|
+
registry,
|
|
71
|
+
policy: { rules: [] },
|
|
72
|
+
});
|
|
73
|
+
expect(dispatcher.resolveServer('my_tool')).toBe('desc-server');
|
|
74
|
+
});
|
|
75
|
+
it('falls back to defaultServer when no rules or descriptor match', () => {
|
|
76
|
+
registry.register(makeTool({ name: 'orphan' }));
|
|
77
|
+
const dispatcher = new ToolDispatcher({
|
|
78
|
+
registry,
|
|
79
|
+
policy: { rules: [], defaultServer: 'fallback' },
|
|
80
|
+
});
|
|
81
|
+
expect(dispatcher.resolveServer('orphan')).toBe('fallback');
|
|
82
|
+
});
|
|
83
|
+
it('returns undefined when nothing matches and no default is set', () => {
|
|
84
|
+
const dispatcher = new ToolDispatcher({
|
|
85
|
+
registry,
|
|
86
|
+
policy: { rules: [] },
|
|
87
|
+
});
|
|
88
|
+
expect(dispatcher.resolveServer('unknown_tool')).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
it('higher priority rule wins over lower priority', () => {
|
|
91
|
+
const dispatcher = new ToolDispatcher({
|
|
92
|
+
registry,
|
|
93
|
+
policy: { rules: [] },
|
|
94
|
+
});
|
|
95
|
+
dispatcher.addRule({ match: 'file_*', server: 'low-priority', priority: 1 });
|
|
96
|
+
dispatcher.addRule({ match: 'file_*', server: 'high-priority', priority: 100 });
|
|
97
|
+
expect(dispatcher.resolveServer('file_read')).toBe('high-priority');
|
|
98
|
+
});
|
|
99
|
+
it('first matching rule wins among equal priorities', () => {
|
|
100
|
+
const dispatcher = new ToolDispatcher({
|
|
101
|
+
registry,
|
|
102
|
+
policy: {
|
|
103
|
+
rules: [
|
|
104
|
+
{ match: 'tool_*', server: 'server-a', priority: 5 },
|
|
105
|
+
{ match: 'tool_read', server: 'server-b', priority: 5 },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
// Both match 'tool_read', but the glob comes first (same priority, stable order)
|
|
110
|
+
expect(dispatcher.resolveServer('tool_read')).toBe('server-a');
|
|
111
|
+
});
|
|
112
|
+
it('setPolicy replaces the entire dispatch policy', () => {
|
|
113
|
+
const dispatcher = new ToolDispatcher({
|
|
114
|
+
registry,
|
|
115
|
+
policy: {
|
|
116
|
+
rules: [{ match: 'old_*', server: 'old-server' }],
|
|
117
|
+
defaultServer: 'old-default',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
expect(dispatcher.resolveServer('old_tool')).toBe('old-server');
|
|
121
|
+
dispatcher.setPolicy({
|
|
122
|
+
rules: [{ match: 'new_*', server: 'new-server' }],
|
|
123
|
+
defaultServer: 'new-default',
|
|
124
|
+
});
|
|
125
|
+
expect(dispatcher.resolveServer('old_tool')).toBe('new-default');
|
|
126
|
+
expect(dispatcher.resolveServer('new_tool')).toBe('new-server');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
/* ---------------------------------------------------------------------- */
|
|
130
|
+
/* dispatch */
|
|
131
|
+
/* ---------------------------------------------------------------------- */
|
|
132
|
+
describe('dispatch', () => {
|
|
133
|
+
it('returns error result when tool is not registered', async () => {
|
|
134
|
+
const dispatcher = new ToolDispatcher({ registry });
|
|
135
|
+
const result = await dispatcher.dispatch(makeContext({ toolName: 'nonexistent' }), noopExecutor);
|
|
136
|
+
expect(result.error).toBe('Tool not found: nonexistent');
|
|
137
|
+
expect(result.output).toBeNull();
|
|
138
|
+
expect(result.durationMs).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
it('executes tool and returns output with duration', async () => {
|
|
141
|
+
registry.register(makeTool({ name: 'echo' }));
|
|
142
|
+
const dispatcher = new ToolDispatcher({ registry });
|
|
143
|
+
const executor = async () => 'hello world';
|
|
144
|
+
const result = await dispatcher.dispatch(makeContext({ toolName: 'echo', input: { text: 'hi' } }), executor);
|
|
145
|
+
expect(result.output).toBe('hello world');
|
|
146
|
+
expect(result.error).toBeUndefined();
|
|
147
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
148
|
+
});
|
|
149
|
+
it('catches executor errors and returns them in the result', async () => {
|
|
150
|
+
registry.register(makeTool({ name: 'failing' }));
|
|
151
|
+
const dispatcher = new ToolDispatcher({ registry });
|
|
152
|
+
const executor = async () => {
|
|
153
|
+
throw new Error('kaboom');
|
|
154
|
+
};
|
|
155
|
+
const result = await dispatcher.dispatch(makeContext({ toolName: 'failing' }), executor);
|
|
156
|
+
expect(result.error).toBe('kaboom');
|
|
157
|
+
expect(result.output).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
it('denies execution when beforeToolUse hook returns deny', async () => {
|
|
160
|
+
registry.register(makeTool({ name: 'blocked' }));
|
|
161
|
+
const hooks = {
|
|
162
|
+
async beforeToolUse() {
|
|
163
|
+
return { decision: 'deny', reason: 'not allowed' };
|
|
164
|
+
},
|
|
165
|
+
async afterToolUse() {
|
|
166
|
+
return undefined;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
const dispatcher = new ToolDispatcher({ registry, hooks });
|
|
170
|
+
const executor = vi.fn(noopExecutor);
|
|
171
|
+
const result = await dispatcher.dispatch(makeContext({ toolName: 'blocked' }), executor);
|
|
172
|
+
expect(result.error).toBe('not allowed');
|
|
173
|
+
expect(result.output).toBeNull();
|
|
174
|
+
expect(executor).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
it('calls afterToolUse hook with result after execution', async () => {
|
|
177
|
+
registry.register(makeTool({ name: 'hooked' }));
|
|
178
|
+
const afterSpy = vi.fn();
|
|
179
|
+
const hooks = {
|
|
180
|
+
async beforeToolUse() {
|
|
181
|
+
return undefined;
|
|
182
|
+
},
|
|
183
|
+
afterToolUse: afterSpy,
|
|
184
|
+
};
|
|
185
|
+
const dispatcher = new ToolDispatcher({ registry, hooks });
|
|
186
|
+
await dispatcher.dispatch(makeContext({ toolName: 'hooked' }), noopExecutor);
|
|
187
|
+
expect(afterSpy).toHaveBeenCalledOnce();
|
|
188
|
+
const [ctx, desc, res] = afterSpy.mock.calls[0];
|
|
189
|
+
expect(ctx.toolName).toBe('hooked');
|
|
190
|
+
expect(desc.name).toBe('hooked');
|
|
191
|
+
expect(res.output).toBe('executed');
|
|
192
|
+
});
|
|
193
|
+
it('provides default deny message when hook reason is absent', async () => {
|
|
194
|
+
registry.register(makeTool({ name: 'denied_no_reason' }));
|
|
195
|
+
const hooks = {
|
|
196
|
+
async beforeToolUse() {
|
|
197
|
+
return { decision: 'deny' };
|
|
198
|
+
},
|
|
199
|
+
async afterToolUse() {
|
|
200
|
+
return undefined;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
const dispatcher = new ToolDispatcher({ registry, hooks });
|
|
204
|
+
const result = await dispatcher.dispatch(makeContext({ toolName: 'denied_no_reason' }), noopExecutor);
|
|
205
|
+
expect(result.error).toBe('Tool use denied by hook');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=dispatch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.test.js","sourceRoot":"","sources":["../../src/__tests__/dispatch.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hooks.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { NoopToolHookBridge } from '../hooks.js';
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* Helpers */
|
|
5
|
+
/* -------------------------------------------------------------------------- */
|
|
6
|
+
function makeContext(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
toolName: 'test_tool',
|
|
9
|
+
input: { key: 'value' },
|
|
10
|
+
...overrides,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function makeDescriptor(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
name: 'test_tool',
|
|
16
|
+
description: 'A test tool',
|
|
17
|
+
source: 'builtin',
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function makeResult(overrides = {}) {
|
|
22
|
+
return {
|
|
23
|
+
output: 'ok',
|
|
24
|
+
durationMs: 42,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/* ========================================================================== */
|
|
29
|
+
/* Hooks */
|
|
30
|
+
/* ========================================================================== */
|
|
31
|
+
describe('NoopToolHookBridge', () => {
|
|
32
|
+
const noop = new NoopToolHookBridge();
|
|
33
|
+
it('beforeToolUse returns undefined (implicitly allowed)', async () => {
|
|
34
|
+
const result = await noop.beforeToolUse(makeContext(), makeDescriptor());
|
|
35
|
+
expect(result).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
it('afterToolUse resolves without error', async () => {
|
|
38
|
+
const result = await noop.afterToolUse(makeContext(), makeDescriptor(), makeResult());
|
|
39
|
+
expect(result).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('Custom ToolHookBridge', () => {
|
|
43
|
+
it('can deny tool use with a reason', async () => {
|
|
44
|
+
const denyBridge = {
|
|
45
|
+
async beforeToolUse() {
|
|
46
|
+
return { decision: 'deny', reason: 'Forbidden by policy' };
|
|
47
|
+
},
|
|
48
|
+
async afterToolUse() {
|
|
49
|
+
return undefined;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const result = await denyBridge.beforeToolUse(makeContext(), makeDescriptor());
|
|
53
|
+
expect(result).toBeDefined();
|
|
54
|
+
expect(result.decision).toBe('deny');
|
|
55
|
+
expect(result.reason).toBe('Forbidden by policy');
|
|
56
|
+
});
|
|
57
|
+
it('receives correct context and descriptor in beforeToolUse', async () => {
|
|
58
|
+
let capturedContext;
|
|
59
|
+
let capturedDescriptor;
|
|
60
|
+
const captureBridge = {
|
|
61
|
+
async beforeToolUse(ctx, desc) {
|
|
62
|
+
capturedContext = ctx;
|
|
63
|
+
capturedDescriptor = desc;
|
|
64
|
+
return { decision: 'allow' };
|
|
65
|
+
},
|
|
66
|
+
async afterToolUse() {
|
|
67
|
+
return undefined;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const ctx = makeContext({ toolName: 'special_tool', caller: 'agent-1', runId: 'run-99' });
|
|
71
|
+
const desc = makeDescriptor({ name: 'special_tool', source: 'mcp', server: 'srv-5' });
|
|
72
|
+
await captureBridge.beforeToolUse(ctx, desc);
|
|
73
|
+
expect(capturedContext).toBeDefined();
|
|
74
|
+
expect(capturedContext.toolName).toBe('special_tool');
|
|
75
|
+
expect(capturedContext.caller).toBe('agent-1');
|
|
76
|
+
expect(capturedContext.runId).toBe('run-99');
|
|
77
|
+
expect(capturedDescriptor).toBeDefined();
|
|
78
|
+
expect(capturedDescriptor.name).toBe('special_tool');
|
|
79
|
+
expect(capturedDescriptor.source).toBe('mcp');
|
|
80
|
+
expect(capturedDescriptor.server).toBe('srv-5');
|
|
81
|
+
});
|
|
82
|
+
it('afterToolUse receives the execution result', async () => {
|
|
83
|
+
let capturedResult;
|
|
84
|
+
const captureBridge = {
|
|
85
|
+
async beforeToolUse() {
|
|
86
|
+
return undefined;
|
|
87
|
+
},
|
|
88
|
+
async afterToolUse(_ctx, _desc, result) {
|
|
89
|
+
capturedResult = result;
|
|
90
|
+
return undefined;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const res = makeResult({ output: { data: [1, 2, 3] }, durationMs: 150 });
|
|
94
|
+
await captureBridge.afterToolUse(makeContext(), makeDescriptor(), res);
|
|
95
|
+
expect(capturedResult).toBeDefined();
|
|
96
|
+
expect(capturedResult.output).toEqual({ data: [1, 2, 3] });
|
|
97
|
+
expect(capturedResult.durationMs).toBe(150);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=hooks.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.test.js","sourceRoot":"","sources":["../../src/__tests__/hooks.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-bridge.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mcp-bridge.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { McpBridge } from '../mcp-bridge.js';
|
|
3
|
+
import { ToolRegistry } from '../registry.js';
|
|
4
|
+
/* -------------------------------------------------------------------------- */
|
|
5
|
+
/* Helpers */
|
|
6
|
+
/* -------------------------------------------------------------------------- */
|
|
7
|
+
function makeServerConfig(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'mcp-srv-1',
|
|
10
|
+
name: 'Test MCP Server',
|
|
11
|
+
transport: 'stdio',
|
|
12
|
+
command: 'node',
|
|
13
|
+
args: ['server.js'],
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function makeMcpTool(overrides = {}) {
|
|
18
|
+
return {
|
|
19
|
+
name: 'mcp_tool',
|
|
20
|
+
description: 'An MCP tool',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
query: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/* ========================================================================== */
|
|
31
|
+
/* McpBridge */
|
|
32
|
+
/* ========================================================================== */
|
|
33
|
+
describe('McpBridge', () => {
|
|
34
|
+
let registry;
|
|
35
|
+
let bridge;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
registry = new ToolRegistry();
|
|
38
|
+
bridge = new McpBridge(registry);
|
|
39
|
+
});
|
|
40
|
+
it('registerServer adds tools to the underlying registry', () => {
|
|
41
|
+
const config = makeServerConfig({ id: 'srv-a', name: 'Server A' });
|
|
42
|
+
const tools = [
|
|
43
|
+
makeMcpTool({ name: 'search' }),
|
|
44
|
+
makeMcpTool({ name: 'fetch' }),
|
|
45
|
+
];
|
|
46
|
+
bridge.registerServer(config, tools);
|
|
47
|
+
expect(registry.has('search')).toBe(true);
|
|
48
|
+
expect(registry.has('fetch')).toBe(true);
|
|
49
|
+
expect(registry.size).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
it('unregisterServer removes tools from the registry', () => {
|
|
52
|
+
const config = makeServerConfig({ id: 'srv-b' });
|
|
53
|
+
bridge.registerServer(config, [makeMcpTool({ name: 'tool_to_remove' })]);
|
|
54
|
+
expect(registry.has('tool_to_remove')).toBe(true);
|
|
55
|
+
bridge.unregisterServer('srv-b');
|
|
56
|
+
expect(registry.has('tool_to_remove')).toBe(false);
|
|
57
|
+
expect(registry.getServer('srv-b')).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
it('listServers returns all registered MCP server configs', () => {
|
|
60
|
+
bridge.registerServer(makeServerConfig({ id: 's1', name: 'S1' }), []);
|
|
61
|
+
bridge.registerServer(makeServerConfig({ id: 's2', name: 'S2' }), []);
|
|
62
|
+
const servers = bridge.listServers();
|
|
63
|
+
expect(servers).toHaveLength(2);
|
|
64
|
+
expect(servers.map((s) => s.id).sort()).toEqual(['s1', 's2']);
|
|
65
|
+
});
|
|
66
|
+
it('listServers returns empty array when nothing is registered', () => {
|
|
67
|
+
expect(bridge.listServers()).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
it('getServerTools returns the correct tools for a server', () => {
|
|
70
|
+
const config = makeServerConfig({ id: 'query-srv' });
|
|
71
|
+
bridge.registerServer(config, [
|
|
72
|
+
makeMcpTool({ name: 'q_search' }),
|
|
73
|
+
makeMcpTool({ name: 'q_list' }),
|
|
74
|
+
]);
|
|
75
|
+
const tools = bridge.getServerTools('query-srv');
|
|
76
|
+
expect(tools).toHaveLength(2);
|
|
77
|
+
expect(tools.map((t) => t.name).sort()).toEqual(['q_list', 'q_search']);
|
|
78
|
+
});
|
|
79
|
+
it('getServerTools returns empty array for unknown server', () => {
|
|
80
|
+
expect(bridge.getServerTools('ghost-server')).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
/* ---------------------------------------------------------------------- */
|
|
83
|
+
/* mcpToolToDescriptor (static) */
|
|
84
|
+
/* ---------------------------------------------------------------------- */
|
|
85
|
+
describe('mcpToolToDescriptor', () => {
|
|
86
|
+
it('sets source to "mcp" and correct server id', () => {
|
|
87
|
+
const mcpTool = makeMcpTool({ name: 'convert_me' });
|
|
88
|
+
const descriptor = McpBridge.mcpToolToDescriptor(mcpTool, 'my-server');
|
|
89
|
+
expect(descriptor.name).toBe('convert_me');
|
|
90
|
+
expect(descriptor.source).toBe('mcp');
|
|
91
|
+
expect(descriptor.server).toBe('my-server');
|
|
92
|
+
});
|
|
93
|
+
it('maps inputSchema to parameters', () => {
|
|
94
|
+
const schema = {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
path: { type: 'string' },
|
|
98
|
+
recursive: { type: 'boolean' },
|
|
99
|
+
},
|
|
100
|
+
required: ['path'],
|
|
101
|
+
};
|
|
102
|
+
const mcpTool = makeMcpTool({
|
|
103
|
+
name: 'file_list',
|
|
104
|
+
inputSchema: schema,
|
|
105
|
+
});
|
|
106
|
+
const descriptor = McpBridge.mcpToolToDescriptor(mcpTool, 'fs-server');
|
|
107
|
+
expect(descriptor.parameters).toEqual(schema);
|
|
108
|
+
});
|
|
109
|
+
it('preserves description from MCP tool definition', () => {
|
|
110
|
+
const mcpTool = makeMcpTool({
|
|
111
|
+
name: 'described_tool',
|
|
112
|
+
description: 'Does important things',
|
|
113
|
+
});
|
|
114
|
+
const descriptor = McpBridge.mcpToolToDescriptor(mcpTool, 'srv');
|
|
115
|
+
expect(descriptor.description).toBe('Does important things');
|
|
116
|
+
});
|
|
117
|
+
it('handles MCP tool with no inputSchema', () => {
|
|
118
|
+
const mcpTool = {
|
|
119
|
+
name: 'no_schema',
|
|
120
|
+
description: 'Schemaless',
|
|
121
|
+
};
|
|
122
|
+
const descriptor = McpBridge.mcpToolToDescriptor(mcpTool, 'srv');
|
|
123
|
+
expect(descriptor.name).toBe('no_schema');
|
|
124
|
+
expect(descriptor.parameters).toBeUndefined();
|
|
125
|
+
expect(descriptor.source).toBe('mcp');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=mcp-bridge.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-bridge.test.js","sourceRoot":"","sources":["../../src/__tests__/mcp-bridge.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/registry.test.ts"],"names":[],"mappings":""}
|