@slbdn/mcp-tester 1.4.1 → 1.5.3
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/CHANGELOG.md +96 -0
- package/README.md +49 -0
- package/dist/ai/provider.d.ts +145 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +428 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/cli/index.js +260 -151
- package/dist/cli/index.js.map +1 -1
- package/dist/client/MCPClient.d.ts +16 -2
- package/dist/client/MCPClient.d.ts.map +1 -1
- package/dist/client/MCPClient.js +77 -20
- package/dist/client/MCPClient.js.map +1 -1
- package/dist/generate-cases.d.ts +113 -0
- package/dist/generate-cases.d.ts.map +1 -0
- package/dist/generate-cases.js +565 -0
- package/dist/generate-cases.js.map +1 -0
- package/dist/generate-tests.d.ts +106 -0
- package/dist/generate-tests.d.ts.map +1 -0
- package/dist/generate-tests.js +441 -0
- package/dist/generate-tests.js.map +1 -0
- package/dist/generate-types.d.ts +86 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +484 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +31 -21
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js.map +1 -1
- package/dist/templates/full-stack/.github/workflows/test.yml +34 -0
- package/dist/templates/full-stack/README.md +73 -0
- package/dist/templates/full-stack/examples/mock-server.js +115 -0
- package/dist/templates/full-stack/jest.config.js +39 -0
- package/dist/templates/full-stack/package.json +31 -0
- package/dist/templates/full-stack/scripts/generate-tests.ts +32 -0
- package/dist/templates/full-stack/scripts/generate-types.ts +28 -0
- package/dist/templates/full-stack/src/server.ts +78 -0
- package/dist/templates/full-stack/tests/helpers.ts +29 -0
- package/dist/templates/full-stack/tests/parallel.test.ts +44 -0
- package/dist/templates/full-stack/tests/server.test.ts +36 -0
- package/dist/templates/full-stack/tests/tools.test.ts +61 -0
- package/dist/templates/full-stack/tsconfig.json +19 -0
- package/dist/templates/minimal-jest/README.md +42 -0
- package/dist/templates/minimal-jest/examples/mock-server.js +62 -0
- package/dist/templates/minimal-jest/jest.config.js +24 -0
- package/dist/templates/minimal-jest/package.json +24 -0
- package/dist/templates/minimal-jest/tests/server.test.ts +64 -0
- package/dist/templates/minimal-jest/tsconfig.json +19 -0
- package/dist/templates/standard-jest/.github/workflows/test.yml +32 -0
- package/dist/templates/standard-jest/README.md +44 -0
- package/dist/templates/standard-jest/examples/mock-server.js +122 -0
- package/dist/templates/standard-jest/jest.config.js +39 -0
- package/dist/templates/standard-jest/package.json +27 -0
- package/dist/templates/standard-jest/tests/helpers.ts +43 -0
- package/dist/templates/standard-jest/tests/prompts.test.ts +37 -0
- package/dist/templates/standard-jest/tests/resources.test.ts +37 -0
- package/dist/templates/standard-jest/tests/server.test.ts +30 -0
- package/dist/templates/standard-jest/tests/tools.test.ts +87 -0
- package/dist/templates/standard-jest/tsconfig.json +19 -0
- package/dist/utils/masking.d.ts.map +1 -1
- package/dist/utils/masking.js +0 -3
- package/dist/utils/masking.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +9 -0
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/version.d.ts +13 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +81 -0
- package/dist/utils/version.js.map +1 -0
- package/docs/advanced.md +424 -0
- package/docs/ai-generation.md +211 -0
- package/docs/api-reference.md +819 -0
- package/docs/cicd.md +289 -0
- package/docs/cli.md +225 -0
- package/docs/examples.md +300 -0
- package/docs/nodejs-compatibility.md +81 -0
- package/docs/releases.md +413 -0
- package/docs/starter-templates.md +110 -0
- package/docs/testing.md +443 -0
- package/docs/troubleshooting.md +139 -0
- package/jest.d.ts +43 -0
- package/package.json +11 -20
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests for the full-stack server.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
|
6
|
+
import type { MCPClient } from '@slbdn/mcp-tester';
|
|
7
|
+
import { contentText, createConnectedClient } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
describe('Tools', () => {
|
|
10
|
+
let client: MCPClient;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
client = await createConnectedClient('tools-test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
if (client?.isConnected()) {
|
|
18
|
+
await client.stop();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('greet', () => {
|
|
23
|
+
it('greets by name', async () => {
|
|
24
|
+
const result = await client.callTool({ name: 'greet', arguments: { name: 'Alice' } });
|
|
25
|
+
expect(contentText(result)).toBe('Hello, Alice!');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('sum', () => {
|
|
30
|
+
it('sums an array of numbers', async () => {
|
|
31
|
+
const result = await client.callTool({
|
|
32
|
+
name: 'sum',
|
|
33
|
+
arguments: { numbers: [1, 2, 3, 4] },
|
|
34
|
+
});
|
|
35
|
+
expect(contentText(result)).toBe('10');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns 0 for an empty array', async () => {
|
|
39
|
+
const result = await client.callTool({ name: 'sum', arguments: { numbers: [] } });
|
|
40
|
+
expect(contentText(result)).toBe('0');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('uppercase', () => {
|
|
45
|
+
it('uppercases the input', async () => {
|
|
46
|
+
const result = await client.callTool({
|
|
47
|
+
name: 'uppercase',
|
|
48
|
+
arguments: { text: 'hello' },
|
|
49
|
+
});
|
|
50
|
+
expect(contentText(result)).toBe('HELLO');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('preserves non-letter characters', async () => {
|
|
54
|
+
const result = await client.callTool({
|
|
55
|
+
name: 'uppercase',
|
|
56
|
+
arguments: { text: 'abc 123!' },
|
|
57
|
+
});
|
|
58
|
+
expect(contentText(result)).toBe('ABC 123!');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./",
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"allowSyntheticDefaultImports": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts", "examples/**/*.js"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# MCP Test Minimal — Starter Template
|
|
2
|
+
|
|
3
|
+
The smallest working project that uses [`@slbdn/mcp-tester`](https://www.npmjs.com/package/@slbdn/mcp-tester) to test an MCP server.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
- A mock MCP server (`examples/mock-server.js`) with two tools: `echo` and `add`.
|
|
7
|
+
- One Jest test file (`tests/server.test.ts`) that connects, lists, and calls.
|
|
8
|
+
- TypeScript + `ts-jest` ESM configuration.
|
|
9
|
+
- A `package.json` with everything you need.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
You should see something like:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
PASS tests/server.test.ts
|
|
22
|
+
Mock MCP server
|
|
23
|
+
✓ should connect to the server
|
|
24
|
+
✓ should list at least one tool
|
|
25
|
+
✓ should echo a message
|
|
26
|
+
✓ should add two numbers
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Point this at your own server
|
|
30
|
+
|
|
31
|
+
Replace `examples/mock-server.js` with your server (or update `tests/server.test.ts` to point at a real one):
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
await client.start({
|
|
35
|
+
command: 'node',
|
|
36
|
+
args: ['./path/to/your-server.js'],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Next steps
|
|
41
|
+
- **Standard tests**: see `templates/standard-jest/` for tools/resources/prompts suites, fixtures, and CI.
|
|
42
|
+
- **Generation**: see `templates/full-stack/` for `generateTests` / `generateTypes` integration.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal mock MCP server for testing.
|
|
5
|
+
*
|
|
6
|
+
* Tools:
|
|
7
|
+
* - echo(message): echoes back the input message
|
|
8
|
+
* - add(a, b): returns the sum of two numbers
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
|
|
15
|
+
const server = new Server(
|
|
16
|
+
{ name: 'mock-mcp-server', version: '1.0.0' },
|
|
17
|
+
{ capabilities: { tools: {} } }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
21
|
+
tools: [
|
|
22
|
+
{
|
|
23
|
+
name: 'echo',
|
|
24
|
+
description: 'Echo back the input message',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: { message: { type: 'string', description: 'Message to echo' } },
|
|
28
|
+
required: ['message'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'add',
|
|
33
|
+
description: 'Add two numbers',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
a: { type: 'number', description: 'First number' },
|
|
38
|
+
b: { type: 'number', description: 'Second number' },
|
|
39
|
+
},
|
|
40
|
+
required: ['a', 'b'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
47
|
+
const { name, arguments: args } = request.params;
|
|
48
|
+
|
|
49
|
+
switch (name) {
|
|
50
|
+
case 'echo':
|
|
51
|
+
return { content: [{ type: 'text', text: String(args.message) }] };
|
|
52
|
+
case 'add': {
|
|
53
|
+
const sum = Number(args.a) + Number(args.b);
|
|
54
|
+
return { content: [{ type: 'text', text: String(sum) }] };
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const transport = new StdioServerTransport();
|
|
62
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
preset: 'ts-jest/presets/default-esm',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
6
|
+
moduleNameMapper: {
|
|
7
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
8
|
+
},
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.tsx?$': [
|
|
11
|
+
'ts-jest',
|
|
12
|
+
{
|
|
13
|
+
useESM: true,
|
|
14
|
+
tsconfig: {
|
|
15
|
+
esModuleInterop: true,
|
|
16
|
+
allowSyntheticDefaultImports: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
testMatch: ['**/tests/**/*.test.ts'],
|
|
22
|
+
testTimeout: 30000,
|
|
23
|
+
maxWorkers: 1,
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Minimal MCP server test scaffold using @slbdn/mcp-tester + Jest",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@slbdn/mcp-tester": "^1.4.3",
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/jest": "^30.0.0",
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"jest": "^30.5.2",
|
|
21
|
+
"ts-jest": "^29.4.11",
|
|
22
|
+
"typescript": "^5.3.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal MCP server test scaffold.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - Starting a stdio MCP server
|
|
6
|
+
* - Listing tools
|
|
7
|
+
* - Calling a tool
|
|
8
|
+
* - Cleanup
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { MCPClient, setupJestMatchers } from '@slbdn/mcp-tester';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const serverPath = join(here, '..', 'examples', 'mock-server.js');
|
|
17
|
+
|
|
18
|
+
describe('Mock MCP server', () => {
|
|
19
|
+
let client: MCPClient;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
setupJestMatchers();
|
|
23
|
+
client = new MCPClient({
|
|
24
|
+
name: 'minimal-test',
|
|
25
|
+
version: '0.1.0',
|
|
26
|
+
timeout: 10000,
|
|
27
|
+
});
|
|
28
|
+
await client.start({
|
|
29
|
+
command: 'node',
|
|
30
|
+
args: [serverPath],
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
if (client?.isConnected()) {
|
|
36
|
+
await client.stop();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should connect to the server', () => {
|
|
41
|
+
expect(client.isConnected()).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should list at least one tool', async () => {
|
|
45
|
+
const tools = await client.listTools();
|
|
46
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should echo a message', async () => {
|
|
50
|
+
const result = await client.callTool({
|
|
51
|
+
name: 'echo',
|
|
52
|
+
arguments: { message: 'hello' },
|
|
53
|
+
});
|
|
54
|
+
expect(result).toReturnText('hello');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should add two numbers', async () => {
|
|
58
|
+
const result = await client.callTool({
|
|
59
|
+
name: 'add',
|
|
60
|
+
arguments: { a: 2, b: 3 },
|
|
61
|
+
});
|
|
62
|
+
expect(result).toReturnText('5');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./",
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"allowSyntheticDefaultImports": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["tests/**/*.ts", "examples/**/*.js"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [20, 22]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
18
|
+
uses: actions/setup-node@v5
|
|
19
|
+
with:
|
|
20
|
+
node-version: ${{ matrix.node-version }}
|
|
21
|
+
cache: 'npm'
|
|
22
|
+
- name: Install
|
|
23
|
+
run: npm ci
|
|
24
|
+
- name: Test
|
|
25
|
+
run: npm test
|
|
26
|
+
- name: Upload HTML report
|
|
27
|
+
if: always()
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: test-report-${{ matrix.node-version }}
|
|
31
|
+
path: reports/
|
|
32
|
+
retention-days: 14
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# MCP Test Standard — Starter Template
|
|
2
|
+
|
|
3
|
+
A "blessed" layout for testing MCP servers with [`@slbdn/mcp-tester`](https://www.npmjs.com/package/@slbdn/mcp-tester). Splits tests by capability, ships an HTML reporter, includes a CI workflow, and demonstrates richer mocking patterns.
|
|
4
|
+
|
|
5
|
+
## What's included
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
standard-jest/
|
|
9
|
+
├── examples/
|
|
10
|
+
│ └── mock-server.js # Stdio mock server: echo, add, slow, version resource, greet prompt
|
|
11
|
+
├── tests/
|
|
12
|
+
│ ├── helpers.ts # Shared client setup
|
|
13
|
+
│ ├── server.test.ts # Lifecycle and health
|
|
14
|
+
│ ├── tools.test.ts # Tool listing, schemas, calls (incl. timeout)
|
|
15
|
+
│ ├── resources.test.ts # Resource listing and reads
|
|
16
|
+
│ └── prompts.test.ts # Prompt listing and gets
|
|
17
|
+
├── .github/workflows/
|
|
18
|
+
│ └── test.yml # CI matrix (Node 20, 22) + HTML report upload
|
|
19
|
+
├── jest.config.js
|
|
20
|
+
├── tsconfig.json
|
|
21
|
+
└── package.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
npm test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open `reports/test-report.html` for the visual report.
|
|
32
|
+
|
|
33
|
+
## What's different from `minimal-jest`
|
|
34
|
+
- **Test split**: separate files per capability for clearer failure attribution.
|
|
35
|
+
- **HTML reporter**: jest-html-reporters preconfigured.
|
|
36
|
+
- **Coverage**: `npm run test:coverage` runs with text/lcov reporters.
|
|
37
|
+
- **CI workflow**: GitHub Actions matrix on Node 20 and 22.
|
|
38
|
+
- **Timeout testing**: demonstrates `timeout` per-call option.
|
|
39
|
+
|
|
40
|
+
## Point this at your own server
|
|
41
|
+
Replace `examples/mock-server.js` with your server, or update `tests/helpers.ts` to point at it.
|
|
42
|
+
|
|
43
|
+
## Next steps
|
|
44
|
+
- **Full stack**: see `templates/full-stack/` for code generation (`generateTests` / `generateTypes`) and typed tool calls.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standard mock MCP server — used by all test files in this template.
|
|
5
|
+
*
|
|
6
|
+
* Tools:
|
|
7
|
+
* - echo(message): returns the input message
|
|
8
|
+
* - add(a, b): returns the sum
|
|
9
|
+
* - slow(ms): waits N ms then returns (good for timeout tests)
|
|
10
|
+
*
|
|
11
|
+
* Resources:
|
|
12
|
+
* - config://version: returns "1.0.0"
|
|
13
|
+
*
|
|
14
|
+
* Prompts:
|
|
15
|
+
* - greet(user): returns a greeting
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
+
import {
|
|
21
|
+
CallToolRequestSchema,
|
|
22
|
+
ListToolsRequestSchema,
|
|
23
|
+
ListResourcesRequestSchema,
|
|
24
|
+
ReadResourceRequestSchema,
|
|
25
|
+
ListPromptsRequestSchema,
|
|
26
|
+
GetPromptRequestSchema,
|
|
27
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
28
|
+
|
|
29
|
+
const server = new Server(
|
|
30
|
+
{ name: 'standard-mock-server', version: '1.0.0' },
|
|
31
|
+
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
35
|
+
tools: [
|
|
36
|
+
{
|
|
37
|
+
name: 'echo',
|
|
38
|
+
description: 'Echo back the input message',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: { message: { type: 'string' } },
|
|
42
|
+
required: ['message'],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'add',
|
|
47
|
+
description: 'Add two numbers',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
a: { type: 'number' },
|
|
52
|
+
b: { type: 'number' },
|
|
53
|
+
},
|
|
54
|
+
required: ['a', 'b'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'slow',
|
|
59
|
+
description: 'A slow tool for testing timeouts',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: { ms: { type: 'number', default: 100 } },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
69
|
+
resources: [
|
|
70
|
+
{
|
|
71
|
+
uri: 'config://version',
|
|
72
|
+
name: 'version',
|
|
73
|
+
description: 'Server version',
|
|
74
|
+
mimeType: 'text/plain',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
80
|
+
if (request.params.uri === 'config://version') {
|
|
81
|
+
return { contents: [{ uri: 'config://version', mimeType: 'text/plain', text: '1.0.0' }] };
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
87
|
+
prompts: [
|
|
88
|
+
{
|
|
89
|
+
name: 'greet',
|
|
90
|
+
description: 'Greet a user by name',
|
|
91
|
+
arguments: [{ name: 'user', description: 'Name', required: true }],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
97
|
+
if (request.params.name === 'greet') {
|
|
98
|
+
const user = String(request.params.arguments?.user ?? 'world');
|
|
99
|
+
return {
|
|
100
|
+
messages: [{ role: 'assistant', content: { type: 'text', text: `Hello, ${user}!` } }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
107
|
+
const { name, arguments: args } = request.params;
|
|
108
|
+
switch (name) {
|
|
109
|
+
case 'echo':
|
|
110
|
+
return { content: [{ type: 'text', text: String(args.message) }] };
|
|
111
|
+
case 'add':
|
|
112
|
+
return { content: [{ type: 'text', text: String(Number(args.a) + Number(args.b)) }] };
|
|
113
|
+
case 'slow':
|
|
114
|
+
await new Promise((r) => setTimeout(r, Number(args.ms ?? 100)));
|
|
115
|
+
return { content: [{ type: 'text', text: `waited ${args.ms ?? 100}ms` }] };
|
|
116
|
+
default:
|
|
117
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const transport = new StdioServerTransport();
|
|
122
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
preset: 'ts-jest/presets/default-esm',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
cacheDirectory: '/tmp/mcp-tester-cache',
|
|
6
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
7
|
+
moduleNameMapper: {
|
|
8
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
9
|
+
},
|
|
10
|
+
transform: {
|
|
11
|
+
'^.+\\.tsx?$': [
|
|
12
|
+
'ts-jest',
|
|
13
|
+
{
|
|
14
|
+
useESM: true,
|
|
15
|
+
tsconfig: {
|
|
16
|
+
esModuleInterop: true,
|
|
17
|
+
allowSyntheticDefaultImports: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
testMatch: ['**/tests/**/*.test.ts'],
|
|
23
|
+
testTimeout: 30000,
|
|
24
|
+
maxWorkers: 1,
|
|
25
|
+
reporters: [
|
|
26
|
+
'default',
|
|
27
|
+
[
|
|
28
|
+
'jest-html-reporters',
|
|
29
|
+
{
|
|
30
|
+
publicPath: './reports',
|
|
31
|
+
filename: 'test-report.html',
|
|
32
|
+
expand: true,
|
|
33
|
+
pageTitle: 'MCP Test Report',
|
|
34
|
+
hideIcon: true,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
],
|
|
38
|
+
coverageReporters: ['text', 'text-summary', 'lcov'],
|
|
39
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Standard MCP server test scaffold using @slbdn/mcp-tester + Jest (tools, resources, prompts, fixtures, CI)",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
9
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
10
|
+
"test:report": "node --experimental-vm-modules node_modules/jest/bin/jest.js --reporters=default --reporters=jest-html-reporters"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@slbdn/mcp-tester": "^1.4.3",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/jest": "^30.0.0",
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"jest": "^30.5.2",
|
|
23
|
+
"jest-html-reporters": "^3.0.11",
|
|
24
|
+
"ts-jest": "^29.4.11",
|
|
25
|
+
"typescript": "^5.3.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test helpers: path resolution and per-suite client lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MCPClient } from '@slbdn/mcp-tester';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export const serverPath = join(here, '..', 'examples', 'mock-server.js');
|
|
11
|
+
|
|
12
|
+
/** The result type of {@link MCPClient.callTool}. */
|
|
13
|
+
type ToolCallResult = Awaited<ReturnType<MCPClient['callTool']>>;
|
|
14
|
+
|
|
15
|
+
/** Extract the text of the first content item (empty string if not text). */
|
|
16
|
+
export function contentText(result: ToolCallResult): string {
|
|
17
|
+
const first = result.content[0];
|
|
18
|
+
return first && first.type === 'text' ? first.text : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Extract the text of the first prompt message (empty string if not text). */
|
|
22
|
+
export function promptText(result: Awaited<ReturnType<MCPClient['getPrompt']>>): string {
|
|
23
|
+
const first = result.messages[0];
|
|
24
|
+
const content = first?.content;
|
|
25
|
+
return content && content.type === 'text' ? content.text : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Extract the text of the first resource contents entry (empty string if not text). */
|
|
29
|
+
export function resourceText(result: Awaited<ReturnType<MCPClient['readResource']>>): string {
|
|
30
|
+
const first = result.contents[0];
|
|
31
|
+
return first && 'text' in first ? (first.text as string) : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create a connected MCPClient pointed at the standard mock server. */
|
|
35
|
+
export async function createConnectedClient(name = 'standard-test'): Promise<MCPClient> {
|
|
36
|
+
const client = new MCPClient({
|
|
37
|
+
name,
|
|
38
|
+
version: '0.1.0',
|
|
39
|
+
timeout: 10000,
|
|
40
|
+
});
|
|
41
|
+
await client.start({ command: 'node', args: [serverPath] });
|
|
42
|
+
return client;
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
|
6
|
+
import type { MCPClient } from '@slbdn/mcp-tester';
|
|
7
|
+
import { contentText, createConnectedClient, promptText, resourceText } from './helpers.js';
|
|
8
|
+
|
|
9
|
+
describe('Prompts', () => {
|
|
10
|
+
let client: MCPClient;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
client = await createConnectedClient('prompts-test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
if (client?.isConnected()) {
|
|
18
|
+
await client.stop();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('lists available prompts', async () => {
|
|
23
|
+
const prompts = await client.listPrompts();
|
|
24
|
+
expect(prompts.map((p) => p.name)).toContain('greet');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('gets the greet prompt with arguments', async () => {
|
|
28
|
+
const result = await client.getPrompt('greet', { user: 'Alice' });
|
|
29
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
30
|
+
const text = promptText(result);
|
|
31
|
+
expect(text).toContain('Alice');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('rejects unknown prompts', async () => {
|
|
35
|
+
await expect(client.getPrompt('unknown-prompt')).rejects.toThrow();
|
|
36
|
+
});
|
|
37
|
+
});
|