$npx -y skills add softspark/ai-toolkit --skill mcp-patternsMCP server design: tool schemas, resources, stdio/SSE, capability negotiation. Triggers: MCP, Model Context Protocol, JSON-RPC, stdio, SSE, Claude Desktop.
| 1 | # MCP Patterns Skill |
| 2 | |
| 3 | ## MCP Specification (2025-06-18) |
| 4 | |
| 5 | ### Core Concepts |
| 6 | |
| 7 | | Concept | Description | |
| 8 | |---------|-------------| |
| 9 | | **Server** | Exposes tools, resources, prompts to clients | |
| 10 | | **Client** | Connects to servers, invokes tools | |
| 11 | | **Transport** | Communication layer (stdio, HTTP, SSE) | |
| 12 | | **Tool** | Executable function with JSON Schema | |
| 13 | | **Resource** | Read-only data (files, URLs) | |
| 14 | | **Prompt** | Reusable prompt template | |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## Tool Definition Pattern |
| 19 | |
| 20 | ```typescript |
| 21 | // TypeScript with @modelcontextprotocol/sdk |
| 22 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ |
| 23 | tools: [{ |
| 24 | name: "search_kb", |
| 25 | description: "Search the knowledge base", |
| 26 | inputSchema: { |
| 27 | type: "object", |
| 28 | properties: { |
| 29 | query: { |
| 30 | type: "string", |
| 31 | description: "Search query" |
| 32 | }, |
| 33 | limit: { |
| 34 | type: "number", |
| 35 | description: "Max results", |
| 36 | default: 10 |
| 37 | } |
| 38 | }, |
| 39 | required: ["query"] |
| 40 | }, |
| 41 | annotations: { |
| 42 | readOnlyHint: true, // Doesn't modify state |
| 43 | idempotentHint: true, // Same input = same output |
| 44 | openWorldHint: false // Bounded result set |
| 45 | } |
| 46 | }] |
| 47 | })); |
| 48 | ``` |
| 49 | |
| 50 | ### Tool Annotations |
| 51 | |
| 52 | | Annotation | Meaning | Use When | |
| 53 | |------------|---------|----------| |
| 54 | | `readOnlyHint` | No side effects | Read operations | |
| 55 | | `destructiveHint` | Deletes/modifies data | Write operations | |
| 56 | | `idempotentHint` | Safe to retry | GET-like operations | |
| 57 | | `openWorldHint` | Results may change | External API calls | |
| 58 | |
| 59 | --- |
| 60 | |
| 61 | ## How to Write a Tool Description |
| 62 | |
| 63 | The `description` is the only signal the model uses to route a request. The schema constrains the *call*; the description decides *whether the call happens at all*. Treat it as a routing contract, not API prose. A good one has five parts, in this order: |
| 64 | |
| 65 | 1. **One-line purpose** — what the tool does, in plain terms. Lead with a verb. `Search indexed knowledge-base documents and return ranked passages.` Skip the HTTP verb and endpoint; `calls GET /v2/search` tells the model nothing about intent. |
| 66 | 2. **WHEN TO USE** — concrete trigger phrasings the user might say, not abstract categories. List the actual shapes: *"find docs about X", "what does the KB say about Y", "look up the runbook for Z"*. Models match on surface form, so give them surface forms. |
| 67 | 3. **WHEN NOT TO USE** — the section that does most of the disambiguation work. Name the near-miss tools and the boundary that separates them. This is where you prevent the model from firing the wrong tool on a request that *looks* similar. Empty WHEN NOT TO USE = the tool is under-specified. |
| 68 | 4. **CRITICAL** — one line for the single non-obvious failure mode. The constraint a reader would not guess from the schema: a required ordering, an ID that must come from another call, a cost/irreversibility warning. One line, not a checklist. |
| 69 | 5. **Self-test** — close with a question the model can apply to itself to decide fit. `Ask: is the user looking up existing content, or asking me to create new content? This tool is read-only — if they want to create, stop.` |
| 70 | |
| 71 | ### Why negative examples outweigh positive ones |
| 72 | |
| 73 | Positive triggers tell the model when a tool *could* apply; negative ones are what stop it firing on overlapping requests. When two tools have similar purposes (`search_kb` vs `search_code`, `get_document` vs `list_documents`), the only thing keeping the model off the wrong one is each description naming the other and drawing the line. Budget more words for the boundary than the bullseye. |
| 74 | |
| 75 | ### Disambiguating near-miss tools |
| 76 | |
| 77 | When tools overlap, make each WHEN NOT TO USE point at its neighbour and state the discriminator explicitly: |
| 78 | |
| 79 | ```text |
| 80 | search_kb |
| 81 | WHEN NOT TO USE: do not use to fetch a document you already have the id for — |
| 82 | that is get_document. Use search_kb only when you need to discover *which* |
| 83 | document, by meaning or keyword. |
| 84 | |
| 85 | get_document |
| 86 | WHEN NOT TO USE: do not use to find a document by topic or keyword — you must |
| 87 | already hold an exact id (from search_kb results). For discovery, use search_kb. |
| 88 | ``` |
| 89 | |
| 90 | ### Worked example |
| 91 | |
| 92 | ```text |
| 93 | name: cancel_workflow |
| 94 | description: | |
| 95 | Stop a running agent workflow and discard its in-flight results. |
| 96 | |
| 97 | WHEN TO USE: the user says "cancel the workflow", "stop run abc123", |
| 98 | "kill that job", or asks to halt a workflow that get_workflow_status |
| 99 | reports as RUNNING. |
| 100 | |
| 101 | WHEN NOT TO USE: |
| 102 | - To inspect progress without stopping — use get_workflow_status. |
| 103 | - To start a fresh run — use start_workflow. |
| 104 | - On a workflow already in a terminal state (COMPLETED/FAILED) — the call |
| 105 | is a no-op and signals the model misread the status. |
| 106 | |
| 107 | CRITICAL: cancellation is irreversible and drops partial output. Confirm the |