Chapter 4: Tooling Your Environment
The fastest way to make MCP feel harder than it is is to start with a messy toolchain.
You do not need every SDK, every example server, or three clients open at once. You need one local setup, one SDK you trust, and one test client that gives you fast feedback. The gap between “I understand MCP conceptually” and “I have a working server” is almost always a tooling gap—wrong Python version, misconfigured paths, logs sent to stdout instead of stderr, a client pointing at a stale binary. This chapter eliminates that gap.
By the end of this chapter you will have run the Inspector against a real server and seen a tool call round-trip. Everything else here serves that. The rule to hold on to: the Inspector tells you whether your server is correct; the host client tells you whether it is usable. Test in that order.
The Golden Path
For most readers, the simplest reliable path starts with one language, one package manager, one editor, and two test surfaces.
Language: Pick Python or TypeScript. Not both. If your team already lives in one of these, use that one. If you have no strong preference, Python with the official SDK’s MCPServer gives you the shortest path from idea to working tool call. TypeScript gives you the strongest compile-time type safety and the tightest integration with the web ecosystem. You can always port later—the protocol does not care what language your server uses.
Package manager: For Python, use uv. For TypeScript, use npm or pnpm. uv is a single binary from Astral that manages Python versions, virtual environments and dependencies together, which removes a whole class of “works on my machine” failures.
Install uv if you do not have it:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Restart your terminal after installation so the uv command is on your PATH.
For a Python MCP project, the setup is three commands:
uv init my-mcp-server
cd my-mcp-server
uv add "mcp[cli]"
This creates a pyproject.toml, a virtual environment pinned to a specific Python version, and installs the MCP SDK with its CLI extras, including the mcp command-line tool. uv run handles the virtual environment for you; you never need to activate it. That, more than install speed, is the reason to choose uv: every command in this book that starts with uv run uses the right interpreter, whether you type it or a host client launches it.
For TypeScript:
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/node
npx tsc --init
The v2 TypeScript SDK is split into packages: @modelcontextprotocol/server and @modelcontextprotocol/client, plus transport helpers such as @modelcontextprotocol/node and @modelcontextprotocol/express when you go remote (Chapter 7). The older single @modelcontextprotocol/sdk package is the v1 line; examples that import @modelcontextprotocol/sdk/... paths need it.
Editor: Use whatever you are productive in, with a linter, a formatter, and the active interpreter visible.
Test surfaces: You need exactly two. First, the MCP Inspector—a browser-based tool that talks directly to your server over the protocol, showing you raw tool lists, schemas, and call results without any model or conversation layer in the way. Second, one real host client (Claude Desktop, Cursor, VS Code with Copilot, or similar) to validate that the full experience works end to end. The Inspector tells you whether your server is correct. The host client tells you whether it is usable.
Choosing an SDK
Official SDKs exist in ten languages; Python and TypeScript are the most complete. Check modelcontextprotocol.io/docs/sdk for current tiers.
For most new projects, the decision is between Python and TypeScript. Here is how to think about it.
Choose Python when:
- You are wrapping data access layers, databases, or ML pipelines.
- You want the fastest path from idea to working server. The Python SDK’s decorator-based
MCPServerAPI means a tool is a function with a decorator and a docstring—the SDK infers the JSON Schema from your type hints. - You are prototyping and may throw the server away.
Choose TypeScript when:
- You need compile-time type checking across your tool schemas. TypeScript catches type mismatches (a number where a string was expected, a missing required field) before your server ever runs.
- You are building a server that will be maintained by multiple developers over time.
- You want tighter integration with web frameworks and existing Node services.
Choose something else when:
- Your organization has a strong platform commitment (C# for .NET shops, Java for Spring Boot shops, Go for infrastructure teams) and the SDK coverage is sufficient. “Our team knows this language and maintains everything else in it” is a real engineering constraint. Respect it.
- The server will run as a serverless function. Then prefer Go or TypeScript for their cold starts; for a long-running process, runtime choice doesn’t affect latency enough to matter.
What you should not do is pick an SDK because it would be interesting to try a new language. MCP server code is not where you want your team learning Rust for the first time. The protocol is straightforward. The product work—designing good tool interfaces, handling errors, managing auth—is where the real complexity lives. Adding a language learning curve on top makes everything slower.
Before committing to any SDK, check two things the tier list will not tell you: that it speaks the current protocol revision (2026-07-28) over both stdio and Streamable HTTP, and that you can unit-test a tool handler without starting a server. The Python and TypeScript SDKs both pass.
The High-Level Python Layer (MCPServer)
The Python ecosystem has two main entry points: the official mcp SDK’s high-level server class and its low-level Server. The high-level class was called FastMCP until mcp 2.0 renamed it MCPServer. (A separate, standalone fastmcp package still uses the old name; it is a different project.) The high-level layer is the usual starting point because it eliminates ceremony. A tool definition looks like this:
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
mcp = MCPServer("my-server")
@mcp.tool()
async def lookup_order(order_id: str) -> str:
"""Look up an order by its ID and return current status.
Args:
order_id: The unique order identifier (e.g., ORD-12345)
"""
# Your implementation here
order = await db.get_order(order_id)
if not order:
raise ToolError(f"No order found with ID {order_id}. Check the ID format (ORD-12345).")
return f"Order {order_id}: status={order.status}, total=${order.total}"
MCPServer reads the function signature and docstring to generate the tool’s JSON Schema and description automatically. The order_id: str type hint becomes a required string parameter. The docstring becomes the tool description that the model sees. A raised ToolError goes back as a tool result with isError: true, so the model can read the message and retry; Chapter 5 builds on that. This is not magic—it is introspection applied well. But it means you need to write good docstrings and accurate type hints, because they are no longer just documentation. They are your interface contract.
The low-level Server gives you more control when you need it—custom transports, manual schema construction, hand-written handlers. For most servers, MCPServer is the right starting point. Drop to the low-level API when its conventions get in the way.
The Inspector Isolates Your Server
Run the Inspector with npx—no installation required:
# For a local Python server
npx @modelcontextprotocol/inspector uv run server.py
# For a local TypeScript server
npx @modelcontextprotocol/inspector node dist/server.js
# For a published package
npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/my-repo
# For a remote server over HTTP (Inspector v2 flag)
npx @modelcontextprotocol/inspector --server-url https://mcp.example.com/mcp
The Inspector opens a browser-based interface that connects to your server and shows you exactly what it exposes: every tool with its schema and description, every resource with its URI and MIME type, every prompt with its arguments and template. You can call tools with custom inputs and see the raw results.
Why this matters more than testing directly in a chat client: the Inspector strips away the conversational layer. When you test in Claude Desktop or Cursor, you are testing the combined behavior of your server, the model’s tool selection, the client’s approval UX, and the model’s interpretation of results—all at once. When something goes wrong, you cannot tell which layer failed. The Inspector isolates your server. If a tool surface is confusing in the Inspector, the model will not rescue it in conversation.
Prove the Environment Works
Before you write a line of server code, point the Inspector at a server someone else already shipped:
git init /tmp/smoke && cd /tmp/smoke
npx @modelcontextprotocol/inspector uvx mcp-server-git --repository /tmp/smoke
# In the UI: Tools → git_status, repo_path = /tmp/smoke → Run Tool
If git_status comes back with a result, npx, uv and the Inspector all work, and any later failure is in your code rather than your toolchain. If it fails, fix that now; nothing in Chapter 5 will work either.
Use the Inspector on your own servers to verify:
- Tool listing. Are all your tools showing up? Are any missing because of a registration error you did not notice?
- Schema quality. Are parameter names clear? Are descriptions helpful? Is the schema too noisy (too many optional parameters) or too sparse (missing descriptions)?
- Error behavior. What happens when you call a tool with invalid input? Does the error message help the caller recover, or is it a raw stack trace?
The Inspector also shows the raw JSON-RPC messages, which is where you look when a response is malformed.
Keep the Inspector open in one browser tab while you edit server code. After each change, restart the server, reconnect the Inspector, and verify the change. This loop should take seconds, not minutes. If it takes minutes, fix your build before writing more server code.
Chapter 5 connects a real host client to the server you build there; that is where client configuration lives.
Setup Failures, Most Frequent First
The failures repeat often enough to be predictable. They are listed in the order they tend to bite.
“It works in my terminal but not in the client”
A frequent first-day failure is an environment mismatch. When you run your server in a terminal, your shell has loaded your profile, activated your virtual environment, and set your PATH. When Claude Desktop or another host client spawns your server, it uses a minimal environment. The Python binary it finds may be different. The virtual environment may not be activated. Environment variables you set in .bashrc are not present.
The fix sequence:
- Use absolute paths for the command in your client configuration, and let
uv run --directorypick the project’s environment (Chapter 5 shows the config). - If your server needs environment variables, pass them in the client configuration’s
"env"key. - Test by running the exact command from your client config in a fresh terminal (one where you have not sourced any profile scripts). If it fails there, it will fail in the client.
Logging Breaks a Local Server
For stdio-based servers, the transport is stdout. Your server writes JSON-RPC messages to stdout, and the client reads them. If anything else writes to stdout—a print() statement, a logging handler configured with the default stream, a library that emits startup banners—the JSON-RPC stream is corrupted and the connection dies.
The fix: send all logs to stderr or to a file. In Python:
import logging
import sys
# Configure logging to stderr
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
# If you must use print for debugging
print("debug info", file=sys.stderr)
Python’s logging module writes to stderr by default, which is what a stdio server needs. But third-party libraries you import may have their own logging configuration. Audit any library that might write to stdout.
The Rest of the List
JSON configuration syntax errors. A trailing comma or missing quote in a client config silently stops your server from starting; the client shows no error. Validate with python -m json.tool before restarting the client.
Config clobbering. If you edit a client’s config file while the client is running, it may overwrite your changes on shutdown. Close the client first.
A working tool looks invisible. The Inspector shows the tool, but the model never calls it. That is an interface problem: a tool named process_data described as “Processes data” gives the model nothing to match. Read the name, description and schema in the Inspector as if you were the model; Chapter 6 covers the fix.
Dependency conflicts between servers. Two servers sharing Python dependencies at conflicting versions will break one of them. Give each server its own uv project.
Upgrading everything at once. When you upgrade, upgrade deliberately. Read the changelog. Run your tests. Check the Inspector. Do not upgrade the SDK, the client, and your server code simultaneously—if something breaks, you will not know which change caused it.
Inspector version drift. The Inspector is fetched via npx and updates automatically. If results change suddenly, pin a known-good version (npx @modelcontextprotocol/inspector@<version>). Inspector v2 renamed several flags (for example, --url became --server-url), so a silent major-version bump can break scripts.
Implementation Checklist
Before moving to the next chapter, confirm:
- One language chosen. Python or TypeScript, not both.
- Package manager working.
uvfor Python ornpm/pnpmfor TypeScript. Dependencies install cleanly. -
uv run python -c "import mcp"succeeds (Python) ornode_modulesis present (TypeScript). - Smoke test passed. The Inspector called
git_statusonmcp-server-gitand showed a result. - Logging goes to stderr (for stdio servers). No output touches stdout except JSON-RPC messages.
- Build-test loop is fast. You can make a change, restart, reconnect the Inspector, and verify in under thirty seconds.
What Comes Next
You have now watched a real MCP server answer a tool call through the Inspector. Chapter 5 replaces that server with one you wrote.
For recurring environment issues, keep Appendix E: Troubleshooting Guide open while you work.