Converted CLI tests to use vitest (#1012)

* Pinned server-everything, fixed all tests to work with current pinned version, fixed problem with undetected failures (isError: true payloads).

* First working vitest implementation

* Refactoring some single-use configs fixtures and into the refeencing tests

* No tests refere to server-everything (or any other server from a registry), all tests actually validate what they say they test.

* Removed server-everything dep from CI, minor cleanup

* Addressed Claude PR review comments: Added tsx dev dependency, beefed up process termination (possible leak on Windows), beefed up http server cleanup (close all connections), removed unused hasValidJsonOutput, reduced CLI timeout to give it breathing room with vitest timeout.

* Made both stdio and http test servers fully composable, cleaned up types, terminology, and usage.
This commit is contained in:
Bob Dickinson 2026-01-19 07:12:00 -08:00 • committed by GitHub
commit 44d0e580f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 4206 additions and 2490 deletions

View file

@ -0,0 +1,52 @@
import { expect } from "vitest";
import type { CliResult } from "./cli-runner.js";
/**
* Assert that CLI command succeeded (exit code 0)
*/
export function expectCliSuccess(result: CliResult) {
expect(result.exitCode).toBe(0);
}
/**
* Assert that CLI command failed (non-zero exit code)
*/
export function expectCliFailure(result: CliResult) {
expect(result.exitCode).not.toBe(0);
}
/**
* Assert that output contains expected text
*/
export function expectOutputContains(result: CliResult, text: string) {
expect(result.output).toContain(text);
}
/**
* Assert that output contains valid JSON
* Uses stdout (not stderr) since JSON is written to stdout and warnings go to stderr
*/
export function expectValidJson(result: CliResult) {
expect(() => JSON.parse(result.stdout)).not.toThrow();
return JSON.parse(result.stdout);
}
/**
* Assert that output contains JSON with error flag
*/
export function expectJsonError(result: CliResult) {
const json = expectValidJson(result);
expect(json.isError).toBe(true);
return json;
}
/**
* Assert that output contains expected JSON structure
*/
export function expectJsonStructure(result: CliResult, expectedKeys: string[]) {
const json = expectValidJson(result);
expectedKeys.forEach((key) => {
expect(json).toHaveProperty(key);
});
return json;
}

View file

@ -0,0 +1,98 @@
import { spawn } from "child_process";
import { resolve } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CLI_PATH = resolve(__dirname, "../../build/cli.js");
export interface CliResult {
exitCode: number | null;
stdout: string;
stderr: string;
output: string; // Combined stdout + stderr
}
export interface CliOptions {
timeout?: number;
cwd?: string;
env?: Record<string, string>;
signal?: AbortSignal;
}
/**
* Run the CLI with given arguments and capture output
*/
export async function runCli(
args: string[],
options: CliOptions = {},
): Promise<CliResult> {
return new Promise((resolve, reject) => {
const child = spawn("node", [CLI_PATH, ...args], {
stdio: ["pipe", "pipe", "pipe"],
cwd: options.cwd,
env: { ...process.env, ...options.env },
signal: options.signal,
// Kill child process tree on exit
detached: false,
});
let stdout = "";
let stderr = "";
let resolved = false;
// Default timeout of 10 seconds (less than vitest's 15s)
const timeoutMs = options.timeout ?? 10000;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
// Kill the process and all its children
try {
if (process.platform === "win32") {
child.kill("SIGTERM");
} else {
// On Unix, kill the process group
process.kill(-child.pid!, "SIGTERM");
}
} catch (e) {
// Process might already be dead, try direct kill
try {
child.kill("SIGKILL");
} catch (e2) {
// Process is definitely dead
}
}
reject(new Error(`CLI command timed out after ${timeoutMs}ms`));
}
}, timeoutMs);
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
resolve({
exitCode: code,
stdout,
stderr,
output: stdout + stderr,
});
}
});
child.on("error", (error) => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
reject(error);
}
});
});
}

View file

