DeepSeek Harness extensions: choose Tool, MCP, or LLM Adapter without breaking replay

Quick answer

At the source snapshot checked for this guide—official default branch master, commit b150a551b8d465e31e418e1b2eaf5e79bbb7d28e—DeepSeek Harness gives these three extension jobs different contracts:

Local capability owned by your plugin  -> define and register a Tool
Remote capability exposed by a server  -> mount one MCP client plugin per server
New model-provider wire protocol        -> implement and register an LLM Adapter

Do not wrap a local function in MCP just to make it look portable. Do not implement a new model provider as a Tool. Do not let any of the three return prose where the runtime expects a typed JSON value, a protocol block, or an ordered stream event.

DeepSeek Harness remains a Developer Preview. The refreshed snapshot is tagged dsh-v0.1.1-rc.2, and npm exposes @deepseek-ai/dsh@0.1.1-rc.2 under both latest and next. The repository still warns that core plugins and APIs will evolve, so pin the exact package set and commit that your tests cover.

Who this is for

This is for developers adding a business capability, connecting an MCP server, or supporting another model provider without forking the agent loop. Start with the Cordis architecture guide if you have not chosen a plugin seam. Review the tool permission pipeline before granting writes, and the session replay guide before claiming an extension survives resume.

Choose the extension seam

Question Choose Contract you own Do not claim
Is the capability local TypeScript under the same Cordis lifecycle? Tool Input validation, canonical JSON output, rendering, cancellation, and execution policy A Tool schema is an OS sandbox or an authorization policy
Does another process or HTTP endpoint publish MCP tools? MCP client Transport config, namespace, discovery, resync, call timeout, reconnect, output mapping DSH currently bridges MCP Resources or Prompts; it bridges Tools only
Are you translating model requests and streamed provider responses? LLM Adapter Route ownership, request serialization, stream order, errors, usage, replay state, model metadata An OpenAI-compatible label guarantees identical streaming or replay behavior

If two rows seem to apply, separate them. A plugin can mount an MCP client and also register a local administration Tool, but each surface keeps its own schema, lifecycle, and failure evidence.

Minimal typed Tool

For a first-party Tool, use defineTool and register the definition on ctx.tools. Keep one canonical value separate from its human rendering:

import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'project-health'
export const inject = ['tools']

export function apply(ctx) {
  ctx.tools.register(defineTool({
    name: 'project_health',
    description: 'Check one project without changing it.',
    parameters: {
      root: { type: 'string', required: true },
    },
    output: {
      schema: {
        type: 'object',
        properties: {
          ok: { type: 'boolean' },
          findings: { type: 'array', items: { type: 'string' } },
        },
        required: ['ok', 'findings'],
        additionalProperties: false,
      },
      render: (_args, value) => [{
        type: 'text',
        text: value.ok ? 'Project checks passed.' : value.findings.join('\n'),
      }],
    },
    async execute(args, exec) {
      return inspectProject(args.root, { signal: exec.signal })
    },
  }))
}

The typed helper validates model arguments, then validates and freezes the returned JSON before rendering it. Cross-field rules that the schema cannot express still belong in execute. Honor exec.signal. Throw for infrastructure failure; represent a valid but unfavorable domain result in the canonical value. If work must continue after the call returns, publish it through the Job runtime instead of inventing a detached promise.

Policy stays outside the Tool body where possible. Use tools/pre-execute for allow, deny, or ask; ctx.tools.guard() for a monotonic final denial; tools/execute for timeout, retry, or metrics around dispatch; and tools/result to observe the immutable final outcome.

Mount an MCP server without losing tool identity

The official MCP bridge creates one plugin instance per server. Use a stable serverName, choose stdio or streamable-http, and inject secrets from the environment:

- id: mcp-issues
  name: '@deepseek-ai/dsh-mcp-client'
  config:
    serverName: issues
    transport: stdio
    command: npx
    args: ['-y', '@example/issues-mcp']
    env:
      API_TOKEN: !!js process.env.ISSUES_API_TOKEN
    toolCallTimeoutMs: 30000
    failOnStartupError: true

