DSH Quality
Intermediate
8 min read

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.

Last updated: 2026-08-19

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:

ts
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:

ts
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.