@ -0,0 +1,89 @@
import fs from "fs";
import path from "path";
import os from "os";
import crypto from "crypto";
import { getTestMcpServerCommand } from "./test-server-stdio.js";
/**
* Sentinel value for tests that don't need a real server
* (tests that expect failure before connecting)
*/
export const NO_SERVER_SENTINEL = "invalid-command-that-does-not-exist";
/**
* Create a sample test config with test-stdio and test-http servers
* Returns a temporary config file path that should be cleaned up with deleteConfigFile()
* @param httpUrl - Optional full URL (including /mcp path) for test-http server.
* If not provided, uses a placeholder URL. The test-http server exists
* to test server selection logic and may not actually be used.
*/
export function createSampleTestConfig(httpUrl?: string): string {
const { command, args } = getTestMcpServerCommand();
return createTestConfig({
mcpServers: {
"test-stdio": {
type: "stdio",
command,
args,
env: {
HELLO: "Hello MCP!",
},
},
"test-http": {
type: "streamable-http",
url: httpUrl || "http://localhost:3001/mcp",
},
},
});
}
/**
* Create a temporary directory for test files
* Uses crypto.randomUUID() to ensure uniqueness even when called in parallel
*/
function createTempDir(prefix: string = "mcp-inspector-test-"): string {
const uniqueId = crypto.randomUUID();
const tempDir = path.join(os.tmpdir(), `${prefix}${uniqueId}`);
fs.mkdirSync(tempDir, { recursive: true });
return tempDir;
}
/**
* Clean up temporary directory
*/
function cleanupTempDir(dir: string) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors
}
}
/**
* Create a test config file
*/
export function createTestConfig(config: {
mcpServers: Record<string, any>;
}): string {
const tempDir = createTempDir("mcp-inspector-config-");
const configPath = path.join(tempDir, "config.json");
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
return configPath;
}
/**
* Create an invalid config file (malformed JSON)
*/
export function createInvalidConfig(): string {
const tempDir = createTempDir("mcp-inspector-config-");
const configPath = path.join(tempDir, "invalid-config.json");
fs.writeFileSync(configPath, '{\n "mcpServers": {\n "invalid": {');
return configPath;
}
/**
* Delete a config file and its containing directory
*/
export function deleteConfigFile(configPath: string): void {
cleanupTempDir(path.dirname(configPath));
}

View file

