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.
- package/README.md +461 -62
- package/dist/backendClient.d.ts +1 -1
- package/dist/backendClient.d.ts.map +1 -1
- package/dist/backendClient.js +6 -1
- package/dist/backendClient.js.map +1 -1
- package/dist/core.d.ts +309 -35
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1023 -186
- package/dist/core.js.map +1 -1
- package/dist/delegation.d.ts +89 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/delegation.js +175 -0
- package/dist/delegation.js.map +1 -0
- package/dist/eventStore.d.ts +12 -0
- package/dist/eventStore.d.ts.map +1 -1
- package/dist/eventStore.js +43 -5
- package/dist/eventStore.js.map +1 -1
- package/dist/hiddenTools.d.ts +42 -0
- package/dist/hiddenTools.d.ts.map +1 -0
- package/dist/hiddenTools.js +53 -0
- package/dist/hiddenTools.js.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/mesh.d.ts +82 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +141 -0
- package/dist/mesh.js.map +1 -0
- package/dist/middleware.d.ts +13 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +27 -3
- package/dist/middleware.js.map +1 -1
- package/dist/proxyContext.d.ts +65 -5
- package/dist/proxyContext.d.ts.map +1 -1
- package/dist/proxyContext.js +39 -4
- package/dist/proxyContext.js.map +1 -1
- package/dist/rejection.d.ts +7 -1
- package/dist/rejection.d.ts.map +1 -1
- package/dist/rejection.js +24 -1
- package/dist/rejection.js.map +1 -1
- package/dist/sanitizeCatalog.d.ts +37 -0
- package/dist/sanitizeCatalog.d.ts.map +1 -0
- package/dist/sanitizeCatalog.js +92 -0
- package/dist/sanitizeCatalog.js.map +1 -0
- package/dist/sessionRegistry.d.ts +79 -0
- package/dist/sessionRegistry.d.ts.map +1 -0
- package/dist/sessionRegistry.js +83 -0
- package/dist/sessionRegistry.js.map +1 -0
- package/dist/telemetry.d.ts +29 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/testing.d.ts +18 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +30 -8
- package/dist/testing.js.map +1 -1
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +22 -8
- package/src/__tests__/_helpers.ts +64 -0
- package/src/__tests__/backendClient.test.ts +198 -0
- package/src/__tests__/core.delegation.test.ts +964 -0
- package/src/__tests__/core.edge.test.ts +314 -0
- package/src/__tests__/core.hiddenTools.predicate.test.ts +236 -0
- package/src/__tests__/core.localTools.test.ts +314 -0
- package/src/__tests__/core.mesh.test.ts +1182 -0
- package/src/__tests__/core.stripRequestMeta.test.ts +226 -0
- package/src/__tests__/core.stripResultMeta.test.ts +217 -0
- package/src/__tests__/core.test.ts +1105 -0
- package/src/__tests__/core.toolCallTelemetry.test.ts +157 -0
- package/src/__tests__/eventStore.test.ts +141 -0
- package/src/__tests__/hasToolContent.test.ts +63 -0
- package/src/__tests__/mapToolResult.test.ts +101 -0
- package/src/__tests__/middleware.edge.test.ts +126 -0
- package/src/__tests__/middleware.test.ts +233 -0
- package/src/__tests__/proxyContext.test.ts +140 -0
- package/src/__tests__/sanitizeCatalog.test.ts +380 -0
- package/src/__tests__/startHttpProxy.lifecycle.test.ts +675 -0
- package/src/__tests__/startHttpProxy.registry.test.ts +409 -0
- package/src/__tests__/startHttpProxy.replay.test.ts +116 -0
- package/src/__tests__/startHttpProxy.security.test.ts +288 -0
- package/src/__tests__/startHttpProxy.test.ts +1065 -0
- package/src/__tests__/testing.test.ts +250 -0
- package/src/__tests__/version.test.ts +12 -0
- package/src/backendClient.ts +83 -0
- package/src/core.ts +2129 -0
- package/src/delegation.ts +224 -0
- package/src/eventStore.ts +106 -0
- package/src/hiddenTools.ts +84 -0
- package/src/identity.ts +15 -0
- package/src/index.ts +63 -0
- package/src/mesh.ts +204 -0
- package/src/middleware.ts +101 -0
- package/src/proxyContext.ts +108 -0
- package/src/rejection.ts +62 -0
- package/src/sanitizeCatalog.ts +126 -0
- package/src/sessionRegistry.ts +142 -0
- package/src/telemetry.ts +46 -0
- package/src/testing.ts +174 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,380 @@
|
|
|
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.each([
|
|
151
|
+
['uppercase', 'HTTPS'],
|
|
152
|
+
['mixed case', 'HtTp'],
|
|
153
|
+
['mixed-case with s', 'HTtpS'],
|
|
154
|
+
])(
|
|
155
|
+
'strips a %s scheme, which RFC 3986 treats as case-insensitive',
|
|
156
|
+
async (_label, scheme) => {
|
|
157
|
+
const upstream: ListToolsResult = {
|
|
158
|
+
tools: [
|
|
159
|
+
{
|
|
160
|
+
name: 't',
|
|
161
|
+
description: `see ${scheme}://internal.host/a for details`,
|
|
162
|
+
inputSchema: { type: 'object' },
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
const result = await run(sanitizeToolDescriptions(), upstream);
|
|
167
|
+
expect(result.tools[0]?.description).toBe('see for details');
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
it('accepts a pattern that already carries the global flag', async () => {
|
|
172
|
+
const upstream: ListToolsResult = {
|
|
173
|
+
tools: [
|
|
174
|
+
{
|
|
175
|
+
name: 't',
|
|
176
|
+
description: 'a db-1 b db-2',
|
|
177
|
+
inputSchema: { type: 'object' },
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
const result = await run(
|
|
182
|
+
sanitizeToolDescriptions({ patterns: [/db-\d/g] }),
|
|
183
|
+
upstream,
|
|
184
|
+
);
|
|
185
|
+
expect(result.tools[0]?.description).toBe('a b ');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('sanitizes descriptions inside schema arrays, keeping untouched siblings by reference', async () => {
|
|
189
|
+
const clean = { type: 'string' };
|
|
190
|
+
const untouchedEnum = ['x', 'y'];
|
|
191
|
+
const upstream: ListToolsResult = {
|
|
192
|
+
tools: [
|
|
193
|
+
{
|
|
194
|
+
name: 't',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
anyOf: [{ description: 'acme-corp branch' }, clean],
|
|
198
|
+
allOf: [
|
|
199
|
+
{ description: 'acme-corp a' },
|
|
200
|
+
{ description: 'acme-corp b' },
|
|
201
|
+
],
|
|
202
|
+
enum: untouchedEnum,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
const result = await run(
|
|
208
|
+
sanitizeToolDescriptions({ patterns: ['acme-corp'] }),
|
|
209
|
+
upstream,
|
|
210
|
+
);
|
|
211
|
+
const schema = result.tools[0]?.inputSchema as unknown as {
|
|
212
|
+
anyOf: [{ description: string }, typeof clean];
|
|
213
|
+
allOf: [{ description: string }, { description: string }];
|
|
214
|
+
enum: string[];
|
|
215
|
+
};
|
|
216
|
+
expect(Array.isArray(schema.anyOf)).toBe(true);
|
|
217
|
+
expect(schema.anyOf[0].description).toBe(' branch');
|
|
218
|
+
expect(schema.anyOf[1]).toBe(clean);
|
|
219
|
+
expect(schema.allOf[0].description).toBe(' a');
|
|
220
|
+
expect(schema.allOf[1].description).toBe(' b');
|
|
221
|
+
expect(schema.enum).toBe(untouchedEnum);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('sanitizes a tool whose only match is in outputSchema', async () => {
|
|
225
|
+
const upstream: ListToolsResult = {
|
|
226
|
+
tools: [
|
|
227
|
+
{
|
|
228
|
+
name: 't',
|
|
229
|
+
description: 'clean',
|
|
230
|
+
inputSchema: { type: 'object' },
|
|
231
|
+
outputSchema: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
url: { type: 'string', description: 'https://internal.host/x' },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
const result = await run(sanitizeToolDescriptions(), upstream);
|
|
241
|
+
const output = result.tools[0]?.outputSchema as unknown as NestedSchema;
|
|
242
|
+
expect(output.properties.url?.description).toBe('');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('ignores string values outside description keys, non-string descriptions, and null nodes', async () => {
|
|
246
|
+
const upstream: ListToolsResult = {
|
|
247
|
+
tools: [
|
|
248
|
+
{
|
|
249
|
+
name: 't',
|
|
250
|
+
description: 'acme-corp tool',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
host: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
pattern: 'acme-corp',
|
|
257
|
+
default: null,
|
|
258
|
+
description: { note: 'acme-corp' },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
} as unknown as ListToolsResult['tools'][number]['inputSchema'],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
const result = await run(
|
|
266
|
+
sanitizeToolDescriptions({ patterns: ['acme-corp'] }),
|
|
267
|
+
upstream,
|
|
268
|
+
);
|
|
269
|
+
expect(result.tools[0]?.description).toBe(' tool');
|
|
270
|
+
const input = result.tools[0]?.inputSchema as unknown as {
|
|
271
|
+
properties: {
|
|
272
|
+
host: {
|
|
273
|
+
pattern: string;
|
|
274
|
+
default: null;
|
|
275
|
+
description: { note: string };
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
expect(input.properties.host.pattern).toBe('acme-corp');
|
|
280
|
+
expect(input.properties.host.default).toBeNull();
|
|
281
|
+
expect(input.properties.host.description).toEqual({ note: 'acme-corp' });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('applies no hidden default patterns beyond the URL strip', async () => {
|
|
285
|
+
const upstream: ListToolsResult = {
|
|
286
|
+
tools: [
|
|
287
|
+
{
|
|
288
|
+
name: 't',
|
|
289
|
+
description: 'Stryker was here and stays',
|
|
290
|
+
inputSchema: { type: 'object' },
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
const result = await run(sanitizeToolDescriptions(), upstream);
|
|
295
|
+
expect(result).toBe(upstream);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('does not inject absent description or outputSchema keys when rebuilding', async () => {
|
|
299
|
+
const upstream: ListToolsResult = {
|
|
300
|
+
tools: [
|
|
301
|
+
{
|
|
302
|
+
name: 't',
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
description: 'see https://internal.host/x',
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
const result = await run(sanitizeToolDescriptions(), upstream);
|
|
311
|
+
const tool = result.tools[0] as object;
|
|
312
|
+
expect(
|
|
313
|
+
(result.tools[0]?.inputSchema as { description?: string }).description,
|
|
314
|
+
).toBe('see ');
|
|
315
|
+
expect('description' in tool).toBe(false);
|
|
316
|
+
expect('outputSchema' in tool).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('rebuilds only the changed tool in a mixed catalog', async () => {
|
|
320
|
+
const upstream: ListToolsResult = {
|
|
321
|
+
tools: [
|
|
322
|
+
{
|
|
323
|
+
name: 'clean',
|
|
324
|
+
description: 'nothing here',
|
|
325
|
+
inputSchema: { type: 'object' },
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'dirty',
|
|
329
|
+
description: 'see https://internal.host/x',
|
|
330
|
+
inputSchema: { type: 'object' },
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
const result = await run(sanitizeToolDescriptions(), upstream);
|
|
335
|
+
expect(result).not.toBe(upstream);
|
|
336
|
+
expect(result.tools[0]).toBe(upstream.tools[0]);
|
|
337
|
+
expect(result.tools[1]?.description).toBe('see ');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('leaves names, titles, and schema structure intact', async () => {
|
|
341
|
+
const result = await run();
|
|
342
|
+
expect(result.tools[0]?.name).toBe('search_issues');
|
|
343
|
+
expect(result.tools[0]?.title).toBe('Search issues');
|
|
344
|
+
const input = result.tools[0]?.inputSchema as unknown as NestedSchema;
|
|
345
|
+
expect(input.type).toBe('object');
|
|
346
|
+
expect(Object.keys(input.properties)).toEqual(['query', 'filters']);
|
|
347
|
+
expect(input.properties.query?.type).toBe('string');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('uses the configured replacement text', async () => {
|
|
351
|
+
const result = await run(
|
|
352
|
+
sanitizeToolDescriptions({ replacement: '[redacted]' }),
|
|
353
|
+
);
|
|
354
|
+
expect(result.tools[0]?.description).toBe(
|
|
355
|
+
'Search issues. Docs: [redacted]',
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('does not mutate the upstream result', async () => {
|
|
360
|
+
const upstream = makeResult();
|
|
361
|
+
const snapshot = structuredClone(upstream);
|
|
362
|
+
await run(sanitizeToolDescriptions({ patterns: ['acme-corp'] }), upstream);
|
|
363
|
+
expect(upstream).toEqual(snapshot);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('returns the original result object when nothing matches', async () => {
|
|
367
|
+
const upstream: ListToolsResult = {
|
|
368
|
+
tools: [
|
|
369
|
+
{
|
|
370
|
+
name: 'clean',
|
|
371
|
+
description: 'nothing to strip here',
|
|
372
|
+
inputSchema: { type: 'object' },
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
const result = await run(sanitizeToolDescriptions(), upstream);
|
|
377
|
+
expect(result).toBe(upstream);
|
|
378
|
+
expect(result.tools[0]).toBe(upstream.tools[0]);
|
|
379
|
+
});
|
|
380
|
+
});
|