Custom MCP tool development guide
DuDuClawのMCP Serverに新しいツールを追加する方法 対象バージョン:v0.12.0+
Overview
Section titled “Overview”DuDuClawはstdin/stdout上のJSON-RPC 2.0経由で、200以上のMCPツールを公開しています(v1.56時点で206個)。本ガイドでは、Claude Codeと連携するカスタムツールを追加する方法を説明します。
Architecture
Section titled “Architecture”Claude Code (client) ↕ JSON-RPC 2.0 (stdin/stdout)DuDuClaw MCP Server (crates/duduclaw-cli/src/mcp.rs) ↕ Rust function callsTool handlers (gateway, agent, memory, inference, etc.)Step 1: Define the tool
Section titled “Step 1: Define the tool”crates/duduclaw-cli/src/mcp.rsのTOOLS配列に新しいToolDefエントリを追加します。
ToolDef { name: "my_custom_tool", description: "Brief description of what this tool does", params: &[ ParamDef { name: "input", description: "The input parameter", required: true, }, ParamDef { name: "options", description: "Optional configuration", required: false, }, ],},Naming conventions
Section titled “Naming conventions”- ツール名は
snake_caseを使う - 関連するツールは共通の接頭辞でグループ化する:
odoo_*、model_*、cost_* - 名前は簡潔かつ説明的に保つ
Parameter rules
Section titled “Parameter rules”required: true— Claude Codeが必ずこのパラメータを渡さなければならないrequired: false— 任意項目。tool handler側でデフォルト値を用意する- すべてのパラメータはJSON値(
serde_json::Value)として渡される
Step 2: Implement the handler
Section titled “Step 2: Implement the handler”handle_tool_call()関数にmatchアームを追加します。
"my_custom_tool" => { let input = get_string_param(¶ms, "input")?; let options = params.get("options") .and_then(|v| v.as_str()) .unwrap_or("default");
// Your logic here let result = do_something(input, options).await?;
Ok(json!({ "status": "ok", "result": result }))}Error handling
Section titled “Error handling”エラーはpanicではなく、構造化されたJSONとして返します。
// Good: structured errorif input.is_empty() { return Ok(json!({ "status": "error", "error": "input parameter cannot be empty" }));}
// Bad: panicassert!(!input.is_empty()); // Never do this in a tool handlerAsync operations
Section titled “Async operations”すべてのtool handlerはTokioの非同期コンテキスト上で動きます。I/Oには.awaitを使ってください。
"my_async_tool" => { let url = get_string_param(¶ms, "url")?;
let response = reqwest::get(&url).await .map_err(|e| DuDuClawError::Network(e.to_string()))?;
let body = response.text().await .map_err(|e| DuDuClawError::Network(e.to_string()))?;
Ok(json!({ "status": "ok", "content": body }))}Step 3: Test the tool
Section titled “Step 3: Test the tool”Unit test
Section titled “Unit test”同じファイル内、または専用のテストモジュールにテストを追加します。
#[cfg(test)]mod tests { use super::*;
#[tokio::test] async fn test_my_custom_tool() { let params = json!({ "input": "test value", "options": "custom" });
let result = handle_tool_call("my_custom_tool", ¶ms).await; assert!(result.is_ok());
let value = result.unwrap(); assert_eq!(value["status"], "ok"); }}Manual test with Claude Code
Section titled “Manual test with Claude Code”# Start the MCP serverduduclaw mcp-server
# In another terminal, verify the tool appears in the tool listecho '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | duduclaw mcp-server続いて、Claude CodeがDuDuClawをMCP serverとして使うよう設定します。
{ "mcpServers": { "duduclaw": { "command": "duduclaw", "args": ["mcp-server"] } }}Step 4: Document the tool
Section titled “Step 4: Document the tool”crates/duduclaw-cli/src/mcp.rsのツール一覧コメントにそのツールを追記し、重要な機能を表す場合はdocs/CLAUDE.mdも更新してください。
Patterns & best practices
Section titled “Patterns & best practices”Accessing agent state
Section titled “Accessing agent state”多くのツールはagentの設定や状態へのアクセスを必要とします。
"agent_info_tool" => { let agent_name = get_string_param(¶ms, "agent")?; let agents_dir = duduclaw_agent::get_agents_dir(); let config = duduclaw_agent::load_agent_config(&agents_dir, &agent_name)?;
Ok(json!({ "status": "ok", "agent": config.identity.name, "role": format!("{:?}", config.identity.role), }))}Accessing memory
Section titled “Accessing memory”"memory_tool" => { let agent_id = get_string_param(¶ms, "agent_id")?; let query = get_string_param(¶ms, "query")?;
let engine = SqliteMemoryEngine::open(&memory_db_path(&agent_id))?; let results = engine.search(&query, 10).await?;
Ok(json!({ "status": "ok", "memories": results.iter().map(|m| json!({ "content": m.content, "tags": m.tags, "importance": m.importance, })).collect::<Vec<_>>() }))}Rate limiting
Section titled “Rate limiting”外部APIを呼ぶツールにはレート制限をかけてください。
use duduclaw_security::rate_limiter::RateLimiter;
static LIMITER: OnceLock<RateLimiter> = OnceLock::new();
"external_api_tool" => { let limiter = LIMITER.get_or_init(|| RateLimiter::new(10, Duration::from_secs(60))); if !limiter.check("external_api") { return Ok(json!({ "status": "error", "error": "rate limit exceeded, try again in 60s" })); } // ... call external API}Security checklist
Section titled “Security checklist”新しいツールをマージする前に確認してください。
- すべてのパラメータに入力検証があるか
- 秘密情報がハードコードされていないか
- 外部API呼び出しにレート制限があるか
- URLパラメータにSSRF対策があるか(
web_fetchのパターンを使う) - 機微な操作に監査ログが残るか
- Pro/Enterprise限定のツールであればfeature gateのチェックがあるか
agent.toml [capabilities] allowed_tools / denied_toolsは、もはやツールごとに個別チェックする必要はありません。stdio、HTTP/SSE、そしてopenai-compat tool-loop内部のMCP clientを含むすべての呼び出しは、共通のMcpDispatcher::dispatch_tool_callという単一のチョークポイント(mcp_dispatch.rs)を経由します。ここで呼び出し元の[capabilities]許可/拒否リストがツールのベース名と照合され(mcp__<server>__のような修飾子は照合前に取り除かれ、denied_toolsは常にallowed_toolsより優先されます)、通過してはじめてあなたのhandlerが呼ばれます。handle_tool_call()に新しく登録したツールは自動的にこの保護下に入ります。これは実在した抜け道を塞ぐものでした。この強制がチョークポイントに移される前は、allowed_tools / denied_toolsはClaude CLI spawnの--allowedTools / --disallowedToolsフラグにしか届いておらず、MCP serverと直接話す呼び出し元(CLI spawnを迂回する経路)はこれらの制限を一切受けていませんでした。
JSON-RPC protocol reference
Section titled “JSON-RPC protocol reference”Request format
Section titled “Request format”{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "my_custom_tool", "arguments": { "input": "value", "options": "config" } }}Response format (success)
Section titled “Response format (success)”{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\"status\":\"ok\",\"result\":\"...\"}" } ] }}Response format (error)
Section titled “Response format (error)”{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Missing required parameter: input" }}Tool categories
Section titled “Tool categories”新しいツールを追加する際は、既存のカテゴリ命名規則に従ってください。
| 接頭辞 | カテゴリ | 例 |
|---|---|---|
send_* |
メッセージ送信 | send_message, send_photo, send_sticker |
web_* |
Web/検索 | web_search, web_fetch_cached, web_extract |
agent_* |
Agent管理 | agent_status, agent_update, agent_remove |
memory_* |
メモリ操作 | memory_search, memory_store |
model_* |
モデル管理 | model_list, model_load, model_unload |
inference_* |
推論制御 | inference_status, inference_mode |
llamafile_* |
Llamafileライフサイクル | llamafile_start, llamafile_stop |
cost_* |
コストテレメトリ | cost_summary, cost_agents, cost_recent |
odoo_* |
Odoo ERP | odoo_crm_leads, odoo_sale_orders |
skill_* |
Skillエコシステム | skill_search, skill_list |
| (なし) | 単独ツール | emergency_stop, tool_approve, schedule_task |