@ -0,0 +1,267 @@
/**
* Shared types and test fixtures for composable MCP test servers
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import * as z from "zod/v4";
import { ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
type ToolInputSchema = ZodRawShapeCompat;
export interface ToolDefinition {
name: string;
description: string;
inputSchema?: ToolInputSchema;
handler: (params: Record<string, any>) => Promise<any>;
}
export interface ResourceDefinition {
uri: string;
name: string;
description?: string;
mimeType?: string;
text?: string;
}
type PromptArgsSchema = ZodRawShapeCompat;
export interface PromptDefinition {
name: string;
description?: string;
argsSchema?: PromptArgsSchema;
}
// This allows us to compose tests servers using the metadata and features we want in a given scenario
export interface ServerConfig {
serverInfo: Implementation; // Server metadata (name, version, etc.) - required
tools?: ToolDefinition[]; // Tools to register (optional, empty array means no tools, but tools capability is still advertised)
resources?: ResourceDefinition[]; // Resources to register (optional, empty array means no resources, but resources capability is still advertised)
prompts?: PromptDefinition[]; // Prompts to register (optional, empty array means no prompts, but prompts capability is still advertised)
logging?: boolean; // Whether to advertise logging capability (default: false)
}
/**
* Create an "echo" tool that echoes back the input message
*/
export function createEchoTool(): ToolDefinition {
return {
name: "echo",
description: "Echo back the input message",
inputSchema: {
message: z.string().describe("Message to echo back"),
},
handler: async (params: Record<string, any>) => {
return { message: `Echo: ${params.message as string}` };
},
};
}
/**
* Create an "add" tool that adds two numbers together
*/
export function createAddTool(): ToolDefinition {
return {
name: "add",
description: "Add two numbers together",
inputSchema: {
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
},
handler: async (params: Record<string, any>) => {
const a = params.a as number;
const b = params.b as number;
return { result: a + b };
},
};
}
/**
* Create a "get-sum" tool that returns the sum of two numbers (alias for add)
*/
export function createGetSumTool(): ToolDefinition {
return {
name: "get-sum",
description: "Get the sum of two numbers",
inputSchema: {
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
},
handler: async (params: Record<string, any>) => {
const a = params.a as number;
const b = params.b as number;
return { result: a + b };
},
};
}
/**
* Create a "get-annotated-message" tool that returns a message with optional image
*/
export function createGetAnnotatedMessageTool(): ToolDefinition {
return {
name: "get-annotated-message",
description: "Get an annotated message",
inputSchema: {
messageType: z
.enum(["success", "error", "warning", "info"])
.describe("Type of message"),
includeImage: z
.boolean()
.optional()
.describe("Whether to include an image"),
},
handler: async (params: Record<string, any>) => {
const messageType = params.messageType as string;
const includeImage = params.includeImage as boolean | undefined;
const message = `This is a ${messageType} message`;
const content: Array<
| { type: "text"; text: string }
| { type: "image"; data: string; mimeType: string }
> = [
{
type: "text",
text: message,
},
];
if (includeImage) {
content.push({
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", // 1x1 transparent PNG
mimeType: "image/png",
});
}
return { content };
},
};
}
/**
* Create a "simple-prompt" prompt definition
*/
export function createSimplePrompt(): PromptDefinition {
return {
name: "simple-prompt",
description: "A simple prompt for testing",
};
}
/**
* Create an "args-prompt" prompt that accepts arguments
*/
export function createArgsPrompt(): PromptDefinition {
return {
name: "args-prompt",
description: "A prompt that accepts arguments for testing",
argsSchema: {
city: z.string().describe("City name"),
state: z.string().describe("State name"),
},
};
}
/**
* Create an "architecture" resource definition
*/
export function createArchitectureResource(): ResourceDefinition {
return {
name: "architecture",
uri: "demo://resource/static/document/architecture.md",
description: "Architecture documentation",
mimeType: "text/markdown",
text: `# Architecture Documentation
This is a test resource for the MCP test server.
## Overview
This resource is used for testing resource reading functionality in the CLI.
## Sections
- Introduction
- Design
- Implementation
- Testing
## Notes
This is a static resource provided by the test MCP server.
`,
};
}
/**
* Create a "test-cwd" resource that exposes the current working directory (generally useful when testing with the stdio test server)
*/
export function createTestCwdResource(): ResourceDefinition {
return {
name: "test-cwd",
uri: "test://cwd",
description: "Current working directory of the test server",
mimeType: "text/plain",
text: process.cwd(),
};
}
/**
* Create a "test-env" resource that exposes environment variables (generally useful when testing with the stdio test server)
*/
export function createTestEnvResource(): ResourceDefinition {
return {
name: "test-env",
uri: "test://env",
description: "Environment variables available to the test server",
mimeType: "application/json",
text: JSON.stringify(process.env, null, 2),
};
}
/**
* Create a "test-argv" resource that exposes command-line arguments (generally useful when testing with the stdio test server)
*/
export function createTestArgvResource(): ResourceDefinition {
return {
name: "test-argv",
uri: "test://argv",
description: "Command-line arguments the test server was started with",
mimeType: "application/json",
text: JSON.stringify(process.argv, null, 2),
};
}
/**
* Create minimal server info for test servers
*/
export function createTestServerInfo(
name: string = "test-server",
version: string = "1.0.0",
): Implementation {
return {
name,
version,
};
}
/**
* Get default server config with common test tools, prompts, and resources
*/
export function getDefaultServerConfig(): ServerConfig {
return {
serverInfo: createTestServerInfo("test-mcp-server", "1.0.0"),
tools: [
createEchoTool(),
createGetSumTool(),
createGetAnnotatedMessageTool(),
],
prompts: [createSimplePrompt(), createArgsPrompt()],
resources: [
createArchitectureResource(),
createTestCwdResource(),
createTestEnvResource(),
createTestArgvResource(),
],
};
}

View file

