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:
- Emit stable block indexes in first-seen order and reuse each index for its later deltas.
- Keep tool-call arguments as raw JSON strings; stream fragments as argument deltas.
- Emit usage before finish, and emit nothing after finish. Buffer provider tail events if necessary.
- Pass
options.signalinto fetch or the provider SDK. - Throw a stable
LlmErrorfor transport or protocol failures, or emit an in-band error/aborted finish for provider-declared termination. Document which path each failure uses. - Reject unsupported generation options instead of silently dropping them.
- Emit the minimum lossless
replayStatewhen 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
- Pin the Harness commit, all related package versions, Profile, Node version, transport/SDK version, and provider route.
- Publish the Tool input/output schema, MCP server namespace and transport, or Adapter stream/replay contract as the interface under test.
- Test disposal and HMR: registrations must disappear with their owning plugin and return exactly once.
- Record which failures are domain results, Tool errors, transport errors, in-band model finishes, or activation failures.
- Verify redaction: no API token, authorization header, raw secret, or sensitive MCP payload enters logs or session history.
- Replay a golden session and compare model-visible Tool names, schemas, canonical results, stream order, usage, finish reason, and replay state.
- Keep a rollback package set and configuration snapshot. Developer Preview compatibility is not a reason to mutate production Profiles in place.
Common mistakes
- Returning rendered prose as the Tool's canonical result, then parsing it later.
- Treating argument validation as authorization or filesystem/process isolation.
- Giving every MCP server the same namespace, or reporting discovery as successful execution.
- Assuming MCP Resources and Prompts are available because the remote server advertises them.
- Passing parsed Tool arguments through an LLM stream when the runtime requires raw JSON strings.
- Emitting finish before late usage, silently ignoring unsupported options, or restoring provider-native state by name alone.
- Calling a Git tag, npm publication, plugin activation, or Tool id proof of a successful user task.
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
- DeepSeek Harness Developer Preview and plugin model
- dsh-v0.1.1-rc.2 release
- Extension plugin shapes at commit b150a551
- Typed Tool authoring and execution contract
- LLM Adapter streaming and replay contract
- Official MCP client configuration, behavior, and limitations
- MCP naming, generation swap, schema, and result mapping source
- DeepSeek LLM Adapter reference implementation