The model-facing name becomes mcp__issues__<rawName>. If normalization or truncation changes it, the bridge adds a deterministic identity hash, so discovery order cannot rename the Tool. Initial discovery registers a complete generation before the first turn. A fetch failure during resync keeps the previous generation; a registration conflict rolls the attempted generation back instead of exposing a partial set.

Treat reconnect and output handling as release gates. During an outage, the last good Tool generation can remain visible while calls fail; after the retry budget is exhausted, the generation is removed. Supported structured output schemas are enforced, while unsupported schema vocabulary falls back to unconstrained JSON. Images need a durable attachment store plus exact model-route capability proof. Audio and embedded resources do not become rich model context. MCP Tools are bridged; Resources and Prompts are currently deferred.

Implement an LLM Adapter as a streaming protocol

An LLM Adapter owns one or more provider routes, not a marketing model name. The minimum shape extends LlmAdapter and registers the instance through ctx.llm.registerAdapter(). The difficult part is the stream contract:

  1. Emit stable block indexes in first-seen order and reuse each index for its later deltas.
  2. Keep tool-call arguments as raw JSON strings; stream fragments as argument deltas.
  3. Emit usage before finish, and emit nothing after finish. Buffer provider tail events if necessary.
  4. Pass options.signal into fetch or the provider SDK.
  5. Throw a stable LlmError for transport or protocol failures, or emit an in-band error/aborted finish for provider-declared termination. Document which path each failure uses.
  6. Reject unsupported generation options instead of silently dropping them.
  7. Emit the minimum lossless replayState when the provider needs response ids or signatures for later turns. Restore it only after validating that the historical and target routes are owned by the same adapter instance and satisfy the adapter's compatibility rule.

Provider credentials belong in the Cordis schema with environment fallbacks. Model discovery belongs in resolveModel(), including verified context, modality, and reasoning-effort metadata. Do not hard-code secrets, infer replay compatibility from matching provider/model strings, or expose provider wire spellings as portable effort ids.

Twelve extension canaries

Run these against a pinned disposable Profile and repository.

# Canary Required evidence
1 Tool rejects a missing or wrong-typed argument execute never runs
2 Tool returns a value outside its output schema Call ends as an explicit error
3 Cancellation fires during Tool work Underlying operation stops and no detached mutation continues
4 Policy denies a write Denial happens before dispatch and final observation records it
5 MCP startup server is unavailable Strict mode fails activation; permissive mode exposes no phantom Tools
6 MCP lists two colliding or duplicate names Whole attempted generation is rejected or rolled back
7 MCP server disconnects and recovers Stable names return once, without schema accumulation
8 MCP returns unsupported rich content or schema vocabulary Bounded diagnostic or JSON fallback is visible; nothing silently disappears
9 Adapter streams text and interleaved Tool calls Indexes and raw argument deltas reconstruct exactly
10 Adapter receives trailing usage Usage appears once before finish; no chunks follow finish
11 Request is aborted or uses an unsupported option Stable aborted/error evidence; no silent option loss
12 A recorded turn is replayed after route change Restore only when adapter-owned replay compatibility passes

Release and compatibility checklist

Common mistakes

FAQ

Should a local database integration be a Tool or MCP server?

Use a Tool when the integration lives inside the same trusted deployment and you own its lifecycle. Use MCP when a separate server boundary, protocol reuse, or independent deployment is the requirement. The security decision still depends on credentials, policy, process isolation, and data access—not the label.

Can one MCP client expose thousands of Tools efficiently?

The official bridge registers every discovered Tool schema. That is correct for identity and lifecycle but can expand the model-visible catalog. Add progressive disclosure only through a scoped Tool-registry design that keeps presentation, lookup, and execution aligned; do not hide Tools in the prompt while leaving them executable through another path.

Does an OpenAI-compatible endpoint remove the need for an Adapter?

Only if an existing Adapter already matches the endpoint's request, stream, errors, usage, Tool calls, model metadata, cancellation, and replay behavior. Compatibility of one HTTP shape is not proof of the full Harness contract.

Sources