First pass at pulling the v1.5 CLI, TUI, and Launcher clients into v2.
This commit is contained in:
parent
d5f12b2a82
commit
467b01edd6
9 changed files with 24043 additions and 203 deletions
44
clients/cli/__tests__/README.md
Normal file
44
clients/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
|
||||
859
clients/cli/__tests__/cli.test.ts
Normal file
859
clients/cli/__tests__/cli.test.ts
Normal file
|
|
@ -0,0 +1,859 @@
|
|||
import { describe, it, 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,
|
||||
createTestServerHttp,
|
||||
createEchoTool,
|
||||
createTestServerInfo,
|
||||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
|
||||
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: { name: string }) => 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 {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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",
|
||||
},
|
||||
},
|
||||
});
|
||||
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",
|
||||
},
|
||||
},
|
||||
});
|
||||
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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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);
|
||||
});
|
||||
});
|
||||
});
|
||||
202
clients/cli/__tests__/headers.test.ts
Normal file
202
clients/cli/__tests__/headers.test.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliFailure,
|
||||
expectOutputContains,
|
||||
expectCliSuccess,
|
||||
} from "./helpers/assertions.js";
|
||||
import {
|
||||
createTestServerHttp,
|
||||
createEchoTool,
|
||||
createTestServerInfo,
|
||||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
|
||||
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 {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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 {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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 {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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 {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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");
|
||||
});
|
||||
});
|
||||
});
|
||||
42
clients/cli/__tests__/helpers/assertions.test.ts
Normal file
42
clients/cli/__tests__/helpers/assertions.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { expectCliSuccess, expectCliFailure } from "./assertions.js";
|
||||
import type { CliResult } from "./cli-runner.js";
|
||||
|
||||
function assertionMessage(fn: () => void): string {
|
||||
try {
|
||||
fn();
|
||||
return "";
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
describe("CLI assertion helpers", () => {
|
||||
it("expectCliSuccess includes stdout and stderr on failure", () => {
|
||||
const result: CliResult = {
|
||||
exitCode: 1,
|
||||
stdout: "tool output",
|
||||
stderr: "connect error",
|
||||
output: "tool outputconnect error",
|
||||
};
|
||||
|
||||
const message = assertionMessage(() => expectCliSuccess(result));
|
||||
expect(message).toContain("CLI exited with code 1");
|
||||
expect(message).toContain("stdout: tool output");
|
||||
expect(message).toContain("stderr: connect error");
|
||||
});
|
||||
|
||||
it("expectCliFailure includes stdout and stderr on unexpected success", () => {
|
||||
const result: CliResult = {
|
||||
exitCode: 0,
|
||||
stdout: "ok",
|
||||
stderr: "",
|
||||
output: "ok",
|
||||
};
|
||||
|
||||
const message = assertionMessage(() => expectCliFailure(result));
|
||||
expect(message).toContain("CLI unexpectedly exited with code 0");
|
||||
expect(message).toContain("stdout: ok");
|
||||
expect(message).toContain("stderr: (empty)");
|
||||
});
|
||||
});
|
||||
64
clients/cli/__tests__/helpers/assertions.ts
Normal file
64
clients/cli/__tests__/helpers/assertions.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { expect } from "vitest";
|
||||
import type { CliResult } from "./cli-runner.js";
|
||||
|
||||
function formatCliOutput(result: CliResult): string {
|
||||
const out = result.stdout?.trim() || "(empty)";
|
||||
const err = result.stderr?.trim() || "(empty)";
|
||||
return `stdout: ${out}\nstderr: ${err}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that CLI command succeeded (exit code 0)
|
||||
*/
|
||||
export function expectCliSuccess(result: CliResult) {
|
||||
expect(
|
||||
result.exitCode,
|
||||
`CLI exited with code ${result.exitCode}. ${formatCliOutput(result)}`,
|
||||
).toBe(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that CLI command failed (non-zero exit code)
|
||||
*/
|
||||
export function expectCliFailure(result: CliResult) {
|
||||
expect(
|
||||
result.exitCode,
|
||||
`CLI unexpectedly exited with code ${result.exitCode}. ${formatCliOutput(result)}`,
|
||||
).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;
|
||||
}
|
||||
97
clients/cli/__tests__/helpers/cli-runner.ts
Normal file
97
clients/cli/__tests__/helpers/cli-runner.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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/index.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,
|
||||
// Own process group on Unix so timeout can kill CLI + stdio grandchildren.
|
||||
detached: process.platform !== "win32",
|
||||
});
|
||||
|
||||
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.pid != null) {
|
||||
process.kill(-child.pid, "SIGTERM");
|
||||
} else {
|
||||
child.kill("SIGTERM");
|
||||
}
|
||||
} catch {
|
||||
// Process might already be dead, try direct kill
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
90
clients/cli/__tests__/helpers/fixtures.ts
Normal file
90
clients/cli/__tests__/helpers/fixtures.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as crypto from "crypto";
|
||||
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server";
|
||||
import type { MCPServerConfig } from "@modelcontextprotocol/inspector-core/mcp/index.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 {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test config file
|
||||
*/
|
||||
export function createTestConfig(config: {
|
||||
mcpServers: Record<string, MCPServerConfig>;
|
||||
}): 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));
|
||||
}
|
||||
934
clients/cli/__tests__/metadata.test.ts
Normal file
934
clients/cli/__tests__/metadata.test.ts
Normal file
|
|
@ -0,0 +1,934 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliSuccess,
|
||||
expectCliFailure,
|
||||
expectValidJson,
|
||||
} from "./helpers/assertions.js";
|
||||
import {
|
||||
createTestServerHttp,
|
||||
createEchoTool,
|
||||
createAddTool,
|
||||
createTestServerInfo,
|
||||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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",
|
||||
promptString: "test prompt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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",
|
||||
promptString: "test prompt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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",
|
||||
promptString: "test prompt",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("SSE Transport Tests", () => {
|
||||
it("should work with tools/list using SSE transport", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverType: "sse",
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--metadata",
|
||||
"client=test-client",
|
||||
"--transport",
|
||||
"sse",
|
||||
]);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
527
clients/cli/__tests__/tools.test.ts
Normal file
527
clients/cli/__tests__/tools.test.ts
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
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 "@modelcontextprotocol/inspector-test-server";
|
||||
|
||||
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: { name: string }) => 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: { type?: string }) => 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: { type?: string }) => 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 (server returns error)
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue