M06: Tool Design — Workshop Guide
Self-directed | 45–60 min | Requires: M06 study guide read beforehand
Before You Start
Section titled “Before You Start”This workshop builds on the three design principles and methodology from the study guide. You’ll move from theory to implementation: building a real MCP server, testing it with Claude Code, and iterating based on agent behavior.
Prerequisites
- M06 study guide and pre-work theory completed
- Node.js 18+ installed and verified (
node --version) - TypeScript familiarity (async/await, interfaces, basic classes)
- A code editor (VS Code recommended)
- Ability to run CLI commands (bash or zsh)
What you’ll do
- Set up the project and server structure
- Define three typed tools with clear descriptions
- Implement tool logic with mock data
- Test your server with Claude Code
- Iterate and refine based on agent behavior
- Build your own tool set in the hands-on exercise
Step 1: Project Setup (5 minutes)
Section titled “Step 1: Project Setup (5 minutes)”Context: What We’re Building
Section titled “Context: What We’re Building”You’re building an MCP server for your team’s internal deployment system. Instead of exposing raw deploy API endpoints, you’ll design three tools:
list_deployments— query deployment history with filtering, returns CSVget_deployment_details— get full status of one deployment, returns JSONtrigger_deployment— start a deployment with validation
These tools consolidate a larger API and return formats optimized for agent reasoning.
Commands
Section titled “Commands”mkdir mcp-deploy-servercd mcp-deploy-servernpm init -ynpm install @modelcontextprotocol/sdk axiosnpm install --save-dev typescript ts-node @types/nodenpx tsc --initStep 2: Stub the Server Structure (10 minutes)
Section titled “Step 2: Stub the Server Structure (10 minutes)”Create src/index.ts:
import { Server, StdioServerTransport, Tool, ToolUseBlock, MessageParam,} from "@modelcontextprotocol/sdk/server/index.js";
const server = new Server({ name: "deploy-server", version: "1.0.0",});
// Placeholder tools arrayconst tools: Tool[] = [];
// Register tool definitionsserver.setRequestHandler(ListToolsRequest, async () => { return { tools };});
// Register tool executionserver.setRequestHandler(CallToolRequest, async (request) => { const { name, arguments: args } = request.params;
if (name === "list_deployments") { // TODO: implement } else if (name === "get_deployment_details") { // TODO: implement } else if (name === "trigger_deployment") { // TODO: implement }
return { content: [{ type: "text", text: "Not implemented" }], };});
// Connect to stdio transportconst transport = new StdioServerTransport();server.connect(transport);This structure is the foundation. Each of the three branches will be filled in during Steps 3 and 4.
Step 3: Implement Tool Definitions (15 minutes)
Section titled “Step 3: Implement Tool Definitions (15 minutes)”Add to src/index.ts:
const tools: Tool[] = [ { name: "list_deployments", description: "Query deployment history with optional filtering. Returns CSV format for easy scanning. Use this to find past deployments, check status history, or find a specific deployment ID.", inputSchema: { type: "object", properties: { environment: { type: "string", enum: ["prod", "staging", "dev"], description: "Deployment environment", }, status: { type: "string", enum: ["success", "failed", "in_progress", "cancelled"], description: "Filter by deployment status (optional)", }, limit: { type: "number", description: "Max results to return (default: 10, max: 100)", }, offset: { type: "number", description: "Pagination offset (default: 0)", }, }, required: ["environment"], }, }, { name: "get_deployment_details", description: "Retrieve full details of a specific deployment: status, logs, timing, errors. Returns JSON. Use this when you need to understand why a deployment failed or see complete logs.", inputSchema: { type: "object", properties: { deployment_id: { type: "string", description: "The deployment ID (e.g., 'deploy-2024-03-28-001')", }, }, required: ["deployment_id"], }, }, { name: "trigger_deployment", description: "Start a new deployment in the specified environment. Returns deployment ID and status. Requires approval from deploy-admins group.", inputSchema: { type: "object", properties: { environment: { type: "string", enum: ["staging", "prod"], description: "Target environment (no dev deployments via this tool)", }, version: { type: "string", description: "Git tag or commit SHA to deploy", }, dry_run: { type: "boolean", description: "If true, validate but don't execute (default: false)", }, }, required: ["environment", "version"], }, },];Step 4: Implement Tool Logic (25 minutes)
Section titled “Step 4: Implement Tool Logic (25 minutes)”Replace the CallToolRequest handler:
server.setRequestHandler(CallToolRequest, async (request) => { const { name, arguments: args } = request.params;
if (name === "list_deployments") { const { environment, status, limit = 10, offset = 0 } = args;
// Mock: in production, call your actual API const deployments = [ { id: "deploy-2024-03-28-001", environment: "prod", status: "success", version: "v1.2.3", createdAt: "2024-03-28T10:15:00Z", duration: "2m 34s", }, { id: "deploy-2024-03-28-000", environment: "staging", status: "failed", version: "v1.2.2", createdAt: "2024-03-28T09:00:00Z", duration: "1m 12s", }, // ... more rows ].filter((d) => d.environment === environment && (!status || d.status === status));
// Format as CSV for agent efficiency const csv = [ "id,environment,status,version,createdAt,duration", ...deployments.map( (d) => `${d.id},${d.environment},${d.status},${d.version},${d.createdAt},${d.duration}` ), ].join("\n");
return { content: [{ type: "text", text: csv }], }; } else if (name === "get_deployment_details") { const { deployment_id } = args;
// Mock: in production, fetch from API const details = { id: deployment_id, environment: "prod", status: "success", version: "v1.2.3", createdAt: "2024-03-28T10:15:00Z", completedAt: "2024-03-28T10:17:34Z", duration: "2m 34s", triggeredBy: "alice@example.com", logs: "✓ Health checks passed\n✓ Database migrations completed\n✓ Services restarted", errors: null, };
return { content: [{ type: "text", text: JSON.stringify(details, null, 2) }], }; } else if (name === "trigger_deployment") { const { environment, version, dry_run = false } = args;
// Mock: validate and create deployment const deploymentId = `deploy-${new Date().toISOString().split("T")[0]}-${Math.random().toString(36).substr(2, 9)}`;
if (dry_run) { return { content: [ { type: "text", text: `DRY RUN: Would deploy ${version} to ${environment}. No changes made.`, }, ], }; }
return { content: [ { type: "text", text: JSON.stringify( { deployment_id: deploymentId, status: "in_progress", environment, version, createdAt: new Date().toISOString(), }, null, 2 ), }, ], }; }
return { content: [{ type: "text", text: `Tool ${name} not found` }], };});Implementation notes:
list_deploymentsreturns CSV (efficient, scannable format)get_deployment_detailsreturns JSON (rich, detailed object)trigger_deploymentincludes a dry_run mode for safety- Mock data is used here; in production, these would call real APIs
Step 5: Test with Claude Code (10 minutes)
Section titled “Step 5: Test with Claude Code (10 minutes)”Compile and start the server:
npx tscnode dist/index.jsIn a separate terminal, in your Claude Code workspace:
# Configure MCP transport (stdio)# Modify .claude/settings.json or create .claude/mcp.json:{ "mcpServers": { "deploy": { "command": "node", "args": ["/path/to/dist/index.js"], "disabled": false } }}Then, in Claude Code, ask:
“List the recent deployments to prod and tell me the status of deploy-2024-03-28-001.”
Observe:
- Does Claude use your tools correctly?
- Are the CSV and JSON outputs readable?
- Did the agent understand what each tool does?
Step 6: Iterate and Refine (5 minutes)
Section titled “Step 6: Iterate and Refine (5 minutes)”If the agent misunderstood a tool:
- Rewrite its description (be more specific about when to use it)
- Adjust output format (too verbose? switch to CSV; too terse? add more details)
- Retest
Common refinements:
- Add more specific language to descriptions: “Use this when you need to…” vs “This gets…”
- If output feels too verbose, remove non-essential fields from JSON or switch to CSV
- If the agent never called a tool, it might not understand its purpose; add an example to the description
Hands-on Exercise: Build Your Own Tool (30–45 minutes)
Section titled “Hands-on Exercise: Build Your Own Tool (30–45 minutes)”Choose one of these scenarios:
Option A: Ticket System
Section titled “Option A: Ticket System”Design and implement three tools for your internal ticket system:
search_tickets(query, project?, assignee?, status?)get_ticket_details(ticket_id)create_ticket(title, description, project, priority)
Return search results as CSV; details as JSON.
Option B: Database Query Tool
Section titled “Option B: Database Query Tool”Design and implement:
query_database(sql, limit?, explain_plan?)— returns CSV or markdown tableget_schema(table_name)— returns JSON schema definitionlist_tables(pattern?)— returns simple text list
Option C: AWS Resource Tool
Section titled “Option C: AWS Resource Tool”Design and implement:
list_resources(resource_type, region?)— returns CSVget_resource_details(resource_id)— returns JSONapply_tag(resource_id, tag_key, tag_value)— returns confirmation
What to Submit:
- Your
src/index.tsimplementation - A 1-page design doc explaining:
- How you consolidated related API functions
- Why you chose CSV vs JSON for each tool
- One example of an agent using your tools effectively
Reflection Questions
Section titled “Reflection Questions”After completing the workshop, consider these questions to consolidate your learning:
- When you were designing your tool descriptions, what changed your thinking about how agents use tools? Consider: clarity of purpose, when to consolidate, output format decisions.
- Which of your three tools did Claude call most effectively, and why? Consider the connection between description quality and agent behavior.
- If you were to expose these tools to your team, what would the biggest risk be? Consider: security (dry_run pattern), verbosity, misuse.
- How would you handle a tool that’s almost always called incorrectly? Consider: iterative refinement, description rewriting, testing with real tasks.
Troubleshooting
Section titled “Troubleshooting”Agent doesn’t call my tool:
- Rewrite the description. Be specific: “Use this to…” not “This does…”
- Add an example in the description: “E.g., trigger a deployment to prod for version v1.2.3”
Tool output is too large/verbose:
- Switch to CSV/TSV for lists
- For JSON, include only essential fields
- Add a
summaryparameter to return brief vs detailed results
Tools conflict with each other:
- Rename for clarity:
list_active_deploymentsnotlist_deployments(if multiple list tools exist) - Document in the description which tool to use when
Node/TypeScript build errors:
- Ensure
tsconfig.jsontargets ES2020+ - Ensure
@types/nodeis installed - Use
npx tsc --listFilesto debug import issues
Completion Checklist
Section titled “Completion Checklist”Before moving on, verify you have:
- A working
src/index.tsthat compiles withnpx tsc - Server running and responding to tool calls
- Tested at least one tool call from Claude Code
- Documented any design decisions or refinements made during iteration
References
Section titled “References”- Anthropic MCP Documentation: https://modelcontextprotocol.io/
- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- Tool Design Checklist: (From M06 study guide pre-work)
- Context Window Budgeting: https://docs.anthropic.com/guides/tokens/faq
- Tool Naming Conventions: https://developers.anthropic.com/guides/tools