mcpose 2.1.1 → 3.0.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 (99) hide show
  1. package/README.md +457 -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 +285 -32
  7. package/dist/core.d.ts.map +1 -1
  8. package/dist/core.js +961 -193
  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 +1 -1
  39. package/dist/rejection.d.ts.map +1 -1
  40. package/dist/rejection.js +1 -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 +86 -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 +1019 -0
  71. package/src/__tests__/eventStore.test.ts +141 -0
  72. package/src/__tests__/mapToolResult.test.ts +101 -0
  73. package/src/__tests__/middleware.edge.test.ts +126 -0
  74. package/src/__tests__/middleware.test.ts +233 -0
  75. package/src/__tests__/proxyContext.test.ts +140 -0
  76. package/src/__tests__/sanitizeCatalog.test.ts +359 -0
  77. package/src/__tests__/startHttpProxy.lifecycle.test.ts +638 -0
  78. package/src/__tests__/startHttpProxy.registry.test.ts +409 -0
  79. package/src/__tests__/startHttpProxy.replay.test.ts +116 -0
  80. package/src/__tests__/startHttpProxy.security.test.ts +288 -0
  81. package/src/__tests__/startHttpProxy.test.ts +985 -0
  82. package/src/__tests__/testing.test.ts +250 -0
  83. package/src/__tests__/version.test.ts +12 -0
  84. package/src/backendClient.ts +83 -0
  85. package/src/core.ts +2042 -0
  86. package/src/delegation.ts +224 -0
  87. package/src/eventStore.ts +106 -0
  88. package/src/hiddenTools.ts +84 -0
  89. package/src/identity.ts +15 -0
  90. package/src/index.ts +63 -0
  91. package/src/mesh.ts +204 -0
  92. package/src/middleware.ts +101 -0
  93. package/src/proxyContext.ts +108 -0
  94. package/src/rejection.ts +37 -0
  95. package/src/sanitizeCatalog.ts +120 -0
  96. package/src/sessionRegistry.ts +142 -0
  97. package/src/telemetry.ts +46 -0
  98. package/src/testing.ts +174 -0
  99. package/src/version.ts +6 -0