@ -0,0 +1,443 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { SetLevelRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import type { Request, Response } from "express";
import express from "express";
import { createServer as createHttpServer, Server as HttpServer } from "http";
import { createServer as createNetServer } from "net";
import * as z from "zod/v4";
import type { ServerConfig } from "./test-fixtures.js";
export interface RecordedRequest {
method: string;
params?: any;
headers?: Record<string, string>;
metadata?: Record<string, string>;
response: any;
timestamp: number;
}
/**
* Find an available port starting from the given port
*/
async function findAvailablePort(startPort: number): Promise<number> {
return new Promise((resolve, reject) => {
const server = createNetServer();
server.listen(startPort, () => {
const port = (server.address() as { port: number })?.port;
server.close(() => resolve(port || startPort));
});
server.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE") {
// Try next port
findAvailablePort(startPort + 1)
.then(resolve)
.catch(reject);
} else {
reject(err);
}
});
});
}
/**
* Extract headers from Express request
*/
function extractHeaders(req: Request): Record<string, string> {
const headers: Record<string, string> = {};
for (const [key, value] of Object.entries(req.headers)) {
if (typeof value === "string") {
headers[key] = value;
} else if (Array.isArray(value) && value.length > 0) {
headers[key] = value[value.length - 1];
}
}
return headers;
}
// With this test server, your test can hold an instance and you can get the server's recorded message history at any time.
//
export class TestServerHttp {
private mcpServer: McpServer;
private config: ServerConfig;
private recordedRequests: RecordedRequest[] = [];
private httpServer?: HttpServer;
private transport?: StreamableHTTPServerTransport | SSEServerTransport;
private url?: string;
private currentRequestHeaders?: Record<string, string>;
private currentLogLevel: string | null = null;
constructor(config: ServerConfig) {
this.config = config;
const capabilities: {
tools?: {};
resources?: {};
prompts?: {};
logging?: {};
} = {};
// Only include capabilities for features that are present in config
if (config.tools !== undefined) {
capabilities.tools = {};
}
if (config.resources !== undefined) {
capabilities.resources = {};
}
if (config.prompts !== undefined) {
capabilities.prompts = {};
}
if (config.logging === true) {
capabilities.logging = {};
}
this.mcpServer = new McpServer(config.serverInfo, {
capabilities,
});
this.setupHandlers();
if (config.logging === true) {
this.setupLoggingHandler();
}
}
private setupHandlers() {
// Set up tools
if (this.config.tools && this.config.tools.length > 0) {
for (const tool of this.config.tools) {
this.mcpServer.registerTool(
tool.name,
{
description: tool.description,
inputSchema: tool.inputSchema,
},
async (args) => {
const result = await tool.handler(args as Record<string, any>);
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
},
);
}
}
// Set up resources
if (this.config.resources && this.config.resources.length > 0) {
for (const resource of this.config.resources) {
this.mcpServer.registerResource(
resource.name,
resource.uri,
{
description: resource.description,
mimeType: resource.mimeType,
},
async () => {
return {
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType || "text/plain",
text: resource.text || "",
},
],
};
},
);
}
}
// Set up prompts
if (this.config.prompts && this.config.prompts.length > 0) {
for (const prompt of this.config.prompts) {
this.mcpServer.registerPrompt(
prompt.name,
{
description: prompt.description,
argsSchema: prompt.argsSchema,
},
async (args) => {
// Return a simple prompt response
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Prompt: ${prompt.name}${args ? ` with args: ${JSON.stringify(args)}` : ""}`,
},
},
],
};
},
);
}
}
}
private setupLoggingHandler() {
// Intercept logging/setLevel requests to track the level
this.mcpServer.server.setRequestHandler(
SetLevelRequestSchema,
async (request) => {
this.currentLogLevel = request.params.level;
// Return empty result as per MCP spec
return {};
},
);
}
/**
* Start the server with the specified transport
*/
async start(
transport: "http" | "sse",
requestedPort?: number,
): Promise<number> {
const port = requestedPort
? await findAvailablePort(requestedPort)
: await findAvailablePort(transport === "http" ? 3001 : 3000);
this.url = `http://localhost:${port}`;
if (transport === "http") {
return this.startHttp(port);
} else {
return this.startSse(port);
}
}
private async startHttp(port: number): Promise<number> {
const app = express();
app.use(express.json());
// Create HTTP server
this.httpServer = createHttpServer(app);
// Create StreamableHTTP transport
this.transport = new StreamableHTTPServerTransport({});
// Set up Express route to handle MCP requests
app.post("/mcp", async (req: Request, res: Response) => {
// Capture headers for this request
this.currentRequestHeaders = extractHeaders(req);
try {
await (this.transport as StreamableHTTPServerTransport).handleRequest(
req,
res,
req.body,
);
} catch (error) {
res.status(500).json({
error: error instanceof Error ? error.message : String(error),
});
}
});
// Intercept messages to record them
const originalOnMessage = this.transport.onmessage;
this.transport.onmessage = async (message) => {
const timestamp = Date.now();
const method =
"method" in message && typeof message.method === "string"
? message.method
: "unknown";
const params = "params" in message ? message.params : undefined;
try {
// Extract metadata from params if present
const metadata =
params && typeof params === "object" && "_meta" in params
? ((params as any)._meta as Record<string, string>)
: undefined;
// Let the server handle the message
if (originalOnMessage) {
await originalOnMessage.call(this.transport, message);
}
// Record successful request (response will be sent by transport)
// Note: We can't easily capture the response here, so we'll record
// that the request was processed
this.recordedRequests.push({
method,
params,
headers: { ...this.currentRequestHeaders },
metadata: metadata ? { ...metadata } : undefined,
response: { processed: true },
timestamp,
});
} catch (error) {
// Extract metadata from params if present
const metadata =
params && typeof params === "object" && "_meta" in params
? ((params as any)._meta as Record<string, string>)
: undefined;
// Record error
this.recordedRequests.push({
method,
params,
headers: { ...this.currentRequestHeaders },
metadata: metadata ? { ...metadata } : undefined,
response: {
error: error instanceof Error ? error.message : String(error),
},
timestamp,
});
throw error;
}
};
// Connect transport to server
await this.mcpServer.connect(this.transport);
// Start listening
return new Promise((resolve, reject) => {
this.httpServer!.listen(port, () => {
resolve(port);
});
this.httpServer!.on("error", reject);
});
}
private async startSse(port: number): Promise<number> {
const app = express();
app.use(express.json());
// Create HTTP server
this.httpServer = createHttpServer(app);
// For SSE, we need to set up an Express route that creates the transport per request
// This is a simplified version - SSE transport is created per connection
app.get("/mcp", async (req: Request, res: Response) => {
this.currentRequestHeaders = extractHeaders(req);
const sseTransport = new SSEServerTransport("/mcp", res);
// Intercept messages
const originalOnMessage = sseTransport.onmessage;
sseTransport.onmessage = async (message) => {
const timestamp = Date.now();
const method =
"method" in message && typeof message.method === "string"
? message.method
: "unknown";
const params = "params" in message ? message.params : undefined;
try {
// Extract metadata from params if present
const metadata =
params && typeof params === "object" && "_meta" in params
? ((params as any)._meta as Record<string, string>)
: undefined;
if (originalOnMessage) {
await originalOnMessage.call(sseTransport, message);
}
this.recordedRequests.push({
method,
params,
headers: { ...this.currentRequestHeaders },
metadata: metadata ? { ...metadata } : undefined,
response: { processed: true },
timestamp,
});
} catch (error) {
// Extract metadata from params if present
const metadata =
params && typeof params === "object" && "_meta" in params
? ((params as any)._meta as Record<string, string>)
: undefined;
this.recordedRequests.push({
method,
params,
headers: { ...this.currentRequestHeaders },
metadata: metadata ? { ...metadata } : undefined,
response: {
error: error instanceof Error ? error.message : String(error),
},
timestamp,
});
throw error;
}
};
await this.mcpServer.connect(sseTransport);
await sseTransport.start();
});
// Note: SSE transport is created per request, so we don't store a single instance
this.transport = undefined;
// Start listening
return new Promise((resolve, reject) => {
this.httpServer!.listen(port, () => {
resolve(port);
});
this.httpServer!.on("error", reject);
});
}
/**
* Stop the server
*/
async stop(): Promise<void> {
await this.mcpServer.close();
if (this.transport) {
await this.transport.close();
this.transport = undefined;
}
if (this.httpServer) {
return new Promise((resolve) => {
// Force close all connections
this.httpServer!.closeAllConnections?.();
this.httpServer!.close(() => {
this.httpServer = undefined;
resolve();
});
});
}
}
/**
* Get all recorded requests
*/
getRecordedRequests(): RecordedRequest[] {
return [...this.recordedRequests];
}
/**
* Clear recorded requests
*/
clearRecordings(): void {
this.recordedRequests = [];
}
/**
* Get the server URL
*/
getUrl(): string {
if (!this.url) {
throw new Error("Server not started");
}
return this.url;
}
/**
* Get the most recent log level that was set
*/
getCurrentLogLevel(): string | null {
return this.currentLogLevel;
}
}
/**
* Create an HTTP/SSE MCP test server
*/
export function createTestServerHttp(config: ServerConfig): TestServerHttp {
return new TestServerHttp(config);
}

View file

@ -0,0 +1,241 @@
#!/usr/bin/env node
/**
* Test MCP server for stdio transport testing
* Can be used programmatically or run as a standalone executable
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod/v4";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import type {
ServerConfig,
ToolDefinition,
PromptDefinition,
ResourceDefinition,
} from "./test-fixtures.js";
import { getDefaultServerConfig } from "./test-fixtures.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
export class TestServerStdio {
private mcpServer: McpServer;
private config: ServerConfig;
private transport?: StdioServerTransport;
constructor(config: ServerConfig) {
this.config = config;
const capabilities: {
tools?: {};
resources?: {};
prompts?: {};
logging?: {};
} = {};
// Only include capabilities for features that are present in config
if (config.tools !== undefined) {
capabilities.tools = {};
}
if (config.resources !== undefined) {
capabilities.resources = {};
}
if (config.prompts !== undefined) {
capabilities.prompts = {};
}
if (config.logging === true) {
capabilities.logging = {};
}
this.mcpServer = new McpServer(config.serverInfo, {
capabilities,
});
this.setupHandlers();
}
private setupHandlers() {
// Set up tools
if (this.config.tools && this.config.tools.length > 0) {
for (const tool of this.config.tools) {
this.mcpServer.registerTool(
tool.name,
{
description: tool.description,
inputSchema: tool.inputSchema,
},
async (args) => {
const result = await tool.handler(args as Record<string, any>);
// If handler returns content array directly (like get-annotated-message), use it
if (result && Array.isArray(result.content)) {
return { content: result.content };
}
// If handler returns message (like echo), format it
if (result && typeof result.message === "string") {
return {
content: [
{
type: "text",
text: result.message,
},
],
};
}
// Otherwise, stringify the result
return {
content: [
{
type: "text",
text: JSON.stringify(result),
},
],
};
},
);
}
}
// Set up resources
if (this.config.resources && this.config.resources.length > 0) {
for (const resource of this.config.resources) {
this.mcpServer.registerResource(
resource.name,
resource.uri,
{
description: resource.description,
mimeType: resource.mimeType,
},
async () => {
// For dynamic resources, get fresh text
let text = resource.text;
if (resource.name === "test-cwd") {
text = process.cwd();
} else if (resource.name === "test-env") {
text = JSON.stringify(process.env, null, 2);
} else if (resource.name === "test-argv") {
text = JSON.stringify(process.argv, null, 2);
}
return {
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType || "text/plain",
text: text || "",
},
],
};
},
);
}
}
// Set up prompts
if (this.config.prompts && this.config.prompts.length > 0) {
for (const prompt of this.config.prompts) {
this.mcpServer.registerPrompt(
prompt.name,
{
description: prompt.description,
argsSchema: prompt.argsSchema,
},
async (args) => {
if (prompt.name === "args-prompt" && args) {
const city = (args as any).city as string;
const state = (args as any).state as string;
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `This is a prompt with arguments: city=${city}, state=${state}`,
},
},
],
};
} else {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "This is a simple prompt for testing purposes.",
},
},
],
};
}
},
);
}
}
}
/**
* Start the server with stdio transport
*/
async start(): Promise<void> {
this.transport = new StdioServerTransport();
await this.mcpServer.connect(this.transport);
}
/**
* Stop the server
*/
async stop(): Promise<void> {
await this.mcpServer.close();
if (this.transport) {
await this.transport.close();
this.transport = undefined;
}
}
}
/**
* Create a stdio MCP test server
*/
export function createTestServerStdio(config: ServerConfig): TestServerStdio {
return new TestServerStdio(config);
}
/**
* Get the path to the test MCP server script
*/
export function getTestMcpServerPath(): string {
return path.resolve(__dirname, "test-server-stdio.ts");
}
/**
* Get the command and args to run the test MCP server
*/
export function getTestMcpServerCommand(): { command: string; args: string[] } {
return {
command: "tsx",
args: [getTestMcpServerPath()],
};
}
// If run as a standalone script, start with default config
// Check if this file is being executed directly (not imported)
const isMainModule =
import.meta.url.endsWith(process.argv[1]) ||
process.argv[1]?.endsWith("test-server-stdio.ts") ||
process.argv[1]?.endsWith("test-server-stdio.js");
if (isMainModule) {
const server = new TestServerStdio(getDefaultServerConfig());
server
.start()
.then(() => {
// Server is now running and listening on stdio
// Keep the process alive
})
.catch((error) => {
console.error("Failed to start test MCP server:", error);
process.exit(1);
});
}