Bridge an MCP Server or Custom Tool into DeepSeek Harness
Two paths to give your model real capabilities: register a custom tool with ctx.tools.register, or wrap an existing MCP server into a dsh plugin.
Tool vs command: who calls whom
A command is triggered by the user through the UI. A tool is called by the model itself when it decides a function would help. If you want the agent to act on its own, you register a tool.
Path A — register a custom tool
The minimal registration node:
ctx.tools.register({
name: 'read_local_file',
description: 'Read a text file from the workspace',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Relative path' },
},
required: ['path'],
},
async execute(args) {
try {
return { ok: true, content: await fs.readFile(args.path, 'utf8') };
} catch (err) {
// Errors must be surfaced to the model, not swallowed
return { ok: false, error: String(err) };
}
},
});Three details matter: a clear description (it steers model selection), a strict JSON Schema for parameters, and structured error returns so the model can recover and retry.
Path B — wrap an existing MCP server
MCP (Model Context Protocol) servers expose tools over stdio or HTTP. Instead of re-implementing them, start the server inside your plugin and register its tools on ctx:
import { startMcpClient } from '@dsh/mcp';
export async function apply(ctx) {
const client = await startMcpClient({
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', './'],
});
const tools = await client.listTools();
for (const tool of tools) {
ctx.tools.register({
name: `mcp_${tool.name}`,
description: tool.description,
parameters: tool.inputSchema,
execute: (args) => client.callTool(tool.name, args),
});
}
}For remote servers, pass a url instead of command/args. Either way, the model now sees one more tool in its toolbox — no protocol work on the model side.
Verify end to end
Start dsh, open a session and ask the model to do something that requires the tool (e.g. "what is in ./package.json?"). Watch the model call read_local_file (or mcp_filesystem_*) on its own, then answer from the result.
Related teardowns
Real plugins show the same pattern at scale: modlens returns structured visual evidence as a tool result, and dsh-at-file extends the input channel with Codex-style @file references.