mcpose 2.1.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +461 -62
  2. package/dist/backendClient.d.ts +1 -1
  3. package/dist/backendClient.d.ts.map +1 -1
  4. package/dist/backendClient.js +6 -1
  5. package/dist/backendClient.js.map +1 -1
  6. package/dist/core.d.ts +309 -35
  7. package/dist/core.d.ts.map +1 -1
  8. package/dist/core.js +1023 -186
  9. package/dist/core.js.map +1 -1
  10. package/dist/delegation.d.ts +89 -0
  11. package/dist/delegation.d.ts.map +1 -0
  12. package/dist/delegation.js +175 -0
  13. package/dist/delegation.js.map +1 -0
  14. package/dist/eventStore.d.ts +12 -0
  15. package/dist/eventStore.d.ts.map +1 -1
  16. package/dist/eventStore.js +43 -5
  17. package/dist/eventStore.js.map +1 -1
  18. package/dist/hiddenTools.d.ts +42 -0
  19. package/dist/hiddenTools.d.ts.map +1 -0
  20. package/dist/hiddenTools.js +53 -0
  21. package/dist/hiddenTools.js.map +1 -0
  22. package/dist/index.d.ts +15 -6
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/mesh.d.ts +82 -0
  27. package/dist/mesh.d.ts.map +1 -0
  28. package/dist/mesh.js +141 -0
  29. package/dist/mesh.js.map +1 -0
  30. package/dist/middleware.d.ts +13 -0
  31. package/dist/middleware.d.ts.map +1 -1
  32. package/dist/middleware.js +27 -3
  33. package/dist/middleware.js.map +1 -1
  34. package/dist/proxyContext.d.ts +65 -5
  35. package/dist/proxyContext.d.ts.map +1 -1
  36. package/dist/proxyContext.js +39 -4
  37. package/dist/proxyContext.js.map +1 -1
  38. package/dist/rejection.d.ts +7 -1
  39. package/dist/rejection.d.ts.map +1 -1
  40. package/dist/rejection.js +24 -1
  41. package/dist/rejection.js.map +1 -1
  42. package/dist/sanitizeCatalog.d.ts +37 -0
  43. package/dist/sanitizeCatalog.d.ts.map +1 -0
  44. package/dist/sanitizeCatalog.js +92 -0
  45. package/dist/sanitizeCatalog.js.map +1 -0
  46. package/dist/sessionRegistry.d.ts +79 -0
  47. package/dist/sessionRegistry.d.ts.map +1 -0
  48. package/dist/sessionRegistry.js +83 -0
  49. package/dist/sessionRegistry.js.map +1 -0
  50. package/dist/telemetry.d.ts +29 -1
  51. package/dist/telemetry.d.ts.map +1 -1
  52. package/dist/testing.d.ts +18 -2
  53. package/dist/testing.d.ts.map +1 -1
  54. package/dist/testing.js +30 -8
  55. package/dist/testing.js.map +1 -1
  56. package/dist/version.d.ts +7 -0
  57. package/dist/version.d.ts.map +1 -0
  58. package/dist/version.js +7 -0
  59. package/dist/version.js.map +1 -0
  60. package/package.json +22 -8
  61. package/src/__tests__/_helpers.ts +64 -0
  62. package/src/__tests__/backendClient.test.ts +198 -0
  63. package/src/__tests__/core.delegation.test.ts +964 -0
  64. package/src/__tests__/core.edge.test.ts +314 -0
  65. package/src/__tests__/core.hiddenTools.predicate.test.ts +236 -0
  66. package/src/__tests__/core.localTools.test.ts +314 -0
  67. package/src/__tests__/core.mesh.test.ts +1182 -0
  68. package/src/__tests__/core.stripRequestMeta.test.ts +226 -0
  69. package/src/__tests__/core.stripResultMeta.test.ts +217 -0
  70. package/src/__tests__/core.test.ts +1105 -0
  71. package/src/__tests__/core.toolCallTelemetry.test.ts +157 -0
  72. package/src/__tests__/eventStore.test.ts +141 -0
  73. package/src/__tests__/hasToolContent.test.ts +63 -0
  74. package/src/__tests__/mapToolResult.test.ts +101 -0
  75. package/src/__tests__/middleware.edge.test.ts +126 -0
  76. package/src/__tests__/middleware.test.ts +233 -0
  77. package/src/__tests__/proxyContext.test.ts +140 -0
  78. package/src/__tests__/sanitizeCatalog.test.ts +380 -0
  79. package/src/__tests__/startHttpProxy.lifecycle.test.ts +675 -0
  80. package/src/__tests__/startHttpProxy.registry.test.ts +409 -0
  81. package/src/__tests__/startHttpProxy.replay.test.ts +116 -0
  82. package/src/__tests__/startHttpProxy.security.test.ts +288 -0
  83. package/src/__tests__/startHttpProxy.test.ts +1065 -0
  84. package/src/__tests__/testing.test.ts +250 -0
  85. package/src/__tests__/version.test.ts +12 -0
  86. package/src/backendClient.ts +83 -0
  87. package/src/core.ts +2129 -0
  88. package/src/delegation.ts +224 -0
  89. package/src/eventStore.ts +106 -0
  90. package/src/hiddenTools.ts +84 -0
  91. package/src/identity.ts +15 -0
  92. package/src/index.ts +63 -0
  93. package/src/mesh.ts +204 -0
  94. package/src/middleware.ts +101 -0
  95. package/src/proxyContext.ts +108 -0
  96. package/src/rejection.ts +62 -0
  97. package/src/sanitizeCatalog.ts +126 -0
  98. package/src/sessionRegistry.ts +142 -0
  99. package/src/telemetry.ts +46 -0
  100. package/src/testing.ts +174 -0
  101. package/src/version.ts +6 -0
