A Model Context Protocol (MCP) server is a program that exposes tools, resources and prompts to an AI client in a standard way. To build one in Python, install the official mcp package, create an MCPServer, decorate typed functions with @mcp.tool() and @mcp.resource(), and run it with mcp.run(transport="stdio"). You can test it with MCP Inspector before you connect it to VS Code or Claude for Desktop.
Who this is for and what you will have
This guide is for developers who want an AI client to reach internal data or actions, such as looking up a support ticket, without writing a custom integration for every client. By the end you will have a small server with one tool and one resource, a way to test it in a browser and from Python code, and a configuration that lets VS Code start it.
The example uses an in-memory ticket list so that you can focus on the protocol. Replace the dictionary with your own data source when you adapt it.
What an MCP server can offer
MCP servers can provide three kinds of capability, according to the Model Context Protocol documentation:
| Capability | What it is | Example |
|---|---|---|
| Resources | File-like data that clients can read | A ticket record, a file, an API response |
| Tools | Functions that the model can call, with user approval | Search tickets, create a note |
| Prompts | Pre-written templates that help users complete a task | A "summarise this ticket" template |
This tutorial covers tools and resources. A client such as VS Code or Claude for Desktop connects to the server, lists what it offers, and decides when to call it.
Prerequisites
- Python 3.10 or higher.
- The Python MCP SDK. The current server quickstart states that you must use SDK version 2.0.0 or higher, and its examples import
MCPServerfrommcp.server. If you have older code that uses a different class name, check the SDK migration guide linked in the references. uv, the Python package manager that the quickstart uses.- Node.js 22.19.0 or newer if you want to run MCP Inspector, which the Inspector documentation states as its requirement.
- VS Code if you want to connect the server to an editor client.
Install uv on macOS or Linux:
curl -LsSf https://astral.sh/uv/install.sh | shOn Windows, use the PowerShell installer from the same documentation:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Restart your terminal so that the uv command is found.
Create the project
uv init tickets
cd tickets
uv venv
source .venv/bin/activate
uv add "mcp[cli]"
touch server.pyOn Windows, activate the environment with .venv\Scripts\activate and create the file with new-item server.py. The cli extra installs the mcp command-line tool, which provides mcp dev and mcp run.
Write the server
Create server.py. The server uses a typed function for each tool, and the SDK builds the tool definition from the type hints and the docstring, so write the docstring for the model as well as for people.
import logging
from mcp.server import MCPServer
logger = logging.getLogger(__name__)
mcp = MCPServer("tickets")
TICKETS = {
"T-1001": {"title": "VPN drops every hour", "status": "open", "owner": "alex"},
"T-1002": {"title": "Shared mailbox not visible", "status": "closed", "owner": "sam"},
"T-1003": {"title": "Printer queue stuck", "status": "open", "owner": "alex"},
}
@mcp.tool()
def search_tickets(status: str) -> list[dict]:
"""Return tickets with the given status.
Args:
status: Ticket status to filter by, either "open" or "closed"
"""
logger.info("search_tickets called with status=%s", status)
return [
{"id": ticket_id, **ticket}
for ticket_id, ticket in TICKETS.items()
if ticket["status"] == status
]
@mcp.resource("ticket://{ticket_id}")
def ticket_record(ticket_id: str) -> str:
"""Return one ticket as text."""
ticket = TICKETS.get(ticket_id)
if ticket is None:
return f"No ticket found with id {ticket_id}."
return f"{ticket_id}: {ticket['title']} ({ticket['status']}, owner {ticket['owner']})"
if __name__ == "__main__":
mcp.run(transport="stdio")The tool and resource decorators match the SDK's own README examples, which register a tool with @mcp.tool() and a resource with a URI template such as greeting://{name}. Here the template is ticket://{ticket_id}, and the ticket_id argument receives the part of the URI that matches.
Do not write to stdout
For a stdio server, the client reads JSON-RPC messages from the server's standard output. The documentation is explicit: never write to stdout, because it corrupts the messages and breaks the server. print() writes to stdout by default, so keep it out of the server. Use the standard library logging module, which writes to stderr, and create one logger per module with logging.getLogger(__name__), as the example does.
For servers that use an HTTP transport, standard output logging does not interfere with responses, but keeping one logging approach is simpler.
Test with MCP Inspector
MCP Inspector is the reference developer tool for testing and debugging MCP servers. It runs through npx with no installation. Pass the command that starts your server as the arguments:
npx @modelcontextprotocol/inspector uv run server.pyThe command prints a URL that carries a per-launch API token and opens it in your browser. Connect, open the tools list, and you should see search_tickets with its status parameter. Run it with open and confirm that two tickets come back.
The SDK also has its own shortcut. The README lists mcp dev to launch the Inspector with a server file:
uv run mcp dev server.pyFor automation, the Inspector has a CLI mode that lists tools and exits:
npx @modelcontextprotocol/inspector --cli uv run server.py --method tools/listThat form follows the Inspector documentation, which lists tools with --cli ... --method tools/list. It is useful in a CI job that fails when a tool disappears or changes its schema.
Be aware that the Inspector stores secrets, such as OAuth tokens and environment values for stdio servers, in the operating system keychain when one exists. Where there is none, it writes them to ~/.mcp-inspector/secrets.json unencrypted unless you supply a key. Do not put real credentials in a test configuration on a shared machine.
Test from Python code
You can also exercise a server from a script with the SDK's Client class. The README shows this against a server running over Streamable HTTP. Start the server with the HTTP transport:
uv run mcp run server.py --transport streamable-httpThe README's client example connects to http://localhost:8000/mcp after the server is started this way; check the address that the server prints on startup if yours differs. Then call the tool from a second script:
import asyncio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
result = await client.call_tool("search_tickets", {"status": "open"})
print(result.structured_content)
asyncio.run(main())The README example prints result.structured_content for a tool that returns an integer. Inspect result in your own run to see how a list of dictionaries is represented, because the documentation does not show that shape.
Connect the server to VS Code
VS Code reads MCP server definitions from .vscode/mcp.json in a workspace, or from an mcp.json in your user profile, which you can open with the MCP: Open User Configuration command. The VS Code format uses a top-level servers object, not mcpServers. A local server needs type set to stdio and a command, and may have args, cwd and env.
{
"servers": {
"tickets": {
"type": "stdio",
"command": "uv",
"args": ["--directory", "/absolute/path/to/tickets", "run", "server.py"]
}
}
}Use an absolute path for the project directory. VS Code documents that the command must be on your system path or given as a full path, so if uv is not found, put its full path in command. Local MCP servers can run code on your machine, so review the command before you start one, and keep credentials out of the file; VS Code supports input variables for secrets.
Connect the server to Claude for Desktop
Claude for Desktop uses a different file and key. Edit claude_desktop_config.json and add the server under mcpServers. The documented locations are ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, ~/.config/Claude/claude_desktop_config.json on Linux as shown in the quickstart, and $env:AppData\Claude\claude_desktop_config.json on Windows.
{
"mcpServers": {
"tickets": {
"command": "uv",
"args": ["--directory", "/ABSOLUTE/PATH/TO/tickets", "run", "server.py"]
}
}
}Save the file and restart Claude for Desktop. The quickstart notes that you may need the full path to uv in command, which you can find with which uv on macOS and Linux or where uv on Windows. On Windows, use double backslashes or forward slashes in JSON paths.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Client shows the server as failed or disconnected | Output written to stdout, often a stray print() | Remove it and log with logging |
uv or command not found in the client | The client does not use your shell path | Use the full path to uv |
| Server starts in a terminal but not in the client | Relative path in the config | Use an absolute project path |
| Tool does not appear in Inspector | Decorator missing or the file is not the one launched | Confirm @mcp.tool() and the command you passed |
ImportError for MCPServer | Older SDK installed | Upgrade the package and check the SDK migration guide |
| Inspector will not start | Node older than 22.19.0 | Upgrade Node |
Design tips for tools that models call
Keep each tool narrow and give parameters clear types, because the SDK derives the schema from them. Describe what the tool returns and what values are valid in the docstring, as with the status argument above. Return data that is already safe to show: the client may pass anything a tool returns into a model's context, so filter out fields that the user should not expose. If a tool changes data, make that obvious in its name and description, since the protocol relies on user approval for tool calls.
Checklist
- Python 3.10 or higher, the
mcppackage at the version the quickstart requires, anduvinstalled. - Tools registered with
@mcp.tool()and resources with@mcp.resource()and a URI template. - No
print()in a stdio server; log throughlogging. - Tested with MCP Inspector or the SDK
Clientbefore connecting an AI client. - VS Code config under
serverswithtype: stdio; Claude for Desktop config undermcpServers. - Absolute paths and, where needed, the full path to
uv.
For related reading on tool calling from the model side, see Claude Tool Use in Python: Build an Agent Loop.
References
- Build an MCP server, Model Context Protocol documentation: https://modelcontextprotocol.io/docs/develop/build-server
- MCP Inspector: https://modelcontextprotocol.io/docs/tools/inspector
- Python SDK repository and README: https://github.com/modelcontextprotocol/python-sdk
- Add and manage MCP servers in VS Code: https://code.visualstudio.com/docs/copilot/customization/mcp-servers
- MCP configuration reference for VS Code: https://code.visualstudio.com/docs/agents/reference/mcp-configuration
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.