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.
- package/README.md +457 -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 +285 -32
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +961 -193
- 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 +1 -1
- package/dist/rejection.d.ts.map +1 -1
- package/dist/rejection.js +1 -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 +86 -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 +1019 -0
- package/src/__tests__/eventStore.test.ts +141 -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 +359 -0
- package/src/__tests__/startHttpProxy.lifecycle.test.ts +638 -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 +985 -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 +2042 -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 +37 -0
- package/src/sanitizeCatalog.ts +120 -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,250 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
runToolMiddleware,
|
|
4
|
+
runListToolsMiddleware,
|
|
5
|
+
runResourceMiddleware,
|
|
6
|
+
createMockBackendClient,
|
|
7
|
+
} from '../testing.js';
|
|
8
|
+
import type {
|
|
9
|
+
ListToolsMiddleware,
|
|
10
|
+
ResourceMiddleware,
|
|
11
|
+
ToolMiddleware,
|
|
12
|
+
} from '../core.js';
|
|
13
|
+
import type {
|
|
14
|
+
CallToolRequest,
|
|
15
|
+
ListToolsRequest,
|
|
16
|
+
ReadResourceRequest,
|
|
17
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
+
|
|
19
|
+
const callReq: CallToolRequest = {
|
|
20
|
+
method: 'tools/call',
|
|
21
|
+
params: { name: 'echo', arguments: {} },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const listReq: ListToolsRequest = { method: 'tools/list', params: {} };
|
|
25
|
+
|
|
26
|
+
const readReq: ReadResourceRequest = {
|
|
27
|
+
method: 'resources/read',
|
|
28
|
+
params: { uri: 'res://a' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('runToolMiddleware()', () => {
|
|
32
|
+
it('returns the narrowed CallToolResult when middleware produces { content: [...] }', async () => {
|
|
33
|
+
const passthrough: ToolMiddleware = (req, next) => next(req);
|
|
34
|
+
const result = await runToolMiddleware(passthrough, callReq, async () => ({
|
|
35
|
+
content: [{ type: 'text', text: 'hi' }],
|
|
36
|
+
}));
|
|
37
|
+
expect(result.content[0]).toMatchObject({ type: 'text', text: 'hi' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('accepts an empty content array', async () => {
|
|
41
|
+
const passthrough: ToolMiddleware = (req, next) => next(req);
|
|
42
|
+
const result = await runToolMiddleware(passthrough, callReq, async () => ({
|
|
43
|
+
content: [],
|
|
44
|
+
}));
|
|
45
|
+
expect(result.content).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('throws when middleware returns the legacy { toolResult } shape', async () => {
|
|
49
|
+
const legacy: ToolMiddleware = async () => ({ toolResult: 'old' });
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
runToolMiddleware(legacy, callReq, async () => ({ content: [] })),
|
|
53
|
+
).rejects.toThrow(/legacy toolResult shape/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('forwards a custom ProxyContext to the middleware', async () => {
|
|
57
|
+
let seenRequestId: string | undefined;
|
|
58
|
+
const capture: ToolMiddleware = async (req, next, context) => {
|
|
59
|
+
seenRequestId = context.requestId;
|
|
60
|
+
return next(req);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await runToolMiddleware(capture, callReq, async () => ({ content: [] }), {
|
|
64
|
+
requestId: 'abc-123',
|
|
65
|
+
transport: 'stdio',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(seenRequestId).toBe('abc-123');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('runListToolsMiddleware()', () => {
|
|
73
|
+
it('runs the middleware with a fresh default ProxyContext — no cast needed', async () => {
|
|
74
|
+
let seenRequestId: string | undefined;
|
|
75
|
+
const capture: ListToolsMiddleware = async (req, next, context) => {
|
|
76
|
+
seenRequestId = context.requestId;
|
|
77
|
+
return next(req);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const result = await runListToolsMiddleware(capture, listReq, async () => ({
|
|
81
|
+
tools: [],
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
expect(result.tools).toEqual([]);
|
|
85
|
+
expect(seenRequestId).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('forwards a custom ProxyContext to the middleware', async () => {
|
|
89
|
+
let seenRequestId: string | undefined;
|
|
90
|
+
const capture: ListToolsMiddleware = async (req, next, context) => {
|
|
91
|
+
seenRequestId = context.requestId;
|
|
92
|
+
return next(req);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await runListToolsMiddleware(
|
|
96
|
+
capture,
|
|
97
|
+
listReq,
|
|
98
|
+
async () => ({ tools: [] }),
|
|
99
|
+
{
|
|
100
|
+
requestId: 'abc-123',
|
|
101
|
+
transport: 'stdio',
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(seenRequestId).toBe('abc-123');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('runResourceMiddleware()', () => {
|
|
110
|
+
it('runs the middleware with a fresh default ProxyContext — no cast needed', async () => {
|
|
111
|
+
let seenRequestId: string | undefined;
|
|
112
|
+
const capture: ResourceMiddleware = async (req, next, context) => {
|
|
113
|
+
seenRequestId = context.requestId;
|
|
114
|
+
return next(req);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result = await runResourceMiddleware(capture, readReq, async () => ({
|
|
118
|
+
contents: [{ uri: 'res://a', text: 'body' }],
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
expect(result.contents[0]).toMatchObject({ text: 'body' });
|
|
122
|
+
expect(seenRequestId).toBeTruthy();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('forwards a custom ProxyContext to the middleware', async () => {
|
|
126
|
+
let seenRequestId: string | undefined;
|
|
127
|
+
const capture: ResourceMiddleware = async (req, next, context) => {
|
|
128
|
+
seenRequestId = context.requestId;
|
|
129
|
+
return next(req);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await runResourceMiddleware(
|
|
133
|
+
capture,
|
|
134
|
+
readReq,
|
|
135
|
+
async () => ({
|
|
136
|
+
contents: [],
|
|
137
|
+
}),
|
|
138
|
+
{
|
|
139
|
+
requestId: 'abc-123',
|
|
140
|
+
transport: 'stdio',
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(seenRequestId).toBe('abc-123');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('createMockBackendClient()', () => {
|
|
149
|
+
it('returns the static callToolResponse when provided', async () => {
|
|
150
|
+
const backend = createMockBackendClient({
|
|
151
|
+
callToolResponse: { content: [{ type: 'text', text: 'static' }] },
|
|
152
|
+
});
|
|
153
|
+
const result = await backend.callTool({ name: 'x', arguments: {} });
|
|
154
|
+
expect((result.content as { text: string }[])[0]).toMatchObject({
|
|
155
|
+
text: 'static',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('passes the call params to a factory callToolResponse', async () => {
|
|
160
|
+
let seen:
|
|
161
|
+
| { name: string; arguments?: Record<string, unknown> | undefined }
|
|
162
|
+
| undefined;
|
|
163
|
+
const backend = createMockBackendClient({
|
|
164
|
+
callToolResponse: (params) => {
|
|
165
|
+
seen = params;
|
|
166
|
+
return { content: [{ type: 'text', text: `got:${params.name}` }] };
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = await backend.callTool({
|
|
171
|
+
name: 'echo',
|
|
172
|
+
arguments: { a: 1 },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(seen?.name).toBe('echo');
|
|
176
|
+
expect(seen?.arguments).toEqual({ a: 1 });
|
|
177
|
+
expect((result.content as { text: string }[])[0]).toMatchObject({
|
|
178
|
+
text: 'got:echo',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('defaults capabilities to all three scopes', () => {
|
|
183
|
+
const backend = createMockBackendClient();
|
|
184
|
+
expect(backend.getServerCapabilities()).toEqual({
|
|
185
|
+
tools: {},
|
|
186
|
+
resources: {},
|
|
187
|
+
prompts: {},
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('exposes user-supplied tools verbatim', async () => {
|
|
192
|
+
const backend = createMockBackendClient({
|
|
193
|
+
tools: [
|
|
194
|
+
{
|
|
195
|
+
name: 't',
|
|
196
|
+
description: 'd',
|
|
197
|
+
inputSchema: { type: 'object', properties: {} },
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
const result = await backend.listTools();
|
|
202
|
+
expect(result.tools).toHaveLength(1);
|
|
203
|
+
expect(result.tools[0]?.name).toBe('t');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('createMockBackendClient() fixtures', () => {
|
|
208
|
+
it('serves documented defaults when no fixtures are given', async () => {
|
|
209
|
+
const backend = createMockBackendClient();
|
|
210
|
+
expect(await backend.callTool({ name: 'x', arguments: {} })).toEqual({
|
|
211
|
+
content: [{ type: 'text', text: 'mock response' }],
|
|
212
|
+
});
|
|
213
|
+
expect(await backend.listTools()).toEqual({ tools: [] });
|
|
214
|
+
expect(await backend.listResources()).toEqual({ resources: [] });
|
|
215
|
+
expect(await backend.readResource({ uri: 'res://a' })).toEqual({
|
|
216
|
+
contents: [{ uri: '', text: 'mock resource' }],
|
|
217
|
+
});
|
|
218
|
+
expect(await backend.listPrompts()).toEqual({ prompts: [] });
|
|
219
|
+
expect(await backend.getPrompt({ name: 'p' })).toEqual({ messages: [] });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('serves supplied resource and prompt fixtures', async () => {
|
|
223
|
+
const backend = createMockBackendClient({
|
|
224
|
+
capabilities: { tools: {} },
|
|
225
|
+
resources: [{ uri: 'res://a', name: 'a' }],
|
|
226
|
+
readResourceResponse: { contents: [{ uri: 'res://a', text: 'body' }] },
|
|
227
|
+
prompts: [{ name: 'p' }],
|
|
228
|
+
getPromptResponse: {
|
|
229
|
+
messages: [{ role: 'user', content: { type: 'text', text: 'hi' } }],
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
expect(backend.getServerCapabilities()).toEqual({ tools: {} });
|
|
233
|
+
expect((await backend.listResources()).resources).toEqual([
|
|
234
|
+
{ uri: 'res://a', name: 'a' },
|
|
235
|
+
]);
|
|
236
|
+
expect((await backend.readResource({ uri: 'res://a' })).contents).toEqual([
|
|
237
|
+
{ uri: 'res://a', text: 'body' },
|
|
238
|
+
]);
|
|
239
|
+
expect((await backend.listPrompts()).prompts).toEqual([{ name: 'p' }]);
|
|
240
|
+
expect((await backend.getPrompt({ name: 'p' })).messages).toHaveLength(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('clones fixtures per call so a mutating caller cannot corrupt them', async () => {
|
|
244
|
+
const backend = createMockBackendClient({
|
|
245
|
+
resources: [{ uri: 'res://a', name: 'a' }],
|
|
246
|
+
});
|
|
247
|
+
(await backend.listResources()).resources.pop();
|
|
248
|
+
expect((await backend.listResources()).resources).toHaveLength(1);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { VERSION } from '../version.js';
|
|
4
|
+
|
|
5
|
+
describe('VERSION', () => {
|
|
6
|
+
it('matches package.json (bump src/version.ts together with the package)', () => {
|
|
7
|
+
const pkg = createRequire(import.meta.url)('../../package.json') as {
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
expect(VERSION).toBe(pkg.version);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend MCP client factory.
|
|
3
|
+
* Modes: stdio (spawns child process) or HTTP/SSE (connects to running server).
|
|
4
|
+
*/
|
|
5
|
+
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
import {
|
|
9
|
+
StreamableHTTPClientTransport,
|
|
10
|
+
type StreamableHTTPClientTransportOptions,
|
|
11
|
+
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
12
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
13
|
+
import { VERSION } from './version.js';
|
|
14
|
+
|
|
15
|
+
/** Backend connection config. */
|
|
16
|
+
export interface BackendConfig {
|
|
17
|
+
/** Shell command to spawn backend (e.g. `"node"`). Required if no `url`. */
|
|
18
|
+
command?: string;
|
|
19
|
+
/** Args passed to command (e.g. `["/path/to/server.mjs"]`). */
|
|
20
|
+
args?: string[];
|
|
21
|
+
/** HTTP/SSE URL of running backend. Takes precedence over stdio. */
|
|
22
|
+
url?: string;
|
|
23
|
+
/** Custom HTTP headers sent on every request to the backend (HTTP/SSE only). */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** OAuth provdider for interactive/browser auth + transparent token refresh (HHT/SSE only). */
|
|
26
|
+
authProvider?: OAuthClientProvider;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type BackendClient = Client;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates and connects an MCP client to the backend.
|
|
33
|
+
* @throws If neither `command` nor `url` provided, or connection fails.
|
|
34
|
+
*/
|
|
35
|
+
export async function createBackendClient(
|
|
36
|
+
config: BackendConfig,
|
|
37
|
+
): Promise<BackendClient> {
|
|
38
|
+
if (!config.command && !config.url) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'mcpose: either command or url must be provided in BackendConfig',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const client = new Client(
|
|
45
|
+
{ name: 'mcpose-backend', version: VERSION },
|
|
46
|
+
{ capabilities: {} },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const transport = config.url
|
|
50
|
+
? getHttpTransport(config.url, config.headers, config.authProvider)
|
|
51
|
+
: new StdioClientTransport({
|
|
52
|
+
command: config.command!,
|
|
53
|
+
args: config.args ?? [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// The SDK's own transports are not assignable to its own `Transport`
|
|
57
|
+
// interface under `exactOptionalPropertyTypes` (the interface declares
|
|
58
|
+
// `sessionId?: string`, the implementations `sessionId: string | undefined`).
|
|
59
|
+
// Purely a declaration mismatch upstream — the value is a real Transport.
|
|
60
|
+
await client.connect(transport as Transport);
|
|
61
|
+
return client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const getHttpTransport = (
|
|
65
|
+
urlString: string,
|
|
66
|
+
headers?: Record<string, string>,
|
|
67
|
+
authProvider?: OAuthClientProvider,
|
|
68
|
+
) => {
|
|
69
|
+
const url = new URL(urlString);
|
|
70
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`mcpose: unsupported URL protocol "${url.protocol}" — only http: and https: are allowed`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const opts: Partial<StreamableHTTPClientTransportOptions> = {};
|
|
76
|
+
if (headers) {
|
|
77
|
+
opts.requestInit = { headers };
|
|
78
|
+
}
|
|
79
|
+
if (authProvider) {
|
|
80
|
+
opts.authProvider = authProvider;
|
|
81
|
+
}
|
|
82
|
+
return new StreamableHTTPClientTransport(url, opts);
|
|
83
|
+
};
|