Research across 8 multi-server agent clients (Claude Code, Codex, Gemini
CLI, VS Code, Cline, Roo Code, Goose, OpenCode) showed all of them keep
the server namespace in model-facing MCP tool names; the RFC's premise
for raw names ("servers already prefix their tools") is false for the
official GitHub/filesystem/Sentry servers.
- Config: drop toolPrefix; require serverName ([A-Za-z0-9_-]{1,32}),
duplicate serverName fails the later instance at load (per-root
reservation, released on dispose)
- Names: always mcp__<serverName>__<rawName>; normalize to the DeepSeek
64-char [A-Za-z0-9_-] contract with a deterministic 12-hex identity
hash on lossy normalization; raw name is the only thing sent on the
wire (tools/call)
- Sync: two-phase fetch/swap — fetch failure keeps the previous
generation; a swap conflict rolls back the whole generation (never a
partial set); duplicate raw names reject the tool list
- RFC: moved to implemented/ (status + skeleton rewritten per the
format contract), naming design + tier-level test coverage recorded
- Tests: naming algorithm unit suite; keyless Streamable HTTP e2e
against an in-process StreamableHTTPServerTransport (namespace
discovery, execution, per-request auth headers); dotted-name
normalization e2e via a new fixture tool
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
/**
|
|
* Minimal MCP server over stdio for e2e testing of the dsh-mcp-client plugin.
|
|
* Registers controlled tools with predictable behavior for asserting edge cases.
|
|
*
|
|
* Run: node --import tsx fixture-server.ts
|
|
*/
|
|
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
import { z } from 'zod'
|
|
|
|
const server = new McpServer(
|
|
{ name: 'fixture-server', version: '1.0.0' },
|
|
{ capabilities: { tools: { listChanged: true } } },
|
|
)
|
|
|
|
server.registerTool('add', {
|
|
title: 'Add Tool',
|
|
description: 'Adds two numbers.',
|
|
inputSchema: { a: z.number().describe('First number'), b: z.number().describe('Second number') },
|
|
}, async args => ({
|
|
content: [{ type: 'text', text: String(args.a + args.b) }],
|
|
}))
|
|
|
|
server.registerTool('greet', {
|
|
title: 'Greet Tool',
|
|
description: 'Greets a person by name.',
|
|
inputSchema: { name: z.string().describe('Name to greet') },
|
|
}, async args => ({
|
|
content: [{ type: 'text', text: `Hello, ${args.name}!` }],
|
|
}))
|
|
|
|
server.registerTool('fail', {
|
|
title: 'Fail Tool',
|
|
description: 'Always returns an error.',
|
|
inputSchema: {},
|
|
}, async () => ({
|
|
content: [{ type: 'text', text: 'Something went wrong' }],
|
|
isError: true,
|
|
}))
|
|
|
|
server.registerTool('image', {
|
|
title: 'Image Tool',
|
|
description: 'Returns an image content block.',
|
|
inputSchema: {},
|
|
}, async () => ({
|
|
content: [
|
|
{ type: 'text', text: 'Here is an image:' },
|
|
{ type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' },
|
|
{ type: 'text', text: 'End of image.' },
|
|
],
|
|
}))
|
|
|
|
// Dotted name: legal in MCP, illegal in the DeepSeek function-name contract.
|
|
// Exercises the bridge's normalize-and-hash public-name path end to end.
|
|
server.registerTool('admin.reset', {
|
|
title: 'Admin Reset Tool',
|
|
description: 'Tool with a dotted name (normalization test).',
|
|
inputSchema: {},
|
|
}, async () => ({
|
|
content: [{ type: 'text', text: 'reset done' }],
|
|
}))
|
|
|
|
const transport = new StdioServerTransport()
|
|
await server.connect(transport)
|