@@ -0,0 +1,314 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createProxyServer, startHttpProxy, type LocalTool } from '../core.js';
3
+ import type { ToolMiddleware, ListToolsMiddleware } from '../core.js';
4
+ import type { BackendClient } from '../backendClient.js';
5
+ import type { ProxyContext } from '../proxyContext.js';
6
+ import { ErrorCode } from '@modelcontextprotocol/sdk/types.js';
7
+
8
+ // ── Test helpers ────────────────────────────────────────────────────────────
9
+
10
+ function makeMockBackend(capabilities: object = { tools: {} }): BackendClient {
11
+ return {
12
+ getServerCapabilities: vi.fn().mockReturnValue(capabilities),
13
+ listTools: vi.fn().mockResolvedValue({
14
+ tools: [
15
+ {
16
+ name: 'upstream_tool',
17
+ description: 'Upstream',
18
+ inputSchema: { type: 'object', properties: {} },
19
+ },
20
+ {
21
+ name: 'shadowed_tool',
22
+ description: 'Upstream version',
23
+ inputSchema: { type: 'object', properties: {} },
24
+ },
25
+ ],
26
+ }),
27
+ callTool: vi.fn().mockResolvedValue({
28
+ content: [{ type: 'text', text: 'upstream response' }],
29
+ }),
30
+ setNotificationHandler: vi.fn(),
31
+ removeNotificationHandler: vi.fn(),
32
+ close: vi.fn().mockResolvedValue(undefined),
33
+ } as unknown as BackendClient;
34
+ }
35
+
36
+ function makeLocalTool(
37
+ name: string,
38
+ handler?: LocalTool['handler'],
39
+ ): LocalTool {
40
+ return {
41
+ tool: {
42
+ name,
43
+ description: `Local ${name}`,
44
+ inputSchema: { type: 'object', properties: {} },
45
+ },
46
+ handler:
47
+ handler ??
48
+ (async () => ({ content: [{ type: 'text', text: `local ${name}` }] })),
49
+ };
50
+ }
51
+
52
+ /** Invokes a registered handler directly via `_requestHandlers` — no transport needed. */
53
+ async function invokeHandler(
54
+ server: ReturnType<typeof createProxyServer>,
55
+ method: string,
56
+ params: Record<string, unknown> = {},
57
+ ) {
58
+ const { _requestHandlers: handlers } = server as unknown as {
59
+ _requestHandlers: Map<
60
+ string,
61
+ (
62
+ req: { method: string; params: Record<string, unknown> },
63
+ extra: object,
64
+ ) => Promise<unknown>
65
+ >;
66
+ };
67
+ const handler = handlers.get(method);
68
+ if (!handler) throw new Error(`No handler registered for method: ${method}`);
69
+ return handler({ method, params }, {});
70
+ }
71
+
72
+ type ListResult = { tools: { name: string; description?: string }[] };
73
+
74
+ // ── Listing ─────────────────────────────────────────────────────────────────
75
+
76
+ describe('createProxyServer() — localTools appear in tools/list', () => {
77
+ it('lists local tools alongside upstream tools', async () => {
78
+ const server = createProxyServer(makeMockBackend(), {
79
+ name: 'test-server',
80
+ localTools: [makeLocalTool('local_tool')],
81
+ });
82
+
83
+ const result = (await invokeHandler(server, 'tools/list')) as ListResult;
84
+ expect(result.tools.map((t) => t.name)).toEqual([
85
+ 'upstream_tool',
86
+ 'shadowed_tool',
87
+ 'local_tool',
88
+ ]);
89
+ });
90
+
91
+ it('adds local tools inside the innermost next, so listToolsMiddleware sees them', async () => {
92
+ let seenByMiddleware: string[] = [];
93
+ const observe: ListToolsMiddleware = async (req, next) => {
94
+ const result = await next(req);
95
+ seenByMiddleware = result.tools.map((t) => t.name);
96
+ return result;
97
+ };
98
+ const server = createProxyServer(makeMockBackend(), {
99
+ name: 'test-server',
100
+ localTools: [makeLocalTool('local_tool')],
101
+ listToolsMiddleware: [observe],
102
+ });
103
+
104
+ await invokeHandler(server, 'tools/list');
105
+ expect(seenByMiddleware).toContain('local_tool');
106
+ });
107
+
108
+ it('adds local tools to the first page only, so pagination does not duplicate them', async () => {
109
+ const server = createProxyServer(makeMockBackend(), {
110
+ name: 'test-server',
111
+ localTools: [makeLocalTool('local_tool')],
112
+ });
113
+
114
+ const page2 = (await invokeHandler(server, 'tools/list', {
115
+ cursor: 'page-2',
116
+ })) as ListResult;
117
+ expect(page2.tools.map((t) => t.name)).not.toContain('local_tool');
118
+ });
119
+
120
+ it('filters a shadowed upstream duplicate on later pages too, so the client sees one entry per name', async () => {
121
+ const server = createProxyServer(makeMockBackend(), {
122
+ name: 'test-server',
123
+ localTools: [makeLocalTool('shadowed_tool')],
124
+ });
125
+
126
+ const page2 = (await invokeHandler(server, 'tools/list', {
127
+ cursor: 'page-2',
128
+ })) as ListResult;
129
+ expect(page2.tools.map((t) => t.name)).toEqual(['upstream_tool']);
130
+ });
131
+
132
+ it('a local tool shadows an upstream tool of the same name — one entry, the local one', async () => {
133
+ const server = createProxyServer(makeMockBackend(), {
134
+ name: 'test-server',
135
+ localTools: [makeLocalTool('shadowed_tool')],
136
+ });
137
+
138
+ const result = (await invokeHandler(server, 'tools/list')) as ListResult;
139
+ const entries = result.tools.filter((t) => t.name === 'shadowed_tool');
140
+ expect(entries).toHaveLength(1);
141
+ expect(entries[0]?.description).toBe('Local shadowed_tool');
142
+ });
143
+ });
144
+
145
+ // ── Calling ─────────────────────────────────────────────────────────────────
146
+
147
+ describe('createProxyServer() — a local tool call routes to its handler', () => {
148
+ it('runs the handler with params and context; the upstream is never consulted', async () => {
149
+ const backend = makeMockBackend();
150
+ let seenContext: ProxyContext | undefined;
151
+ const handler = vi.fn(
152
+ async (params: Record<string, unknown>, context: ProxyContext) => {
153
+ seenContext = context;
154
+ return {
155
+ content: [
156
+ { type: 'text' as const, text: JSON.stringify(params.arguments) },
157
+ ],
158
+ };
159
+ },
160
+ );
161
+ const server = createProxyServer(backend, {
162
+ name: 'test-server',
163
+ localTools: [makeLocalTool('local_tool', handler)],
164
+ });
165
+
166
+ const result = await invokeHandler(server, 'tools/call', {
167
+ name: 'local_tool',
168
+ arguments: { a: 1 },
169
+ });
170
+
171
+ expect(result).toMatchObject({ content: [{ text: '{"a":1}' }] });
172
+ expect(handler).toHaveBeenCalledOnce();
173
+ expect(seenContext?.requestId).toBeTruthy();
174
+ expect(backend.callTool).not.toHaveBeenCalled();
175
+ });
176
+
177
+ it('runs the full toolMiddleware pipeline around the handler', async () => {
178
+ const redact: ToolMiddleware = async (req, next) => {
179
+ const result = await next(req);
180
+ return { ...result, content: [{ type: 'text', text: 'redacted' }] };
181
+ };
182
+ const server = createProxyServer(makeMockBackend(), {
183
+ name: 'test-server',
184
+ localTools: [makeLocalTool('local_tool')],
185
+ toolMiddleware: [redact],
186
+ });
187
+
188
+ const result = await invokeHandler(server, 'tools/call', {
189
+ name: 'local_tool',
190
+ arguments: {},
191
+ });
192
+ expect(result).toMatchObject({ content: [{ text: 'redacted' }] });
193
+ });
194
+
195
+ it('a shadowed name routes to the local handler, not the upstream', async () => {
196
+ const backend = makeMockBackend();
197
+ const server = createProxyServer(backend, {
198
+ name: 'test-server',
199
+ localTools: [makeLocalTool('shadowed_tool')],
200
+ });
201
+
202
+ const result = await invokeHandler(server, 'tools/call', {
203
+ name: 'shadowed_tool',
204
+ arguments: {},
205
+ });
206
+ expect(result).toMatchObject({
207
+ content: [{ text: 'local shadowed_tool' }],
208
+ });
209
+ expect(backend.callTool).not.toHaveBeenCalled();
210
+ });
211
+ });
212
+
213
+ // ── Precedence ──────────────────────────────────────────────────────────────
214
+
215
+ describe('createProxyServer() — localTools precedence', () => {
216
+ it('hiddenTools beats a local tool: filtered from the list, rejected with TOOL_HIDDEN', async () => {
217
+ const handler = vi.fn(async () => ({ content: [] }));
218
+ const server = createProxyServer(makeMockBackend(), {
219
+ name: 'test-server',
220
+ localTools: [makeLocalTool('local_tool', handler)],
221
+ hiddenTools: ['local_tool'],
222
+ });
223
+
224
+ const listed = (await invokeHandler(server, 'tools/list')) as ListResult;
225
+ expect(listed.tools.map((t) => t.name)).not.toContain('local_tool');
226
+
227
+ await expect(
228
+ invokeHandler(server, 'tools/call', {
229
+ name: 'local_tool',
230
+ arguments: {},
231
+ }),
232
+ ).rejects.toMatchObject({ data: { rejectionReason: 'TOOL_HIDDEN' } });
233
+ expect(handler).not.toHaveBeenCalled();
234
+ });
235
+
236
+ it('passThroughTools has no effect on a local tool: transforming middleware still runs', async () => {
237
+ const transform: ToolMiddleware = async (req, next) => {
238
+ const result = await next(req);
239
+ return { ...result, content: [{ type: 'text', text: 'transformed' }] };
240
+ };
241
+ const server = createProxyServer(makeMockBackend(), {
242
+ name: 'test-server',
243
+ localTools: [makeLocalTool('local_tool')],
244
+ passThroughTools: ['local_tool'],
245
+ toolMiddleware: [transform],
246
+ });
247
+
248
+ const result = await invokeHandler(server, 'tools/call', {
249
+ name: 'local_tool',
250
+ arguments: {},
251
+ });
252
+ expect(result).toMatchObject({ content: [{ text: 'transformed' }] });
253
+ });
254
+
255
+ it('throws at createProxyServer on a duplicate local tool name', () => {
256
+ expect(() =>
257
+ createProxyServer(makeMockBackend(), {
258
+ name: 'test-server',
259
+ localTools: [makeLocalTool('dup'), makeLocalTool('dup')],
260
+ }),
261
+ ).toThrow(/duplicate local tool name "dup"/);
262
+ });
263
+
264
+ it('throws at startHttpProxy startup on a duplicate local tool name, not on first initialize', () => {
265
+ expect(() =>
266
+ startHttpProxy(
267
+ makeMockBackend(),
268
+ {
269
+ name: 'test-server',
270
+ localTools: [makeLocalTool('dup'), makeLocalTool('dup')],
271
+ },
272
+ { port: 0 },
273
+ ),
274
+ ).toThrow(/duplicate local tool name "dup"/);
275
+ });
276
+ });
277
+
278
+ // ── Tools-less upstream ─────────────────────────────────────────────────────
279
+
280
+ describe('createProxyServer() — localTools against an upstream without tools', () => {
281
+ it('advertises the tools capability and serves the local tool', async () => {
282
+ const backend = makeMockBackend({ resources: {} });
283
+ const server = createProxyServer(backend, {
284
+ name: 'test-server',
285
+ localTools: [makeLocalTool('local_tool')],
286
+ });
287
+
288
+ const listed = (await invokeHandler(server, 'tools/list')) as ListResult;
289
+ expect(listed.tools.map((t) => t.name)).toEqual(['local_tool']);
290
+ expect(backend.listTools).not.toHaveBeenCalled();
291
+
292
+ const result = await invokeHandler(server, 'tools/call', {
293
+ name: 'local_tool',
294
+ arguments: {},
295
+ });
296
+ expect(result).toMatchObject({ content: [{ text: 'local local_tool' }] });
297
+ });
298
+
299
+ it('rejects an unknown tool with MethodNotFound instead of forwarding upstream', async () => {
300
+ const backend = makeMockBackend({ resources: {} });
301
+ const server = createProxyServer(backend, {
302
+ name: 'test-server',
303
+ localTools: [makeLocalTool('local_tool')],
304
+ });
305
+
306
+ await expect(
307
+ invokeHandler(server, 'tools/call', {
308
+ name: 'no_such_tool',
309
+ arguments: {},
310
+ }),
311
+ ).rejects.toMatchObject({ code: ErrorCode.MethodNotFound });
312
+ expect(backend.callTool).not.toHaveBeenCalled();
313
+ });
314
+ });