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:
parent
94ba4c3b56
commit
44d0e580f6
19 changed files with 4206 additions and 2490 deletions
3
.github/workflows/cli_tests.yml
vendored
3
.github/workflows/cli_tests.yml
vendored
|
|
@ -31,9 +31,6 @@ jobs:
|
|||
- name: Build CLI
|
||||
run: npm run build
|
||||
|
||||
- name: Explicitly pre-install test dependencies
|
||||
run: npx -y @modelcontextprotocol/server-everything --help || true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
env:
|
||||
|
|
|
|||
44
cli/__tests__/README.md
Normal file
44
cli/__tests__/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# CLI Tests
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run in watch mode (useful for test file changes; won't work on CLI source changes without rebuild)
|
||||
npm run test:watch
|
||||
|
||||
# Run specific test file
|
||||
npm run test:cli # cli.test.ts
|
||||
npm run test:cli-tools # tools.test.ts
|
||||
npm run test:cli-headers # headers.test.ts
|
||||
npm run test:cli-metadata # metadata.test.ts
|
||||
```
|
||||
|
||||
## Test Files
|
||||
|
||||
- `cli.test.ts` - Basic CLI functionality: CLI mode, environment variables, config files, resources, prompts, logging, transport types
|
||||
- `tools.test.ts` - Tool-related tests: Tool discovery, JSON argument parsing, error handling, prompts
|
||||
- `headers.test.ts` - Header parsing and validation
|
||||
- `metadata.test.ts` - Metadata functionality: General metadata, tool-specific metadata, parsing, merging, validation
|
||||
|
||||
## Helpers
|
||||
|
||||
The `helpers/` directory contains shared utilities:
|
||||
|
||||
- `cli-runner.ts` - Spawns CLI as subprocess and captures output
|
||||
- `test-mcp-server.ts` - Standalone stdio MCP server script for stdio transport testing
|
||||
- `instrumented-server.ts` - In-process MCP test server for HTTP/SSE transports with request recording
|
||||
- `assertions.ts` - Custom assertion helpers for CLI output validation
|
||||
- `fixtures.ts` - Test config file generators and temporary directory management
|
||||
|
||||
## Notes
|
||||
|
||||
- Tests run in parallel across files (Vitest default)
|
||||
- Tests within a file run sequentially (we have isolated config files and ports, so we could get more aggressive if desired)
|
||||
- Config files use `crypto.randomUUID()` for uniqueness in parallel execution
|
||||
- HTTP/SSE servers use dynamic port allocation to avoid conflicts
|
||||
- Coverage is not used because much of the code that we want to measure is run by a spawned process, so it can't be tracked by Vitest
|
||||
- /sample-config.json is no longer used by tests - not clear if this file serves some other purpose so leaving it for now
|
||||
- All tests now use built-in MCP test servers, there are no external dependencies on servers from a registry
|
||||
871
cli/__tests__/cli.test.ts
Normal file
871
cli/__tests__/cli.test.ts
Normal file
|
|
@ -0,0 +1,871 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from "vitest";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliSuccess,
|
||||
expectCliFailure,
|
||||
expectValidJson,
|
||||
} from "./helpers/assertions.js";
|
||||
import {
|
||||
NO_SERVER_SENTINEL,
|
||||
createSampleTestConfig,
|
||||
createTestConfig,
|
||||
createInvalidConfig,
|
||||
deleteConfigFile,
|
||||
} from "./helpers/fixtures.js";
|
||||
import { getTestMcpServerCommand } from "./helpers/test-server-stdio.js";
|
||||
import { createTestServerHttp } from "./helpers/test-server-http.js";
|
||||
import {
|
||||
createEchoTool,
|
||||
createTestServerInfo,
|
||||
} from "./helpers/test-fixtures.js";
|
||||
|
||||
describe("CLI Tests", () => {
|
||||
describe("Basic CLI Mode", () => {
|
||||
it("should execute tools/list successfully", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
|
||||
// Validate expected tools from test-mcp-server
|
||||
const toolNames = json.tools.map((tool: any) => tool.name);
|
||||
expect(toolNames).toContain("echo");
|
||||
expect(toolNames).toContain("get-sum");
|
||||
expect(toolNames).toContain("get-annotated-message");
|
||||
});
|
||||
|
||||
it("should fail with nonexistent method", async () => {
|
||||
const result = await runCli([
|
||||
NO_SERVER_SENTINEL,
|
||||
"--cli",
|
||||
"--method",
|
||||
"nonexistent/method",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should fail without method", async () => {
|
||||
const result = await runCli([NO_SERVER_SENTINEL, "--cli"]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Environment Variables", () => {
|
||||
it("should accept environment variables", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"-e",
|
||||
"KEY1=value1",
|
||||
"-e",
|
||||
"KEY2=value2",
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://env",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("contents");
|
||||
expect(Array.isArray(json.contents)).toBe(true);
|
||||
expect(json.contents.length).toBeGreaterThan(0);
|
||||
|
||||
// Parse the env vars from the resource
|
||||
const envVars = JSON.parse(json.contents[0].text);
|
||||
expect(envVars.KEY1).toBe("value1");
|
||||
expect(envVars.KEY2).toBe("value2");
|
||||
});
|
||||
|
||||
it("should reject invalid environment variable format", async () => {
|
||||
const result = await runCli([
|
||||
NO_SERVER_SENTINEL,
|
||||
"-e",
|
||||
"INVALID_FORMAT",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should handle environment variable with equals sign in value", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"-e",
|
||||
"API_KEY=abc123=xyz789==",
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://env",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
const envVars = JSON.parse(json.contents[0].text);
|
||||
expect(envVars.API_KEY).toBe("abc123=xyz789==");
|
||||
});
|
||||
|
||||
it("should handle environment variable with base64-encoded value", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"-e",
|
||||
"JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://env",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
const envVars = JSON.parse(json.contents[0].text);
|
||||
expect(envVars.JWT_TOKEN).toBe(
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Config File", () => {
|
||||
it("should use config file with CLI mode", async () => {
|
||||
const configPath = createSampleTestConfig();
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
expect(json.tools.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail when using config file without server name", async () => {
|
||||
const configPath = createSampleTestConfig();
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail when using server name without config file", async () => {
|
||||
const result = await runCli([
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should fail with nonexistent config file", async () => {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
"./nonexistent-config.json",
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should fail with invalid config file format", async () => {
|
||||
// Create invalid config temporarily
|
||||
const invalidConfigPath = createInvalidConfig();
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
invalidConfigPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(invalidConfigPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with nonexistent server in config", async () => {
|
||||
const configPath = createSampleTestConfig();
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"nonexistent",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resource Options", () => {
|
||||
it("should read resource with URI", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"demo://resource/static/document/architecture.md",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("contents");
|
||||
expect(Array.isArray(json.contents)).toBe(true);
|
||||
expect(json.contents.length).toBeGreaterThan(0);
|
||||
expect(json.contents[0]).toHaveProperty(
|
||||
"uri",
|
||||
"demo://resource/static/document/architecture.md",
|
||||
);
|
||||
expect(json.contents[0]).toHaveProperty("mimeType", "text/markdown");
|
||||
expect(json.contents[0]).toHaveProperty("text");
|
||||
expect(json.contents[0].text).toContain("Architecture Documentation");
|
||||
});
|
||||
|
||||
it("should fail when reading resource without URI", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Prompt Options", () => {
|
||||
it("should get prompt by name", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"simple-prompt",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("messages");
|
||||
expect(Array.isArray(json.messages)).toBe(true);
|
||||
expect(json.messages.length).toBeGreaterThan(0);
|
||||
expect(json.messages[0]).toHaveProperty("role", "user");
|
||||
expect(json.messages[0]).toHaveProperty("content");
|
||||
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||
expect(json.messages[0].content.text).toBe(
|
||||
"This is a simple prompt for testing purposes.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should get prompt with arguments", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"args-prompt",
|
||||
"--prompt-args",
|
||||
"city=New York",
|
||||
"state=NY",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("messages");
|
||||
expect(Array.isArray(json.messages)).toBe(true);
|
||||
expect(json.messages.length).toBeGreaterThan(0);
|
||||
expect(json.messages[0]).toHaveProperty("role", "user");
|
||||
expect(json.messages[0]).toHaveProperty("content");
|
||||
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||
// Verify that the arguments were actually used in the response
|
||||
expect(json.messages[0].content.text).toContain("city=New York");
|
||||
expect(json.messages[0].content.text).toContain("state=NY");
|
||||
});
|
||||
|
||||
it("should fail when getting prompt without name", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Logging Options", () => {
|
||||
it("should set log level", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
logging: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const port = await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"logging/setLevel",
|
||||
"--log-level",
|
||||
"debug",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
// Validate the response - logging/setLevel should return an empty result
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toEqual({});
|
||||
|
||||
// Validate that the server actually received and recorded the log level
|
||||
expect(server.getCurrentLogLevel()).toBe("debug");
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should reject invalid log level", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"logging/setLevel",
|
||||
"--log-level",
|
||||
"invalid",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Combined Options", () => {
|
||||
it("should handle config file with environment variables", async () => {
|
||||
const configPath = createSampleTestConfig();
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"-e",
|
||||
"CLI_ENV_VAR=cli_value",
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://env",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("contents");
|
||||
expect(Array.isArray(json.contents)).toBe(true);
|
||||
expect(json.contents.length).toBeGreaterThan(0);
|
||||
|
||||
// Parse the env vars from the resource
|
||||
const envVars = JSON.parse(json.contents[0].text);
|
||||
expect(envVars).toHaveProperty("CLI_ENV_VAR");
|
||||
expect(envVars.CLI_ENV_VAR).toBe("cli_value");
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle all options together", async () => {
|
||||
const configPath = createSampleTestConfig();
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"-e",
|
||||
"CLI_ENV_VAR=cli_value",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=Hello",
|
||||
"--log-level",
|
||||
"debug",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content.length).toBeGreaterThan(0);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
expect(json.content[0].text).toBe("Echo: Hello");
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Config Transport Types", () => {
|
||||
it("should work with stdio transport type", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
"test-stdio": {
|
||||
type: "stdio",
|
||||
command,
|
||||
args,
|
||||
env: {
|
||||
TEST_ENV: "test-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
// First validate tools/list works
|
||||
const toolsResult = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(toolsResult);
|
||||
const toolsJson = expectValidJson(toolsResult);
|
||||
expect(toolsJson).toHaveProperty("tools");
|
||||
expect(Array.isArray(toolsJson.tools)).toBe(true);
|
||||
expect(toolsJson.tools.length).toBeGreaterThan(0);
|
||||
|
||||
// Then validate env vars from config are passed to server
|
||||
const envResult = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://env",
|
||||
]);
|
||||
|
||||
expectCliSuccess(envResult);
|
||||
const envJson = expectValidJson(envResult);
|
||||
const envVars = JSON.parse(envJson.contents[0].text);
|
||||
expect(envVars).toHaveProperty("TEST_ENV");
|
||||
expect(envVars.TEST_ENV).toBe("test-value");
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with SSE transport type in CLI mode (connection error)", async () => {
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
"test-sse": {
|
||||
type: "sse",
|
||||
url: "http://localhost:3000/sse",
|
||||
note: "Test SSE server",
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with HTTP transport type in CLI mode (connection error)", async () => {
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
"test-http": {
|
||||
type: "streamable-http",
|
||||
url: "http://localhost:3001/mcp",
|
||||
note: "Test HTTP server",
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with legacy config without type field", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
"test-legacy": {
|
||||
command,
|
||||
args,
|
||||
env: {
|
||||
LEGACY_ENV: "legacy-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
// First validate tools/list works
|
||||
const toolsResult = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-legacy",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(toolsResult);
|
||||
const toolsJson = expectValidJson(toolsResult);
|
||||
expect(toolsJson).toHaveProperty("tools");
|
||||
expect(Array.isArray(toolsJson.tools)).toBe(true);
|
||||
expect(toolsJson.tools.length).toBeGreaterThan(0);
|
||||
|
||||
// Then validate env vars from config are passed to server
|
||||
const envResult = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--server",
|
||||
"test-legacy",
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://env",
|
||||
]);
|
||||
|
||||
expectCliSuccess(envResult);
|
||||
const envJson = expectValidJson(envResult);
|
||||
const envVars = JSON.parse(envJson.contents[0].text);
|
||||
expect(envVars).toHaveProperty("LEGACY_ENV");
|
||||
expect(envVars.LEGACY_ENV).toBe("legacy-value");
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Default Server Selection", () => {
|
||||
it("should auto-select single server", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
"only-server": {
|
||||
command,
|
||||
args,
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
expect(json.tools.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should require explicit server selection even with default-server key (multiple servers)", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
"default-server": {
|
||||
command,
|
||||
args,
|
||||
},
|
||||
"other-server": {
|
||||
command: "node",
|
||||
args: ["other.js"],
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("should require explicit server selection with multiple servers", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const configPath = createTestConfig({
|
||||
mcpServers: {
|
||||
server1: {
|
||||
command,
|
||||
args,
|
||||
},
|
||||
server2: {
|
||||
command: "node",
|
||||
args: ["other.js"],
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await runCli([
|
||||
"--config",
|
||||
configPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
deleteConfigFile(configPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP Transport", () => {
|
||||
it("should infer HTTP transport from URL ending with /mcp", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
expect(json.tools.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with explicit --transport http flag", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
expect(json.tools.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with explicit transport flag and URL suffix", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
expect(json.tools.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail when SSE transport is given to HTTP server", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--transport",
|
||||
"sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail when HTTP transport is specified without URL", async () => {
|
||||
const result = await runCli([
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should fail when SSE transport is specified without URL", async () => {
|
||||
const result = await runCli([
|
||||
"--transport",
|
||||
"sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
210
cli/__tests__/headers.test.ts
Normal file
210
cli/__tests__/headers.test.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliFailure,
|
||||
expectOutputContains,
|
||||
expectCliSuccess,
|
||||
} from "./helpers/assertions.js";
|
||||
import { createTestServerHttp } from "./helpers/test-server-http.js";
|
||||
import {
|
||||
createEchoTool,
|
||||
createTestServerInfo,
|
||||
} from "./helpers/test-fixtures.js";
|
||||
|
||||
describe("Header Parsing and Validation", () => {
|
||||
describe("Valid Headers", () => {
|
||||
it("should parse valid single header and send it to server", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
const port = await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Authorization: Bearer token123",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Check that the server received the request with the correct headers
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
expect(recordedRequests.length).toBeGreaterThan(0);
|
||||
|
||||
// Find the tools/list request (should be the last one)
|
||||
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest.method).toBe("tools/list");
|
||||
|
||||
// Express normalizes headers to lowercase
|
||||
expect(toolsListRequest.headers).toHaveProperty("authorization");
|
||||
expect(toolsListRequest.headers?.authorization).toBe("Bearer token123");
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should parse multiple headers", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
const port = await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Authorization: Bearer token123",
|
||||
"--header",
|
||||
"X-API-Key: secret123",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||
expect(toolsListRequest.method).toBe("tools/list");
|
||||
expect(toolsListRequest.headers?.authorization).toBe("Bearer token123");
|
||||
expect(toolsListRequest.headers?.["x-api-key"]).toBe("secret123");
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle header with colons in value", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
const port = await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"X-Time: 2023:12:25:10:30:45",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||
expect(toolsListRequest.method).toBe("tools/list");
|
||||
expect(toolsListRequest.headers?.["x-time"]).toBe(
|
||||
"2023:12:25:10:30:45",
|
||||
);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle whitespace in headers", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
const port = await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
" X-Header : value with spaces ",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests[recordedRequests.length - 1];
|
||||
expect(toolsListRequest.method).toBe("tools/list");
|
||||
// Header values should be trimmed by the CLI parser
|
||||
expect(toolsListRequest.headers?.["x-header"]).toBe(
|
||||
"value with spaces",
|
||||
);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Invalid Header Formats", () => {
|
||||
it("should reject header format without colon", async () => {
|
||||
const result = await runCli([
|
||||
"https://example.com",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"InvalidHeader",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
expectOutputContains(result, "Invalid header format");
|
||||
});
|
||||
|
||||
it("should reject header format with empty name", async () => {
|
||||
const result = await runCli([
|
||||
"https://example.com",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
": value",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
expectOutputContains(result, "Invalid header format");
|
||||
});
|
||||
|
||||
it("should reject header format with empty value", async () => {
|
||||
const result = await runCli([
|
||||
"https://example.com",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Header:",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
expectOutputContains(result, "Invalid header format");
|
||||
});
|
||||
});
|
||||
});
|
||||
52
cli/__tests__/helpers/assertions.ts
Normal file
52
cli/__tests__/helpers/assertions.ts
Normal 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;
|
||||
}
|
||||
98
cli/__tests__/helpers/cli-runner.ts
Normal file
98
cli/__tests__/helpers/cli-runner.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
89
cli/__tests__/helpers/fixtures.ts
Normal file
89
cli/__tests__/helpers/fixtures.ts
Normal 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));
|
||||
}
|
||||
267
cli/__tests__/helpers/test-fixtures.ts
Normal file
267
cli/__tests__/helpers/test-fixtures.ts
Normal 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(),
|
||||
],
|
||||
};
|
||||
}
|
||||
443
cli/__tests__/helpers/test-server-http.ts
Normal file
443
cli/__tests__/helpers/test-server-http.ts
Normal 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);
|
||||
}
|
||||
241
cli/__tests__/helpers/test-server-stdio.ts
Normal file
241
cli/__tests__/helpers/test-server-stdio.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
933
cli/__tests__/metadata.test.ts
Normal file
933
cli/__tests__/metadata.test.ts
Normal file
|
|
@ -0,0 +1,933 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliSuccess,
|
||||
expectCliFailure,
|
||||
expectValidJson,
|
||||
} from "./helpers/assertions.js";
|
||||
import { createTestServerHttp } from "./helpers/test-server-http.js";
|
||||
import {
|
||||
createEchoTool,
|
||||
createAddTool,
|
||||
createTestServerInfo,
|
||||
} from "./helpers/test-fixtures.js";
|
||||
import { NO_SERVER_SENTINEL } from "./helpers/fixtures.js";
|
||||
|
||||
describe("Metadata Tests", () => {
|
||||
describe("General Metadata", () => {
|
||||
it("should work with tools/list", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({ client: "test-client" });
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with resources/list", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
resources: [
|
||||
{
|
||||
uri: "test://resource",
|
||||
name: "test-resource",
|
||||
text: "test content",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("resources");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const resourcesListRequest = recordedRequests.find(
|
||||
(r) => r.method === "resources/list",
|
||||
);
|
||||
expect(resourcesListRequest).toBeDefined();
|
||||
expect(resourcesListRequest?.metadata).toEqual({
|
||||
client: "test-client",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with prompts/list", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
prompts: [
|
||||
{
|
||||
name: "test-prompt",
|
||||
description: "A test prompt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("prompts");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const promptsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "prompts/list",
|
||||
);
|
||||
expect(promptsListRequest).toBeDefined();
|
||||
expect(promptsListRequest?.metadata).toEqual({
|
||||
client: "test-client",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with resources/read", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
resources: [
|
||||
{
|
||||
uri: "test://resource",
|
||||
name: "test-resource",
|
||||
text: "test content",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://resource",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("contents");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const readRequest = recordedRequests.find(
|
||||
(r) => r.method === "resources/read",
|
||||
);
|
||||
expect(readRequest).toBeDefined();
|
||||
expect(readRequest?.metadata).toEqual({ client: "test-client" });
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with prompts/get", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
prompts: [
|
||||
{
|
||||
name: "test-prompt",
|
||||
description: "A test prompt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"test-prompt",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("messages");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const getPromptRequest = recordedRequests.find(
|
||||
(r) => r.method === "prompts/get",
|
||||
);
|
||||
expect(getPromptRequest).toBeDefined();
|
||||
expect(getPromptRequest?.metadata).toEqual({ client: "test-client" });
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tool-Specific Metadata", () => {
|
||||
it("should work with tools/call", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello world",
|
||||
"--tool-metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({ client: "test-client" });
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with complex tool", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createAddTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"add",
|
||||
"--tool-arg",
|
||||
"a=10",
|
||||
"b=20",
|
||||
"--tool-metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({ client: "test-client" });
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Merging", () => {
|
||||
it("should merge general and tool-specific metadata (tool-specific overrides)", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello world",
|
||||
"--metadata",
|
||||
"client=general-client",
|
||||
"shared_key=shared_value",
|
||||
"--tool-metadata",
|
||||
"client=tool-specific-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate metadata was merged correctly (tool-specific overrides general)
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({
|
||||
client: "tool-specific-client", // Tool-specific overrides general
|
||||
shared_key: "shared_value", // General metadata is preserved
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Parsing", () => {
|
||||
it("should handle numeric values", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"integer_value=42",
|
||||
"decimal_value=3.14159",
|
||||
"negative_value=-10",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate metadata values are sent as strings
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({
|
||||
integer_value: "42",
|
||||
decimal_value: "3.14159",
|
||||
negative_value: "-10",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle JSON values", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
'json_object="{\\"key\\":\\"value\\"}"',
|
||||
'json_array="[1,2,3]"',
|
||||
'json_string="\\"quoted\\""',
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate JSON values are sent as strings
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({
|
||||
json_object: '{"key":"value"}',
|
||||
json_array: "[1,2,3]",
|
||||
json_string: '"quoted"',
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle special characters", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"unicode=🚀🎉✨",
|
||||
"special_chars=!@#$%^&*()",
|
||||
"spaces=hello world with spaces",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate special characters are preserved
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({
|
||||
unicode: "🚀🎉✨",
|
||||
special_chars: "!@#$%^&*()",
|
||||
spaces: "hello world with spaces",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Edge Cases", () => {
|
||||
it("should handle single metadata entry", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"single_key=single_value",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate single metadata entry
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({
|
||||
single_key: "single_value",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle many metadata entries", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
"key3=value3",
|
||||
"key4=value4",
|
||||
"key5=value5",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate all metadata entries
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({
|
||||
key1: "value1",
|
||||
key2: "value2",
|
||||
key3: "value3",
|
||||
key4: "value4",
|
||||
key5: "value5",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Error Cases", () => {
|
||||
it("should fail with invalid metadata format (missing equals)", async () => {
|
||||
const result = await runCli([
|
||||
NO_SERVER_SENTINEL,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"invalid_format_no_equals",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should fail with invalid tool-metadata format (missing equals)", async () => {
|
||||
const result = await runCli([
|
||||
NO_SERVER_SENTINEL,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=test",
|
||||
"--tool-metadata",
|
||||
"invalid_format_no_equals",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Impact", () => {
|
||||
it("should handle tool-specific metadata precedence over general", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=precedence test",
|
||||
"--metadata",
|
||||
"client=general-client",
|
||||
"--tool-metadata",
|
||||
"client=tool-specific-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate tool-specific metadata overrides general
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({
|
||||
client: "tool-specific-client",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with resources methods", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
resources: [
|
||||
{
|
||||
uri: "test://resource",
|
||||
name: "test-resource",
|
||||
text: "test content",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/list",
|
||||
"--metadata",
|
||||
"resource_client=test-resource-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const resourcesListRequest = recordedRequests.find(
|
||||
(r) => r.method === "resources/list",
|
||||
);
|
||||
expect(resourcesListRequest).toBeDefined();
|
||||
expect(resourcesListRequest?.metadata).toEqual({
|
||||
resource_client: "test-resource-client",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should work with prompts methods", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
prompts: [
|
||||
{
|
||||
name: "test-prompt",
|
||||
description: "A test prompt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"test-prompt",
|
||||
"--metadata",
|
||||
"prompt_client=test-prompt-client",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const getPromptRequest = recordedRequests.find(
|
||||
(r) => r.method === "prompts/get",
|
||||
);
|
||||
expect(getPromptRequest).toBeDefined();
|
||||
expect(getPromptRequest?.metadata).toEqual({
|
||||
prompt_client: "test-prompt-client",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Validation", () => {
|
||||
it("should handle special characters in keys", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=special keys test",
|
||||
"--metadata",
|
||||
"key-with-dashes=value1",
|
||||
"key_with_underscores=value2",
|
||||
"key.with.dots=value3",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate special characters in keys are preserved
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({
|
||||
"key-with-dashes": "value1",
|
||||
key_with_underscores: "value2",
|
||||
"key.with.dots": "value3",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata Integration", () => {
|
||||
it("should work with all MCP methods", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"integration_test=true",
|
||||
"test_phase=all_methods",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate metadata was sent
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolsListRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/list",
|
||||
);
|
||||
expect(toolsListRequest).toBeDefined();
|
||||
expect(toolsListRequest?.metadata).toEqual({
|
||||
integration_test: "true",
|
||||
test_phase: "all_methods",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle complex metadata scenario", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=complex test",
|
||||
"--metadata",
|
||||
"session_id=12345",
|
||||
"user_id=67890",
|
||||
"timestamp=2024-01-01T00:00:00Z",
|
||||
"request_id=req-abc-123",
|
||||
"--tool-metadata",
|
||||
"tool_session=session-xyz-789",
|
||||
"execution_context=test",
|
||||
"priority=high",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate complex metadata merging
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({
|
||||
session_id: "12345",
|
||||
user_id: "67890",
|
||||
timestamp: "2024-01-01T00:00:00Z",
|
||||
request_id: "req-abc-123",
|
||||
tool_session: "session-xyz-789",
|
||||
execution_context: "test",
|
||||
priority: "high",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle metadata parsing validation", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start("http");
|
||||
const serverUrl = `${server.getUrl()}/mcp`;
|
||||
|
||||
const result = await runCli([
|
||||
serverUrl,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=parsing validation test",
|
||||
"--metadata",
|
||||
"valid_key=valid_value",
|
||||
"numeric_key=123",
|
||||
"boolean_key=true",
|
||||
'json_key=\'{"test":"value"}\'',
|
||||
"special_key=!@#$%^&*()",
|
||||
"unicode_key=🚀🎉✨",
|
||||
"--transport",
|
||||
"http",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
|
||||
// Validate all value types are sent as strings
|
||||
// Note: The CLI parses metadata values, so single-quoted JSON strings
|
||||
// are preserved with their quotes
|
||||
const recordedRequests = server.getRecordedRequests();
|
||||
const toolCallRequest = recordedRequests.find(
|
||||
(r) => r.method === "tools/call",
|
||||
);
|
||||
expect(toolCallRequest).toBeDefined();
|
||||
expect(toolCallRequest?.metadata).toEqual({
|
||||
valid_key: "valid_value",
|
||||
numeric_key: "123",
|
||||
boolean_key: "true",
|
||||
json_key: '\'{"test":"value"}\'', // Single quotes are preserved
|
||||
special_key: "!@#$%^&*()",
|
||||
unicode_key: "🚀🎉✨",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
523
cli/__tests__/tools.test.ts
Normal file
523
cli/__tests__/tools.test.ts
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliSuccess,
|
||||
expectCliFailure,
|
||||
expectValidJson,
|
||||
expectJsonError,
|
||||
} from "./helpers/assertions.js";
|
||||
import { getTestMcpServerCommand } from "./helpers/test-server-stdio.js";
|
||||
|
||||
describe("Tool Tests", () => {
|
||||
describe("Tool Discovery", () => {
|
||||
it("should list available tools", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("tools");
|
||||
expect(Array.isArray(json.tools)).toBe(true);
|
||||
expect(json.tools.length).toBeGreaterThan(0);
|
||||
// Validate that tools have required properties
|
||||
expect(json.tools[0]).toHaveProperty("name");
|
||||
expect(json.tools[0]).toHaveProperty("description");
|
||||
// Validate expected tools from test-mcp-server
|
||||
const toolNames = json.tools.map((tool: any) => tool.name);
|
||||
expect(toolNames).toContain("echo");
|
||||
expect(toolNames).toContain("get-sum");
|
||||
expect(toolNames).toContain("get-annotated-message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON Argument Parsing", () => {
|
||||
it("should handle string arguments (backward compatibility)", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello world",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content.length).toBeGreaterThan(0);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
expect(json.content[0].text).toBe("Echo: hello world");
|
||||
});
|
||||
|
||||
it("should handle integer number arguments", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"get-sum",
|
||||
"--tool-arg",
|
||||
"a=42",
|
||||
"b=58",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content.length).toBeGreaterThan(0);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// test-mcp-server returns JSON with {result: a+b}
|
||||
const resultData = JSON.parse(json.content[0].text);
|
||||
expect(resultData.result).toBe(100);
|
||||
});
|
||||
|
||||
it("should handle decimal number arguments", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"get-sum",
|
||||
"--tool-arg",
|
||||
"a=19.99",
|
||||
"b=20.01",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content.length).toBeGreaterThan(0);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// test-mcp-server returns JSON with {result: a+b}
|
||||
const resultData = JSON.parse(json.content[0].text);
|
||||
expect(resultData.result).toBeCloseTo(40.0, 2);
|
||||
});
|
||||
|
||||
it("should handle boolean arguments - true", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"get-annotated-message",
|
||||
"--tool-arg",
|
||||
"messageType=success",
|
||||
"includeImage=true",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
// Should have both text and image content
|
||||
expect(json.content.length).toBeGreaterThan(1);
|
||||
const hasImage = json.content.some((item: any) => item.type === "image");
|
||||
expect(hasImage).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle boolean arguments - false", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"get-annotated-message",
|
||||
"--tool-arg",
|
||||
"messageType=error",
|
||||
"includeImage=false",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
// Should only have text content, no image
|
||||
const hasImage = json.content.some((item: any) => item.type === "image");
|
||||
expect(hasImage).toBe(false);
|
||||
// test-mcp-server returns "This is a {messageType} message"
|
||||
expect(json.content[0].text.toLowerCase()).toContain("error");
|
||||
});
|
||||
|
||||
it("should handle null arguments", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message="null"',
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// The string "null" should be passed through
|
||||
expect(json.content[0].text).toBe("Echo: null");
|
||||
});
|
||||
|
||||
it("should handle multiple arguments with mixed types", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"get-sum",
|
||||
"--tool-arg",
|
||||
"a=42.5",
|
||||
"b=57.5",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content.length).toBeGreaterThan(0);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// test-mcp-server returns JSON with {result: a+b}
|
||||
const resultData = JSON.parse(json.content[0].text);
|
||||
expect(resultData.result).toBeCloseTo(100.0, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON Parsing Edge Cases", () => {
|
||||
it("should fall back to string for invalid JSON", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message={invalid json}",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// Should treat invalid JSON as a string
|
||||
expect(json.content[0].text).toBe("Echo: {invalid json}");
|
||||
});
|
||||
|
||||
it("should handle empty string value", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message=""',
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// Empty string should be preserved
|
||||
expect(json.content[0].text).toBe("Echo: ");
|
||||
});
|
||||
|
||||
it("should handle special characters in strings", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message="C:\\\\Users\\\\test"',
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// Special characters should be preserved
|
||||
expect(json.content[0].text).toContain("C:");
|
||||
expect(json.content[0].text).toContain("Users");
|
||||
expect(json.content[0].text).toContain("test");
|
||||
});
|
||||
|
||||
it("should handle unicode characters", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message="🚀🎉✨"',
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// Unicode characters should be preserved
|
||||
expect(json.content[0].text).toContain("🚀");
|
||||
expect(json.content[0].text).toContain("🎉");
|
||||
expect(json.content[0].text).toContain("✨");
|
||||
});
|
||||
|
||||
it("should handle arguments with equals signs in values", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=2+2=4",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// Equals signs in values should be preserved
|
||||
expect(json.content[0].text).toBe("Echo: 2+2=4");
|
||||
});
|
||||
|
||||
it("should handle base64-like strings", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const base64String =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=";
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
`message=${base64String}`,
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// Base64-like strings should be preserved
|
||||
expect(json.content[0].text).toBe(`Echo: ${base64String}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tool Error Handling", () => {
|
||||
it("should fail with nonexistent tool", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"nonexistent_tool",
|
||||
"--tool-arg",
|
||||
"message=test",
|
||||
]);
|
||||
|
||||
// CLI returns exit code 0 but includes isError: true in JSON
|
||||
expectJsonError(result);
|
||||
});
|
||||
|
||||
it("should fail when tool name is missing", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-arg",
|
||||
"message=test",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
|
||||
it("should fail with invalid tool argument format", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"invalid_format_no_equals",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Prompt JSON Arguments", () => {
|
||||
it("should handle prompt with JSON arguments", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"args-prompt",
|
||||
"--prompt-args",
|
||||
"city=New York",
|
||||
"state=NY",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("messages");
|
||||
expect(Array.isArray(json.messages)).toBe(true);
|
||||
expect(json.messages.length).toBeGreaterThan(0);
|
||||
expect(json.messages[0]).toHaveProperty("content");
|
||||
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||
// Validate that the arguments were actually used in the response
|
||||
// test-mcp-server formats it as "This is a prompt with arguments: city={city}, state={state}"
|
||||
expect(json.messages[0].content.text).toContain("city=New York");
|
||||
expect(json.messages[0].content.text).toContain("state=NY");
|
||||
});
|
||||
|
||||
it("should handle prompt with simple arguments", async () => {
|
||||
// Note: simple-prompt doesn't accept arguments, but the CLI should still
|
||||
// accept the command and the server should ignore the arguments
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"simple-prompt",
|
||||
"--prompt-args",
|
||||
"name=test",
|
||||
"count=5",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("messages");
|
||||
expect(Array.isArray(json.messages)).toBe(true);
|
||||
expect(json.messages.length).toBeGreaterThan(0);
|
||||
expect(json.messages[0]).toHaveProperty("content");
|
||||
expect(json.messages[0].content).toHaveProperty("type", "text");
|
||||
// test-mcp-server's simple-prompt returns standard message (ignoring args)
|
||||
expect(json.messages[0].content.text).toBe(
|
||||
"This is a simple prompt for testing purposes.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Backward Compatibility", () => {
|
||||
it("should support existing string-only usage", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
expect(json.content[0].text).toBe("Echo: hello");
|
||||
});
|
||||
|
||||
it("should support multiple string arguments", async () => {
|
||||
const { command, args } = getTestMcpServerCommand();
|
||||
const result = await runCli([
|
||||
command,
|
||||
...args,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"get-sum",
|
||||
"--tool-arg",
|
||||
"a=10",
|
||||
"b=20",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
const json = expectValidJson(result);
|
||||
expect(json).toHaveProperty("content");
|
||||
expect(Array.isArray(json.content)).toBe(true);
|
||||
expect(json.content.length).toBeGreaterThan(0);
|
||||
expect(json.content[0]).toHaveProperty("type", "text");
|
||||
// test-mcp-server returns JSON with {result: a+b}
|
||||
const resultData = JSON.parse(json.content[0].text);
|
||||
expect(resultData.result).toBe(30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -17,15 +17,22 @@
|
|||
"scripts": {
|
||||
"build": "tsc",
|
||||
"postbuild": "node scripts/make-executable.js",
|
||||
"test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js",
|
||||
"test:cli": "node scripts/cli-tests.js",
|
||||
"test:cli-tools": "node scripts/cli-tool-tests.js",
|
||||
"test:cli-headers": "node scripts/cli-header-tests.js"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:cli": "vitest run cli.test.ts",
|
||||
"test:cli-tools": "vitest run tools.test.ts",
|
||||
"test:cli-headers": "vitest run headers.test.ts",
|
||||
"test:cli-metadata": "vitest run metadata.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"tsx": "^4.7.0",
|
||||
"vitest": "^4.0.17"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"commander": "^13.1.0",
|
||||
"express": "^5.2.1",
|
||||
"spawn-rx": "^5.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,252 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Integration tests for header functionality
|
||||
* Tests the CLI header parsing end-to-end
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const CLI_PATH = resolve(__dirname, "..", "build", "index.js");
|
||||
|
||||
// ANSI colors for output
|
||||
const colors = {
|
||||
GREEN: "\x1b[32m",
|
||||
RED: "\x1b[31m",
|
||||
YELLOW: "\x1b[33m",
|
||||
BLUE: "\x1b[34m",
|
||||
NC: "\x1b[0m", // No Color
|
||||
};
|
||||
|
||||
let testsPassed = 0;
|
||||
let testsFailed = 0;
|
||||
|
||||
/**
|
||||
* Run a CLI test with given arguments and check for expected behavior
|
||||
*/
|
||||
function runHeaderTest(
|
||||
testName,
|
||||
args,
|
||||
expectSuccess = false,
|
||||
expectedInOutput = null,
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n${colors.BLUE}Testing: ${testName}${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${CLI_PATH} ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
const child = spawn("node", [CLI_PATH, ...args], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
const output = stdout + stderr;
|
||||
let passed = true;
|
||||
let reason = "";
|
||||
|
||||
// Check exit code expectation
|
||||
if (expectSuccess && code !== 0) {
|
||||
passed = false;
|
||||
reason = `Expected success (exit code 0) but got ${code}`;
|
||||
} else if (!expectSuccess && code === 0) {
|
||||
passed = false;
|
||||
reason = `Expected failure (non-zero exit code) but got success`;
|
||||
}
|
||||
|
||||
// Check expected output
|
||||
if (passed && expectedInOutput && !output.includes(expectedInOutput)) {
|
||||
passed = false;
|
||||
reason = `Expected output to contain "${expectedInOutput}"`;
|
||||
}
|
||||
|
||||
if (passed) {
|
||||
console.log(`${colors.GREEN}PASS: ${testName}${colors.NC}`);
|
||||
testsPassed++;
|
||||
} else {
|
||||
console.log(`${colors.RED}FAIL: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.RED}Reason: ${reason}${colors.NC}`);
|
||||
console.log(`${colors.RED}Exit code: ${code}${colors.NC}`);
|
||||
console.log(`${colors.RED}Output: ${output}${colors.NC}`);
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.log(
|
||||
`${colors.RED}ERROR: ${testName} - ${error.message}${colors.NC}`,
|
||||
);
|
||||
testsFailed++;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runHeaderIntegrationTests() {
|
||||
console.log(
|
||||
`${colors.YELLOW}=== MCP Inspector CLI Header Integration Tests ===${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}Testing header parsing and validation${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 1: Valid header format should parse successfully (connection will fail)
|
||||
await runHeaderTest(
|
||||
"Valid single header",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Authorization: Bearer token123",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test 2: Multiple headers should parse successfully
|
||||
await runHeaderTest(
|
||||
"Multiple headers",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Authorization: Bearer token123",
|
||||
"--header",
|
||||
"X-API-Key: secret123",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test 3: Invalid header format - no colon
|
||||
await runHeaderTest(
|
||||
"Invalid header format - no colon",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"InvalidHeader",
|
||||
],
|
||||
false,
|
||||
"Invalid header format",
|
||||
);
|
||||
|
||||
// Test 4: Invalid header format - empty name
|
||||
await runHeaderTest(
|
||||
"Invalid header format - empty name",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
": value",
|
||||
],
|
||||
false,
|
||||
"Invalid header format",
|
||||
);
|
||||
|
||||
// Test 5: Invalid header format - empty value
|
||||
await runHeaderTest(
|
||||
"Invalid header format - empty value",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Header:",
|
||||
],
|
||||
false,
|
||||
"Invalid header format",
|
||||
);
|
||||
|
||||
// Test 6: Header with colons in value
|
||||
await runHeaderTest(
|
||||
"Header with colons in value",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"X-Time: 2023:12:25:10:30:45",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test 7: Whitespace handling
|
||||
await runHeaderTest(
|
||||
"Whitespace handling in headers",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
" X-Header : value with spaces ",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}=== Test Results ===${colors.NC}`);
|
||||
console.log(`${colors.GREEN}Tests passed: ${testsPassed}${colors.NC}`);
|
||||
console.log(`${colors.RED}Tests failed: ${testsFailed}${colors.NC}`);
|
||||
|
||||
if (testsFailed === 0) {
|
||||
console.log(
|
||||
`${colors.GREEN}All header integration tests passed!${colors.NC}`,
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(
|
||||
`${colors.RED}Some header integration tests failed.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on("SIGINT", () => {
|
||||
console.log(`\n${colors.YELLOW}Test interrupted by user${colors.NC}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log(`\n${colors.YELLOW}Test terminated${colors.NC}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Run the tests
|
||||
runHeaderIntegrationTests().catch((error) => {
|
||||
console.error(`${colors.RED}Test runner error: ${error.message}${colors.NC}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,676 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Colors for output
|
||||
const colors = {
|
||||
GREEN: "\x1b[32m",
|
||||
YELLOW: "\x1b[33m",
|
||||
RED: "\x1b[31m",
|
||||
BLUE: "\x1b[34m",
|
||||
ORANGE: "\x1b[33m",
|
||||
NC: "\x1b[0m", // No Color
|
||||
};
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import os from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// Get directory paths with ESM compatibility
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Track test results
|
||||
let PASSED_TESTS = 0;
|
||||
let FAILED_TESTS = 0;
|
||||
let SKIPPED_TESTS = 0;
|
||||
let TOTAL_TESTS = 0;
|
||||
|
||||
console.log(
|
||||
`${colors.YELLOW}=== MCP Inspector CLI Metadata Tests ===${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}This script tests the MCP Inspector CLI's metadata functionality:${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- General metadata with --metadata option${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Tool-specific metadata with --tool-metadata option${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Metadata parsing with various data types${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Metadata merging (tool-specific overrides general)${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Metadata evaluation in different MCP methods${colors.NC}`,
|
||||
);
|
||||
console.log(`\n`);
|
||||
|
||||
// Get directory paths
|
||||
const SCRIPTS_DIR = __dirname;
|
||||
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
|
||||
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
|
||||
|
||||
// Define the test server command using npx
|
||||
const TEST_CMD = "npx";
|
||||
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
|
||||
|
||||
// Create output directory for test results
|
||||
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "metadata-test-output");
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a temporary directory for test files
|
||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-metadata-tests");
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
|
||||
// Track servers for cleanup
|
||||
let runningServers = [];
|
||||
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Function to run a basic test
|
||||
async function runBasicTest(testName, ...args) {
|
||||
const outputFile = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${testName.replace(/\//g, "_")}.log`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
|
||||
TOTAL_TESTS++;
|
||||
|
||||
// Run the command and capture output
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a write stream for the output file
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
// Spawn the process
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
||||
child.kill();
|
||||
}, 15000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
||||
// Also capture output for display
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
// Check if we got valid JSON output (indicating success) even if process didn't exit cleanly
|
||||
const hasValidJsonOutput =
|
||||
output.includes('"tools"') ||
|
||||
output.includes('"resources"') ||
|
||||
output.includes('"prompts"') ||
|
||||
output.includes('"content"') ||
|
||||
output.includes('"messages"') ||
|
||||
output.includes('"contents"');
|
||||
|
||||
if (code === 0 || hasValidJsonOutput) {
|
||||
console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
|
||||
const firstFewLines = output
|
||||
.split("\n")
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(firstFewLines);
|
||||
PASSED_TESTS++;
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.RED}Error output:${colors.NC}`);
|
||||
console.log(
|
||||
output
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n"),
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
|
||||
// Stop after any error is encountered
|
||||
console.log(
|
||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to run an error test (expected to fail)
|
||||
async function runErrorTest(testName, ...args) {
|
||||
const outputFile = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${testName.replace(/\//g, "_")}.log`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
|
||||
TOTAL_TESTS++;
|
||||
|
||||
// Run the command and capture output
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a write stream for the output file
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
// Spawn the process
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(
|
||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
||||
);
|
||||
child.kill();
|
||||
}, 15000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
||||
// Also capture output for display
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
// For error tests, we expect a non-zero exit code
|
||||
if (code !== 0) {
|
||||
console.log(
|
||||
`${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
|
||||
const firstFewLines = output
|
||||
.split("\n")
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(firstFewLines);
|
||||
PASSED_TESTS++;
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(
|
||||
`${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.RED}Output:${colors.NC}`);
|
||||
console.log(
|
||||
output
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n"),
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
|
||||
// Stop after any error is encountered
|
||||
console.log(
|
||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
async function runTests() {
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running General Metadata Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 1: General metadata with tools/list
|
||||
await runBasicTest(
|
||||
"metadata_tools_list",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
// Test 2: General metadata with resources/list
|
||||
await runBasicTest(
|
||||
"metadata_resources_list",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
// Test 3: General metadata with prompts/list
|
||||
await runBasicTest(
|
||||
"metadata_prompts_list",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
// Test 4: General metadata with resources/read
|
||||
await runBasicTest(
|
||||
"metadata_resources_read",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://static/resource/1",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
// Test 5: General metadata with prompts/get
|
||||
await runBasicTest(
|
||||
"metadata_prompts_get",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"simple_prompt",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Tool-Specific Metadata Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 6: Tool-specific metadata with tools/call
|
||||
await runBasicTest(
|
||||
"metadata_tools_call_tool_meta",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello world",
|
||||
"--tool-metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
// Test 7: Tool-specific metadata with complex tool
|
||||
await runBasicTest(
|
||||
"metadata_tools_call_complex_tool_meta",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"add",
|
||||
"--tool-arg",
|
||||
"a=10",
|
||||
"b=20",
|
||||
"--tool-metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Merging Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 8: General metadata + tool-specific metadata (tool-specific should override)
|
||||
await runBasicTest(
|
||||
"metadata_merging_general_and_tool",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello world",
|
||||
"--metadata",
|
||||
"client=general-client",
|
||||
"--tool-metadata",
|
||||
"client=test-client",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Parsing Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 10: Metadata with numeric values (should be converted to strings)
|
||||
await runBasicTest(
|
||||
"metadata_parsing_numbers",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"integer_value=42",
|
||||
"decimal_value=3.14159",
|
||||
"negative_value=-10",
|
||||
);
|
||||
|
||||
// Test 11: Metadata with JSON values (should be converted to strings)
|
||||
await runBasicTest(
|
||||
"metadata_parsing_json",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
'json_object="{\\"key\\":\\"value\\"}"',
|
||||
'json_array="[1,2,3]"',
|
||||
'json_string="\\"quoted\\""',
|
||||
);
|
||||
|
||||
// Test 12: Metadata with special characters
|
||||
await runBasicTest(
|
||||
"metadata_parsing_special_chars",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"unicode=🚀🎉✨",
|
||||
"special_chars=!@#$%^&*()",
|
||||
"spaces=hello world with spaces",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Edge Cases ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 13: Single metadata entry
|
||||
await runBasicTest(
|
||||
"metadata_single_entry",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"single_key=single_value",
|
||||
);
|
||||
|
||||
// Test 14: Many metadata entries
|
||||
await runBasicTest(
|
||||
"metadata_many_entries",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
"key3=value3",
|
||||
"key4=value4",
|
||||
"key5=value5",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Error Cases ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 15: Invalid metadata format (missing equals)
|
||||
await runErrorTest(
|
||||
"metadata_error_invalid_format",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"invalid_format_no_equals",
|
||||
);
|
||||
|
||||
// Test 16: Invalid tool-meta format (missing equals)
|
||||
await runErrorTest(
|
||||
"metadata_error_invalid_tool_meta_format",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=test",
|
||||
"--tool-metadata",
|
||||
"invalid_format_no_equals",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Impact Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 17: Test tool-specific metadata vs general metadata precedence
|
||||
await runBasicTest(
|
||||
"metadata_precedence_tool_overrides_general",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=precedence test",
|
||||
"--metadata",
|
||||
"client=general-client",
|
||||
"--tool-metadata",
|
||||
"client=tool-specific-client",
|
||||
);
|
||||
|
||||
// Test 18: Test metadata with resources methods
|
||||
await runBasicTest(
|
||||
"metadata_resources_methods",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/list",
|
||||
"--metadata",
|
||||
"resource_client=test-resource-client",
|
||||
);
|
||||
|
||||
// Test 19: Test metadata with prompts methods
|
||||
await runBasicTest(
|
||||
"metadata_prompts_methods",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"simple_prompt",
|
||||
"--metadata",
|
||||
"prompt_client=test-prompt-client",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Validation Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 20: Test metadata with special characters in keys
|
||||
await runBasicTest(
|
||||
"metadata_special_key_characters",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=special keys test",
|
||||
"--metadata",
|
||||
"key-with-dashes=value1",
|
||||
"key_with_underscores=value2",
|
||||
"key.with.dots=value3",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Metadata Integration Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 21: Metadata with all MCP methods
|
||||
await runBasicTest(
|
||||
"metadata_integration_all_methods",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"integration_test=true",
|
||||
"test_phase=all_methods",
|
||||
);
|
||||
|
||||
// Test 22: Complex metadata scenario
|
||||
await runBasicTest(
|
||||
"metadata_complex_scenario",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=complex test",
|
||||
"--metadata",
|
||||
"session_id=12345",
|
||||
"user_id=67890",
|
||||
"timestamp=2024-01-01T00:00:00Z",
|
||||
"request_id=req-abc-123",
|
||||
"--tool-metadata",
|
||||
"tool_session=session-xyz-789",
|
||||
"execution_context=test",
|
||||
"priority=high",
|
||||
);
|
||||
|
||||
// Test 23: Metadata parsing validation test
|
||||
await runBasicTest(
|
||||
"metadata_parsing_validation",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=parsing validation test",
|
||||
"--metadata",
|
||||
"valid_key=valid_value",
|
||||
"numeric_key=123",
|
||||
"boolean_key=true",
|
||||
'json_key=\'{"test":"value"}\'',
|
||||
"special_key=!@#$%^&*()",
|
||||
"unicode_key=🚀🎉✨",
|
||||
);
|
||||
|
||||
// Print test summary
|
||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
||||
console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
|
||||
console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
|
||||
console.log(`Total: ${TOTAL_TESTS}`);
|
||||
console.log(
|
||||
`${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.GREEN}All metadata tests completed!${colors.NC}`);
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
runTests().catch((error) => {
|
||||
console.error(
|
||||
`${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,935 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Colors for output
|
||||
const colors = {
|
||||
GREEN: "\x1b[32m",
|
||||
YELLOW: "\x1b[33m",
|
||||
RED: "\x1b[31m",
|
||||
BLUE: "\x1b[34m",
|
||||
ORANGE: "\x1b[33m",
|
||||
NC: "\x1b[0m", // No Color
|
||||
};
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import os from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// Get directory paths with ESM compatibility
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Track test results
|
||||
let PASSED_TESTS = 0;
|
||||
let FAILED_TESTS = 0;
|
||||
let SKIPPED_TESTS = 0;
|
||||
let TOTAL_TESTS = 0;
|
||||
|
||||
console.log(
|
||||
`${colors.YELLOW}=== MCP Inspector CLI Test Script ===${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}This script tests the MCP Inspector CLI's ability to handle various command line options:${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}- Basic CLI mode${colors.NC}`);
|
||||
console.log(`${colors.BLUE}- Environment variables (-e)${colors.NC}`);
|
||||
console.log(`${colors.BLUE}- Config file (--config)${colors.NC}`);
|
||||
console.log(`${colors.BLUE}- Server selection (--server)${colors.NC}`);
|
||||
console.log(`${colors.BLUE}- Method selection (--method)${colors.NC}`);
|
||||
console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}- Transport types (--transport http/sse/stdio)${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Transport inference from URL suffixes (/mcp, /sse)${colors.NC}`,
|
||||
);
|
||||
console.log(`\n`);
|
||||
|
||||
// Get directory paths
|
||||
const SCRIPTS_DIR = __dirname;
|
||||
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
|
||||
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
|
||||
|
||||
// Define the test server command using npx
|
||||
const TEST_CMD = "npx";
|
||||
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
|
||||
|
||||
// Create output directory for test results
|
||||
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "test-output");
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a temporary directory for test files
|
||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tests");
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
|
||||
// Track servers for cleanup
|
||||
let runningServers = [];
|
||||
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Use the existing sample config file
|
||||
console.log(
|
||||
`${colors.BLUE}Using existing sample config file: ${PROJECT_ROOT}/sample-config.json${colors.NC}`,
|
||||
);
|
||||
try {
|
||||
const sampleConfig = fs.readFileSync(
|
||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
||||
"utf8",
|
||||
);
|
||||
console.log(sampleConfig);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error reading sample config: ${error.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create an invalid config file for testing
|
||||
const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json");
|
||||
fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {');
|
||||
|
||||
// Create config files with different transport types for testing
|
||||
const sseConfigPath = path.join(TEMP_DIR, "sse-config.json");
|
||||
fs.writeFileSync(
|
||||
sseConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"test-sse": {
|
||||
type: "sse",
|
||||
url: "http://localhost:3000/sse",
|
||||
note: "Test SSE server",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
const httpConfigPath = path.join(TEMP_DIR, "http-config.json");
|
||||
fs.writeFileSync(
|
||||
httpConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"test-http": {
|
||||
type: "streamable-http",
|
||||
url: "http://localhost:3000/mcp",
|
||||
note: "Test HTTP server",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json");
|
||||
fs.writeFileSync(
|
||||
stdioConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"test-stdio": {
|
||||
type: "stdio",
|
||||
command: "npx",
|
||||
args: ["@modelcontextprotocol/server-everything"],
|
||||
env: {
|
||||
TEST_ENV: "test-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Config without type field (backward compatibility)
|
||||
const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json");
|
||||
fs.writeFileSync(
|
||||
legacyConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"test-legacy": {
|
||||
command: "npx",
|
||||
args: ["@modelcontextprotocol/server-everything"],
|
||||
env: {
|
||||
LEGACY_ENV: "legacy-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Function to run a basic test
|
||||
async function runBasicTest(testName, ...args) {
|
||||
const outputFile = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${testName.replace(/\//g, "_")}.log`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
|
||||
TOTAL_TESTS++;
|
||||
|
||||
// Run the command and capture output
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a write stream for the output file
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
// Spawn the process
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
||||
child.kill();
|
||||
}, 10000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
||||
// Also capture output for display
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
if (code === 0) {
|
||||
console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
|
||||
const firstFewLines = output
|
||||
.split("\n")
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(firstFewLines);
|
||||
PASSED_TESTS++;
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.RED}Error output:${colors.NC}`);
|
||||
console.log(
|
||||
output
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n"),
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
|
||||
// Stop after any error is encountered
|
||||
console.log(
|
||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to run an error test (expected to fail)
|
||||
async function runErrorTest(testName, ...args) {
|
||||
const outputFile = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${testName.replace(/\//g, "_")}.log`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
|
||||
TOTAL_TESTS++;
|
||||
|
||||
// Run the command and capture output
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a write stream for the output file
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
// Spawn the process
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(
|
||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
||||
);
|
||||
child.kill();
|
||||
}, 10000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
||||
// Also capture output for display
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
// For error tests, we expect a non-zero exit code
|
||||
if (code !== 0) {
|
||||
console.log(
|
||||
`${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
|
||||
const firstFewLines = output
|
||||
.split("\n")
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(firstFewLines);
|
||||
PASSED_TESTS++;
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(
|
||||
`${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.RED}Output:${colors.NC}`);
|
||||
console.log(
|
||||
output
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n"),
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
|
||||
// Stop after any error is encountered
|
||||
console.log(
|
||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
async function runTests() {
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Basic CLI Mode Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 1: Basic CLI mode with method
|
||||
await runBasicTest(
|
||||
"basic_cli_mode",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 2: CLI mode with non-existent method (should fail)
|
||||
await runErrorTest(
|
||||
"nonexistent_method",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"nonexistent/method",
|
||||
);
|
||||
|
||||
// Test 3: CLI mode without method (should fail)
|
||||
await runErrorTest("missing_method", TEST_CMD, ...TEST_ARGS, "--cli");
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Environment Variable Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 4: CLI mode with environment variables
|
||||
await runBasicTest(
|
||||
"env_variables",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"-e",
|
||||
"KEY1=value1",
|
||||
"-e",
|
||||
"KEY2=value2",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 5: CLI mode with invalid environment variable format (should fail)
|
||||
await runErrorTest(
|
||||
"invalid_env_format",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"-e",
|
||||
"INVALID_FORMAT",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 5b: CLI mode with environment variable containing equals sign in value
|
||||
await runBasicTest(
|
||||
"env_variable_with_equals",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"-e",
|
||||
"API_KEY=abc123=xyz789==",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 5c: CLI mode with environment variable containing base64-encoded value
|
||||
await runBasicTest(
|
||||
"env_variable_with_base64",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"-e",
|
||||
"JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Config File Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 6: Using config file with CLI mode
|
||||
await runBasicTest(
|
||||
"config_file",
|
||||
"--config",
|
||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
||||
"--server",
|
||||
"everything",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 7: Using config file without server name (should fail)
|
||||
await runErrorTest(
|
||||
"config_without_server",
|
||||
"--config",
|
||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 8: Using server name without config file (should fail)
|
||||
await runErrorTest(
|
||||
"server_without_config",
|
||||
"--server",
|
||||
"everything",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 9: Using non-existent config file (should fail)
|
||||
await runErrorTest(
|
||||
"nonexistent_config",
|
||||
"--config",
|
||||
"./nonexistent-config.json",
|
||||
"--server",
|
||||
"everything",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 10: Using invalid config file format (should fail)
|
||||
await runErrorTest(
|
||||
"invalid_config",
|
||||
"--config",
|
||||
invalidConfigPath,
|
||||
"--server",
|
||||
"everything",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 11: Using config file with non-existent server (should fail)
|
||||
await runErrorTest(
|
||||
"nonexistent_server",
|
||||
"--config",
|
||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
||||
"--server",
|
||||
"nonexistent",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 16: CLI mode with resource read
|
||||
await runBasicTest(
|
||||
"resource_read",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
"--uri",
|
||||
"test://static/resource/1",
|
||||
);
|
||||
|
||||
// Test 17: CLI mode with resource read but missing URI (should fail)
|
||||
await runErrorTest(
|
||||
"missing_uri",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"resources/read",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Prompt-Related Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 18: CLI mode with prompt get
|
||||
await runBasicTest(
|
||||
"prompt_get",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"simple_prompt",
|
||||
);
|
||||
|
||||
// Test 19: CLI mode with prompt get and args
|
||||
await runBasicTest(
|
||||
"prompt_get_with_args",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"complex_prompt",
|
||||
"--prompt-args",
|
||||
"temperature=0.7",
|
||||
"style=concise",
|
||||
);
|
||||
|
||||
// Test 20: CLI mode with prompt get but missing prompt name (should fail)
|
||||
await runErrorTest(
|
||||
"missing_prompt_name",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}=== Running Logging Tests ===${colors.NC}`);
|
||||
|
||||
// Test 21: CLI mode with log level
|
||||
await runBasicTest(
|
||||
"log_level",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"logging/setLevel",
|
||||
"--log-level",
|
||||
"debug",
|
||||
);
|
||||
|
||||
// Test 22: CLI mode with invalid log level (should fail)
|
||||
await runErrorTest(
|
||||
"invalid_log_level",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"logging/setLevel",
|
||||
"--log-level",
|
||||
"invalid",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Combined Option Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Note about the combined options issue
|
||||
console.log(
|
||||
`${colors.BLUE}Testing combined options with environment variables and config file.${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 23: CLI mode with config file, environment variables, and tool call
|
||||
await runBasicTest(
|
||||
"combined_options",
|
||||
"--config",
|
||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
||||
"--server",
|
||||
"everything",
|
||||
"-e",
|
||||
"CLI_ENV_VAR=cli_value",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 24: CLI mode with all possible options (that make sense together)
|
||||
await runBasicTest(
|
||||
"all_options",
|
||||
"--config",
|
||||
path.join(PROJECT_ROOT, "sample-config.json"),
|
||||
"--server",
|
||||
"everything",
|
||||
"-e",
|
||||
"CLI_ENV_VAR=cli_value",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=Hello",
|
||||
"--log-level",
|
||||
"debug",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 25: Config with stdio transport type
|
||||
await runBasicTest(
|
||||
"config_stdio_type",
|
||||
"--config",
|
||||
stdioConfigPath,
|
||||
"--server",
|
||||
"test-stdio",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 26: Config with SSE transport type (CLI mode) - expects connection error
|
||||
await runErrorTest(
|
||||
"config_sse_type_cli",
|
||||
"--config",
|
||||
sseConfigPath,
|
||||
"--server",
|
||||
"test-sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 27: Config with streamable-http transport type (CLI mode) - expects connection error
|
||||
await runErrorTest(
|
||||
"config_http_type_cli",
|
||||
"--config",
|
||||
httpConfigPath,
|
||||
"--server",
|
||||
"test-http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 28: Legacy config without type field (backward compatibility)
|
||||
await runBasicTest(
|
||||
"config_legacy_no_type",
|
||||
"--config",
|
||||
legacyConfigPath,
|
||||
"--server",
|
||||
"test-legacy",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Default Server Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Create config with single server for auto-selection
|
||||
const singleServerConfigPath = path.join(
|
||||
TEMP_DIR,
|
||||
"single-server-config.json",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
singleServerConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"only-server": {
|
||||
command: "npx",
|
||||
args: ["@modelcontextprotocol/server-everything"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Create config with default-server
|
||||
const defaultServerConfigPath = path.join(
|
||||
TEMP_DIR,
|
||||
"default-server-config.json",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
defaultServerConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
"default-server": {
|
||||
command: "npx",
|
||||
args: ["@modelcontextprotocol/server-everything"],
|
||||
},
|
||||
"other-server": {
|
||||
command: "node",
|
||||
args: ["other.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Create config with multiple servers (no default)
|
||||
const multiServerConfigPath = path.join(TEMP_DIR, "multi-server-config.json");
|
||||
fs.writeFileSync(
|
||||
multiServerConfigPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
server1: {
|
||||
command: "npx",
|
||||
args: ["@modelcontextprotocol/server-everything"],
|
||||
},
|
||||
server2: {
|
||||
command: "node",
|
||||
args: ["other.js"],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Test 29: Config with single server auto-selection
|
||||
await runBasicTest(
|
||||
"single_server_auto_select",
|
||||
"--config",
|
||||
singleServerConfigPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 30: Config with default-server should now require explicit selection (multiple servers)
|
||||
await runErrorTest(
|
||||
"default_server_requires_explicit_selection",
|
||||
"--config",
|
||||
defaultServerConfigPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 31: Config with multiple servers and no default (should fail)
|
||||
await runErrorTest(
|
||||
"multi_server_no_default",
|
||||
"--config",
|
||||
multiServerConfigPath,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`${colors.BLUE}Starting server-everything in streamableHttp mode.${colors.NC}`,
|
||||
);
|
||||
const httpServer = spawn(
|
||||
"npx",
|
||||
["@modelcontextprotocol/server-everything", "streamableHttp"],
|
||||
{
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
runningServers.push(httpServer);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Test 32: HTTP transport inferred from URL ending with /mcp
|
||||
await runBasicTest(
|
||||
"http_transport_inferred",
|
||||
"http://127.0.0.1:3001/mcp",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 33: HTTP transport with explicit --transport http flag
|
||||
await runBasicTest(
|
||||
"http_transport_with_explicit_flag",
|
||||
"http://127.0.0.1:3001/mcp",
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 34: HTTP transport with suffix and --transport http flag
|
||||
await runBasicTest(
|
||||
"http_transport_with_explicit_flag_and_suffix",
|
||||
"http://127.0.0.1:3001/mcp",
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 35: SSE transport given to HTTP server (should fail)
|
||||
await runErrorTest(
|
||||
"sse_transport_given_to_http_server",
|
||||
"http://127.0.0.1:3001",
|
||||
"--transport",
|
||||
"sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 36: HTTP transport without URL (should fail)
|
||||
await runErrorTest(
|
||||
"http_transport_without_url",
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 37: SSE transport without URL (should fail)
|
||||
await runErrorTest(
|
||||
"sse_transport_without_url",
|
||||
"--transport",
|
||||
"sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Kill HTTP server
|
||||
try {
|
||||
process.kill(-httpServer.pid);
|
||||
console.log(
|
||||
`${colors.BLUE}HTTP server killed, waiting for port to be released...${colors.NC}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`${colors.RED}Error killing HTTP server: ${e.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Print test summary
|
||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
||||
console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
|
||||
console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
|
||||
console.log(`Total: ${TOTAL_TESTS}`);
|
||||
console.log(
|
||||
`${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.GREEN}All tests completed!${colors.NC}`);
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
runTests().catch((error) => {
|
||||
console.error(
|
||||
`${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,614 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Colors for output
|
||||
const colors = {
|
||||
GREEN: "\x1b[32m",
|
||||
YELLOW: "\x1b[33m",
|
||||
RED: "\x1b[31m",
|
||||
BLUE: "\x1b[34m",
|
||||
ORANGE: "\x1b[33m",
|
||||
NC: "\x1b[0m", // No Color
|
||||
};
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import os from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// Get directory paths with ESM compatibility
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Track test results
|
||||
let PASSED_TESTS = 0;
|
||||
let FAILED_TESTS = 0;
|
||||
let SKIPPED_TESTS = 0;
|
||||
let TOTAL_TESTS = 0;
|
||||
|
||||
console.log(`${colors.YELLOW}=== MCP Inspector CLI Tool Tests ===${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}This script tests the MCP Inspector CLI's tool-related functionality:${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}- Tool discovery and listing${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}- Tool schema validation${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}- Tool execution with various argument types${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`,
|
||||
);
|
||||
console.log(`\n`);
|
||||
|
||||
// Get directory paths
|
||||
const SCRIPTS_DIR = __dirname;
|
||||
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
|
||||
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
|
||||
|
||||
// Define the test server command using npx
|
||||
const TEST_CMD = "npx";
|
||||
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
|
||||
|
||||
// Create output directory for test results
|
||||
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "tool-test-output");
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a temporary directory for test files
|
||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tool-tests");
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
|
||||
// Track servers for cleanup
|
||||
let runningServers = [];
|
||||
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Function to run a basic test
|
||||
async function runBasicTest(testName, ...args) {
|
||||
const outputFile = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${testName.replace(/\//g, "_")}.log`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
|
||||
TOTAL_TESTS++;
|
||||
|
||||
// Run the command and capture output
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a write stream for the output file
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
// Spawn the process
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
||||
child.kill();
|
||||
}, 10000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
||||
// Also capture output for display
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
if (code === 0) {
|
||||
console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
|
||||
const firstFewLines = output
|
||||
.split("\n")
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(firstFewLines);
|
||||
PASSED_TESTS++;
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.RED}Error output:${colors.NC}`);
|
||||
console.log(
|
||||
output
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n"),
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
|
||||
// Stop after any error is encountered
|
||||
console.log(
|
||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to run an error test (expected to fail)
|
||||
async function runErrorTest(testName, ...args) {
|
||||
const outputFile = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${testName.replace(/\//g, "_")}.log`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
|
||||
TOTAL_TESTS++;
|
||||
|
||||
// Run the command and capture output
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Create a write stream for the output file
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
// Spawn the process
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(
|
||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
||||
);
|
||||
child.kill();
|
||||
}, 10000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
||||
// Also capture output for display
|
||||
let output = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
child.stderr.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
// For error tests, we expect a non-zero exit code
|
||||
if (code !== 0) {
|
||||
console.log(
|
||||
`${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
|
||||
const firstFewLines = output
|
||||
.split("\n")
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(firstFewLines);
|
||||
PASSED_TESTS++;
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(
|
||||
`${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.RED}Output:${colors.NC}`);
|
||||
console.log(
|
||||
output
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n"),
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
|
||||
// Stop after any error is encountered
|
||||
console.log(
|
||||
`${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`${colors.RED}Error running test: ${error.message}${colors.NC}`,
|
||||
);
|
||||
FAILED_TESTS++;
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
async function runTests() {
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Tool Discovery Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 1: List available tools
|
||||
await runBasicTest(
|
||||
"tool_discovery_list",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running JSON Argument Parsing Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 2: String arguments (backward compatibility)
|
||||
await runBasicTest(
|
||||
"json_args_string",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello world",
|
||||
);
|
||||
|
||||
// Test 3: Number arguments
|
||||
await runBasicTest(
|
||||
"json_args_number_integer",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"add",
|
||||
"--tool-arg",
|
||||
"a=42",
|
||||
"b=58",
|
||||
);
|
||||
|
||||
// Test 4: Number arguments with decimals (using add tool with decimal numbers)
|
||||
await runBasicTest(
|
||||
"json_args_number_decimal",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"add",
|
||||
"--tool-arg",
|
||||
"a=19.99",
|
||||
"b=20.01",
|
||||
);
|
||||
|
||||
// Test 5: Boolean arguments - true
|
||||
await runBasicTest(
|
||||
"json_args_boolean_true",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"annotatedMessage",
|
||||
"--tool-arg",
|
||||
"messageType=success",
|
||||
"includeImage=true",
|
||||
);
|
||||
|
||||
// Test 6: Boolean arguments - false
|
||||
await runBasicTest(
|
||||
"json_args_boolean_false",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"annotatedMessage",
|
||||
"--tool-arg",
|
||||
"messageType=error",
|
||||
"includeImage=false",
|
||||
);
|
||||
|
||||
// Test 7: Null arguments (using echo with string "null")
|
||||
await runBasicTest(
|
||||
"json_args_null",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message="null"',
|
||||
);
|
||||
|
||||
// Test 14: Multiple arguments with mixed types (using add tool)
|
||||
await runBasicTest(
|
||||
"json_args_multiple_mixed",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"add",
|
||||
"--tool-arg",
|
||||
"a=42.5",
|
||||
"b=57.5",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running JSON Parsing Edge Cases ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 15: Invalid JSON should fall back to string
|
||||
await runBasicTest(
|
||||
"json_args_invalid_fallback",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message={invalid json}",
|
||||
);
|
||||
|
||||
// Test 16: Empty string value
|
||||
await runBasicTest(
|
||||
"json_args_empty_value",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message=""',
|
||||
);
|
||||
|
||||
// Test 17: Special characters in strings
|
||||
await runBasicTest(
|
||||
"json_args_special_chars",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message="C:\\\\Users\\\\test"',
|
||||
);
|
||||
|
||||
// Test 18: Unicode characters
|
||||
await runBasicTest(
|
||||
"json_args_unicode",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
'message="🚀🎉✨"',
|
||||
);
|
||||
|
||||
// Test 19: Arguments with equals signs in values
|
||||
await runBasicTest(
|
||||
"json_args_equals_in_value",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=2+2=4",
|
||||
);
|
||||
|
||||
// Test 20: Base64-like strings
|
||||
await runBasicTest(
|
||||
"json_args_base64_like",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Tool Error Handling Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 21: Non-existent tool
|
||||
await runErrorTest(
|
||||
"tool_error_nonexistent",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"nonexistent_tool",
|
||||
"--tool-arg",
|
||||
"message=test",
|
||||
);
|
||||
|
||||
// Test 22: Missing tool name
|
||||
await runErrorTest(
|
||||
"tool_error_missing_name",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-arg",
|
||||
"message=test",
|
||||
);
|
||||
|
||||
// Test 23: Invalid tool argument format
|
||||
await runErrorTest(
|
||||
"tool_error_invalid_arg_format",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"invalid_format_no_equals",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Prompt JSON Argument Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 24: Prompt with JSON arguments
|
||||
await runBasicTest(
|
||||
"prompt_json_args_mixed",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"complex_prompt",
|
||||
"--prompt-args",
|
||||
"temperature=0.7",
|
||||
'style="concise"',
|
||||
'options={"format":"json","max_tokens":100}',
|
||||
);
|
||||
|
||||
// Test 25: Prompt with simple arguments
|
||||
await runBasicTest(
|
||||
"prompt_json_args_simple",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"prompts/get",
|
||||
"--prompt-name",
|
||||
"simple_prompt",
|
||||
"--prompt-args",
|
||||
"name=test",
|
||||
"count=5",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running Backward Compatibility Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 26: Ensure existing string-only usage still works
|
||||
await runBasicTest(
|
||||
"backward_compatibility_strings",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"echo",
|
||||
"--tool-arg",
|
||||
"message=hello",
|
||||
);
|
||||
|
||||
// Test 27: Multiple string arguments (existing pattern) - using add tool
|
||||
await runBasicTest(
|
||||
"backward_compatibility_multiple_strings",
|
||||
TEST_CMD,
|
||||
...TEST_ARGS,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/call",
|
||||
"--tool-name",
|
||||
"add",
|
||||
"--tool-arg",
|
||||
"a=10",
|
||||
"b=20",
|
||||
);
|
||||
|
||||
// Print test summary
|
||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
||||
console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
|
||||
console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
|
||||
console.log(`Total: ${TOTAL_TESTS}`);
|
||||
console.log(
|
||||
`${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.GREEN}All tool tests completed!${colors.NC}`);
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
runTests().catch((error) => {
|
||||
console.error(
|
||||
`${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
10
cli/vitest.config.ts
Normal file
10
cli/vitest.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["**/__tests__/**/*.test.ts"],
|
||||
testTimeout: 15000, // 15 seconds - CLI tests spawn subprocesses that need time
|
||||
},
|
||||
});
|
||||
418
package-lock.json
generated
418
package-lock.json
generated
|
|
@ -17,7 +17,7 @@
|
|||
"@modelcontextprotocol/inspector-cli": "^0.18.0",
|
||||
"@modelcontextprotocol/inspector-client": "^0.18.0",
|
||||
"@modelcontextprotocol/inspector-server": "^0.18.0",
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"concurrently": "^9.2.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"open": "^10.2.0",
|
||||
|
|
@ -51,14 +51,55 @@
|
|||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"commander": "^13.1.0",
|
||||
"express": "^5.2.1",
|
||||
"spawn-rx": "^5.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"mcp-inspector-cli": "build/cli.js"
|
||||
},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"tsx": "^4.7.0",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
},
|
||||
"cli/node_modules/@types/express": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
"@types/serve-static": "^2"
|
||||
}
|
||||
},
|
||||
"cli/node_modules/@types/express-serve-static-core": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
|
||||
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"cli/node_modules/@types/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"cli/node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
|
|
@ -74,7 +115,7 @@
|
|||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
|
@ -3804,6 +3845,13 @@
|
|||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
|
|
@ -3978,6 +4026,17 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
|
|
@ -3998,6 +4057,13 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -4586,6 +4652,117 @@
|
|||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz",
|
||||
"integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz",
|
||||
"integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.17",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz",
|
||||
"integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz",
|
||||
"integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz",
|
||||
"integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz",
|
||||
"integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz",
|
||||
"integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
|
|
@ -4817,6 +4994,16 @@
|
|||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -5222,6 +5409,16 @@
|
|||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
|
@ -6029,6 +6226,13 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
|
|
@ -6330,6 +6534,16 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
|
|
@ -6427,6 +6641,16 @@
|
|||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
|
|
@ -9317,6 +9541,16 @@
|
|||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
|
|
@ -9709,6 +9943,17 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
|
|
@ -9964,6 +10209,13 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -11245,6 +11497,13 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
|
|
@ -11370,6 +11629,13 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
|
|
@ -11379,6 +11645,13 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-argv": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
||||
|
|
@ -11668,6 +11941,23 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
|
@ -11716,6 +12006,16 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
|
|
@ -12826,6 +13126,97 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz",
|
||||
"integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.17",
|
||||
"@vitest/mocker": "4.0.17",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"@vitest/runner": "4.0.17",
|
||||
"@vitest/snapshot": "4.0.17",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.17",
|
||||
"@vitest/browser-preview": "4.0.17",
|
||||
"@vitest/browser-webdriverio": "4.0.17",
|
||||
"@vitest/ui": "4.0.17",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
|
||||
|
|
@ -12933,6 +13324,23 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
|
@ -13235,7 +13643,7 @@
|
|||
"version": "0.18.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"shell-quote": "^1.8.3",
|
||||
|
|
|
|||
Loading…
Reference in a new issue