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.

💡 Tip: The core of defineTool is "making the LLM understand your tool" — name and description are for the LLM, parameters describe the input format, and execute is the actual implementation. Write good descriptions so the Agent selects your tool in the right scenarios.

📋 Prerequisites: Completed 14-inject.md, understand dependency injection

1. What You'll Learn


Tool Pipeline

2. defineTool DSL Syntax

(1) ▶ Example 1

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

TYPESCRIPT
// 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:

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

TYPESCRIPT
// ❌ 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:

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
properties: {
  sort_by: {
    type: 'string',
    enum: ['name', 'size', 'date'],
    description: 'Sort criteria'
  }
}

(5) Array Parameters

TYPESCRIPT
properties: {
  extensions: {
    type: 'array',
    items: { type: 'string' },
    description: 'File extensions to include (e.g. [".ts", ".js"])'
  }
}

(6) Nested Objects

TYPESCRIPT
properties: {
  options: {
    type: 'object',
    properties: {
      recursive: { type: 'boolean', default: false },
      includeHidden: { type: 'boolean', default: false }
    }
  }
}

(7) required Field

TYPESCRIPT
required: ['path']          // path is required
// pattern has default, not required

5. execute Function Implementation

(1) Function Signature

TYPESCRIPT
async execute(params: Params, ctx: Context): Promise<Result>

(2) ▶ Example 2

TYPESCRIPT
async execute({ path }, ctx) {
  const count = await countFiles(path)
  return { count, path }
}

(3) Accessing Services

Inside execute, you can access registered services via ctx:

TYPESCRIPT
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

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

TYPESCRIPT
// ✅ 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:

TYPESCRIPT
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

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.tools.register(fileCountTool)
  ctx.tools.register(fileSizeTool)
  ctx.tools.register(fileSearchTool)
}

(3) Dynamic Registration

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

TEXT 📖 Display only
┌─────────────────────────────────────┐
│ 🔧 Tools                            │
├─────────────────────────────────────┤
│ file_count  │ Count files in a dir  │
│ file_size   │ Get file size info    │
│ file_search │ Search for files      │
└─────────────────────────────────────┘

(2) name and description Display

(3) Agent Call Display

When the Agent calls your tool, the Web UI shows:

TEXT 📖 Display only
🤖 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:

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

BASH
# 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

Q What's the difference between defineTool and ctx.tools.register?
A 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.
Q Can tool names be duplicated?
A No. A later-registered tool with the same name overwrites the earlier one. If you need same-name tools to coexist, use scope isolation (see 20-scope.md).
Q Can I skip description in parameters?
A Technically yes, but strongly not recommended. Parameter descriptions help the LLM understand parameter meaning; missing them leads to incorrect parameter passing by the Agent.
Q Can execute return streaming data?
A Currently defineTool's execute only supports returning complete results. Streaming output is achieved through the LLM adapter's StreamChunk protocol (see 25-stream-error.md).
Q Can a tool call other tools?
A Yes, via ctx.tools.execute('other_tool', params). But be careful to avoid circular calls.
Q How do I debug tool parameter parsing?
A Log params at the start of execute: typescript async execute(params, ctx) { ctx.logger.info('params:', JSON.stringify(params)) // ... }

📖 Summary


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

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