Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build an MCP server in Python with the official MCP Python SDK v2: install the CLI extra, create an MCPServer, and expose typed Python functions with decorators such as @mcp.tool(). Use stdio for a local subprocess, Streamable HTTP for a remote endpoint, and Client(mcp) for an in-memory test. This guide walks through the design choices, runnable starter code, testing, transport selection, and deployment security.

What you need before building

Use Python 3.10 or newer and the current v2 line of the official MCP Python SDK. Install the SDK with its CLI extra, which provides the mcp command used in the development workflow:

uv add "mcp[cli]"

If you manage dependencies with pip instead, use:

pip install "mcp[cli]"

The v2 SDK supports tools, resources, and prompts, and can communicate over stdio, Streamable HTTP, or SSE. If an existing project must remain on SDK v1, constrain the dependency to mcp<2; do not leave the version unbounded, since that does not express a deliberate compatibility choice.

What the SDK handles for you

A typed Python function can provide much of the information an MCP client needs: the function name identifies the tool, type hints inform the input schema, and the docstring supplies its description. You do not need to begin by hand-writing a JSON Schema and a separate request parser for each tool. Good descriptions and precise types still matter: they help a model or host understand which inputs the function accepts and what it does.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the right MCP primitive

Choose a primitive based on who should control its use, not just on the shape of the Python code. The MCP design distinction is: “Tools are model-controlled, resources are application-controlled, prompts are user-controlled.”

Primitive Invocation boundary Good fit
Tool Model-controlled An operation the model may call, including an action that could have side effects.
Resource Application-controlled Context or data that the host application decides to load.
Prompt User-controlled A reusable message template that a user chooses to invoke.

For a function that performs an operation, start by asking whether the model should be able to request it as a tool and what guardrails that action needs. For information the application should provide as context, consider a resource instead. A prompt is not a substitute for either: it represents a reusable user-invoked template.

Create a small Python MCP server

Save the following as server.py. It exposes one tool and one templated resource:

from mcp.server import MCPServer

