mcp-test-kits 0.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 +94 -0
- package/dist/capabilities/prompts.d.ts +9 -0
- package/dist/capabilities/prompts.d.ts.map +1 -0
- package/dist/capabilities/prompts.js +76 -0
- package/dist/capabilities/prompts.js.map +1 -0
- package/dist/capabilities/resources.d.ts +9 -0
- package/dist/capabilities/resources.d.ts.map +1 -0
- package/dist/capabilities/resources.js +76 -0
- package/dist/capabilities/resources.js.map +1 -0
- package/dist/capabilities/tools.d.ts +9 -0
- package/dist/capabilities/tools.d.ts.map +1 -0
- package/dist/capabilities/tools.js +77 -0
- package/dist/capabilities/tools.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -0
- package/dist/extract-spec.d.ts +14 -0
- package/dist/extract-spec.d.ts.map +1 -0
- package/dist/extract-spec.js +117 -0
- package/dist/extract-spec.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/introspection.d.ts +68 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +135 -0
- package/dist/introspection.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +28 -0
- package/dist/server.js.map +1 -0
- package/dist/transports/http.d.ts +10 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +31 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/index.d.ts +7 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +7 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/transports/sse.d.ts +10 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +44 -0
- package/dist/transports/sse.js.map +1 -0
- package/dist/transports/stdio.d.ts +10 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +14 -0
- package/dist/transports/stdio.js.map +1 -0
- package/eslint.config.js +22 -0
- package/package.json +51 -0
- package/src/capabilities/prompts.ts +108 -0
- package/src/capabilities/resources.ts +108 -0
- package/src/capabilities/tools.ts +124 -0
- package/src/config.ts +60 -0
- package/src/extract-spec.ts +189 -0
- package/src/index.ts +110 -0
- package/src/introspection.ts +216 -0
- package/src/server.ts +34 -0
- package/src/transports/http.ts +42 -0
- package/src/transports/index.ts +7 -0
- package/src/transports/sse.ts +60 -0
- package/src/transports/stdio.ts +21 -0
- package/tests/fixtures.ts +146 -0
- package/tests/http.test.ts +34 -0
- package/tests/sse.test.ts +34 -0
- package/tests/stdio.test.ts +98 -0
- package/tsconfig.json +27 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Introspection utilities for extracting specifications from MCP Server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
McpServer,
|
|
7
|
+
RegisteredTool,
|
|
8
|
+
RegisteredResource,
|
|
9
|
+
RegisteredResourceTemplate,
|
|
10
|
+
RegisteredPrompt,
|
|
11
|
+
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Type representing the internal structure of McpServer with private fields exposed
|
|
15
|
+
*/
|
|
16
|
+
interface McpServerInternal {
|
|
17
|
+
_registeredTools: Record<string, RegisteredTool>;
|
|
18
|
+
_registeredResources: Record<string, RegisteredResource>;
|
|
19
|
+
_registeredResourceTemplates: Record<string, RegisteredResourceTemplate>;
|
|
20
|
+
_registeredPrompts: Record<string, RegisteredPrompt>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extended McpServer with introspection capabilities.
|
|
25
|
+
* Provides access to registered tools, resources, and prompts for spec extraction.
|
|
26
|
+
*/
|
|
27
|
+
export class IntrospectableMcpServer extends McpServer {
|
|
28
|
+
/**
|
|
29
|
+
* Get all registered tools with their metadata
|
|
30
|
+
*/
|
|
31
|
+
getRegisteredTools(): Record<string, RegisteredTool> {
|
|
32
|
+
// Access the private _registeredTools field (it's an object, not a Map)
|
|
33
|
+
return (this as unknown as McpServerInternal)._registeredTools;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get all registered resources with their metadata
|
|
38
|
+
*/
|
|
39
|
+
getRegisteredResources(): Record<string, RegisteredResource> {
|
|
40
|
+
// Access the private _registeredResources field (it's an object, not a Map)
|
|
41
|
+
return (this as unknown as McpServerInternal)._registeredResources;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get all registered resource templates with their metadata
|
|
46
|
+
*/
|
|
47
|
+
getRegisteredResourceTemplates(): Record<string, RegisteredResourceTemplate> {
|
|
48
|
+
// Access the private _registeredResourceTemplates field (it's an object, not a Map)
|
|
49
|
+
return (this as unknown as McpServerInternal)._registeredResourceTemplates;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all registered prompts with their metadata
|
|
54
|
+
*/
|
|
55
|
+
getRegisteredPrompts(): Record<string, RegisteredPrompt> {
|
|
56
|
+
// Access the private _registeredPrompts field (it's an object, not a Map)
|
|
57
|
+
return (this as unknown as McpServerInternal)._registeredPrompts;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ToolSpec {
|
|
62
|
+
name: string;
|
|
63
|
+
description: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ResourceSpec {
|
|
67
|
+
name: string;
|
|
68
|
+
uri: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
mimeType?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface PromptArgSpec {
|
|
74
|
+
name: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
required?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface PromptSpec {
|
|
80
|
+
name: string;
|
|
81
|
+
description: string;
|
|
82
|
+
arguments?: PromptArgSpec[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract tool specifications from an introspectable server
|
|
87
|
+
*/
|
|
88
|
+
export function extractToolsSpec(server: IntrospectableMcpServer): ToolSpec[] {
|
|
89
|
+
const tools: ToolSpec[] = [];
|
|
90
|
+
const registeredTools = server.getRegisteredTools();
|
|
91
|
+
|
|
92
|
+
for (const [name, tool] of Object.entries(registeredTools)) {
|
|
93
|
+
if (tool.enabled) {
|
|
94
|
+
tools.push({
|
|
95
|
+
name,
|
|
96
|
+
description: tool.description || "",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract resource specifications from an introspectable server
|
|
106
|
+
*/
|
|
107
|
+
export function extractResourcesSpec(
|
|
108
|
+
server: IntrospectableMcpServer,
|
|
109
|
+
): ResourceSpec[] {
|
|
110
|
+
const resources: ResourceSpec[] = [];
|
|
111
|
+
const registeredResources = server.getRegisteredResources();
|
|
112
|
+
|
|
113
|
+
for (const [uri, resource] of Object.entries(registeredResources)) {
|
|
114
|
+
if (resource.enabled) {
|
|
115
|
+
resources.push({
|
|
116
|
+
name: resource.name,
|
|
117
|
+
uri,
|
|
118
|
+
mimeType: resource.metadata?.mimeType || "text/plain",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Note: Resource templates are not included in static spec extraction
|
|
124
|
+
// as they represent dynamic URI patterns
|
|
125
|
+
|
|
126
|
+
return resources.sort((a, b) => a.uri.localeCompare(b.uri));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Type representing Zod schema internal structure
|
|
131
|
+
*/
|
|
132
|
+
interface ZodSchemaDef {
|
|
133
|
+
typeName?: string;
|
|
134
|
+
shape?: () => Record<string, ZodSchemaInternal>;
|
|
135
|
+
innerType?: ZodSchemaInternal;
|
|
136
|
+
description?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface ZodSchemaInternal {
|
|
140
|
+
_def: ZodSchemaDef;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract argument specifications from Zod schema
|
|
145
|
+
*/
|
|
146
|
+
function extractPromptArgs(argsSchema: ZodSchemaInternal): PromptArgSpec[] {
|
|
147
|
+
if (!argsSchema || !argsSchema._def) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const args: PromptArgSpec[] = [];
|
|
152
|
+
|
|
153
|
+
// Handle Zod object schema
|
|
154
|
+
if (argsSchema._def.typeName === "ZodObject") {
|
|
155
|
+
const shape = argsSchema._def.shape?.();
|
|
156
|
+
|
|
157
|
+
if (shape) {
|
|
158
|
+
for (const [argName, argSchema] of Object.entries(shape)) {
|
|
159
|
+
// Check if the argument is optional
|
|
160
|
+
const isOptional = argSchema._def.typeName === "ZodOptional";
|
|
161
|
+
const innerSchema = isOptional ? argSchema._def.innerType : argSchema;
|
|
162
|
+
|
|
163
|
+
// Extract description from the schema
|
|
164
|
+
let description = "";
|
|
165
|
+
if (innerSchema?._def.description) {
|
|
166
|
+
description = innerSchema._def.description;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
args.push({
|
|
170
|
+
name: argName,
|
|
171
|
+
description,
|
|
172
|
+
required: !isOptional,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return args;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract prompt specifications from an introspectable server
|
|
183
|
+
*/
|
|
184
|
+
export function extractPromptsSpec(
|
|
185
|
+
server: IntrospectableMcpServer,
|
|
186
|
+
): PromptSpec[] {
|
|
187
|
+
const prompts: PromptSpec[] = [];
|
|
188
|
+
const registeredPrompts = server.getRegisteredPrompts();
|
|
189
|
+
|
|
190
|
+
for (const [name, prompt] of Object.entries(registeredPrompts)) {
|
|
191
|
+
if (prompt.enabled) {
|
|
192
|
+
const args = prompt.argsSchema
|
|
193
|
+
? extractPromptArgs(prompt.argsSchema as ZodSchemaInternal)
|
|
194
|
+
: [];
|
|
195
|
+
|
|
196
|
+
prompts.push({
|
|
197
|
+
name,
|
|
198
|
+
description: prompt.description || "",
|
|
199
|
+
arguments: args,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return prompts.sort((a, b) => a.name.localeCompare(b.name));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract full specification from an introspectable server
|
|
209
|
+
*/
|
|
210
|
+
export function extractSpec(server: IntrospectableMcpServer) {
|
|
211
|
+
return {
|
|
212
|
+
tools: extractToolsSpec(server),
|
|
213
|
+
resources: extractResourcesSpec(server),
|
|
214
|
+
prompts: extractPromptsSpec(server),
|
|
215
|
+
};
|
|
216
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server setup for MCP Test Kits
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Config } from "./config.js";
|
|
6
|
+
import { IntrospectableMcpServer } from "./introspection.js";
|
|
7
|
+
import { registerTools } from "./capabilities/tools.js";
|
|
8
|
+
import { registerResources } from "./capabilities/resources.js";
|
|
9
|
+
import { registerPrompts } from "./capabilities/prompts.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create and configure the MCP server
|
|
13
|
+
*/
|
|
14
|
+
export function createServer(config: Config): IntrospectableMcpServer {
|
|
15
|
+
const server = new IntrospectableMcpServer({
|
|
16
|
+
name: config.server.name,
|
|
17
|
+
version: config.server.version,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Register capabilities based on config
|
|
21
|
+
if (config.capabilities.tools) {
|
|
22
|
+
registerTools(server);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (config.capabilities.resources) {
|
|
26
|
+
registerResources(server);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.capabilities.prompts) {
|
|
30
|
+
registerPrompts(server);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport for MCP Test Kits (Streamable HTTP)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
+
import type { Config } from "../config.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run the MCP server over HTTP transport (Streamable HTTP)
|
|
12
|
+
*/
|
|
13
|
+
export async function runHttpServer(
|
|
14
|
+
server: McpServer,
|
|
15
|
+
config: Config,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const { host, port } = config.transport.network;
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
|
|
22
|
+
// MCP endpoint - handle all HTTP methods
|
|
23
|
+
app.all("/mcp", async (req, res) => {
|
|
24
|
+
const transport = new StreamableHTTPServerTransport({
|
|
25
|
+
sessionIdGenerator: undefined,
|
|
26
|
+
enableJsonResponse: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
res.on("close", () => transport.close());
|
|
30
|
+
await server.connect(transport);
|
|
31
|
+
await transport.handleRequest(req, res, req.body);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Start server
|
|
35
|
+
console.error(`Starting MCP HTTP server at http://${host}:${port}/mcp`);
|
|
36
|
+
app.listen(port, host, () => {
|
|
37
|
+
console.error(`MCP HTTP server listening on http://${host}:${port}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Keep the process running
|
|
41
|
+
await new Promise(() => {});
|
|
42
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE transport for MCP Test Kits
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
8
|
+
import type { Config } from "../config.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run the MCP server over SSE transport
|
|
12
|
+
*/
|
|
13
|
+
export async function runSseServer(
|
|
14
|
+
server: McpServer,
|
|
15
|
+
config: Config,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const { host, port } = config.transport.network;
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
|
|
22
|
+
// Store active transports for session management
|
|
23
|
+
const transports = new Map<string, SSEServerTransport>();
|
|
24
|
+
|
|
25
|
+
// SSE endpoint for server->client messages
|
|
26
|
+
app.get("/sse", async (req, res) => {
|
|
27
|
+
const transport = new SSEServerTransport("/sse", res);
|
|
28
|
+
const sessionId = transport.sessionId;
|
|
29
|
+
transports.set(sessionId, transport);
|
|
30
|
+
|
|
31
|
+
// Cleanup on close
|
|
32
|
+
transport.onclose = () => {
|
|
33
|
+
transports.delete(sessionId);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
await server.connect(transport);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// POST endpoint for client->server messages
|
|
40
|
+
app.post("/sse", async (req, res) => {
|
|
41
|
+
const sessionId = req.query.sessionId as string | undefined;
|
|
42
|
+
|
|
43
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
44
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const transport = transports.get(sessionId)!;
|
|
49
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Start server
|
|
53
|
+
console.error(`Starting MCP SSE server at http://${host}:${port}/sse`);
|
|
54
|
+
app.listen(port, host, () => {
|
|
55
|
+
console.error(`MCP SSE server listening on http://${host}:${port}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Keep the process running
|
|
59
|
+
await new Promise(() => {});
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio transport for MCP Test Kits
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import type { Config } from "../config.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run the MCP server over stdio transport
|
|
11
|
+
*/
|
|
12
|
+
export async function runStdioServer(
|
|
13
|
+
server: McpServer,
|
|
14
|
+
_config: Config,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
|
|
19
|
+
// Keep the process running
|
|
20
|
+
await new Promise(() => {});
|
|
21
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test fixtures for integration tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create an MCP client connected via stdio transport.
|
|
13
|
+
*/
|
|
14
|
+
export async function createStdioClient(): Promise<{
|
|
15
|
+
client: Client;
|
|
16
|
+
close: () => Promise<void>;
|
|
17
|
+
}> {
|
|
18
|
+
const transport = new StdioClientTransport({
|
|
19
|
+
command: "npx",
|
|
20
|
+
args: ["tsx", "src/index.ts"],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const client = new Client({
|
|
24
|
+
name: "test-client",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await client.connect(transport);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
client,
|
|
32
|
+
close: async () => {
|
|
33
|
+
await client.close();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Start an HTTP server and return the base URL.
|
|
40
|
+
*/
|
|
41
|
+
export async function startHttpServer(port: number = 3001): Promise<{
|
|
42
|
+
url: string;
|
|
43
|
+
stop: () => void;
|
|
44
|
+
}> {
|
|
45
|
+
const proc = spawn("npx", ["tsx", "src/index.ts", "--transport", "http", "--port", String(port)], {
|
|
46
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Wait for server to start
|
|
50
|
+
await new Promise<void>((resolve, reject) => {
|
|
51
|
+
const timeout = setTimeout(() => resolve(), 3000);
|
|
52
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
53
|
+
if (data.toString().includes("listening")) {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
resolve();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
proc.on("error", reject);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
url: `http://localhost:${port}`,
|
|
63
|
+
stop: () => {
|
|
64
|
+
proc.kill();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Start an SSE server and return the base URL.
|
|
71
|
+
*/
|
|
72
|
+
export async function startSseServer(port: number = 3002): Promise<{
|
|
73
|
+
url: string;
|
|
74
|
+
stop: () => void;
|
|
75
|
+
}> {
|
|
76
|
+
const proc = spawn("npx", ["tsx", "src/index.ts", "--transport", "sse", "--port", String(port)], {
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Wait for server to start
|
|
81
|
+
await new Promise<void>((resolve, reject) => {
|
|
82
|
+
const timeout = setTimeout(() => resolve(), 3000);
|
|
83
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
84
|
+
if (data.toString().includes("listening")) {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
proc.on("error", reject);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
url: `http://localhost:${port}`,
|
|
94
|
+
stop: () => {
|
|
95
|
+
proc.kill();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create an MCP client connected via HTTP transport.
|
|
102
|
+
*/
|
|
103
|
+
export async function createHttpClient(serverUrl: string): Promise<{
|
|
104
|
+
client: Client;
|
|
105
|
+
close: () => Promise<void>;
|
|
106
|
+
}> {
|
|
107
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${serverUrl}/mcp`));
|
|
108
|
+
|
|
109
|
+
const client = new Client({
|
|
110
|
+
name: "test-client",
|
|
111
|
+
version: "1.0.0",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await client.connect(transport);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
client,
|
|
118
|
+
close: async () => {
|
|
119
|
+
await client.close();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create an MCP client connected via SSE transport.
|
|
126
|
+
*/
|
|
127
|
+
export async function createSseClient(serverUrl: string): Promise<{
|
|
128
|
+
client: Client;
|
|
129
|
+
close: () => Promise<void>;
|
|
130
|
+
}> {
|
|
131
|
+
const transport = new SSEClientTransport(new URL(`${serverUrl}/sse`));
|
|
132
|
+
|
|
133
|
+
const client = new Client({
|
|
134
|
+
name: "test-client",
|
|
135
|
+
version: "1.0.0",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await client.connect(transport);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
client,
|
|
142
|
+
close: async () => {
|
|
143
|
+
await client.close();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests via HTTP transport.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
6
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { startHttpServer, createHttpClient } from "./fixtures.js";
|
|
8
|
+
|
|
9
|
+
describe("HTTP Transport", () => {
|
|
10
|
+
let serverUrl: string;
|
|
11
|
+
let stopServer: () => void;
|
|
12
|
+
let client: Client;
|
|
13
|
+
let closeClient: () => Promise<void>;
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
const server = await startHttpServer(3001);
|
|
17
|
+
serverUrl = server.url;
|
|
18
|
+
stopServer = server.stop;
|
|
19
|
+
|
|
20
|
+
const conn = await createHttpClient(serverUrl);
|
|
21
|
+
client = conn.client;
|
|
22
|
+
closeClient = conn.close;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await closeClient();
|
|
27
|
+
stopServer();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should connect and call echo tool", async () => {
|
|
31
|
+
const result = await client.callTool({ name: "echo", arguments: { message: "test" } });
|
|
32
|
+
expect((result.content as Array<{ type: string; text: string }>)[0].text).toBe("test");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests via SSE transport - smoke test only.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
6
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { startSseServer, createSseClient } from "./fixtures.js";
|
|
8
|
+
|
|
9
|
+
describe("SSE Transport", () => {
|
|
10
|
+
let serverUrl: string;
|
|
11
|
+
let stopServer: () => void;
|
|
12
|
+
let client: Client;
|
|
13
|
+
let closeClient: () => Promise<void>;
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
const server = await startSseServer(3002);
|
|
17
|
+
serverUrl = server.url;
|
|
18
|
+
stopServer = server.stop;
|
|
19
|
+
|
|
20
|
+
const conn = await createSseClient(serverUrl);
|
|
21
|
+
client = conn.client;
|
|
22
|
+
closeClient = conn.close;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await closeClient();
|
|
27
|
+
stopServer();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should connect and call echo tool", async () => {
|
|
31
|
+
const result = await client.callTool({ name: "echo", arguments: { message: "test" } });
|
|
32
|
+
expect((result.content as Array<{ type: string; text: string }>)[0].text).toBe("test");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests via stdio transport.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
6
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { createStdioClient } from "./fixtures.js";
|
|
8
|
+
|
|
9
|
+
describe("Stdio Transport", () => {
|
|
10
|
+
let client: Client;
|
|
11
|
+
let close: () => Promise<void>;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
const conn = await createStdioClient();
|
|
15
|
+
client = conn.client;
|
|
16
|
+
close = conn.close;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await close();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Tools", () => {
|
|
24
|
+
it("should echo message", async () => {
|
|
25
|
+
const result = await client.callTool({ name: "echo", arguments: { message: "hello" } });
|
|
26
|
+
expect((result.content as Array<{ type: string; text: string }>)[0].text).toBe("hello");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should add numbers", async () => {
|
|
30
|
+
const result = await client.callTool({ name: "add", arguments: { a: 5, b: 3 } });
|
|
31
|
+
expect(Number((result.content as Array<{ type: string; text: string }>)[0].text)).toBe(8);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should multiply numbers", async () => {
|
|
35
|
+
const result = await client.callTool({ name: "multiply", arguments: { x: 4, y: 7 } });
|
|
36
|
+
expect(Number((result.content as Array<{ type: string; text: string }>)[0].text)).toBe(28);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should reverse string", async () => {
|
|
40
|
+
const result = await client.callTool({ name: "reverse_string", arguments: { text: "hello" } });
|
|
41
|
+
expect((result.content as Array<{ type: string; text: string }>)[0].text).toBe("olleh");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should generate valid UUID", async () => {
|
|
45
|
+
const result = await client.callTool({ name: "generate_uuid", arguments: {} });
|
|
46
|
+
const uuid = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
47
|
+
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
48
|
+
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should get timestamp", async () => {
|
|
52
|
+
const result = await client.callTool({ name: "get_timestamp", arguments: { format: "iso" } });
|
|
53
|
+
const timestamp = (result.content as Array<{ type: string; text: string }>)[0].text;
|
|
54
|
+
expect(timestamp).toContain("T");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should return error for sample_error tool", async () => {
|
|
58
|
+
const result = await client.callTool({ name: "sample_error", arguments: {} });
|
|
59
|
+
expect(result.isError).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("Resources", () => {
|
|
64
|
+
it("should list resources", async () => {
|
|
65
|
+
const result = await client.listResources();
|
|
66
|
+
const uris = result.resources.map((r) => r.uri);
|
|
67
|
+
expect(uris).toContain("test://static/greeting");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should read static greeting", async () => {
|
|
71
|
+
const result = await client.readResource({ uri: "test://static/greeting" });
|
|
72
|
+
expect((result.contents[0] as { text: string }).text).toContain("Hello");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should read dynamic timestamp", async () => {
|
|
76
|
+
const result = await client.readResource({ uri: "test://dynamic/timestamp" });
|
|
77
|
+
expect((result.contents[0] as { text: string }).text).toContain("T");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("Prompts", () => {
|
|
82
|
+
it("should list prompts", async () => {
|
|
83
|
+
const result = await client.listPrompts();
|
|
84
|
+
const names = result.prompts.map((p) => p.name);
|
|
85
|
+
expect(names).toContain("simple_prompt");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should get simple prompt", async () => {
|
|
89
|
+
const result = await client.getPrompt({ name: "simple_prompt" });
|
|
90
|
+
expect(result.messages.length).toBeGreaterThanOrEqual(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should get greeting prompt with argument", async () => {
|
|
94
|
+
const result = await client.getPrompt({ name: "greeting_prompt", arguments: { name: "Alice" } });
|
|
95
|
+
expect((result.messages[0].content as { type: string; text: string }).text).toContain("Alice");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|