mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-03-19 11:13:08 +00:00
Three skills guiding developers through MCP server design: - build-mcp-server: entry-point decision guide (remote HTTP vs MCPB vs local) - build-mcp-app: interactive UI widgets rendered in chat - build-mcpb: bundled local servers with runtime Includes reference files for scaffolds, tool design, auth (DCR/CIMD), widget templates, manifest schema, and local security hardening.
113 lines
3.6 KiB
Markdown
113 lines
3.6 KiB
Markdown
# Tool Design — Writing Tools Claude Uses Correctly
|
||
|
||
Tool schemas and descriptions are prompt engineering. They land directly in Claude's context and determine whether Claude picks the right tool with the right arguments. Most MCP integration bugs trace back to vague descriptions or loose schemas.
|
||
|
||
---
|
||
|
||
## Descriptions
|
||
|
||
**The description is the contract.** It's the only thing Claude reads before deciding whether to call the tool. Write it like a one-line manpage entry plus disambiguating hints.
|
||
|
||
### Good
|
||
|
||
```
|
||
search_issues — Search issues by keyword across title and body. Returns up
|
||
to `limit` results ranked by recency. Does NOT search comments or PRs —
|
||
use search_comments / search_prs for those.
|
||
```
|
||
|
||
- Says what it does
|
||
- Says what it returns
|
||
- Says what it *doesn't* do (prevents wrong-tool calls)
|
||
|
||
### Bad
|
||
|
||
```
|
||
search_issues — Searches for issues.
|
||
```
|
||
|
||
Claude will call this for anything vaguely search-shaped, including things it can't do.
|
||
|
||
### Disambiguate siblings
|
||
|
||
When two tools are similar, each description should say when to use the *other* one:
|
||
|
||
```
|
||
get_user — Fetch a user by ID. If you only have an email, use find_user_by_email.
|
||
find_user_by_email — Look up a user by email address. Returns null if not found.
|
||
```
|
||
|
||
---
|
||
|
||
## Parameter schemas
|
||
|
||
**Tight schemas prevent bad calls.** Every constraint you express in the schema is one fewer thing that can go wrong at runtime.
|
||
|
||
| Instead of | Use |
|
||
|---|---|
|
||
| `z.string()` for an ID | `z.string().regex(/^usr_[a-z0-9]{12}$/)` |
|
||
| `z.number()` for a limit | `z.number().int().min(1).max(100).default(20)` |
|
||
| `z.string()` for a choice | `z.enum(["open", "closed", "all"])` |
|
||
| optional with no hint | `.optional().describe("Defaults to the caller's workspace")` |
|
||
|
||
**Describe every parameter.** The `.describe()` text shows up in the schema Claude sees. Omitting it is leaving money on the table.
|
||
|
||
```typescript
|
||
{
|
||
query: z.string().describe("Keywords to search for. Supports quoted phrases."),
|
||
status: z.enum(["open", "closed", "all"]).default("open")
|
||
.describe("Filter by status. Use 'all' to include closed items."),
|
||
limit: z.number().int().min(1).max(50).default(10)
|
||
.describe("Max results. Hard cap at 50."),
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Return shapes
|
||
|
||
Claude reads whatever you put in `content[].text`. Make it parseable.
|
||
|
||
**Do:**
|
||
- Return JSON for structured data (`JSON.stringify(result, null, 2)`)
|
||
- Return short confirmations for mutations (`"Created issue #123"`)
|
||
- Include IDs Claude will need for follow-up calls
|
||
- Truncate huge payloads and say so (`"Showing 10 of 847 results. Refine the query to narrow down."`)
|
||
|
||
**Don't:**
|
||
- Return raw HTML
|
||
- Return megabytes of unfiltered API response
|
||
- Return bare success with no identifier (`"ok"` after a create — Claude can't reference what it made)
|
||
|
||
---
|
||
|
||
## How many tools?
|
||
|
||
| Tool count | Guidance |
|
||
|---|---|
|
||
| 1–15 | One tool per action. Sweet spot. |
|
||
| 15–30 | Still workable. Audit for near-duplicates that could merge. |
|
||
| 30+ | Switch to search + execute. Optionally promote the top 3–5 to dedicated tools. |
|
||
|
||
The ceiling isn't a hard protocol limit — it's context-window economics. Every tool schema is tokens Claude spends *every turn*. Thirty tools with rich schemas can eat 3–5k tokens before the conversation even starts.
|
||
|
||
---
|
||
|
||
## Errors
|
||
|
||
Return MCP tool errors, not exceptions that crash the transport. Include enough detail for Claude to recover or retry differently.
|
||
|
||
```typescript
|
||
if (!item) {
|
||
return {
|
||
isError: true,
|
||
content: [{
|
||
type: "text",
|
||
text: `Item ${id} not found. Use search_items to find valid IDs.`,
|
||
}],
|
||
};
|
||
}
|
||
```
|
||
|
||
The hint ("use search_items…") turns a dead end into a next step.
|