This tutorial walks you through building a tool for DeepSeek Harness: declare a parameter schema, execute, and output rendering with defineTool, register it with ctx.tools.register, and let the model call your code in conversation. Tools are the most model-facing kind of plugin capability — once you finish, the model gains a new skill.
Prerequisites
Before you start, make sure:
- you have completed Build Your First Plugin:
scratch-plugin/exists at the repo root andcordis.ymlalready registerssrc/my-plugin.ts; pnpm dsh web --patch ./scratch-plugin/cordis.ymllaunches the web UI;- you understand the three exports
name,inject, andapply.
How a tool works
A tool lets the model call your code during a conversation. A regular plugin loads passively at startup and does background work; a tool exposes a capability to the model in a structured way — the model decides when to call it and what arguments to pass, then folds the result into its reply.
A tool call has five steps: the model reads the tool’s name and description and decides whether the current question needs it; if so, it assembles arguments per the parameters schema; the framework validates them and hands them to execute; execute’s return value is converted to content blocks by output.render; the model reads the result and writes its final reply.
Write the tool plugin
Open scratch-plugin/src/my-plugin.ts and replace its contents with:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
Compared with the first plugin, the skeleton is unchanged: it still exports the name, inject, apply trio. What changed is that apply now registers a tool defined with defineTool via ctx.tools.register — this is the star of the tutorial.
Understanding defineTool field by field
Every field of defineTool maps to one link in the chain. Let’s go through them:
name and description: a manual written for the model
name is the tool’s call identifier — the model references it by name in tool calls, so a lowercase verb phrase such as greet or search_docs is clearest. description is usage guidance written for the model: it decides “when should I use this tool” from this text. Describe the scenario, not the implementation.
parameters: the argument contract
parameters declares each argument: type is one of string, number, boolean; required marks it mandatory; description is shown to the model as well. defineTool infers and validates args against this schema at runtime, so execute receives typed, validated arguments.
output and execute: separate execution from presentation
execute is the tool’s actual logic and returns the canonical value declared by output.schema — a string here. output.render converts that value into model-readable content blocks, e.g. [{ type: "text", text: value }]. Splitting execution from presentation means execute only cares about computing the right result, while how it is presented can evolve independently.
Why you must inject tools
The tools service is a built-in Harness service — a plugin must declare the dependency before using it safely:
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
// the tools service is ready — ctx.tools is guaranteed to exist
ctx.tools.register(defineTool({ /* ... */ }))
}
Restart and verify
With the plugin code in place, restart to load it:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# open http://127.0.0.1:3080
Type this into the chat box:
Use the greet tool to say hi to Ada.
The model recognizes that greet fits, calls it with name: 'Ada', receives Hello, Ada!, and works it into its reply. Seeing the greeting in the reply means the whole tool chain works.
Writing a good description
Whether a tool gets used at all comes down to its description. On every turn the model reads the name and description of every registered tool and decides from them. Compare two styles:
// bad: implementation detail — the model cannot tell when to use it
description: 'calls the greet function and returns a string'
// good: usage guidance — the model knows when to reach for it
description: 'Greet someone by name when the user asks to say hello.'
Each parameter’s description is shown to the model too — the clearer the argument, the less likely the model passes the wrong value. Treat the model as a new teammate: explain what the tool does, when to use it, and how to fill in each argument, and it will use the tool correctly.
Iterate and debug
Tool development is a fast iteration loop:
# 1. edit scratch-plugin/src/my-plugin.ts
# 2. restart
pnpm dsh web --patch ./scratch-plugin/cordis.yml
# 3. verify at http://127.0.0.1:3080 with different phrasings
One plugin can register multiple tools — call ctx.tools.register as many times as you like. This is a natural way to ship several capabilities from one domain together.
Tool development FAQ
The model never calls my tool — what now?
First check whether description explains the usage scenario clearly — the model decides from it. Then make sure the tool name is intuitive and each parameter’s description is explicit. You can explicitly prompt “use the greet tool to say hi to Ada” to verify the tool itself works, then gradually remove the hint to test the model’s autonomous choice.
What happens if execute throws?
The error is returned to the model as a failed tool call. The model typically retries, tries different arguments, or tells the user the call failed. During development, log inside execute and restart to pinpoint the issue quickly.
What does output.schema declare?
It declares the canonical type of execute’s return value (a string in this example). render converts that value into model-readable content blocks. The schema is a contract for both the framework and the model: the framework knows the shape of the result, and the model knows what it will receive.
Can one plugin register multiple tools?
Yes. Call ctx.tools.register multiple times inside apply, once per defineTool definition. Shipping several tools from one domain in a single plugin is a common pattern.
Do I have to restart after editing tool code?
Yes. The plugin module is loaded once at startup — rerun pnpm dsh web --patch ./scratch-plugin/cordis.yml after each edit. During development the patch only contributes config and never touches your global profile, so restarting is side-effect free.