DeepSeek Harness: Developing Your First Tool: defineTool
Last updated: 2026-08-31
Tools are the Agent's hands — defining a tool gives the Agent a new capability. defineTool is DSH's tool definition DSL, using a declarative approach to describe a tool's name, parameters, and execution logic, so the Agent understands when and how to call your tool.
📋 Prerequisites: Completed 14-inject.md, understand dependency injection
1. What You'll Learn
- defineTool DSL syntax
- name/description/parameters declarations
- JSON Schema parameter definitions
- execute function implementation
- Tool registration to ctx.tools
- Tool display in the Web UI
- Complete example: file count tool
2. defineTool DSL Syntax
(1) ▶ Example 1
import { defineTool } from '@deepseek-ai/dsh'
export default defineTool({
name: 'tool_name',
description: 'What this tool does',
parameters: {
type: 'object',
properties: { /* ... */ },
required: [ /* ... */ ]
},
async execute(params, ctx) {
// Tool logic
return result
}
})
(2) Field Descriptions
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ✅ | Tool unique identifier, lowercase + underscores |
description |
string | ✅ | Tool functionality description, for the LLM to read |
parameters |
JSON Schema | ✅ | Parameter definition |
execute |
function | ✅ | Execution logic |
(3) Export Methods
defineTool returns a tool definition object that can be directly used as the default export:
// Method 1: Default export
export default defineTool({ ... })
// Method 2: Named export
export const myTool = defineTool({ ... })
3. name and description
(1) Naming Convention
Tool names use snake_case format:
name: 'file_count' // ✅
name: 'fileCount' // ❌ Not recommended
name: 'FileCount' // ❌ Not recommended
name: 'file-count' // ❌ Not recommended
(2) Description Writing
The description is the basis for the LLM to choose a tool. Key points:
- Explain what the tool does
- Explain when it should be used
- Avoid meaningless descriptions like "test tool" or "example tool"
// ❌ Poor description
description: 'A tool for testing'
// ✅ Good description
description: 'Count the number of files in a directory. Returns total count and breakdown by file extension. Use when user asks about file statistics or directory contents.'
(3) Multi-language Description
Descriptions currently only support English. The LLM understands tool purpose from the description, even if user input is in another language.
4. JSON Schema Parameter Definition
(1) Basic Structure
parameters follow the JSON Schema specification:
parameters: {
type: 'object',
properties: {
param_name: {
type: 'string',
description: 'Parameter description'
}
},
required: ['param_name']
}
(2) Supported Types
| JSON Schema Type | TypeScript Type | Description |
|---|---|---|
string |
string | String |
number |
number | Number |
integer |
number | Integer |
boolean |
boolean | Boolean |
object |
object | Nested object |
array |
array | Array |
(3) String Parameters
properties: {
path: {
type: 'string',
description: 'Directory path to count files in'
},
pattern: {
type: 'string',
description: 'Glob pattern to filter files (e.g. *.ts)',
default: '*'
}
}
(4) Enum Parameters
properties: {
sort_by: {
type: 'string',
enum: ['name', 'size', 'date'],
description: 'Sort criteria'
}
}
(5) Array Parameters
properties: {
extensions: {
type: 'array',
items: { type: 'string' },
description: 'File extensions to include (e.g. [".ts", ".js"])'
}
}
(6) Nested Objects
properties: {
options: {
type: 'object',
properties: {
recursive: { type: 'boolean', default: false },
includeHidden: { type: 'boolean', default: false }
}
}
}
(7) required Field
required: ['path'] // path is required
// pattern has default, not required
5. execute Function Implementation
(1) Function Signature
async execute(params: Params, ctx: Context): Promise<Result>
params: Parameters passed by the user (Agent), type inferred from parameters definitionctx: Cordis context, can access injected services- Return value: Any JSON-serializable data
(2) ▶ Example 2
async execute({ path }, ctx) {
const count = await countFiles(path)
return { count, path }
}
(3) Accessing Services
Inside execute, you can access registered services via ctx:
export const inject = ['fs']
export default defineTool({
name: 'file_count',
// ...
async execute({ path }, ctx) {
const files = await ctx.fs.readdir(path)
return { count: files.length }
}
})
(4) Error Handling
async execute({ path }, ctx) {
try {
const files = await ctx.fs.readdir(path)
return { count: files.length, path }
} catch (error) {
return {
error: true,
message: `Failed to read directory: ${error.message}`
}
}
}
Tools should not throw uncaught exceptions — returning an error object lets the Agent understand the failure reason, which is friendlier than crashing.
(5) Return Value Format
Recommended to return structured objects:
// ✅ Structured return
return {
count: 42,
path: '/home/alice/project',
breakdown: {
typescript: 28,
javascript: 10,
other: 4
}
}
// ❌ Plain text return
return 'Found 42 files in /home/alice/project'
Structured returns let the Agent use results programmatically, rather than just displaying text.
6. Tool Registration to ctx.tools
(1) Registering in a Plugin
Tools defined with defineTool need to be registered to ctx.tools through a plugin:
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'
const fileCountTool = defineTool({
name: 'file_count',
description: 'Count files in a directory',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'Directory path' }
},
required: ['path']
},
async execute({ path }, ctx) {
const files = await ctx.fs.readdir(path)
return { count: files.length }
}
})
export const name = 'tool-file-count'
export const inject = ['tools', 'fs']
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
}
(2) Registering Multiple Tools
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
ctx.tools.register(fileSizeTool)
ctx.tools.register(fileSearchTool)
}
(3) Dynamic Registration
export function apply(ctx: Context) {
const tools = [fileCountTool, fileSizeTool]
for (const tool of tools) {
ctx.tools.register(tool)
ctx.logger.info(`registered tool: ${tool.name}`)
}
}
7. Tool Display in the Web UI
(1) Automatic Display
Tools registered to ctx.tools automatically appear in the Web UI's tool list:
┌─────────────────────────────────────┐
│ 🔧 Tools │
├─────────────────────────────────────┤
│ file_count │ Count files in a dir │
│ file_size │ Get file size info │
│ file_search │ Search for files │
└─────────────────────────────────────┘
(2) name and description Display
name: Displayed as the tool identifierdescription: Displayed as the tool description
(3) Agent Call Display
When the Agent calls your tool, the Web UI shows:
🤖 Agent:
🔧 Using tool: file_count
→ path: /home/alice/project
Result: { count: 42, path: "/home/alice/project" }
▶ Example 8: File Count Tool
Combine all the knowledge into a complete tool plugin:
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'
export const name = 'tool-file-count'
export const inject = ['tools', 'fs']
const fileCountTool = defineTool({
name: 'file_count',
description: 'Count the number of files in a directory. Returns total count and breakdown by file extension. Use when user asks about file statistics or directory contents.',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Absolute or relative path to the directory'
},
recursive: {
type: 'boolean',
description: 'Whether to count files in subdirectories',
default: false
},
extensions: {
type: 'array',
items: { type: 'string' },
description: 'Filter by file extensions (e.g. [".ts", ".js"]). Count all if omitted.'
}
},
required: ['path']
},
async execute({ path, recursive, extensions }, ctx) {
try {
const entries = recursive
? await ctx.fs.readdirRecursive(path)
: await ctx.fs.readdir(path)
let files = entries.filter(e => !e.isDirectory)
if (extensions && extensions.length > 0) {
files = files.filter(f =>
extensions.some(ext => f.name.endsWith(ext))
)
}
const breakdown: Record<string, number> = {}
for (const f of files) {
const ext = f.name.includes('.')
? '.' + f.name.split('.').pop()
: '(no extension)'
breakdown[ext] = (breakdown[ext] || 0) + 1
}
return {
count: files.length,
path,
recursive,
breakdown
}
} catch (error: any) {
return {
error: true,
message: `Failed to count files: ${error.message}`
}
}
}
})
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
ctx.logger.info('file_count tool registered')
}
Start and verify:
# Register in cordis.yml
pnpm dsh web --patch
# Test in Web UI
# 👤 Alice: How many files are in the /home/alice/project directory?
# 🤖 Agent: 🔧 file_count → { count: 42, breakdown: { ".ts": 28, ".js": 10, ".json": 4 } }
❓ FAQ
defineTool is a DSL that creates a tool definition object. ctx.tools.register is the registration method that adds the definition to the tool service. They work together: first defineTool defines, then register registers.ctx.tools.execute('other_tool', params). But be careful to avoid circular calls.typescript async execute(params, ctx) { ctx.logger.info('params:', JSON.stringify(params)) // ... } 📖 Summary
- defineTool is DSH's tool definition DSL: name + description + parameters + execute
- name uses snake_case; description should clearly explain the tool's purpose and use cases
- parameters follow JSON Schema, supporting string/number/boolean/object/array
- execute receives params and ctx, returns structured results
- Tools are registered via
ctx.tools.register()and automatically appear in the Web UI - Return structured objects over plain text; on errors, return error objects instead of throwing exceptions
📝 Exercises
1. ⭐ Basic: Use defineTool to create a current_time tool that accepts an optional timezone parameter (default UTC) and returns the current time string. Register it in DSH and have the Agent successfully call it.
2. ⭐⭐ Intermediate: Extend the file_count tool by adding min_size and max_size parameters (in bytes) to filter by file size range. Test: count files in /tmp larger than 1KB and smaller than 1MB.
3. ⭐⭐⭐ Challenge: Create a code_stats tool that counts lines of code in a specified directory. Parameters: path (directory path), languages (language filter array). Return total lines, per-language lines, blank lines, comment lines. Use regex to distinguish code lines / blank lines / comment lines.