Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build production-grade MCP (Model Context Protocol) servers that connect AI agents to external APIs, databases, and services. Use when the user wants to build an MCP server, integrate an external API with Claude or any AI agent, create custom tools for an AI agent, expose a service via MCP, or asks how to connect their app to an AI model using the MCP standard.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 73% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 111% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 128% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 92% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 163% | 0% |
MCP (Model Context Protocol) is the standard for connecting AI agents to external tools and services. An MCP server exposes tools, resources, and prompts that any compatible AI agent can call — Claude, GPT-4, Gemini, or any other model that speaks the protocol.
Build MCP servers like you build APIs: with clear contracts, proper error handling, and the caller (the AI agent) as your user. A badly designed MCP tool confuses the model. A well-designed one makes the model dramatically more capable.
description field in your tool schema is what the model reads to decide when and how to call it. Write it like a senior engineer explaining an API to a junior — concrete, precise, with examples.Answer these before writing a line of code:
stdio for local tools (runs on user's machine), streamable HTTP for remote servers (deployed, shareable).| Transport | When to use | How it works | |---|---|---| | stdio | Local tools, developer utilities, file system access | Agent spawns the server as a subprocess; communicates via stdin/stdout | | Streamable HTTP | Remote services, shared team servers, deployed APIs | Server runs independently; agent connects via HTTP; stateless JSON preferred |
Rule: stdio for anything touching the local machine (files, databases, local dev tools). HTTP for anything deployed or shared.
One tool per major API endpoint. Simple, predictable, composable.
typescriptserver.registerTool("github_list_repos", { description: "List repositories for a GitHub user or organization. Returns name, description, stars, and clone URL for each repo.", inputSchema: { owner: z.string().describe("GitHub username or organization name"), type: z.enum(["all", "public", "private"]).default("public").describe("Filter by repository type"), per_page: z.number().min(1).max(100).default(30).describe("Number of repos to return (max 100)") } }, async ({ owner, type, per_page }) => { // implementation })
Higher-level tools that combine multiple operations for common use cases.
typescriptserver.registerTool("github_create_pr_from_branch", { description: "Create a pull request from a feature branch to main. Automatically sets title from branch name if not provided.", inputSchema: { repo: z.string().describe("Repository in format 'owner/repo'"), branch: z.string().describe("Source branch name"), title: z.string().optional().describe("PR title (defaults to branch name with hyphens replaced by spaces)"), body: z.string().optional().describe("PR description in Markdown") } }, async ({ repo, branch, title, body }) => { // implementation combining list-branches, create-pr operations })
my-mcp-server/
├── src/
│ ├── index.ts # Server entry point
│ ├── tools/ # One file per tool group
│ │ ├── repos.ts
│ │ └── issues.ts
│ ├── client.ts # API client with auth + retry
│ └── errors.ts # Error types and formatters
├── package.json
├── tsconfig.json
└── README.mdjson{ "name": "my-mcp-server", "version": "1.0.0", "type": "module", "scripts": { "build": "tsc", "dev": "tsx watch src/index.ts", "start": "node dist/index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "zod": "^3.23.0" }, "devDependencies": { "typescript": "^5.4.0", "tsx": "^4.11.0" } }
typescript// src/index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { registerRepoTools } from "./tools/repos.js"; import { registerIssueTools } from "./tools/issues.js"; const server = new McpServer({ name: "github-mcp", version: "1.0.0", }); // Register all tool groups registerRepoTools(server); registerIssueTools(server); // Start with stdio transport const transport = new StdioServerTransport(); await server.connect(transport);
typescript// src/index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; const app = express(); app.use(express.json()); app.post("/mcp", async (req, res) => { const server = new McpServer({ name: "my-mcp", version: "1.0.0" }); // register tools... const transport = new StreamableHTTPServerTransport({ sessionType: "stateless" }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); app.listen(3000);
typescript// src/tools/repos.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; export function registerRepoTools(server: McpServer) { server.registerTool( "github_get_repo", { description: "Get details about a GitHub repository including description, stars, forks, language, and open issue count.", inputSchema: { repo: z.string().describe("Repository in 'owner/repo' format, e.g. 'microsoft/vscode'"), }, // Annotations help agents understand tool behavior annotations: { readOnlyHint: true, // Does not modify data destructiveHint: false, idempotentHint: true, // Same input = same output } }, async ({ repo }) => { // Input validation (beyond schema) if (!repo.includes("/")) { return { isError: true, content: [{ type: "text", text: `Invalid repo format: "${repo}". Expected "owner/repo", e.g. "microsoft/vscode".` }] }; } try { const response = await fetch(`https://api.github.com/repos/${repo}`, { headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Accept: "application/vnd.github.v3+json", }, }); if (response.status === 404) { return { isError: true, content: [{ type: "text", text: `Repository "${repo}" not found. Check the name and ensure it is public or your token has access.` }] }; } if (response.status === 401) { return { isError: true, content: [{ type: "text", text: "GitHub authentication failed. Set the GITHUB_TOKEN environment variable with a valid personal access token." }] }; } if (!response.ok) { return { isError: true, content: [{ type: "text", text: `GitHub API error ${response.status}: ${await response.text()}` }] }; } const data = await response.json(); // Return both structured data and human-readable text return { content: [ { type: "text", text: `**${data.full_name}**\n${data.description ?? "No description"}\n\n⭐ ${data.stargazers_count} stars · 🍴 ${data.forks_count} forks · 🐛 ${data.open_issues_count} open issues\nLanguage: ${data.language ?? "Unknown"}\nURL: ${data.html_url}` } ], structuredContent: data // Full JSON for programmatic use }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Network error connecting to GitHub API: ${error instanceof Error ? error.message : String(error)}. Check your internet connection.` }] }; } } ); }
python# server.py from fastmcp import FastMCP import httpx import os mcp = FastMCP("my-mcp-server") @mcp.tool() async def get_repo(repo: str) -> str: """Get details about a GitHub repository. Args: repo: Repository in 'owner/repo' format, e.g. 'microsoft/vscode' Returns: Repository details including description, stars, and language. """ if "/" not in repo: raise ValueError(f'Invalid repo format: "{repo}". Expected "owner/repo".') token = os.environ.get("GITHUB_TOKEN") if not token: raise EnvironmentError( "GITHUB_TOKEN not set. Create a token at https://github.com/settings/tokens" ) async with httpx.AsyncClient() as client: response = await client.get( f"https://api.github.com/repos/{repo}", headers={"Authorization": f"Bearer {token}"}, timeout=10.0 ) response.raise_for_status() data = response.json() return ( f"**{data['full_name']}**\n" f"{data.get('description', 'No description')}\n" f"⭐ {data['stargazers_count']} stars · Language: {data.get('language', 'Unknown')}" ) if __name__ == "__main__": mcp.run()
The description field is what the model reads. Write it like documentation for a developer:
Bad description:
"Gets stuff from GitHub"Good description:
"Get details about a GitHub repository including name, description, star count,
fork count, open issue count, primary language, and clone URL. Use when you need
to look up information about a specific repo before performing operations on it."Template for every tool description:
[Verb] [what it does] including [key data returned].
Use when [specific trigger condition].
[Auth note if non-obvious.]
[Format note if output is structured.]bash# TypeScript server npx @modelcontextprotocol/inspector node dist/index.js # Python server npx @modelcontextprotocol/inspector python server.py
Opens a web UI where you can call every tool manually and inspect request/response.
typescript// Test each tool in isolation import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const server = new McpServer({ name: "test", version: "1.0.0" }); registerRepoTools(server); await server.connect(serverTransport); // Call tool and assert response const result = await client.callTool("github_get_repo", { repo: "microsoft/vscode" }); assert(result.content[0].text.includes("microsoft/vscode"));
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
json{ "mcpServers": { "my-server": { "command": "node", "args": ["/path/to/my-mcp-server/dist/index.js"], "env": { "API_KEY": "your-key-here" } } } }
For Python:
json{ "mcpServers": { "my-server": { "command": "python", "args": ["/path/to/server.py"], "env": { "GITHUB_TOKEN": "ghp_xxx" } } } }
readOnlyHint, destructiveHint) — helps agents and UIs warn before destructive callsRead tool-design.md when defining a tool inventory, descriptions, schemas, and destructive-operation safeguards. It is a review aid; the project API contract remains the source of truth. For a JSON tool inventory, run python scripts/validate_tool_contract.py tools.json before implementation.
describe() stringnpx @modelcontextprotocol/inspector — passes manual smoke testOther measured skills in the registry, with their headline benchmark lift.