About mcp
A development skill for building, reviewing, and debugging MCP servers. It combines implementation workflows with security checks for tools, transports, authentication, and injection risks.
Key features
- 01Server build and review workflows
- 02Tool, resource, and prompt design
- 03Transport and OAuth 2.1 guidance
- 04Injection and tool-poisoning checks
- 05Inspector-based testing guidance
Use cases
- 01Creating an MCP server with tools and resources
- 02Hardening Streamable HTTP transport
- 03Reviewing handlers for injection risks
- 04Debugging client-server MCP connections
MCP support
Guides development of MCP servers that expose tools, resources, and prompts to AI coding assistants, including transport setup and connection debugging.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ iuliandita/ skills --skill mcpAfter installing, type/mcpin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/agent-hygiene.mdoutput-contract.mdsecurity.mdDetails
From the project READMEMCP: Model Context Protocol Server Development
Build, review, and debug MCP servers that expose tools, resources, and prompts to AI coding assistants. The goal is secure, well-structured servers that follow the protocol spec and don't become yet another server with preventable injection vulnerabilities.
Target versions (October 2026):
- MCP specification: 2026-07-28 (current stable; stateless core, extensions framework, and no initialize/session handshake)
- TypeScript SDK:
@modelcontextprotocol/server,@modelcontextprotocol/client, and@modelcontextprotocol/core2.3.0 (the monolithic@modelcontextprotocol/sdk1.32.0 is the legacy line; both 2026-10-02 releases restrict client redirects to the same origin, and 2.3.0 serves one connection perServerinstance). Client floors for the high-severity OAuth credential-redirect advisory (CVE-2026-104850) are client 2.2.0 and sdk 1.31.0 - Python SDK: mcp 2.3.0 (2.x stable; review the v1-to-v2 migration guide). 2.2.0 (or 1.30.0 on 1.x) is the floor for the high-severity OAuth credential-redirect and unreclaimed-session advisories (GHSA-qx49-fqc8-xw99, CVE-2026-59951)
- Protocol transports: stdio and Streamable HTTP. The standalone HTTP+SSE transport is deprecated and available only as a temporary legacy bridge
When to use
- Building a new MCP server (tools, resources, prompts)
- Adding tool handlers to an existing MCP server
- Configuring MCP transport (stdio for local, streamable HTTP for remote)
- Implementing MCP authentication (OAuth 2.1)
- Implementing MCP elicitation (interactive dialogs)
- Reviewing MCP server code for injection or tool poisoning vulnerabilities
- Debugging MCP connection issues between client and server
- Migrating from a custom tool integration to MCP
When NOT to use
- General REST API development that doesn't use MCP - use backend-api
- Claude API / Anthropic SDK usage in an application - use llm-app-development
- Security auditing existing servers across a codebase - use security-audit (it has an MCP section)
- Using MCP browsing tools to browse or scrape web pages - use the host's browsing tools
- Writing prompts for LLMs (not MCP prompt resources) - use prompt-generator
AI Self-Check
When generating or reviewing MCP server code, verify each item before presenting the result:
- All tool handler inputs validated server-side (no raw string interpolation into shell commands, SQL, file paths, or URLs)
- Every tool ships a non-empty, accurate, concise description in
tools/list(FastMCP takes it from the docstring, so an undocumented function registers an empty description; some clients truncate long ones) - Resource URIs use a defined scheme and are validated before use
- Error responses use proper MCP error codes, not raw stack traces
- Authentication implemented for remote transports that handle user data (OAuth 2.1 with PKCE)
- No secrets hardcoded in tool handlers or server configuration
-
inputSchemauses specific JSON Schema types withrequired,maxLength, constraints - Server handles graceful shutdown (cleanup on SIGINT/SIGTERM)
- Streamable HTTP: binds to
127.0.0.1(not0.0.0.0) when local - Streamable HTTP: validates
Originheader (DNS rebinding prevention) - Rate limiting on tool invocations
- Tool annotations treated as untrusted by client-side code
- Elicitation does not request passwords, tokens, or secrets
- Spec version checked: transports, auth, resources, tools, and prompts match current MCP docs and SDK behavior
- Tool poisoning considered: tool descriptions, dynamic metadata, and server updates cannot silently expand authority
- SDK methods verified: see Common Mistakes #8 - verify every API call against actual SDK docs rather than inventing method names
- Cross-cutting agent hygiene applied - see
references/agent-hygiene.md
Performance
- Keep tool schemas tight and responses small; large unstructured tool outputs waste model context.
- Use resources for reusable context instead of returning the same large payload from every tool call.
- Batch read-only lookups where latency matters, but keep side-effecting tools separate and auditable.
Best Practices
- Treat MCP servers as security boundaries: authenticate, authorize, and log side effects explicitly.
- Make tool names and schemas stable; version breaking changes instead of changing semantics in place.
- Require user confirmation for tools that spend money, mutate infrastructure, delete data, or expose secrets.
Workflow
Build vs. Review: Steps 1-6 are for building new servers. When reviewing existing MCP server code: (1) scope using Step 1 questions - what tools, transport, and auth does the server use; (2) audit each tool handler against Step 3 injection vectors and the AI Self-Check; (3) cross-reference the Common Mistakes section for patterns AI models frequently introduce.
Copy this checklist when building a server and track progress:
- Step 1: Purpose, tools, transport, and auth decided
- Step 2: Server scaffolded with graceful shutdown
- Step 3: Every tool handler validates input with no interpolation
- Step 4: Transport configured and hardened
- Step 5: Elicitation handles accept, decline, and cancel
- Step 6: Inspector and malicious-input tests pass (on failure, fix and return to Step 3)
Step 1: Determine the server's purpose
Before writing code, clarify:
- What tools will it expose? Each tool = one operation the AI can invoke.
- What resources will it serve? Resources = read-only data the AI can access.
- What transport? stdio for local CLI integration, streamable HTTP for remote/production.
- What authentication? None for stdio. OAuth 2.1 recommended for remote servers handling user data.
- What language? TypeScript (most mature SDK) or Python (simpler, FastMCP).
Step 2: Scaffold the server
TypeScript (recommended for production):
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import * as z from "zod/v4";
const DOCS_ROOT = path.resolve(process.env.DOCS_ROOT ?? "./docs");
const MAX_DOCS = 1_000;
const MAX_DOC_BYTES = 256_000;
const config = { mode: "read-only" };
async function loadMarkdownDocs(): Promise<Array<{ title: string; body: string }>> {
const entries = await readdir(DOCS_ROOT, { withFileTypes: true });
const names = entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
.map((entry) => entry.name)
.sort()
.slice(0, MAX_DOCS);
const docs: Array<{ title: string; body: string }> = [];
for (const name of names) {
const filePath = path.join(DOCS_ROOT, name);
if ((await stat(filePath)).size > MAX_DOC_BYTES) continue;
docs.push({ title: name, body: await readFile(filePath, "utf8") });
}
return docs;
}
let docsPromise: ReturnType<typeof loadMarkdownDocs> | undefined;
function docsIndex(): ReturnType<typeof loadMarkdownDocs> {
docsPromise ??= loadMarkdownDocs();
return docsPromise;
}
function createServer(): McpServer {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool(
"search_docs",
{
title: "Search docs",
description: "Search documentation by keyword",
inputSchema: z.object({
query: z.string().max(200).describe("Search query"),
limit: z.number().int().min(1).max(100).default(10),
}),
outputSchema: z.object({
results: z.array(z.object({ title: z.string(), snippet: z.string() })),
}),
},
async ({ query, limit }) => {
try {
const needle = query.normalize("NFKC").trim().toLocaleLowerCase();
if (!/[\p{L}\p{N}]/u.test(needle)) {
return { isError: true, content: [{ type: "text", text: "Query needs a letter or number." }] };
}
const results = (await docsIndex())
.flatMap(({ title, body }) => {
const text = `${title}\n${body}`;
const index = text.toLocaleLowerCase().indexOf(needle);
if (index < 0) return [];
const start = Math.max(0, index - 160);
const end = Math.min(text.length, index + needle.length + 320);
return [{ title, snippet: text.slice(start, end) }];
})
.slice(0, limit);
const output = { results };
return {
content: [{ type: "text", text: JSON.stringify(output) }],
structuredContent: output,
};
} catch (error: unknown) {
console.error("search_docs failed", error);
return { isError: true, content: [{ type: "text", text: "Search failed while loading documentation." }] };
}
}
);
server.registerResource(
"config",
"config://app/settings",
{ title: "Application config", mimeType: "application/json" },
async (uri) => ({
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(config) }],
})
);
return server;
}
async function main(): Promise<void> {
const handle = await serveStdio(createServer);
let closing = false;
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
if (closing) return;
closing = true;
try {
await handle.close();
} catch (error: unknown) {
console.error(`Failed to shut down after ${signal}:`, error);
process.exitCode = 1;
}
};
process.once("SIGINT", () => void shutdown("SIGINT"));
process.once("SIGTERM", () => void shutdown("SIGTERM"));
}
void main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
serveStdio owns the stdio transport and returns a handle whose close() shuts down both the
pinned server instance and transport. Keep all stdio diagnostics on stderr; stdout is the protocol
channel.
Python (FastMCP for quick prototyping):
import json, re
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-server")
docs = [{"title": "Getting started", "body": "Install the server and connect over stdio."}]
config = {"mode": "read-only"}
@mcp.tool()
def search_docs(query: str, limit: int = 10) -> dict[str, list[dict[str, str]]]:
"""Search documentation; return bounded title/snippet matches."""
if not 1 <= len(query) <= 200 or not 1 <= limit <= 20:
raise ValueError("Require a 1-200 character query and limit 1-20")
needle = re.sub(r"[^\w\s-]", "", query).strip().casefold()
if not any(char.isalnum() for char in needle):
raise ValueError("Query must contain a letter or number")
matches = [doc for doc in docs if needle in f"{doc['title']}\n{doc['body']}".casefold()]
return {"matches": [{"title": doc["title"][:120], "snippet": doc["body"][:240]}
for doc in matches[:limit]]}
@mcp.resource("config://app/settings")
def get_config() -> str:
"""Application configuration."""
return json.dumps(config)
if __name__ == "__main__":
mcp.run()
Step 3: Implement tools securely
Injection is the top MCP vulnerability class. Every tool handler is an attack surface.
The #1 rule: never interpolate user input into commands, queries, or paths.
Common injection vectors in MCP tools:
| Vector | Bad pattern | Safe pattern |
|---|---|---|
| Shell | Interpolated command strings | execFile with argument arrays + path validation |
| SQL | String concatenation in queries | Parameterized queries with $1 placeholders |
| File paths | Direct readFile(userPath) | Resolve path, validate prefix against allowlist |
| URLs | Direct fetch(userUrl) | Parse URL, validate scheme + host against allowlist |
| Templates | Dynamic code evaluation | Sandboxed template engine with no code execution |
Path traversal prevention:
import path from "node:path";
import { realpath } from "node:fs/promises";
async function safeExistingPath(base: string, userInput: string): Promise<string> {
const baseReal = await realpath(base);
const targetReal = await realpath(path.resolve(baseReal, userInput));
const relative = path.relative(baseReal, targetReal);
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error("Path traversal detected");
}
return targetReal;
}
Read references/security.md (Path traversal) for a before/after tool handler using this
helper and the pattern for creating new files.
SSRF prevention (when tools fetch URLs from user input):
- Block private IP ranges:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - Block link-local:
169.254.0.0/16(includes cloud metadata at169.254.169.254) - Block loopback:
127.0.0.0/8 - Require HTTPS in production
- Pin DNS resolution between check and use (TOCTOU defense)
Step 4: Configure transport
| Transport | Use case | Auth needed | Notes |
|---|---|---|---|
| stdio | Local tools, CLI integration | No | Runs as user's process. Most secure. |
| Streamable HTTP | Remote/multi-client servers | Recommended | Single endpoint, POST for messages, optional SSE streaming. |
The standalone HTTP+SSE transport (spec 2024-11-05) is deprecated; use Streamable HTTP for remote servers. MCP 2026-07-28 removes the initialize/session handshake from the core protocol. Use the SDK migration helpers when one endpoint must also serve legacy 2025 clients. Auth is optional per spec but strongly recommended for servers handling user data. When implementing auth, use OAuth 2.1 with PKCE. Prefer Client ID Metadata Documents over Dynamic Client Registration (DCR is a fallback, not a requirement).
Streamable HTTP security:
- Bind to
127.0.0.1for local servers (never0.0.0.0) - Validate
Originheader on all requests (DNS rebinding prevention) - Treat every 2026-07-28 request as self-contained; do not require
MCP-Session-Id - Client sends
MCP-Protocol-Version: 2026-07-28plus identity and capabilities in_meta - Consider using
createMcpExpressApp()/createMcpHonoApp()for built-in DNS rebinding protection - these ship from the separate@modelcontextprotocol/expressand@modelcontextprotocol/honopackages, not the runtime-neutral core package
Step 5: Handle elicitation safely
MCP elicitation lets servers request structured input from users mid-task.
Schema restrictions - elicitation schemas are limited to flat objects with primitive fields:
string(with optionalformat: email, uri, date, date-time)number/integer(withminimum,maximum)booleanenum(string withenum; useanyOfwithtitlefor labeled choices)arrayof enum strings (for multi-select)
No nested objects. Keep schemas simple for broad client support.
Security: never request credentials via elicitation. Clients should show which server is
requesting input and allow decline/cancel at any time. Handle all three responses: accept
(with data), decline, and cancel.
Step 6: Test the server
command -v npx >/dev/null || echo "install Node.js to get npx for the Inspector"
command -v uv >/dev/null || echo "install uv for the Python dev inspector"
# Test with MCP Inspector (official debugging tool)
npx @modelcontextprotocol/inspector your-server-command
# Python alternative
uv run mcp dev server.py
Test each tool handler with: valid inputs (happy path), missing required fields, malicious inputs (injection, path traversal, oversized payloads), concurrent requests. If a case fails, fix the handler (Step 3) and rerun every case until all pass.
Read references/security.md for specific injection test payloads.
Tool Poisoning and Rug Pull Defense
These attacks target tool metadata, not tool execution: hidden instructions in tool
descriptions, or definitions that change after approval. Write honest, minimal descriptions
with no embedded instructions, and read references/security.md (Tool Poisoning and Rug Pull
Attacks) for the attack example and the server-side and client-side defenses.
Common Mistakes
AI models consistently make these errors when generating MCP server code:
- Shell commands via string interpolation - the #1 vulnerability. Always use argument arrays for system commands.
- Missing server-side validation - generating
inputSchemabut never validating against it in the handler. The client may skip validation. - Bare
"type": "string"in schemas - nomaxLength, nopattern, no constraints. Accepts any string of any length. - Binding HTTP to
0.0.0.0- exposes local servers to the network. Use127.0.0.1. - No
Originheader validation - enables DNS rebinding against local servers. - Leaking error details - stack traces, file paths, or DB errors in tool responses.
- Token passthrough - accepting OAuth tokens meant for other services without audience validation.
- Hallucinating SDK methods - inventing API calls that don't exist. See AI Self-Check for the verification checklist item.
- Ignoring elicitation actions - handling
acceptbut crashing ondecline/cancel. - No graceful shutdown - missing SIGINT/SIGTERM handlers on stdio servers.
Reference Files
references/security.md- OAuth 2.1 details, known CVEs, injection test payloads, path traversal and SSRF prevention, session management, and tool poisoning defense. Read it when implementing auth, hardening handlers, or reviewing a server
Output Contract
See references/output-contract.md for the full contract.
- Skill name: MCP
- Deliverable bucket:
audits - Mode: conditional. When invoked to analyze, review, audit, or improve existing repo content, apply the reporting size and evidence rules in
references/output-contract.mdand write the deliverable todocs/local/audits/mcp/<YYYY-MM-DD>-<slug>.md. When invoked to answer a question, teach a concept, build a new artifact, or generate content, respond freely without the contract. - Severity scale:
P0 | P1 | P2 | P3 | info(see shared contract; only used in audit/review mode).
Related Skills
- security-audit - for auditing MCP servers as part of a broader security review. The security-audit skill's ASI and MCP sections cover vulnerability patterns; this skill covers building servers correctly from the start.
- code-review - for reviewing MCP server code for correctness beyond security.
- docker - for containerizing MCP servers with minimal capabilities.
- llm-app-development - for Claude API / Anthropic SDK usage in the application that calls MCP tools. Use llm-app-development, not this skill, for Anthropic SDK integration code.
- backend-api - for general REST/GraphQL API development that does not use the MCP protocol.
- Use the host's browsing tools to read or interact with web pages; this skill builds MCP servers.
- prompt-generator - for writing LLM prompts (not MCP prompt resources); route there when the request is about prompt engineering rather than MCP server construction.
Rules
- Validate all tool inputs server-side. Never trust the client or model. Use schema validation (Zod, Pydantic) with explicit types, ranges, and constraints.
- No shell execution with string interpolation. Use argument arrays for system commands.
- Keep descriptions concise. Some clients truncate long descriptions. A few sentences covering what the tool does and its parameters - not implementation details.
- Authenticate when handling user data. Use OAuth 2.1 with PKCE for remote servers that access user data. Auth is optional per spec but strongly recommended.