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,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Identity } from '../identity.js';
3
+ import {
4
+ createProxyContext,
5
+ outboundDelegationChain,
6
+ } from '../proxyContext.js';
7
+
8
+ describe('createProxyContext()', () => {
9
+ it('defaults to stdio transport with a generated request ID', () => {
10
+ const context = createProxyContext();
11
+
12
+ expect(context.transport).toBe('stdio');
13
+ expect(context.requestId).toEqual(expect.any(String));
14
+ expect(context.requestId.length).toBeGreaterThan(0);
15
+ expect(context.sessionId).toBeUndefined();
16
+ expect(context.headers).toBeUndefined();
17
+ });
18
+
19
+ it('preserves supplied context fields', () => {
20
+ const headers = { authorization: 'Bearer token' };
21
+ const context = createProxyContext({
22
+ requestId: 'req-123',
23
+ transport: 'http',
24
+ sessionId: 'session-1',
25
+ headers,
26
+ });
27
+
28
+ expect(context).toEqual({
29
+ requestId: 'req-123',
30
+ transport: 'http',
31
+ sessionId: 'session-1',
32
+ headers,
33
+ });
34
+ });
35
+
36
+ it('carries a stamped policy decision through', () => {
37
+ const policy = { decision: 'allow', ruleId: 'r1' } as const;
38
+ expect(createProxyContext({ policy }).policy).toEqual(policy);
39
+ });
40
+
41
+ it('omits sessionId/headers/signal keys when they are not provided', () => {
42
+ const ctx = createProxyContext({ transport: 'http' });
43
+
44
+ expect('sessionId' in ctx).toBe(false);
45
+ expect('headers' in ctx).toBe(false);
46
+ expect('signal' in ctx).toBe(false);
47
+ });
48
+
49
+ it('omits signal key when explicitly passed as undefined', () => {
50
+ const ctx = createProxyContext({ signal: undefined });
51
+
52
+ expect('signal' in ctx).toBe(false);
53
+ });
54
+
55
+ it('preserves an AbortSignal reference', () => {
56
+ const controller = new AbortController();
57
+ const ctx = createProxyContext({ signal: controller.signal });
58
+
59
+ expect(ctx.signal).toBe(controller.signal);
60
+ });
61
+
62
+ it('keeps the original headers reference (Readonly is shallow)', () => {
63
+ const headers = { 'x-trace': 'abc' };
64
+ const ctx = createProxyContext({ headers });
65
+
66
+ expect(ctx.headers).toBe(headers);
67
+ });
68
+
69
+ it('regenerates a UUID when requestId is provided as an empty string', () => {
70
+ const ctx = createProxyContext({ requestId: '' });
71
+
72
+ expect(ctx.requestId).not.toBe('');
73
+ expect(ctx.requestId.length).toBeGreaterThan(0);
74
+ });
75
+
76
+ it('generates distinct requestIds across calls', () => {
77
+ const a = createProxyContext();
78
+ const b = createProxyContext();
79
+
80
+ expect(a.requestId).not.toEqual(b.requestId);
81
+ });
82
+
83
+ it('preserves the proxy identity reference and omits the key when absent', () => {
84
+ const proxy = { name: 'payments-proxy', version: '1.2.3' };
85
+
86
+ expect(createProxyContext({ proxy }).proxy).toBe(proxy);
87
+ expect('proxy' in createProxyContext()).toBe(false);
88
+ expect('proxy' in createProxyContext({ proxy: undefined })).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe('outboundDelegationChain()', () => {
93
+ const identity = (sub: string): Identity => ({
94
+ sub,
95
+ type: 'agent',
96
+ roles: [],
97
+ claims: {},
98
+ resolvedAt: '2026-01-01T00:00:00Z',
99
+ source: 'custom',
100
+ });
101
+
102
+ it('appends identity after the existing delegatedFrom chain oldest-first', () => {
103
+ const root = identity('root');
104
+ const mid = identity('mid');
105
+ const caller = identity('caller');
106
+ const ctx = createProxyContext({
107
+ identity: caller,
108
+ delegatedFrom: [root, mid],
109
+ });
110
+
111
+ expect(outboundDelegationChain(ctx)).toEqual([root, mid, caller]);
112
+ });
113
+
114
+ it('returns an empty array when neither identity nor delegatedFrom is set', () => {
115
+ expect(outboundDelegationChain(createProxyContext())).toEqual([]);
116
+ });
117
+
118
+ it('returns only the identity when delegatedFrom is absent', () => {
119
+ const caller = identity('caller');
120
+ const ctx = createProxyContext({ identity: caller });
121
+
122
+ expect(outboundDelegationChain(ctx)).toEqual([caller]);
123
+ });
124
+
125
+ it('returns a fresh array and does not mutate or alias ctx.delegatedFrom', () => {
126
+ const root = identity('root');
127
+ const delegatedFrom = [root];
128
+ const ctx = createProxyContext({
129
+ identity: identity('caller'),
130
+ delegatedFrom,
131
+ });
132
+
133
+ const chain = outboundDelegationChain(ctx);
134
+
135
+ expect(chain).not.toBe(delegatedFrom);
136
+ expect(ctx.delegatedFrom).toBe(delegatedFrom);
137
+ expect(ctx.delegatedFrom).toEqual([root]);
138
+ expect(outboundDelegationChain(ctx)).not.toBe(chain);
139
+ });
140
+ });
@@ -0,0 +1,359 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sanitizeToolDescriptions } from '../sanitizeCatalog.js';
3
+ import { runListToolsMiddleware } from '../testing.js';
4
+ import type {
5
+ ListToolsRequest,
6
+ ListToolsResult,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+
9
+ const listReq: ListToolsRequest = { method: 'tools/list', params: {} };
10
+
11
+ interface NestedSchema {
12
+ type: string;
13
+ properties: Record<
14
+ string,
15
+ {
16
+ type?: string;
17
+ description?: string;
18
+ items?: { properties: Record<string, { description?: string }> };
19
+ }
20
+ >;
21
+ }
22
+
23
+ function makeResult(): ListToolsResult {
24
+ return {
25
+ tools: [
26
+ {
27
+ name: 'search_issues',
28
+ title: 'Search issues',
29
+ description: 'Search issues. Docs: https://internal.acme-corp.dev/api',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ query: {
34
+ type: 'string',
35
+ description: 'Query, see https://acme-corp.dev/syntax',
36
+ },
37
+ filters: {
38
+ type: 'array',
39
+ items: {
40
+ type: 'object',
41
+ properties: {
42
+ field: { type: 'string', description: 'acme-corp field id' },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ outputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ url: { type: 'string', description: 'Link into acme-corp' },
52
+ },
53
+ },
54
+ },
55
+ ],
56
+ };
57
+ }
58
+
59
+ async function run(
60
+ mw = sanitizeToolDescriptions(),
61
+ result: ListToolsResult = makeResult(),
62
+ ) {
63
+ return runListToolsMiddleware(mw, listReq, async () => result);
64
+ }
65
+
66
+ describe('sanitizeToolDescriptions()', () => {
67
+ it('strips http(s) URLs from tool descriptions by default', async () => {
68
+ const result = await run();
69
+ expect(result.tools[0]?.description).toBe('Search issues. Docs: ');
70
+ });
71
+
72
+ it('applies string patterns to every occurrence', async () => {
73
+ const upstream: ListToolsResult = {
74
+ tools: [
75
+ {
76
+ name: 't',
77
+ description: 'acme-corp does acme-corp things',
78
+ inputSchema: { type: 'object' },
79
+ },
80
+ ],
81
+ };
82
+ const result = await run(
83
+ sanitizeToolDescriptions({ patterns: ['acme-corp'] }),
84
+ upstream,
85
+ );
86
+ expect(result.tools[0]?.description).toBe(' does things');
87
+ });
88
+
89
+ it('normalizes a non-global regex so every occurrence is replaced', async () => {
90
+ const upstream: ListToolsResult = {
91
+ tools: [
92
+ {
93
+ name: 't',
94
+ description: 'host db-1 and host db-2',
95
+ inputSchema: { type: 'object' },
96
+ },
97
+ ],
98
+ };
99
+ const result = await run(
100
+ sanitizeToolDescriptions({ patterns: [/db-\d/] }),
101
+ upstream,
102
+ );
103
+ expect(result.tools[0]?.description).toBe('host and host ');
104
+ });
105
+
106
+ it('drops a sticky flag so a sticky pattern still replaces every occurrence', async () => {
107
+ const upstream: ListToolsResult = {
108
+ tools: [
109
+ {
110
+ name: 't',
111
+ description: 'a db-1 b db-2',
112
+ inputSchema: { type: 'object' },
113
+ },
114
+ ],
115
+ };
116
+ const result = await run(
117
+ sanitizeToolDescriptions({ patterns: [/db-\d/y] }),
118
+ upstream,
119
+ );
120
+ expect(result.tools[0]?.description).toBe('a b ');
121
+ });
122
+
123
+ it('sanitizes description fields nested in inputSchema and outputSchema', async () => {
124
+ const result = await run(
125
+ sanitizeToolDescriptions({ patterns: ['acme-corp'] }),
126
+ );
127
+ const input = result.tools[0]?.inputSchema as unknown as NestedSchema;
128
+ expect(input.properties.query?.description).toBe('Query, see ');
129
+ expect(input.properties.filters?.items?.properties.field?.description).toBe(
130
+ ' field id',
131
+ );
132
+ const output = result.tools[0]?.outputSchema as unknown as NestedSchema;
133
+ expect(output.properties.url?.description).toBe('Link into ');
134
+ });
135
+
136
+ it('strips plain http URLs too', async () => {
137
+ const upstream: ListToolsResult = {
138
+ tools: [
139
+ {
140
+ name: 't',
141
+ description: 'see http://internal.host/a for details',
142
+ inputSchema: { type: 'object' },
143
+ },
144
+ ],
145
+ };
146
+ const result = await run(sanitizeToolDescriptions(), upstream);
147
+ expect(result.tools[0]?.description).toBe('see for details');
148
+ });
149
+
150
+ it('accepts a pattern that already carries the global flag', async () => {
151
+ const upstream: ListToolsResult = {
152
+ tools: [
153
+ {
154
+ name: 't',
155
+ description: 'a db-1 b db-2',
156
+ inputSchema: { type: 'object' },
157
+ },
158
+ ],
159
+ };
160
+ const result = await run(
161
+ sanitizeToolDescriptions({ patterns: [/db-\d/g] }),
162
+ upstream,
163
+ );
164
+ expect(result.tools[0]?.description).toBe('a b ');
165
+ });
166
+
167
+ it('sanitizes descriptions inside schema arrays, keeping untouched siblings by reference', async () => {
168
+ const clean = { type: 'string' };
169
+ const untouchedEnum = ['x', 'y'];
170
+ const upstream: ListToolsResult = {
171
+ tools: [
172
+ {
173
+ name: 't',
174
+ inputSchema: {
175
+ type: 'object',
176
+ anyOf: [{ description: 'acme-corp branch' }, clean],
177
+ allOf: [
178
+ { description: 'acme-corp a' },
179
+ { description: 'acme-corp b' },
180
+ ],
181
+ enum: untouchedEnum,
182
+ },
183
+ },
184
+ ],
185
+ };
186
+ const result = await run(
187
+ sanitizeToolDescriptions({ patterns: ['acme-corp'] }),
188
+ upstream,
189
+ );
190
+ const schema = result.tools[0]?.inputSchema as unknown as {
191
+ anyOf: [{ description: string }, typeof clean];
192
+ allOf: [{ description: string }, { description: string }];
193
+ enum: string[];
194
+ };
195
+ expect(Array.isArray(schema.anyOf)).toBe(true);
196
+ expect(schema.anyOf[0].description).toBe(' branch');
197
+ expect(schema.anyOf[1]).toBe(clean);
198
+ expect(schema.allOf[0].description).toBe(' a');
199
+ expect(schema.allOf[1].description).toBe(' b');
200
+ expect(schema.enum).toBe(untouchedEnum);
201
+ });
202
+
203
+ it('sanitizes a tool whose only match is in outputSchema', async () => {
204
+ const upstream: ListToolsResult = {
205
+ tools: [
206
+ {
207
+ name: 't',
208
+ description: 'clean',
209
+ inputSchema: { type: 'object' },
210
+ outputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ url: { type: 'string', description: 'https://internal.host/x' },
214
+ },
215
+ },
216
+ },
217
+ ],
218
+ };
219
+ const result = await run(sanitizeToolDescriptions(), upstream);
220
+ const output = result.tools[0]?.outputSchema as unknown as NestedSchema;
221
+ expect(output.properties.url?.description).toBe('');
222
+ });
223
+
224
+ it('ignores string values outside description keys, non-string descriptions, and null nodes', async () => {
225
+ const upstream: ListToolsResult = {
226
+ tools: [
227
+ {
228
+ name: 't',
229
+ description: 'acme-corp tool',
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ host: {
234
+ type: 'string',
235
+ pattern: 'acme-corp',
236
+ default: null,
237
+ description: { note: 'acme-corp' },
238
+ },
239
+ },
240
+ } as unknown as ListToolsResult['tools'][number]['inputSchema'],
241
+ },
242
+ ],
243
+ };
244
+ const result = await run(
245
+ sanitizeToolDescriptions({ patterns: ['acme-corp'] }),
246
+ upstream,
247
+ );
248
+ expect(result.tools[0]?.description).toBe(' tool');
249
+ const input = result.tools[0]?.inputSchema as unknown as {
250
+ properties: {
251
+ host: {
252
+ pattern: string;
253
+ default: null;
254
+ description: { note: string };
255
+ };
256
+ };
257
+ };
258
+ expect(input.properties.host.pattern).toBe('acme-corp');
259
+ expect(input.properties.host.default).toBeNull();
260
+ expect(input.properties.host.description).toEqual({ note: 'acme-corp' });
261
+ });
262
+
263
+ it('applies no hidden default patterns beyond the URL strip', async () => {
264
+ const upstream: ListToolsResult = {
265
+ tools: [
266
+ {
267
+ name: 't',
268
+ description: 'Stryker was here and stays',
269
+ inputSchema: { type: 'object' },
270
+ },
271
+ ],
272
+ };
273
+ const result = await run(sanitizeToolDescriptions(), upstream);
274
+ expect(result).toBe(upstream);
275
+ });
276
+
277
+ it('does not inject absent description or outputSchema keys when rebuilding', async () => {
278
+ const upstream: ListToolsResult = {
279
+ tools: [
280
+ {
281
+ name: 't',
282
+ inputSchema: {
283
+ type: 'object',
284
+ description: 'see https://internal.host/x',
285
+ },
286
+ },
287
+ ],
288
+ };
289
+ const result = await run(sanitizeToolDescriptions(), upstream);
290
+ const tool = result.tools[0] as object;
291
+ expect(
292
+ (result.tools[0]?.inputSchema as { description?: string }).description,
293
+ ).toBe('see ');
294
+ expect('description' in tool).toBe(false);
295
+ expect('outputSchema' in tool).toBe(false);
296
+ });
297
+
298
+ it('rebuilds only the changed tool in a mixed catalog', async () => {
299
+ const upstream: ListToolsResult = {
300
+ tools: [
301
+ {
302
+ name: 'clean',
303
+ description: 'nothing here',
304
+ inputSchema: { type: 'object' },
305
+ },
306
+ {
307
+ name: 'dirty',
308
+ description: 'see https://internal.host/x',
309
+ inputSchema: { type: 'object' },
310
+ },
311
+ ],
312
+ };
313
+ const result = await run(sanitizeToolDescriptions(), upstream);
314
+ expect(result).not.toBe(upstream);
315
+ expect(result.tools[0]).toBe(upstream.tools[0]);
316
+ expect(result.tools[1]?.description).toBe('see ');
317
+ });
318
+
319
+ it('leaves names, titles, and schema structure intact', async () => {
320
+ const result = await run();
321
+ expect(result.tools[0]?.name).toBe('search_issues');
322
+ expect(result.tools[0]?.title).toBe('Search issues');
323
+ const input = result.tools[0]?.inputSchema as unknown as NestedSchema;
324
+ expect(input.type).toBe('object');
325
+ expect(Object.keys(input.properties)).toEqual(['query', 'filters']);
326
+ expect(input.properties.query?.type).toBe('string');
327
+ });
328
+
329
+ it('uses the configured replacement text', async () => {
330
+ const result = await run(
331
+ sanitizeToolDescriptions({ replacement: '[redacted]' }),
332
+ );
333
+ expect(result.tools[0]?.description).toBe(
334
+ 'Search issues. Docs: [redacted]',
335
+ );
336
+ });
337
+
338
+ it('does not mutate the upstream result', async () => {
339
+ const upstream = makeResult();
340
+ const snapshot = structuredClone(upstream);
341
+ await run(sanitizeToolDescriptions({ patterns: ['acme-corp'] }), upstream);
342
+ expect(upstream).toEqual(snapshot);
343
+ });
344
+
345
+ it('returns the original result object when nothing matches', async () => {
346
+ const upstream: ListToolsResult = {
347
+ tools: [
348
+ {
349
+ name: 'clean',
350
+ description: 'nothing to strip here',
351
+ inputSchema: { type: 'object' },
352
+ },
353
+ ],
354
+ };
355
+ const result = await run(sanitizeToolDescriptions(), upstream);
356
+ expect(result).toBe(upstream);
357
+ expect(result.tools[0]).toBe(upstream.tools[0]);
358
+ });
359
+ });