@@ -0,0 +1,226 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createProxyServer } from '../core.js';
3
+ import type { ToolMiddleware } from '../core.js';
4
+ import type { BackendClient } from '../backendClient.js';
5
+
6
+ // ── Test helpers ────────────────────────────────────────────────────────────
7
+
8
+ const CLIENT_META = {
9
+ progressToken: 3,
10
+ traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
11
+ 'vscode/conversationId': 'a-uuid',
12
+ };
13
+
14
+ function makeMockBackend(): BackendClient {
15
+ return {
16
+ getServerCapabilities: vi
17
+ .fn()
18
+ .mockReturnValue({ tools: {}, resources: {}, prompts: {} }),
19
+ listTools: vi.fn().mockResolvedValue({ tools: [] }),
20
+ callTool: vi.fn().mockResolvedValue({
21
+ content: [{ type: 'text', text: 'ok' }],
22
+ }),
23
+ listResources: vi.fn().mockResolvedValue({ resources: [] }),
24
+ readResource: vi.fn().mockResolvedValue({ contents: [] }),
25
+ listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
26
+ getPrompt: vi.fn().mockResolvedValue({ messages: [] }),
27
+ setNotificationHandler: vi.fn(),
28
+ removeNotificationHandler: vi.fn(),
29
+ close: vi.fn().mockResolvedValue(undefined),
30
+ } as unknown as BackendClient;
31
+ }
32
+
33
+ /** Invokes a registered handler directly via `_requestHandlers` — no transport needed. */
34
+ async function invokeHandler(
35
+ server: ReturnType<typeof createProxyServer>,
36
+ method: string,
37
+ params: Record<string, unknown> = {},
38
+ extra: object = {},
39
+ ) {
40
+ const { _requestHandlers: handlers } = server as unknown as {
41
+ _requestHandlers: Map<
42
+ string,
43
+ (
44
+ req: { method: string; params: Record<string, unknown> },
45
+ extra: object,
46
+ ) => Promise<unknown>
47
+ >;
48
+ };
49
+ const handler = handlers.get(method);
50
+ if (!handler) throw new Error(`No handler registered for method: ${method}`);
51
+ return handler({ method, params }, extra);
52
+ }
53
+
54
+ function upstreamParams(mock: unknown): Record<string, unknown> {
55
+ return (mock as { mock: { calls: [Record<string, unknown>][] } }).mock
56
+ .calls[0]![0];
57
+ }
58
+
59
+ // ── The default ─────────────────────────────────────────────────────────────
60
+
61
+ describe('createProxyServer() — stripRequestMeta defaults to on', () => {
62
+ it('strips _meta from a tool call before the upstream sees it', async () => {
63
+ const backend = makeMockBackend();
64
+ const server = createProxyServer(backend, { name: 'test-server' });
65
+
66
+ await invokeHandler(server, 'tools/call', {
67
+ name: 'search_issues',
68
+ arguments: { q: 'x' },
69
+ _meta: CLIENT_META,
70
+ });
71
+
72
+ const params = upstreamParams(backend.callTool);
73
+ expect(params).not.toHaveProperty('_meta');
74
+ expect(params).toMatchObject({
75
+ name: 'search_issues',
76
+ arguments: { q: 'x' },
77
+ });
78
+ });
79
+
80
+ it.each([
81
+ ['tools/list', 'listTools'],
82
+ ['resources/list', 'listResources'],
83
+ ['prompts/list', 'listPrompts'],
84
+ ] as const)('strips _meta from %s', async (method, backendMethod) => {
85
+ const backend = makeMockBackend();
86
+ const server = createProxyServer(backend, { name: 'test-server' });
87
+
88
+ await invokeHandler(server, method, { _meta: CLIENT_META });
89
+ expect(
90
+ upstreamParams(backend[backendMethod as keyof BackendClient]),
91
+ ).not.toHaveProperty('_meta');
92
+ });
93
+
94
+ it('strips _meta from a resource read and a prompt get', async () => {
95
+ const backend = makeMockBackend();
96
+ const server = createProxyServer(backend, { name: 'test-server' });
97
+
98
+ await invokeHandler(server, 'resources/read', {
99
+ uri: 'res://a',
100
+ _meta: CLIENT_META,
101
+ });
102
+ expect(upstreamParams(backend.readResource)).not.toHaveProperty('_meta');
103
+
104
+ await invokeHandler(server, 'prompts/get', {
105
+ name: 'p',
106
+ _meta: CLIENT_META,
107
+ });
108
+ expect(upstreamParams(backend.getPrompt)).not.toHaveProperty('_meta');
109
+ });
110
+
111
+ it('applies to pass-through tools too', async () => {
112
+ const backend = makeMockBackend();
113
+ const server = createProxyServer(backend, {
114
+ name: 'test-server',
115
+ passThroughTools: ['search_issues'],
116
+ });
117
+
118
+ await invokeHandler(server, 'tools/call', {
119
+ name: 'search_issues',
120
+ arguments: {},
121
+ _meta: CLIENT_META,
122
+ });
123
+ expect(upstreamParams(backend.callTool)).not.toHaveProperty('_meta');
124
+ });
125
+ });
126
+
127
+ // ── The boundary ────────────────────────────────────────────────────────────
128
+
129
+ describe('createProxyServer() — the strip happens before the pipeline', () => {
130
+ it('middleware sees the stripped request', async () => {
131
+ const backend = makeMockBackend();
132
+ let seenMeta: unknown = 'not-called';
133
+ const observe: ToolMiddleware = async (req, next) => {
134
+ seenMeta = req.params._meta;
135
+ return next(req);
136
+ };
137
+ const server = createProxyServer(backend, {
138
+ name: 'test-server',
139
+ toolMiddleware: [observe],
140
+ });
141
+
142
+ await invokeHandler(server, 'tools/call', {
143
+ name: 'search_issues',
144
+ arguments: {},
145
+ _meta: CLIENT_META,
146
+ });
147
+ expect(seenMeta).toBeUndefined();
148
+ });
149
+
150
+ it('middleware can still add its own _meta deliberately, and it reaches the upstream', async () => {
151
+ const backend = makeMockBackend();
152
+ const addMeta: ToolMiddleware = async (req, next) =>
153
+ next({
154
+ ...req,
155
+ params: { ...req.params, _meta: { tenant: 'bank-pilot' } },
156
+ });
157
+ const server = createProxyServer(backend, {
158
+ name: 'test-server',
159
+ toolMiddleware: [addMeta],
160
+ });
161
+
162
+ await invokeHandler(server, 'tools/call', {
163
+ name: 'search_issues',
164
+ arguments: {},
165
+ _meta: CLIENT_META,
166
+ });
167
+ expect(upstreamParams(backend.callTool)._meta).toEqual({
168
+ tenant: 'bank-pilot',
169
+ });
170
+ });
171
+
172
+ it('progress relay is unaffected: the token is read from the request extra, not params._meta', async () => {
173
+ const backend = makeMockBackend();
174
+ const callToolWithProgress = vi.fn(
175
+ async (
176
+ _params: unknown,
177
+ _schema: unknown,
178
+ options?: { onprogress?: (p: { progress: number }) => void },
179
+ ) => {
180
+ options?.onprogress?.({ progress: 1 });
181
+ return { content: [] };
182
+ },
183
+ );
184
+ (backend as unknown as { callTool: unknown }).callTool =
185
+ callToolWithProgress;
186
+ const sendNotification = vi.fn().mockResolvedValue(undefined);
187
+ const server = createProxyServer(backend, { name: 'test-server' });
188
+
189
+ await invokeHandler(
190
+ server,
191
+ 'tools/call',
192
+ { name: 'search_issues', arguments: {}, _meta: CLIENT_META },
193
+ { _meta: { progressToken: 3 }, sendNotification },
194
+ );
195
+
196
+ expect(upstreamParams(callToolWithProgress)).not.toHaveProperty('_meta');
197
+ const notification = sendNotification.mock.calls[0]?.[0] as {
198
+ method: string;
199
+ params: { progressToken: number; progress: number };
200
+ };
201
+ expect(notification.method).toBe('notifications/progress');
202
+ expect(notification.params).toMatchObject({
203
+ progressToken: 3,
204
+ progress: 1,
205
+ });
206
+ });
207
+ });
208
+
209
+ // ── The opt-out ─────────────────────────────────────────────────────────────
210
+
211
+ describe('createProxyServer() — stripRequestMeta: false restores forwarding', () => {
212
+ it('forwards _meta verbatim when disabled', async () => {
213
+ const backend = makeMockBackend();
214
+ const server = createProxyServer(backend, {
215
+ name: 'test-server',
216
+ stripRequestMeta: false,
217
+ });
218
+
219
+ await invokeHandler(server, 'tools/call', {
220
+ name: 'search_issues',
221
+ arguments: {},
222
+ _meta: CLIENT_META,
223
+ });
224
+ expect(upstreamParams(backend.callTool)._meta).toEqual(CLIENT_META);
225
+ });
226
+ });
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createProxyServer } from '../core.js';
3
+ import type { ToolMiddleware } from '../core.js';
4
+ import type { BackendClient } from '../backendClient.js';
5
+
6
+ // ── Test helpers ────────────────────────────────────────────────────────────
7
+
8
+ const UPSTREAM_META = {
9
+ 'io.modelcontextprotocol/related-task': { taskId: 'task-1' },
10
+ 'vendor/trace': 'abc123',
11
+ };
12
+
13
+ const TOOL_RESULT = {
14
+ content: [{ type: 'text', text: 'ok', _meta: { block: 'level' } }],
15
+ structuredContent: { answer: 42 },
16
+ _meta: UPSTREAM_META,
17
+ };
18
+
19
+ function makeMockBackend(): BackendClient {
20
+ return {
21
+ getServerCapabilities: vi
22
+ .fn()
23
+ .mockReturnValue({ tools: {}, resources: {}, prompts: {} }),
24
+ listTools: vi.fn().mockResolvedValue({
25
+ tools: [{ name: 't', inputSchema: { type: 'object' }, _meta: { a: 1 } }],
26
+ _meta: UPSTREAM_META,
27
+ }),
28
+ callTool: vi.fn().mockResolvedValue(TOOL_RESULT),
29
+ listResources: vi
30
+ .fn()
31
+ .mockResolvedValue({ resources: [], _meta: UPSTREAM_META }),
32
+ readResource: vi
33
+ .fn()
34
+ .mockResolvedValue({ contents: [], _meta: UPSTREAM_META }),
35
+ listPrompts: vi
36
+ .fn()
37
+ .mockResolvedValue({ prompts: [], _meta: UPSTREAM_META }),
38
+ getPrompt: vi
39
+ .fn()
40
+ .mockResolvedValue({ messages: [], _meta: UPSTREAM_META }),
41
+ setNotificationHandler: vi.fn(),
42
+ removeNotificationHandler: vi.fn(),
43
+ close: vi.fn().mockResolvedValue(undefined),
44
+ } as unknown as BackendClient;
45
+ }
46
+
47
+ /** Invokes a registered handler directly via `_requestHandlers` — no transport needed. */
48
+ async function invokeHandler(
49
+ server: ReturnType<typeof createProxyServer>,
50
+ method: string,
51
+ params: Record<string, unknown> = {},
52
+ extra: object = {},
53
+ ) {
54
+ const { _requestHandlers: handlers } = server as unknown as {
55
+ _requestHandlers: Map<
56
+ string,
57
+ (
58
+ req: { method: string; params: Record<string, unknown> },
59
+ extra: object,
60
+ ) => Promise<unknown>
61
+ >;
62
+ };
63
+ const handler = handlers.get(method);
64
+ if (!handler) throw new Error(`No handler registered for method: ${method}`);
65
+ return handler({ method, params }, extra);
66
+ }
67
+
68
+ // ── The default ─────────────────────────────────────────────────────────────
69
+
70
+ describe('createProxyServer() — stripResultMeta defaults to on', () => {
71
+ it('strips _meta from a tool call result before the client sees it', async () => {
72
+ const backend = makeMockBackend();
73
+ const server = createProxyServer(backend, { name: 'test-server' });
74
+
75
+ const result = await invokeHandler(server, 'tools/call', {
76
+ name: 'search_issues',
77
+ arguments: {},
78
+ });
79
+
80
+ expect(result).not.toHaveProperty('_meta');
81
+ });
82
+
83
+ it.each([
84
+ ['tools/list', {}],
85
+ ['resources/list', {}],
86
+ ['resources/read', { uri: 'res://a' }],
87
+ ['prompts/list', {}],
88
+ ['prompts/get', { name: 'p' }],
89
+ ] as const)('strips result _meta from %s', async (method, params) => {
90
+ const backend = makeMockBackend();
91
+ const server = createProxyServer(backend, { name: 'test-server' });
92
+
93
+ const result = await invokeHandler(server, method, { ...params });
94
+ expect(result).not.toHaveProperty('_meta');
95
+ });
96
+
97
+ it('applies to pass-through tools and resources too', async () => {
98
+ const backend = makeMockBackend();
99
+ const server = createProxyServer(backend, {
100
+ name: 'test-server',
101
+ passThroughTools: ['search_issues'],
102
+ passThroughResources: ['res://a'],
103
+ });
104
+
105
+ const toolResult = await invokeHandler(server, 'tools/call', {
106
+ name: 'search_issues',
107
+ arguments: {},
108
+ });
109
+ expect(toolResult).not.toHaveProperty('_meta');
110
+
111
+ const readResult = await invokeHandler(server, 'resources/read', {
112
+ uri: 'res://a',
113
+ });
114
+ expect(readResult).not.toHaveProperty('_meta');
115
+ });
116
+
117
+ it('leaves block-level _meta, per-tool _meta, and structuredContent untouched', async () => {
118
+ const backend = makeMockBackend();
119
+ const server = createProxyServer(backend, { name: 'test-server' });
120
+
121
+ const toolResult = (await invokeHandler(server, 'tools/call', {
122
+ name: 'search_issues',
123
+ arguments: {},
124
+ })) as { content: unknown[]; structuredContent: unknown };
125
+ expect(toolResult.content).toEqual([
126
+ { type: 'text', text: 'ok', _meta: { block: 'level' } },
127
+ ]);
128
+ expect(toolResult.structuredContent).toEqual({ answer: 42 });
129
+
130
+ const listResult = (await invokeHandler(server, 'tools/list')) as {
131
+ tools: { _meta?: unknown }[];
132
+ };
133
+ expect(listResult.tools[0]?._meta).toEqual({ a: 1 });
134
+ });
135
+
136
+ it('does NOT strip a local tool result _meta', async () => {
137
+ const backend = makeMockBackend();
138
+ const server = createProxyServer(backend, {
139
+ name: 'test-server',
140
+ localTools: [
141
+ {
142
+ tool: { name: 'local_echo', inputSchema: { type: 'object' } },
143
+ handler: async () => ({
144
+ content: [{ type: 'text', text: 'local' }],
145
+ _meta: { local: true },
146
+ }),
147
+ },
148
+ ],
149
+ });
150
+
151
+ const result = await invokeHandler(server, 'tools/call', {
152
+ name: 'local_echo',
153
+ arguments: {},
154
+ });
155
+ expect(result).toHaveProperty('_meta', { local: true });
156
+ });
157
+ });
158
+
159
+ // ── The boundary ────────────────────────────────────────────────────────────
160
+
161
+ describe('createProxyServer() — the strip happens inside the innermost next', () => {
162
+ it('middleware sees the stripped result', async () => {
163
+ const backend = makeMockBackend();
164
+ let seenMeta: unknown = 'not-called';
165
+ const observe: ToolMiddleware = async (req, next) => {
166
+ const result = await next(req);
167
+ seenMeta = result._meta;
168
+ return result;
169
+ };
170
+ const server = createProxyServer(backend, {
171
+ name: 'test-server',
172
+ toolMiddleware: [observe],
173
+ });
174
+
175
+ await invokeHandler(server, 'tools/call', {
176
+ name: 'search_issues',
177
+ arguments: {},
178
+ });
179
+ expect(seenMeta).toBeUndefined();
180
+ });
181
+
182
+ it('middleware-added result _meta survives to the client', async () => {
183
+ const backend = makeMockBackend();
184
+ const addMeta: ToolMiddleware = async (req, next) => {
185
+ const result = await next(req);
186
+ return { ...result, _meta: { redactedBy: 'pii-mw' } };
187
+ };
188
+ const server = createProxyServer(backend, {
189
+ name: 'test-server',
190
+ toolMiddleware: [addMeta],
191
+ });
192
+
193
+ const result = await invokeHandler(server, 'tools/call', {
194
+ name: 'search_issues',
195
+ arguments: {},
196
+ });
197
+ expect(result).toHaveProperty('_meta', { redactedBy: 'pii-mw' });
198
+ });
199
+ });
200
+
201
+ // ── The opt-out ─────────────────────────────────────────────────────────────
202
+
203
+ describe('createProxyServer() — stripResultMeta: false restores forwarding', () => {
204
+ it('forwards result _meta verbatim when disabled', async () => {
205
+ const backend = makeMockBackend();
206
+ const server = createProxyServer(backend, {
207
+ name: 'test-server',
208
+ stripResultMeta: false,
209
+ });
210
+
211
+ const result = await invokeHandler(server, 'tools/call', {
212
+ name: 'search_issues',
213
+ arguments: {},
214
+ });
215
+ expect(result).toHaveProperty('_meta', UPSTREAM_META);
216
+ });
217
+ });