@mcpkit-dev/testing 1.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 +343 -0
- package/dist/index.d.ts +504 -0
- package/dist/index.js +229 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# @mcpkit-dev/testing
|
|
2
|
+
|
|
3
|
+
Testing utilities for MCPKit MCP servers. Provides mock clients, in-memory transports, and helpers for writing comprehensive tests.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D @mcpkit-dev/testing
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **MockMcpClient** - Full-featured mock MCP client for testing servers
|
|
14
|
+
- **InMemoryTransport** - Zero-latency transport for unit tests
|
|
15
|
+
- **Test helpers** - Utilities for common testing patterns
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
21
|
+
import { MockMcpClient } from '@mcpkit-dev/testing';
|
|
22
|
+
import { listen } from '@mcpkit-dev/core';
|
|
23
|
+
import { MyServer } from './my-server';
|
|
24
|
+
|
|
25
|
+
describe('MyServer', () => {
|
|
26
|
+
let client: MockMcpClient;
|
|
27
|
+
let cleanup: () => Promise<void>;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
const { client: mockClient, serverTransport } = MockMcpClient.create();
|
|
31
|
+
client = mockClient;
|
|
32
|
+
|
|
33
|
+
const server = await listen(MyServer);
|
|
34
|
+
await server.server.connect(serverTransport);
|
|
35
|
+
await client.connect();
|
|
36
|
+
|
|
37
|
+
cleanup = async () => {
|
|
38
|
+
await client.close();
|
|
39
|
+
await server.close();
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
await cleanup();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should list available tools', async () => {
|
|
48
|
+
const result = await client.listTools();
|
|
49
|
+
expect(result.tools).toHaveLength(1);
|
|
50
|
+
expect(result.tools[0].name).toBe('greet');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should call a tool', async () => {
|
|
54
|
+
const result = await client.callTool('greet', { name: 'World' });
|
|
55
|
+
expect(result.content[0].text).toBe('Hello, World!');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API Reference
|
|
61
|
+
|
|
62
|
+
### MockMcpClient
|
|
63
|
+
|
|
64
|
+
A mock MCP client that communicates with servers over in-memory transports.
|
|
65
|
+
|
|
66
|
+
#### Creating a Client
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { MockMcpClient } from '@mcpkit-dev/testing';
|
|
70
|
+
|
|
71
|
+
// Create with default options
|
|
72
|
+
const { client, serverTransport } = MockMcpClient.create();
|
|
73
|
+
|
|
74
|
+
// Create with custom options
|
|
75
|
+
const { client, serverTransport } = MockMcpClient.create({
|
|
76
|
+
name: 'test-client',
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Methods
|
|
82
|
+
|
|
83
|
+
##### `connect(): Promise<void>`
|
|
84
|
+
|
|
85
|
+
Connect the client to the server.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
await client.connect();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
##### `close(): Promise<void>`
|
|
92
|
+
|
|
93
|
+
Close the client connection.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
await client.close();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
##### `listTools(): Promise<ListToolsResult>`
|
|
100
|
+
|
|
101
|
+
List all available tools from the server.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const { tools } = await client.listTools();
|
|
105
|
+
console.log(tools); // [{ name: 'greet', description: '...' }]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
##### `callTool(name: string, args?: Record<string, unknown>)`
|
|
109
|
+
|
|
110
|
+
Call a tool by name with optional arguments.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const result = await client.callTool('greet', { name: 'Alice' });
|
|
114
|
+
console.log(result.content[0].text); // 'Hello, Alice!'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
##### `listResources(): Promise<ListResourcesResult>`
|
|
118
|
+
|
|
119
|
+
List all available resources.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const { resources } = await client.listResources();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
##### `readResource(uri: string): Promise<ReadResourceResult>`
|
|
126
|
+
|
|
127
|
+
Read a resource by URI.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const result = await client.readResource('config://settings');
|
|
131
|
+
console.log(result.contents[0].text);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
##### `listPrompts(): Promise<ListPromptsResult>`
|
|
135
|
+
|
|
136
|
+
List all available prompts.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const { prompts } = await client.listPrompts();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
##### `getPrompt(name: string, args?: Record<string, string>): Promise<GetPromptResult>`
|
|
143
|
+
|
|
144
|
+
Get a prompt by name with optional arguments.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const result = await client.getPrompt('greeting', { name: 'Bob' });
|
|
148
|
+
console.log(result.messages);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
##### `rawClient: Client`
|
|
152
|
+
|
|
153
|
+
Access the underlying MCP SDK client for advanced operations.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const sdkClient = client.rawClient;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### InMemoryTransport
|
|
160
|
+
|
|
161
|
+
A transport implementation that allows direct communication between client and server without network overhead.
|
|
162
|
+
|
|
163
|
+
#### Creating a Transport Pair
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { InMemoryTransport } from '@mcpkit-dev/testing';
|
|
167
|
+
|
|
168
|
+
const { clientTransport, serverTransport } = InMemoryTransport.createPair();
|
|
169
|
+
|
|
170
|
+
// Connect to server
|
|
171
|
+
await server.connect(serverTransport);
|
|
172
|
+
|
|
173
|
+
// Connect client
|
|
174
|
+
await client.connect(clientTransport);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Methods
|
|
178
|
+
|
|
179
|
+
##### `start(): Promise<void>`
|
|
180
|
+
|
|
181
|
+
Start the transport and process any queued messages.
|
|
182
|
+
|
|
183
|
+
##### `send(message: JSONRPCMessage): Promise<void>`
|
|
184
|
+
|
|
185
|
+
Send a message to the peer transport.
|
|
186
|
+
|
|
187
|
+
##### `close(): Promise<void>`
|
|
188
|
+
|
|
189
|
+
Close the transport and its peer.
|
|
190
|
+
|
|
191
|
+
##### `deliverMessage(message: JSONRPCMessage): void`
|
|
192
|
+
|
|
193
|
+
Directly deliver a message (for testing edge cases).
|
|
194
|
+
|
|
195
|
+
### Helper Functions
|
|
196
|
+
|
|
197
|
+
#### `createTestClient(options?)`
|
|
198
|
+
|
|
199
|
+
Convenience function to create a mock client.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { createTestClient } from '@mcpkit-dev/testing';
|
|
203
|
+
|
|
204
|
+
const { client, serverTransport } = createTestClient({
|
|
205
|
+
name: 'my-test-client',
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### `waitForCondition(condition, options?)`
|
|
210
|
+
|
|
211
|
+
Wait for a condition to be true with timeout.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { waitForCondition } from '@mcpkit-dev/testing';
|
|
215
|
+
|
|
216
|
+
// Wait up to 5 seconds for server to be ready
|
|
217
|
+
await waitForCondition(() => server.isReady, {
|
|
218
|
+
timeout: 5000,
|
|
219
|
+
interval: 100,
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Options:**
|
|
224
|
+
|
|
225
|
+
| Option | Type | Default | Description |
|
|
226
|
+
|--------|------|---------|-------------|
|
|
227
|
+
| `timeout` | `number` | `5000` | Maximum time to wait (ms) |
|
|
228
|
+
| `interval` | `number` | `50` | Check interval (ms) |
|
|
229
|
+
|
|
230
|
+
#### `createTestServer(ServerClass, listenFn)`
|
|
231
|
+
|
|
232
|
+
Create a complete test environment with server and connected client.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { createTestServer } from '@mcpkit-dev/testing';
|
|
236
|
+
import { listen } from '@mcpkit-dev/core';
|
|
237
|
+
|
|
238
|
+
const { client, server, instance, cleanup } = await createTestServer(
|
|
239
|
+
MyServer,
|
|
240
|
+
listen
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Run tests...
|
|
244
|
+
const tools = await client.listTools();
|
|
245
|
+
|
|
246
|
+
// Clean up
|
|
247
|
+
await cleanup();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Testing Patterns
|
|
251
|
+
|
|
252
|
+
### Testing Tools
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
it('should handle tool errors gracefully', async () => {
|
|
256
|
+
const result = await client.callTool('divide', { a: 10, b: 0 });
|
|
257
|
+
expect(result.isError).toBe(true);
|
|
258
|
+
expect(result.content[0].text).toContain('Cannot divide by zero');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should validate tool parameters', async () => {
|
|
262
|
+
await expect(
|
|
263
|
+
client.callTool('greet', { invalidParam: 'value' })
|
|
264
|
+
).rejects.toThrow();
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Testing Resources
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
it('should read dynamic resources', async () => {
|
|
272
|
+
const result = await client.readResource('users://123');
|
|
273
|
+
const user = JSON.parse(result.contents[0].text);
|
|
274
|
+
expect(user.id).toBe('123');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should list resource templates', async () => {
|
|
278
|
+
const { resources } = await client.listResources();
|
|
279
|
+
const template = resources.find(r => r.uri.includes('{'));
|
|
280
|
+
expect(template).toBeDefined();
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Testing Prompts
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
it('should generate prompt with arguments', async () => {
|
|
288
|
+
const result = await client.getPrompt('code-review', {
|
|
289
|
+
language: 'typescript',
|
|
290
|
+
focus: 'security',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(result.messages[0].content.text).toContain('typescript');
|
|
294
|
+
expect(result.messages[0].content.text).toContain('security');
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Testing with Mocked Dependencies
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { vi } from 'vitest';
|
|
302
|
+
|
|
303
|
+
// Mock external service
|
|
304
|
+
vi.mock('./weather-api', () => ({
|
|
305
|
+
getWeather: vi.fn().mockResolvedValue({ temp: 22, condition: 'sunny' }),
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
it('should use mocked weather data', async () => {
|
|
309
|
+
const result = await client.callTool('getWeather', { city: 'London' });
|
|
310
|
+
expect(result.content[0].text).toContain('22');
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Integration with Test Frameworks
|
|
315
|
+
|
|
316
|
+
### Vitest
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
320
|
+
|
|
321
|
+
describe('MCP Server Integration', () => {
|
|
322
|
+
// ... setup and tests
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Jest
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
330
|
+
|
|
331
|
+
describe('MCP Server Integration', () => {
|
|
332
|
+
// ... setup and tests
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Related Packages
|
|
337
|
+
|
|
338
|
+
- [@mcpkit-dev/core](https://www.npmjs.com/package/@mcpkit-dev/core) - Core decorators and server framework
|
|
339
|
+
- [@mcpkit-dev/cli](https://www.npmjs.com/package/@mcpkit-dev/cli) - CLI tool for project scaffolding
|
|
340
|
+
|
|
341
|
+
## License
|
|
342
|
+
|
|
343
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
3
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for creating a mock MCP client
|
|
7
|
+
*/
|
|
8
|
+
interface MockClientOptions {
|
|
9
|
+
/** Client name */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Client version */
|
|
12
|
+
version?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Result from creating a mock client
|
|
16
|
+
*/
|
|
17
|
+
interface MockClientResult {
|
|
18
|
+
/** The MCP client instance */
|
|
19
|
+
client: MockMcpClient;
|
|
20
|
+
/** Transport for connecting to a server */
|
|
21
|
+
serverTransport: Transport;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mock MCP client for testing servers
|
|
25
|
+
*
|
|
26
|
+
* Provides a simple interface for testing MCP server implementations
|
|
27
|
+
* without needing actual network connections.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const { client, serverTransport } = MockMcpClient.create();
|
|
32
|
+
*
|
|
33
|
+
* // Connect your server to serverTransport
|
|
34
|
+
* const server = await listen(MyServer);
|
|
35
|
+
* await server.server.connect(serverTransport);
|
|
36
|
+
*
|
|
37
|
+
* // Use the client to test your server
|
|
38
|
+
* const tools = await client.listTools();
|
|
39
|
+
* const result = await client.callTool('myTool', { param: 'value' });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare class MockMcpClient {
|
|
43
|
+
private client;
|
|
44
|
+
private clientTransport;
|
|
45
|
+
private connected;
|
|
46
|
+
private constructor();
|
|
47
|
+
/**
|
|
48
|
+
* Create a mock client with linked transports
|
|
49
|
+
*/
|
|
50
|
+
static create(options?: MockClientOptions): MockClientResult;
|
|
51
|
+
/**
|
|
52
|
+
* Connect to the server
|
|
53
|
+
*/
|
|
54
|
+
connect(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Close the connection
|
|
57
|
+
*/
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* List all available tools
|
|
61
|
+
*/
|
|
62
|
+
listTools(): Promise<{
|
|
63
|
+
[x: string]: unknown;
|
|
64
|
+
tools: {
|
|
65
|
+
inputSchema: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
type: "object";
|
|
68
|
+
properties?: Record<string, object> | undefined;
|
|
69
|
+
required?: string[] | undefined;
|
|
70
|
+
};
|
|
71
|
+
name: string;
|
|
72
|
+
description?: string | undefined;
|
|
73
|
+
outputSchema?: {
|
|
74
|
+
[x: string]: unknown;
|
|
75
|
+
type: "object";
|
|
76
|
+
properties?: Record<string, object> | undefined;
|
|
77
|
+
required?: string[] | undefined;
|
|
78
|
+
} | undefined;
|
|
79
|
+
annotations?: {
|
|
80
|
+
title?: string | undefined;
|
|
81
|
+
readOnlyHint?: boolean | undefined;
|
|
82
|
+
destructiveHint?: boolean | undefined;
|
|
83
|
+
idempotentHint?: boolean | undefined;
|
|
84
|
+
openWorldHint?: boolean | undefined;
|
|
85
|
+
} | undefined;
|
|
86
|
+
execution?: {
|
|
87
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
88
|
+
} | undefined;
|
|
89
|
+
_meta?: Record<string, unknown> | undefined;
|
|
90
|
+
icons?: {
|
|
91
|
+
src: string;
|
|
92
|
+
mimeType?: string | undefined;
|
|
93
|
+
sizes?: string[] | undefined;
|
|
94
|
+
}[] | undefined;
|
|
95
|
+
title?: string | undefined;
|
|
96
|
+
}[];
|
|
97
|
+
_meta?: {
|
|
98
|
+
[x: string]: unknown;
|
|
99
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
100
|
+
[x: string]: unknown;
|
|
101
|
+
taskId: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
} | undefined;
|
|
104
|
+
nextCursor?: string | undefined;
|
|
105
|
+
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Call a tool by name
|
|
108
|
+
*/
|
|
109
|
+
callTool(name: string, args?: Record<string, unknown>): Promise<{
|
|
110
|
+
[x: string]: unknown;
|
|
111
|
+
content: ({
|
|
112
|
+
type: "text";
|
|
113
|
+
text: string;
|
|
114
|
+
annotations?: {
|
|
115
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
116
|
+
priority?: number | undefined;
|
|
117
|
+
lastModified?: string | undefined;
|
|
118
|
+
} | undefined;
|
|
119
|
+
_meta?: Record<string, unknown> | undefined;
|
|
120
|
+
} | {
|
|
121
|
+
type: "image";
|
|
122
|
+
data: string;
|
|
123
|
+
mimeType: string;
|
|
124
|
+
annotations?: {
|
|
125
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
126
|
+
priority?: number | undefined;
|
|
127
|
+
lastModified?: string | undefined;
|
|
128
|
+
} | undefined;
|
|
129
|
+
_meta?: Record<string, unknown> | undefined;
|
|
130
|
+
} | {
|
|
131
|
+
type: "audio";
|
|
132
|
+
data: string;
|
|
133
|
+
mimeType: string;
|
|
134
|
+
annotations?: {
|
|
135
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
136
|
+
priority?: number | undefined;
|
|
137
|
+
lastModified?: string | undefined;
|
|
138
|
+
} | undefined;
|
|
139
|
+
_meta?: Record<string, unknown> | undefined;
|
|
140
|
+
} | {
|
|
141
|
+
type: "resource";
|
|
142
|
+
resource: {
|
|
143
|
+
uri: string;
|
|
144
|
+
text: string;
|
|
145
|
+
mimeType?: string | undefined;
|
|
146
|
+
_meta?: Record<string, unknown> | undefined;
|
|
147
|
+
} | {
|
|
148
|
+
uri: string;
|
|
149
|
+
blob: string;
|
|
150
|
+
mimeType?: string | undefined;
|
|
151
|
+
_meta?: Record<string, unknown> | undefined;
|
|
152
|
+
};
|
|
153
|
+
annotations?: {
|
|
154
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
155
|
+
priority?: number | undefined;
|
|
156
|
+
lastModified?: string | undefined;
|
|
157
|
+
} | undefined;
|
|
158
|
+
_meta?: Record<string, unknown> | undefined;
|
|
159
|
+
} | {
|
|
160
|
+
uri: string;
|
|
161
|
+
name: string;
|
|
162
|
+
type: "resource_link";
|
|
163
|
+
description?: string | undefined;
|
|
164
|
+
mimeType?: string | undefined;
|
|
165
|
+
annotations?: {
|
|
166
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
167
|
+
priority?: number | undefined;
|
|
168
|
+
lastModified?: string | undefined;
|
|
169
|
+
} | undefined;
|
|
170
|
+
_meta?: {
|
|
171
|
+
[x: string]: unknown;
|
|
172
|
+
} | undefined;
|
|
173
|
+
icons?: {
|
|
174
|
+
src: string;
|
|
175
|
+
mimeType?: string | undefined;
|
|
176
|
+
sizes?: string[] | undefined;
|
|
177
|
+
}[] | undefined;
|
|
178
|
+
title?: string | undefined;
|
|
179
|
+
})[];
|
|
180
|
+
_meta?: {
|
|
181
|
+
[x: string]: unknown;
|
|
182
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
183
|
+
[x: string]: unknown;
|
|
184
|
+
taskId: string;
|
|
185
|
+
} | undefined;
|
|
186
|
+
} | undefined;
|
|
187
|
+
structuredContent?: Record<string, unknown> | undefined;
|
|
188
|
+
isError?: boolean | undefined;
|
|
189
|
+
} | {
|
|
190
|
+
[x: string]: unknown;
|
|
191
|
+
toolResult: unknown;
|
|
192
|
+
_meta?: {
|
|
193
|
+
[x: string]: unknown;
|
|
194
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
195
|
+
[x: string]: unknown;
|
|
196
|
+
taskId: string;
|
|
197
|
+
} | undefined;
|
|
198
|
+
} | undefined;
|
|
199
|
+
}>;
|
|
200
|
+
/**
|
|
201
|
+
* List all available resources
|
|
202
|
+
*/
|
|
203
|
+
listResources(): Promise<{
|
|
204
|
+
[x: string]: unknown;
|
|
205
|
+
resources: {
|
|
206
|
+
uri: string;
|
|
207
|
+
name: string;
|
|
208
|
+
description?: string | undefined;
|
|
209
|
+
mimeType?: string | undefined;
|
|
210
|
+
annotations?: {
|
|
211
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
212
|
+
priority?: number | undefined;
|
|
213
|
+
lastModified?: string | undefined;
|
|
214
|
+
} | undefined;
|
|
215
|
+
_meta?: {
|
|
216
|
+
[x: string]: unknown;
|
|
217
|
+
} | undefined;
|
|
218
|
+
icons?: {
|
|
219
|
+
src: string;
|
|
220
|
+
mimeType?: string | undefined;
|
|
221
|
+
sizes?: string[] | undefined;
|
|
222
|
+
}[] | undefined;
|
|
223
|
+
title?: string | undefined;
|
|
224
|
+
}[];
|
|
225
|
+
_meta?: {
|
|
226
|
+
[x: string]: unknown;
|
|
227
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
228
|
+
[x: string]: unknown;
|
|
229
|
+
taskId: string;
|
|
230
|
+
} | undefined;
|
|
231
|
+
} | undefined;
|
|
232
|
+
nextCursor?: string | undefined;
|
|
233
|
+
}>;
|
|
234
|
+
/**
|
|
235
|
+
* Read a resource by URI
|
|
236
|
+
*/
|
|
237
|
+
readResource(uri: string): Promise<{
|
|
238
|
+
[x: string]: unknown;
|
|
239
|
+
contents: ({
|
|
240
|
+
uri: string;
|
|
241
|
+
text: string;
|
|
242
|
+
mimeType?: string | undefined;
|
|
243
|
+
_meta?: Record<string, unknown> | undefined;
|
|
244
|
+
} | {
|
|
245
|
+
uri: string;
|
|
246
|
+
blob: string;
|
|
247
|
+
mimeType?: string | undefined;
|
|
248
|
+
_meta?: Record<string, unknown> | undefined;
|
|
249
|
+
})[];
|
|
250
|
+
_meta?: {
|
|
251
|
+
[x: string]: unknown;
|
|
252
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
253
|
+
[x: string]: unknown;
|
|
254
|
+
taskId: string;
|
|
255
|
+
} | undefined;
|
|
256
|
+
} | undefined;
|
|
257
|
+
}>;
|
|
258
|
+
/**
|
|
259
|
+
* List all available prompts
|
|
260
|
+
*/
|
|
261
|
+
listPrompts(): Promise<{
|
|
262
|
+
[x: string]: unknown;
|
|
263
|
+
prompts: {
|
|
264
|
+
name: string;
|
|
265
|
+
description?: string | undefined;
|
|
266
|
+
arguments?: {
|
|
267
|
+
name: string;
|
|
268
|
+
description?: string | undefined;
|
|
269
|
+
required?: boolean | undefined;
|
|
270
|
+
}[] | undefined;
|
|
271
|
+
_meta?: {
|
|
272
|
+
[x: string]: unknown;
|
|
273
|
+
} | undefined;
|
|
274
|
+
icons?: {
|
|
275
|
+
src: string;
|
|
276
|
+
mimeType?: string | undefined;
|
|
277
|
+
sizes?: string[] | undefined;
|
|
278
|
+
}[] | undefined;
|
|
279
|
+
title?: string | undefined;
|
|
280
|
+
}[];
|
|
281
|
+
_meta?: {
|
|
282
|
+
[x: string]: unknown;
|
|
283
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
284
|
+
[x: string]: unknown;
|
|
285
|
+
taskId: string;
|
|
286
|
+
} | undefined;
|
|
287
|
+
} | undefined;
|
|
288
|
+
nextCursor?: string | undefined;
|
|
289
|
+
}>;
|
|
290
|
+
/**
|
|
291
|
+
* Get a prompt by name
|
|
292
|
+
*/
|
|
293
|
+
getPrompt(name: string, args?: Record<string, string>): Promise<{
|
|
294
|
+
[x: string]: unknown;
|
|
295
|
+
messages: {
|
|
296
|
+
role: "user" | "assistant";
|
|
297
|
+
content: {
|
|
298
|
+
type: "text";
|
|
299
|
+
text: string;
|
|
300
|
+
annotations?: {
|
|
301
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
302
|
+
priority?: number | undefined;
|
|
303
|
+
lastModified?: string | undefined;
|
|
304
|
+
} | undefined;
|
|
305
|
+
_meta?: Record<string, unknown> | undefined;
|
|
306
|
+
} | {
|
|
307
|
+
type: "image";
|
|
308
|
+
data: string;
|
|
309
|
+
mimeType: string;
|
|
310
|
+
annotations?: {
|
|
311
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
312
|
+
priority?: number | undefined;
|
|
313
|
+
lastModified?: string | undefined;
|
|
314
|
+
} | undefined;
|
|
315
|
+
_meta?: Record<string, unknown> | undefined;
|
|
316
|
+
} | {
|
|
317
|
+
type: "audio";
|
|
318
|
+
data: string;
|
|
319
|
+
mimeType: string;
|
|
320
|
+
annotations?: {
|
|
321
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
322
|
+
priority?: number | undefined;
|
|
323
|
+
lastModified?: string | undefined;
|
|
324
|
+
} | undefined;
|
|
325
|
+
_meta?: Record<string, unknown> | undefined;
|
|
326
|
+
} | {
|
|
327
|
+
type: "resource";
|
|
328
|
+
resource: {
|
|
329
|
+
uri: string;
|
|
330
|
+
text: string;
|
|
331
|
+
mimeType?: string | undefined;
|
|
332
|
+
_meta?: Record<string, unknown> | undefined;
|
|
333
|
+
} | {
|
|
334
|
+
uri: string;
|
|
335
|
+
blob: string;
|
|
336
|
+
mimeType?: string | undefined;
|
|
337
|
+
_meta?: Record<string, unknown> | undefined;
|
|
338
|
+
};
|
|
339
|
+
annotations?: {
|
|
340
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
341
|
+
priority?: number | undefined;
|
|
342
|
+
lastModified?: string | undefined;
|
|
343
|
+
} | undefined;
|
|
344
|
+
_meta?: Record<string, unknown> | undefined;
|
|
345
|
+
} | {
|
|
346
|
+
uri: string;
|
|
347
|
+
name: string;
|
|
348
|
+
type: "resource_link";
|
|
349
|
+
description?: string | undefined;
|
|
350
|
+
mimeType?: string | undefined;
|
|
351
|
+
annotations?: {
|
|
352
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
353
|
+
priority?: number | undefined;
|
|
354
|
+
lastModified?: string | undefined;
|
|
355
|
+
} | undefined;
|
|
356
|
+
_meta?: {
|
|
357
|
+
[x: string]: unknown;
|
|
358
|
+
} | undefined;
|
|
359
|
+
icons?: {
|
|
360
|
+
src: string;
|
|
361
|
+
mimeType?: string | undefined;
|
|
362
|
+
sizes?: string[] | undefined;
|
|
363
|
+
}[] | undefined;
|
|
364
|
+
title?: string | undefined;
|
|
365
|
+
};
|
|
366
|
+
}[];
|
|
367
|
+
_meta?: {
|
|
368
|
+
[x: string]: unknown;
|
|
369
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
370
|
+
[x: string]: unknown;
|
|
371
|
+
taskId: string;
|
|
372
|
+
} | undefined;
|
|
373
|
+
} | undefined;
|
|
374
|
+
description?: string | undefined;
|
|
375
|
+
}>;
|
|
376
|
+
/**
|
|
377
|
+
* Ensure the client is connected
|
|
378
|
+
*/
|
|
379
|
+
private ensureConnected;
|
|
380
|
+
/**
|
|
381
|
+
* Get the underlying MCP client
|
|
382
|
+
*/
|
|
383
|
+
get rawClient(): Client;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Result from creating a test server
|
|
388
|
+
*/
|
|
389
|
+
interface TestServerResult<T> {
|
|
390
|
+
/** The server instance */
|
|
391
|
+
instance: T;
|
|
392
|
+
/** The bootstrapped server */
|
|
393
|
+
server: {
|
|
394
|
+
server: unknown;
|
|
395
|
+
transport: unknown;
|
|
396
|
+
connect: () => Promise<void>;
|
|
397
|
+
close: () => Promise<void>;
|
|
398
|
+
};
|
|
399
|
+
/** The test client */
|
|
400
|
+
client: MockMcpClient;
|
|
401
|
+
/** Cleanup function */
|
|
402
|
+
cleanup: () => Promise<void>;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Create a test client for MCP server testing
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* const { client, serverTransport } = createTestClient();
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
412
|
+
declare function createTestClient(options?: MockClientOptions): MockClientResult;
|
|
413
|
+
/**
|
|
414
|
+
* Wait for a condition to be true, with timeout
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```typescript
|
|
418
|
+
* await waitForCondition(() => server.isReady, { timeout: 5000 });
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
declare function waitForCondition(condition: () => boolean | Promise<boolean>, options?: {
|
|
422
|
+
timeout?: number;
|
|
423
|
+
interval?: number;
|
|
424
|
+
}): Promise<void>;
|
|
425
|
+
/**
|
|
426
|
+
* Create a test server with a connected client
|
|
427
|
+
*
|
|
428
|
+
* This is a convenience function that sets up a complete test environment
|
|
429
|
+
* with a server instance, bootstrapped server, and connected client.
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```typescript
|
|
433
|
+
* import { createTestServer } from '@mcpkit-dev/testing';
|
|
434
|
+
* import { listen } from '@mcpkit-dev/core';
|
|
435
|
+
*
|
|
436
|
+
* // In your test
|
|
437
|
+
* const { client, cleanup } = await createTestServer(MyServer, listen);
|
|
438
|
+
*
|
|
439
|
+
* // Test your server
|
|
440
|
+
* const tools = await client.listTools();
|
|
441
|
+
* expect(tools.tools).toHaveLength(1);
|
|
442
|
+
*
|
|
443
|
+
* // Clean up
|
|
444
|
+
* await cleanup();
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
declare function createTestServer<T extends new () => object>(ServerClass: T, listenFn: (ServerClass: T) => Promise<{
|
|
448
|
+
server: unknown;
|
|
449
|
+
transport: unknown;
|
|
450
|
+
connect: () => Promise<void>;
|
|
451
|
+
close: () => Promise<void>;
|
|
452
|
+
}>): Promise<TestServerResult<InstanceType<T>>>;
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* In-memory transport for testing MCP servers
|
|
456
|
+
*
|
|
457
|
+
* This transport allows direct communication between a client and server
|
|
458
|
+
* without any network overhead, perfect for unit and integration tests.
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* const { clientTransport, serverTransport } = InMemoryTransport.createPair();
|
|
463
|
+
*
|
|
464
|
+
* // Connect server to serverTransport
|
|
465
|
+
* await server.connect(serverTransport);
|
|
466
|
+
*
|
|
467
|
+
* // Use clientTransport to send messages
|
|
468
|
+
* await clientTransport.send({ jsonrpc: '2.0', method: 'ping', id: 1 });
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
declare class InMemoryTransport implements Transport {
|
|
472
|
+
private peer;
|
|
473
|
+
private messageQueue;
|
|
474
|
+
private started;
|
|
475
|
+
private closed;
|
|
476
|
+
onclose?: () => void;
|
|
477
|
+
onerror?: (error: Error) => void;
|
|
478
|
+
onmessage?: (message: JSONRPCMessage) => void;
|
|
479
|
+
/**
|
|
480
|
+
* Create a linked pair of transports for testing
|
|
481
|
+
*/
|
|
482
|
+
static createPair(): {
|
|
483
|
+
clientTransport: InMemoryTransport;
|
|
484
|
+
serverTransport: InMemoryTransport;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Start the transport
|
|
488
|
+
*/
|
|
489
|
+
start(): Promise<void>;
|
|
490
|
+
/**
|
|
491
|
+
* Send a message to the peer transport
|
|
492
|
+
*/
|
|
493
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
494
|
+
/**
|
|
495
|
+
* Close the transport
|
|
496
|
+
*/
|
|
497
|
+
close(): Promise<void>;
|
|
498
|
+
/**
|
|
499
|
+
* Deliver a message directly (for testing purposes)
|
|
500
|
+
*/
|
|
501
|
+
deliverMessage(message: JSONRPCMessage): void;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export { InMemoryTransport, type MockClientOptions, type MockClientResult, MockMcpClient, type TestServerResult, createTestClient, createTestServer, waitForCondition };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// src/mock-client.ts
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
|
|
4
|
+
// src/transport.ts
|
|
5
|
+
var InMemoryTransport = class _InMemoryTransport {
|
|
6
|
+
peer = null;
|
|
7
|
+
messageQueue = [];
|
|
8
|
+
started = false;
|
|
9
|
+
closed = false;
|
|
10
|
+
onclose;
|
|
11
|
+
onerror;
|
|
12
|
+
onmessage;
|
|
13
|
+
/**
|
|
14
|
+
* Create a linked pair of transports for testing
|
|
15
|
+
*/
|
|
16
|
+
static createPair() {
|
|
17
|
+
const clientTransport = new _InMemoryTransport();
|
|
18
|
+
const serverTransport = new _InMemoryTransport();
|
|
19
|
+
clientTransport.peer = serverTransport;
|
|
20
|
+
serverTransport.peer = clientTransport;
|
|
21
|
+
return { clientTransport, serverTransport };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start the transport
|
|
25
|
+
*/
|
|
26
|
+
async start() {
|
|
27
|
+
if (this.started) {
|
|
28
|
+
throw new Error("Transport already started");
|
|
29
|
+
}
|
|
30
|
+
if (this.closed) {
|
|
31
|
+
throw new Error("Transport is closed");
|
|
32
|
+
}
|
|
33
|
+
this.started = true;
|
|
34
|
+
while (this.messageQueue.length > 0) {
|
|
35
|
+
const message = this.messageQueue.shift();
|
|
36
|
+
if (message) {
|
|
37
|
+
this.onmessage?.(message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Send a message to the peer transport
|
|
43
|
+
*/
|
|
44
|
+
async send(message) {
|
|
45
|
+
if (this.closed) {
|
|
46
|
+
throw new Error("Transport is closed");
|
|
47
|
+
}
|
|
48
|
+
if (!this.peer) {
|
|
49
|
+
throw new Error("No peer transport connected");
|
|
50
|
+
}
|
|
51
|
+
await Promise.resolve();
|
|
52
|
+
if (this.peer.started) {
|
|
53
|
+
this.peer.onmessage?.(message);
|
|
54
|
+
} else {
|
|
55
|
+
this.peer.messageQueue.push(message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Close the transport
|
|
60
|
+
*/
|
|
61
|
+
async close() {
|
|
62
|
+
if (this.closed) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.closed = true;
|
|
66
|
+
this.onclose?.();
|
|
67
|
+
if (this.peer && !this.peer.closed) {
|
|
68
|
+
await this.peer.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Deliver a message directly (for testing purposes)
|
|
73
|
+
*/
|
|
74
|
+
deliverMessage(message) {
|
|
75
|
+
if (this.started) {
|
|
76
|
+
this.onmessage?.(message);
|
|
77
|
+
} else {
|
|
78
|
+
this.messageQueue.push(message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/mock-client.ts
|
|
84
|
+
var MockMcpClient = class _MockMcpClient {
|
|
85
|
+
client;
|
|
86
|
+
clientTransport;
|
|
87
|
+
connected = false;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
this.client = new Client(
|
|
90
|
+
{
|
|
91
|
+
name: options.name ?? "test-client",
|
|
92
|
+
version: options.version ?? "1.0.0"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
capabilities: {}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
this.clientTransport = new InMemoryTransport();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a mock client with linked transports
|
|
102
|
+
*/
|
|
103
|
+
static create(options) {
|
|
104
|
+
const mockClient = new _MockMcpClient(options);
|
|
105
|
+
const { clientTransport, serverTransport } = InMemoryTransport.createPair();
|
|
106
|
+
mockClient.clientTransport = clientTransport;
|
|
107
|
+
return {
|
|
108
|
+
client: mockClient,
|
|
109
|
+
serverTransport
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Connect to the server
|
|
114
|
+
*/
|
|
115
|
+
async connect() {
|
|
116
|
+
if (this.connected) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await this.client.connect(this.clientTransport);
|
|
120
|
+
this.connected = true;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Close the connection
|
|
124
|
+
*/
|
|
125
|
+
async close() {
|
|
126
|
+
if (!this.connected) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await this.client.close();
|
|
130
|
+
this.connected = false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* List all available tools
|
|
134
|
+
*/
|
|
135
|
+
async listTools() {
|
|
136
|
+
await this.ensureConnected();
|
|
137
|
+
return this.client.listTools();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Call a tool by name
|
|
141
|
+
*/
|
|
142
|
+
async callTool(name, args) {
|
|
143
|
+
await this.ensureConnected();
|
|
144
|
+
return this.client.callTool({ name, arguments: args });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* List all available resources
|
|
148
|
+
*/
|
|
149
|
+
async listResources() {
|
|
150
|
+
await this.ensureConnected();
|
|
151
|
+
return this.client.listResources();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Read a resource by URI
|
|
155
|
+
*/
|
|
156
|
+
async readResource(uri) {
|
|
157
|
+
await this.ensureConnected();
|
|
158
|
+
return this.client.readResource({ uri });
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* List all available prompts
|
|
162
|
+
*/
|
|
163
|
+
async listPrompts() {
|
|
164
|
+
await this.ensureConnected();
|
|
165
|
+
return this.client.listPrompts();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get a prompt by name
|
|
169
|
+
*/
|
|
170
|
+
async getPrompt(name, args) {
|
|
171
|
+
await this.ensureConnected();
|
|
172
|
+
return this.client.getPrompt({ name, arguments: args });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Ensure the client is connected
|
|
176
|
+
*/
|
|
177
|
+
async ensureConnected() {
|
|
178
|
+
if (!this.connected) {
|
|
179
|
+
await this.connect();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get the underlying MCP client
|
|
184
|
+
*/
|
|
185
|
+
get rawClient() {
|
|
186
|
+
return this.client;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/helpers.ts
|
|
191
|
+
function createTestClient(options) {
|
|
192
|
+
return MockMcpClient.create(options);
|
|
193
|
+
}
|
|
194
|
+
async function waitForCondition(condition, options = {}) {
|
|
195
|
+
const { timeout = 5e3, interval = 50 } = options;
|
|
196
|
+
const startTime = Date.now();
|
|
197
|
+
while (Date.now() - startTime < timeout) {
|
|
198
|
+
if (await condition()) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`Condition not met within ${timeout}ms`);
|
|
204
|
+
}
|
|
205
|
+
async function createTestServer(ServerClass, listenFn) {
|
|
206
|
+
const { client, serverTransport } = createTestClient();
|
|
207
|
+
const instance = new ServerClass();
|
|
208
|
+
const bootstrapped = await listenFn(ServerClass);
|
|
209
|
+
const server = bootstrapped.server;
|
|
210
|
+
await server.connect(serverTransport);
|
|
211
|
+
await client.connect();
|
|
212
|
+
return {
|
|
213
|
+
instance,
|
|
214
|
+
server: bootstrapped,
|
|
215
|
+
client,
|
|
216
|
+
cleanup: async () => {
|
|
217
|
+
await client.close();
|
|
218
|
+
await bootstrapped.close();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
InMemoryTransport,
|
|
224
|
+
MockMcpClient,
|
|
225
|
+
createTestClient,
|
|
226
|
+
createTestServer,
|
|
227
|
+
waitForCondition
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mock-client.ts","../src/transport.ts","../src/helpers.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { InMemoryTransport } from './transport.js';\n\n/**\n * Options for creating a mock MCP client\n */\nexport interface MockClientOptions {\n /** Client name */\n name?: string;\n /** Client version */\n version?: string;\n}\n\n/**\n * Result from creating a mock client\n */\nexport interface MockClientResult {\n /** The MCP client instance */\n client: MockMcpClient;\n /** Transport for connecting to a server */\n serverTransport: Transport;\n}\n\n/**\n * Mock MCP client for testing servers\n *\n * Provides a simple interface for testing MCP server implementations\n * without needing actual network connections.\n *\n * @example\n * ```typescript\n * const { client, serverTransport } = MockMcpClient.create();\n *\n * // Connect your server to serverTransport\n * const server = await listen(MyServer);\n * await server.server.connect(serverTransport);\n *\n * // Use the client to test your server\n * const tools = await client.listTools();\n * const result = await client.callTool('myTool', { param: 'value' });\n * ```\n */\nexport class MockMcpClient {\n private client: Client;\n private clientTransport: InMemoryTransport;\n private connected = false;\n\n private constructor(options: MockClientOptions = {}) {\n this.client = new Client(\n {\n name: options.name ?? 'test-client',\n version: options.version ?? '1.0.0',\n },\n {\n capabilities: {},\n },\n );\n\n this.clientTransport = new InMemoryTransport();\n }\n\n /**\n * Create a mock client with linked transports\n */\n static create(options?: MockClientOptions): MockClientResult {\n const mockClient = new MockMcpClient(options);\n const { clientTransport, serverTransport } = InMemoryTransport.createPair();\n\n mockClient.clientTransport = clientTransport;\n\n return {\n client: mockClient,\n serverTransport,\n };\n }\n\n /**\n * Connect to the server\n */\n async connect(): Promise<void> {\n if (this.connected) {\n return;\n }\n\n await this.client.connect(this.clientTransport);\n this.connected = true;\n }\n\n /**\n * Close the connection\n */\n async close(): Promise<void> {\n if (!this.connected) {\n return;\n }\n\n await this.client.close();\n this.connected = false;\n }\n\n /**\n * List all available tools\n */\n async listTools() {\n await this.ensureConnected();\n return this.client.listTools();\n }\n\n /**\n * Call a tool by name\n */\n async callTool(name: string, args?: Record<string, unknown>) {\n await this.ensureConnected();\n return this.client.callTool({ name, arguments: args });\n }\n\n /**\n * List all available resources\n */\n async listResources() {\n await this.ensureConnected();\n return this.client.listResources();\n }\n\n /**\n * Read a resource by URI\n */\n async readResource(uri: string) {\n await this.ensureConnected();\n return this.client.readResource({ uri });\n }\n\n /**\n * List all available prompts\n */\n async listPrompts() {\n await this.ensureConnected();\n return this.client.listPrompts();\n }\n\n /**\n * Get a prompt by name\n */\n async getPrompt(name: string, args?: Record<string, string>) {\n await this.ensureConnected();\n return this.client.getPrompt({ name, arguments: args });\n }\n\n /**\n * Ensure the client is connected\n */\n private async ensureConnected(): Promise<void> {\n if (!this.connected) {\n await this.connect();\n }\n }\n\n /**\n * Get the underlying MCP client\n */\n get rawClient(): Client {\n return this.client;\n }\n}\n","import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';\n\n/**\n * In-memory transport for testing MCP servers\n *\n * This transport allows direct communication between a client and server\n * without any network overhead, perfect for unit and integration tests.\n *\n * @example\n * ```typescript\n * const { clientTransport, serverTransport } = InMemoryTransport.createPair();\n *\n * // Connect server to serverTransport\n * await server.connect(serverTransport);\n *\n * // Use clientTransport to send messages\n * await clientTransport.send({ jsonrpc: '2.0', method: 'ping', id: 1 });\n * ```\n */\nexport class InMemoryTransport implements Transport {\n private peer: InMemoryTransport | null = null;\n private messageQueue: JSONRPCMessage[] = [];\n private started = false;\n private closed = false;\n\n onclose?: () => void;\n onerror?: (error: Error) => void;\n onmessage?: (message: JSONRPCMessage) => void;\n\n /**\n * Create a linked pair of transports for testing\n */\n static createPair(): { clientTransport: InMemoryTransport; serverTransport: InMemoryTransport } {\n const clientTransport = new InMemoryTransport();\n const serverTransport = new InMemoryTransport();\n\n clientTransport.peer = serverTransport;\n serverTransport.peer = clientTransport;\n\n return { clientTransport, serverTransport };\n }\n\n /**\n * Start the transport\n */\n async start(): Promise<void> {\n if (this.started) {\n throw new Error('Transport already started');\n }\n if (this.closed) {\n throw new Error('Transport is closed');\n }\n\n this.started = true;\n\n // Process any queued messages\n while (this.messageQueue.length > 0) {\n const message = this.messageQueue.shift();\n if (message) {\n this.onmessage?.(message);\n }\n }\n }\n\n /**\n * Send a message to the peer transport\n */\n async send(message: JSONRPCMessage): Promise<void> {\n if (this.closed) {\n throw new Error('Transport is closed');\n }\n\n if (!this.peer) {\n throw new Error('No peer transport connected');\n }\n\n // Simulate async behavior\n await Promise.resolve();\n\n if (this.peer.started) {\n this.peer.onmessage?.(message);\n } else {\n this.peer.messageQueue.push(message);\n }\n }\n\n /**\n * Close the transport\n */\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n this.onclose?.();\n\n // Also close peer\n if (this.peer && !this.peer.closed) {\n await this.peer.close();\n }\n }\n\n /**\n * Deliver a message directly (for testing purposes)\n */\n deliverMessage(message: JSONRPCMessage): void {\n if (this.started) {\n this.onmessage?.(message);\n } else {\n this.messageQueue.push(message);\n }\n }\n}\n","import { type MockClientOptions, type MockClientResult, MockMcpClient } from './mock-client.js';\n\n/**\n * Result from creating a test server\n */\nexport interface TestServerResult<T> {\n /** The server instance */\n instance: T;\n /** The bootstrapped server */\n server: {\n server: unknown;\n transport: unknown;\n connect: () => Promise<void>;\n close: () => Promise<void>;\n };\n /** The test client */\n client: MockMcpClient;\n /** Cleanup function */\n cleanup: () => Promise<void>;\n}\n\n/**\n * Create a test client for MCP server testing\n *\n * @example\n * ```typescript\n * const { client, serverTransport } = createTestClient();\n * ```\n */\nexport function createTestClient(options?: MockClientOptions): MockClientResult {\n return MockMcpClient.create(options);\n}\n\n/**\n * Wait for a condition to be true, with timeout\n *\n * @example\n * ```typescript\n * await waitForCondition(() => server.isReady, { timeout: 5000 });\n * ```\n */\nexport async function waitForCondition(\n condition: () => boolean | Promise<boolean>,\n options: { timeout?: number; interval?: number } = {},\n): Promise<void> {\n const { timeout = 5000, interval = 50 } = options;\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n if (await condition()) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, interval));\n }\n\n throw new Error(`Condition not met within ${timeout}ms`);\n}\n\n/**\n * Create a test server with a connected client\n *\n * This is a convenience function that sets up a complete test environment\n * with a server instance, bootstrapped server, and connected client.\n *\n * @example\n * ```typescript\n * import { createTestServer } from '@mcpkit-dev/testing';\n * import { listen } from '@mcpkit-dev/core';\n *\n * // In your test\n * const { client, cleanup } = await createTestServer(MyServer, listen);\n *\n * // Test your server\n * const tools = await client.listTools();\n * expect(tools.tools).toHaveLength(1);\n *\n * // Clean up\n * await cleanup();\n * ```\n */\nexport async function createTestServer<T extends new () => object>(\n ServerClass: T,\n listenFn: (ServerClass: T) => Promise<{\n server: unknown;\n transport: unknown;\n connect: () => Promise<void>;\n close: () => Promise<void>;\n }>,\n): Promise<TestServerResult<InstanceType<T>>> {\n const { client, serverTransport } = createTestClient();\n const instance = new ServerClass() as InstanceType<T>;\n const bootstrapped = await listenFn(ServerClass);\n\n // Connect server to transport\n // We need to access the internal server and connect it\n const server = bootstrapped.server as {\n connect: (transport: unknown) => Promise<void>;\n };\n await server.connect(serverTransport);\n\n // Connect client\n await client.connect();\n\n return {\n instance,\n server: bootstrapped,\n client,\n cleanup: async () => {\n await client.close();\n await bootstrapped.close();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcpkit-dev/testing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Testing utilities for mcpkit MCP servers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@mcpkit-dev/core": ">=0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.10.2",
|
|
35
|
+
"tsup": "^8.3.5",
|
|
36
|
+
"typescript": "^5.7.2",
|
|
37
|
+
"vitest": "^2.1.9"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"mcp",
|
|
44
|
+
"testing",
|
|
45
|
+
"mcpkit",
|
|
46
|
+
"model-context-protocol"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/v-checha/mcpkit.git",
|
|
52
|
+
"directory": "packages/testing"
|
|
53
|
+
}
|
|
54
|
+
}
|