MCP Development Skills: Connecting AI Models to Tools
Step-by-step tutorial on building, validating, securing, and testing a Model Context Protocol (MCP) server in 2026 using the official Python SDK and MCP Inspector.
1. Introduction and Current Context
In Article 18, we explored the high-level architecture of the Model Context Protocol (modelcontextprotocol/modelcontextprotocol). In this practical skill tutorial, we shift to hands-on implementation: how to write, secure, test, and debug your own custom MCP server from scratch so it plugs cleanly into Claude Code, OpenAI Codex CLI, Gemini CLI, or your custom pydantic-ai/langgraph backend.
2. Direct Answer and Key Takeaway
Key Takeaway: Building a production-grade Model Context Protocol (MCP) server in 2026 using
modelcontextprotocol/python-sdk(FastMCP) or the TypeScript SDK requires mastering five engineering rules: (1) Separate Read-Only@mcp.resourceURIs from Executable@mcp.toolFunctions, (2) Enforce Strict Pydantic/JSON Schema Constraints on every parameter, (3) Never Write Debug Logs tostdoutinstdioMode (route all logging tostderrso JSON-RPC 2.0 wire frames are never corrupted), (4) Validate Path Traversal & SQL Injection Inside the Server, and (5) Test Standalone withmodelcontextprotocol/inspectorbefore connecting to any LLM host.
3. Definitions and Technical Background
Before evaluating architectural trade-offs or writing production code for MCP development, it is essential to establish precise engineering definitions for the core components involved:
FastMCPHigh-Level Server API: The ergonomic decorator-based server interface in the officialmodelcontextprotocol/python-sdkthat automatically inspects Python type hints and docstrings to generate MCP JSON Schemas.stdioWire Purity Invariant: The strict protocol rule that when an MCP server runs over standard input/output (stdio),sys.stdoutcarries exclusively newline-delimited JSON-RPC 2.0 frames; any diagnostic log must be written tosys.stderr.- Path Canonicalization & Root Jail (
Path.resolve()): Resolving symlinks and relative../segments on any file path argument and verifyingresolved_path.is_relative_tobefore reading or writing files inside an MCP server.
4. Technical Architecture and Deep Dive
Prerequisites & Required Tools: Python $\ge 3.11$, the official MCP Python SDK, and Node.js $\ge 20$ to run npx @modelcontextprotocol/inspector.
Step-by-Step Learning Path:
- Beginner — First
FastMCPServer & Inspector Test: Create a server with@mcp.resource()for static/URI data and@mcp.tool()with strict PydanticFieldconstraints, routeloggingtosys.stderr, and test it visually withmcp dev server.py.
Minimal Production Code Example (Hardened Python MCP Server Using modelcontextprotocol/python-sdk):
# File: release_notes_mcp_server.py
# Prerequisites: pip install "mcp[cli]" pydantic
import logging
import sys
from pathlib import Path
from typing import Literal
from pydantic import Field
from mcp.server.fastmcp import FastMCP
# CRITICAL: Configure all logging to sys.stderr so stdout remains pure JSON-RPC 2.0
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
stream=sys.stderr,
)
logger = logging.getLogger("release-notes-mcp")
ALLOWED_DOCS_ROOT = Path("/tmp/safe_release_docs").resolve()
ALLOWED_DOCS_ROOT.mkdir(parents=True, exist_ok=True)
mcp = FastMCP("AniketReleaseNotesServer")
@mcp.resource("releases://manifest/{environment}")
def get_release_manifest(environment: str) -> str:
"""Read-only resource returning the current deployment manifest for an environment."""
if environment not in {"staging", "production"}:
return "Error: environment must be 'staging' or 'production'."
logger.info("Reading manifest resource for env=%s", environment)
return f"Environment: {environment}\nActive Version: v2026.10.25\nStatus: Healthy"
@mcp.tool()
def read_release_note(
filename: str = Field(..., description="Markdown filename inside release docs (e.g. 'v2.4.md')", pattern=r"^[a-zA-Z0-9._-]+\.md$"),
max_chars: int = Field(default=2000, ge=200, le=8000, description="Maximum characters to return"),
severity_filter: Literal["all", "breaking", "security"] = "all",
) -> str:
"""Safely read a release note markdown file within the approved directory jail."""
candidate = (ALLOWED_DOCS_ROOT / filename).resolve()
# Enforce strict directory jail against ../ or symlink traversal
if not candidate.is_relative_to(ALLOWED_DOCS_ROOT):
logger.warning("Blocked path traversal attempt: %s", filename)
return "Error: Access denied. Path is outside the allowed release documentation root."
if not candidate.is_file():
return f"No release note found named '{filename}'. Available files: {[p.name for p in ALLOWED_DOCS_ROOT.glob('*.md')]}"
content = candidate.read_text(encoding="utf-8")[:max_chars]
return f"[Filter: {severity_filter}]\n{content}"
if __name__ == "__main__":
mcp.run(transport="stdio")
Testing & Verification Steps:
- Interactive Inspector Verification: Run
npx @modelcontextprotocol/inspector python3 release_notes_mcp_server.py. In the Inspector UI, click Connect, inspect the generated JSON Schema under the Tools tab, and test callingread_release_notewithfilename: "../../etc/passwd"—verifying that the regex pattern andis_relative_tojail block the call cleanly.
5. Comparative Analysis: MCP Server Engineering Checklist: Common Bugs vs. Production Best Practices
The following table compares the key engineering trade-offs, operational boundaries, and implementation choices for MCP development:
| MCP Server Layer | Common Developer Bug | Symptom in Host / Agent | Production Fix |
|---|---|---|---|
Logging in stdio Mode |
Using print('Starting...') or default stdout logger |
Client fails handshake with SyntaxError: Unexpected token |
Always pass stream=sys.stderr to logging.basicConfig |
| Parameter Schemas | Untyped args def query(q) with no docstring |
LLM guesses wrong arg formats or never selects the tool | Use Field(description=..., pattern=..., ge=..., le=...) & Literal |
| Filesystem / DB Access | Doing open(base_dir + '/' + filename) directly |
Prompt injection reads ~/.ssh/id_rsa via ../../ |
Use Path.resolve() + candidate.is_relative_to(ALLOWED_ROOT) |
| Error Handling | Raising unhandled internal traceback on missing record | Cryptic stack trace wastes tokens; agent retries blindly | Return concise actionable string listing valid options |
| Payload Size | Returning a 10MB log file or 5,000 DB rows | Host context window overflows or crashes UI | Enforce max_chars / limit parameter with pagination metadata |
6. System Architecture and Execution Flow
The diagram below illustrates the end-to-end control flow, verification boundaries, and state transitions for MCP development in a production environment:
flowchart TD
A[Develop FastMCP Server in Python / TypeScript] --> B[Enforce sys.stderr Logging & Strict Pydantic Field Schemas]
B --> C[Add Security Guards: Path.is_relative_to Jail & Read-Only DB Role]
C --> D[Stage 1 Test: Launch modelcontextprotocol/inspector]
D --> E{Verify Schema, Valid Calls & Adversarial Path/SQL Inputs}
E -->|Fails Check| B
E -->|Passes Inspector Audit| F[Stage 2 Register in Claude Code / Codex / Gemini CLI .mcp.json]Figure 59.1: End-to-end development, security hardening, and modelcontextprotocol/inspector verification workflow for custom MCP servers.
7. Practical Implementation Steps and Engineering Checklist
To implement MCP development reliably in production systems, follow these sequential engineering steps:
- Make
stream=sys.stderrthe very first line of your logging configuration in every PythonstdioMCP server, and set environment variables if imported packages print startup banners tostdout. - Include available valid options inside 'not found' tool responses so the LLM succeeds on its very next turn without guessing.
- Always bound tool output sizes by adding a
limit: int = Fieldormax_charsparameter and appending"... [Truncated, use offset=20 for more]"when results exceed the cap. - Separate read-only tools from write/destructive tools into distinct MCP servers so developers can attach the read-only mode safely during exploratory debugging.
8. Limitations, Operational Risks, and Trade-Offs
Even well-architected deployments of MCP development face concrete engineering constraints that teams must mitigate proactively:
- Exposing a generic
run_arbitrary_sql(sql: str)orrun_bash(cmd: str)tool over a remote HTTP MCP server: never expose raw arbitrary query/command execution over a shared network MCP server; expose parameterized, domain-specific tools backed by a read-only database user. - Hardcoding machine-specific paths in committed
.mcp.jsonfiles: committing/Users/aniket/venv/bin/pythonbreaks for every teammate—use relative workspace variables oruv run/npxcommands. - Long-running synchronous calls blocking the MCP server event loop: if an
async deftool insideFastMCPcalls a synchronous blocking HTTP or database driver, concurrent resource reads and heartbeats stall.
9. Frequently Asked Questions (FAQs)
Why does my MCP server work when I run it in the terminal, but fail immediately inside Claude Code or Codex CLI?
Because when you run it manually in a terminal, you don't notice a 1-line startup message printed to stdout. When an MCP client connects over stdio, it parses every line of stdout as JSON-RPC 2.0.
When should I use @mcp.resource vs @mcp.tool in FastMCP?
Use @mcp.resource for static or URI-addressable read-only context (like configuration manifests, database schemas, or style guides) that the host application or user can attach directly.
How do I test an MCP server without connecting it to an LLM first?
Run npx @modelcontextprotocol/inspector python3 your_server.py (from modelcontextprotocol/inspector).
10. Conclusion
Developing custom Model Context Protocol servers is one of the highest-leverage skills in modern software engineering—allowing you to connect internal databases, test runners, and documentation to every AI agent in your organization through a single clean interface. By enforcing stderr logging purity, strict Pydantic schemas, directory jails, and modelcontextprotocol/inspector verification, your MCP servers will run reliably everywhere.
GitHub Repositories & Technical References
- modelcontextprotocol/python-sdk — Official Python SDK for the Model Context Protocol, providing the
FastMCPserver decorator framework and client libraries. Verified GitHub URL: https://github.com/modelcontextprotocol/python-sdk - modelcontextprotocol/inspector — Official visual testing and debugging tool for inspecting and invoking MCP servers locally. Verified GitHub URL: https://github.com/modelcontextprotocol/inspector
- modelcontextprotocol/servers — Official collection of reference MCP server implementations demonstrating security and schema best practices. Verified GitHub URL: https://github.com/modelcontextprotocol/servers
- modelcontextprotocol/modelcontextprotocol — Official specification and protocol documentation for the Model Context Protocol. Verified GitHub URL: https://github.com/modelcontextprotocol/modelcontextprotocol