mcp = MCPServer("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

What each part does

  • MCPServer("Demo") creates the server object.
  • @mcp.tool() registers the decorated function as a tool. Here, the two integer parameters and return type make the intended operation clear, while the docstring describes it.
  • @mcp.resource("greeting://{name}") registers a resource template whose URI contains a name parameter. The function returns the greeting for that name.

Keep the function signature aligned with the behavior you want clients to see. A vague name, missing type hints, or an unhelpful docstring makes the interface harder to use even if the Python function itself works. Separate model-invoked actions from host-loaded context rather than making every callable function a tool.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Run and inspect the server locally

From the directory containing server.py, run:

uv run mcp dev server.py

This opens the MCP Inspector development workflow so you can interact with the server while iterating. Check that the add tool appears with the expected inputs, try it with two integers, and inspect the resource template. The Inspector provides a quicker feedback loop than connecting a full application for every small change.

The SDK also offers a local HTTP command for trying the server over Streamable HTTP:

uv run mcp run server.py --transport streamable-http

Use that when you specifically need to check the HTTP transport behavior locally. Local inspection and remote deployment are different tasks: a successful local run does not configure production host security or provide the infrastructure needed for a deployed service.

Test a tool without opening a network port

For a deterministic in-process test, pass the server object directly to the asynchronous MCP client. This exercises the tool call without launching a subprocess or binding an HTTP port. Save this as test_server.py:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import pytest
from mcp import Client
from server import mcp

@pytest.mark.anyio
async def test_add():
    async with Client(mcp) as client:
        result = await client.call_tool("add", {"a": 1, "b": 2})
        assert result.structured_content == {"result": 3}

The client API is asynchronous, so the test is an async function and uses the asynchronous context manager. The assertion checks structured output rather than merely checking that the call returned. This makes the expected tool result explicit.

Choose the client lifecycle that matches the test

  • In-process: Client(mcp) uses the server object directly. Choose it for focused tests of tool behavior without transport setup.
  • Local subprocess: StdioServerParameters launches a server process over stdio. Choose this when the behavior of the process boundary matters.
  • Remote or local HTTP URL: Client("http://localhost:8000/mcp") selects Streamable HTTP. Choose it to test an HTTP endpoint rather than directly invoking the server object.

These modes test different boundaries. An in-memory test can verify the tool result, but it cannot by itself demonstrate that a separately launched process or deployed HTTP endpoint is configured correctly.

Handle tool results and errors deliberately

A call to call_tool() exposes content, structured content, and an is_error flag. Decide how your client should use each before building a larger workflow: structured content can be asserted or consumed as data, content can carry the returned material, and is_error lets the caller distinguish a tool error from an ordinary result.

In server design, make failure behavior useful to the caller. Avoid treating every returned payload as successful just because the request completed. In tests, include cases for the outcomes your tool is meant to handle and assert the relevant result or error signal. The example above establishes only the successful addition case; it does not define policy for invalid inputs or application-specific failures.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a transport: stdio, Streamable HTTP, or SSE

Transport Use it when What to verify
stdio A client launches and communicates with a local server subprocess. Test the subprocess lifecycle and its client configuration.
Streamable HTTP The server is reached through an HTTP URL, including a deployed endpoint. Test the URL endpoint and configure host security for the real hostname.
SSE Your client and server setup specifically calls for the supported SSE transport. Check compatibility with the client and deployment arrangement you intend to use.

The SDK supports all three, but they are not interchangeable deployment choices. For a Python service intended to be reached remotely, use Streamable HTTP as the deployment transport. For a local development process controlled by a client, stdio is the relevant boundary. Choose SSE only when it fits the actual client and service arrangement; support in the SDK alone is not a reason to select it.

Deploy a Python MCP server safely

A deployed Streamable HTTP endpoint needs more than MCP application code. Put it behind normal ASGI application infrastructure; production concerns include an ASGI server, a process manager, and a load balancer. Scaling and process behavior depend on that infrastructure and the SDK’s worker behavior, not on MCP by itself.

Configure host protection before using a real hostname

The SDK’s Streamable HTTP app enables DNS-rebinding protection by default and accepts localhost host forms unless transport security is configured for the deployed hostname. A local test against localhost therefore does not establish that a real hostname will be accepted or protected correctly.

  1. Decide the production hostname and the HTTP deployment arrangement before exposing the endpoint.
  2. Configure transport security for that deployed hostname rather than relying on localhost behavior.
  3. Test through the hostname and infrastructure clients will actually use.
  4. Keep the host allowlisting and DNS-rebinding protections in the deployment review; do not disable them casually to make a local-only configuration appear to work in production.

The key operational boundary is the exposed HTTP host. Confirm the security configuration along with the ASGI server, process manager, and load balancer, rather than treating a successful tool call in a local Inspector session as a deployment check.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot common build and connection problems

  • The mcp command is unavailable. The development command relies on the CLI extra. Install mcp[cli] in the environment used to run the command, then run uv run mcp dev server.py from the directory containing the module.
  • The Inspector cannot find the server module. Check the spelling and path of server.py, and run the command from the project directory. The development command takes the module filename as its argument.
  • A tool has unclear or unexpected inputs. Check the Python function’s parameter names and type hints, then improve its docstring. The SDK derives input-schema information from the typed function and uses the docstring for its description.
  • The test fails while using the client. Confirm the test is asynchronous and enters the client with async with. For the in-memory mode, pass the server object itself as in Client(mcp), not an HTTP URL.
  • Structured-content assertion does not match. Inspect the result’s structured content and compare it with the intended output shape. The example’s expected value is {"result": 3}; do not assume another tool returns that shape.
  • A remote HTTP host is rejected although localhost worked. Localhost acceptance does not configure a production hostname. Set the transport security configuration for the deployed host and retain DNS-rebinding protection.
  • A local HTTP test works but production is unreliable. Check the ASGI server, process manager, load balancer, and SDK worker behavior. MCP alone does not supply the complete production process and scaling setup.
  • The client reports a tool failure but code treats it as success. Inspect is_error as well as returned content and structured content; a completed call is not necessarily a successful tool outcome.

Or skip the browser setup

If your goal is to give an AI agent website screenshots rather than build a general-purpose MCP server, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its MCP tools include take_screenshot, get_page_info, and capture_pdf, for Claude, Cursor, and other MCP clients. One GET request can return an image or PDF; here is the supplied cURL example:

ScreenshotNeo API documentation

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Its MCP server lets AI agents request screenshots without you implementing that screenshot workflow in your own server.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month with no card.

How to decide what to build first

Start with one small tool or resource and test the exact boundary you expect the client to use. A useful progression is to verify the typed interface in the Inspector, assert the result through Client(mcp), and then test the chosen transport. Only after that should you add deployment infrastructure and configure security for a real hostname. This sequence separates mistakes in Python logic from mistakes in transport or hosting, which makes failures easier to locate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can an MCP server expose both tools and resources?

Yes. The starter server registers both an add tool and a templated greeting resource on the same MCPServer object.

Does testing with Client(mcp) prove the deployed endpoint works?

No. It tests the in-process server boundary; use a client connected to the relevant subprocess or HTTP URL to test that transport.

Should I start a new project on MCP SDK v1?

The current documentation line is v2. If a project must stay on v1, pin mcp<2 rather than using an unbounded dependency.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.