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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Direct answer: install Python 3.10 or newer and the MCP Python SDK, create mcp.server.Server with asynchronous handler functions, describe each tool’s JSON schema yourself, return typed MCP result objects, and run the server over the transport your host expects. The smallest useful implementation is a tools-only server over stdio; Streamable HTTP is the deployment option when a remote client must connect.

This approach is the protocol-level API beneath the SDK’s convenience server. It is appropriate when an exact input schema, custom metadata, structured content, or an MCP method not exposed by the convenience API matters. For ordinary tool servers, the official guide recommends the higher-level server instead. See the low-level Server guide and API reference while checking examples against the SDK version you install.

Prerequisites and SDK version

  • Python 3.10 or newer, as required by the current official SDK documentation.
  • The MCP package with its CLI extra for development commands.
  • An MCP host or client that can launch a local stdio process, or an ASGI-capable deployment target for Streamable HTTP.

The official documentation currently presents v2 as the stable line and describes it as a major rework supporting the 2026-07-28 MCP specification and earlier revisions. If an existing project must remain on v1, the SDK repository recommends constraining the dependency below v2 until migration. Recheck the repository’s version guidance before pinning production dependencies.

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.

Install with uv

uv add "mcp[cli]"

Install with pip

pip install "mcp[cli]"

The [cli] extra includes the mcp command, which the overview describes as useful during development.

What “low-level” means

The low-level Server accepts request handlers in its constructor. You provide protocol objects, schemas, and results directly instead of using decorators, inferred types, or a manager that hides registration details. That gives you precise control over the wire contract, including _meta and structuredContent, but makes correctness your responsibility.

Every handler is asynchronous and receives (ctx, params). A tools-only server generally supplies on_list_tools and on_call_tool. Other method families—resources, prompts, and completions—are advertised only when their corresponding handlers are registered.

Build a minimal tools server over stdio

Create server.py with this implementation:

import asyncio

from mcp import types
from mcp.server import Server
from mcp.server.stdio import stdio_server

async def list_tools(ctx, params):
    return types.ListToolsResult(
        tools=[
            types.Tool(
                name="add",
                description="Add two integers",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "a": {"type": "integer"},
                        "b": {"type": "integer"},
                    },
                    "required": ["a", "b"],
                },
            )
        ]
    )

async def call_tool(ctx, params):
    if params.name != "add":
        return types.CallToolResult(
            content=[types.TextContent(type="text", text="Unknown tool")],
            isError=True,
        )
    args = params.arguments
    result = args["a"] + args["b"]
    return types.CallToolResult(
        content=[types.TextContent(type="text", text=str(result))],
        structuredContent={"result": result},
    )

server = Server(
    "example",
    on_list_tools=list_tools,
    on_call_tool=call_tool,
)

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options(),
        )

asyncio.run(main())

The example follows the documented constructor and stream shape. Confirm exact type names and field casing against the installed SDK, because major-version changes can affect examples.

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

How discovery works

When a client initializes, it can call the server’s tools-list method. list_tools returns a ListToolsResult containing a Tool with a name, description, and JSON Schema under inputSchema. The schema is not inferred from the Python function signature. If you add a property but omit it from required, clients may treat it as optional; if you declare the wrong type, validation and model behavior can diverge.

How invocation works

call_tool checks params.name, reads params.arguments, performs the operation, and constructs a CallToolResult. The human-readable value goes in a TextContent item. structuredContent can carry machine-readable data for clients that support it.

Validate arguments and report failures correctly

There are two distinct failure paths:

  • Protocol failure: an exception escaping a low-level handler becomes a protocol error (-32603). The SDK deliberately returns a generic message so a traceback is not exposed to a remote caller. Log diagnostics on the server side.
  • Tool failure: if the model should see a recoverable, tool-level problem, validate the arguments and return CallToolResult(..., isError=True) with explanatory content.

For example, replace direct dictionary indexing with explicit checks when inputs are untrusted:

async def call_tool(ctx, params):
    if params.name != "add":
        return types.CallToolResult(
            content=[types.TextContent(type="text", text="Unknown tool")],
            isError=True,
        )

    args = params.arguments or {}
    if not isinstance(args.get("a"), int) or not isinstance(args.get("b"), int):
        return types.CallToolResult(
            content=[types.TextContent(
                type="text",
                text="Arguments a and b must both be integers",
            )],
            isError=True,
        )

    total = args["a"] + args["b"]
    return types.CallToolResult(
        content=[types.TextContent(type="text", text=str(total))],
        structuredContent={"result": total},
    )

Do not put secrets in a result. The low-level guide says _meta is intended for the client application and is not guaranteed to reach the model. Namespace custom metadata keys and avoid protocol-reserved namespaces.

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

Advertise resources, prompts, and completions deliberately

A server advertises capabilities based on the handler families passed to Server. A tools-only constructor advertises tools; it does not claim to provide resources or prompts. If your server supports those primitives, register the matching callbacks and return their typed result objects.

Additional handler slots

  • on_list_resources and on_read_resource for resource discovery and reads.
  • on_list_prompts and on_get_prompt for prompt templates.
  • on_completion for completion requests.

Implementing a function without attaching it to the constructor does not expose that capability to clients. Keep schemas and result shapes aligned with the method you register.

Run and test the stdio server

Stdio is normally a local subprocess connection. The host starts your command and exchanges MCP messages through standard input and output. Do not print diagnostic text to stdout; it can corrupt the protocol stream. Send logs to stderr instead.

  1. Install dependencies in the same environment used by the host.
  2. Run python server.py manually to catch import and startup errors.
  3. Configure your MCP host to launch the interpreter and script as a stdio server.
  4. Ask the host to list tools and verify that add appears with both required integer fields.
  5. Invoke add with values such as 2 and 3; the text content should be 5 and structured content should contain {"result": 5}.

The client documentation distinguishes a local subprocess configured with StdioServerParameters from a URL-based connection. Match the launch mode to the host rather than trying to pass a transport name into server.run.

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

Choose stdio, Streamable HTTP, or SSE

Transport Best fit Implementation implication
stdio Local desktop or development host Use stdio_server(), obtain read/write streams, then call server.run(...).
Streamable HTTP Remote or multi-client deployment Expose the server as the SDK’s Streamable HTTP ASGI application and deploy it behind an ASGI server.
SSE Hosts that still require the listed SSE transport Use the SDK transport support appropriate to the installed version and client.

The official overview lists all three transports. At the low level there is no server.run(transport=...) convenience call: the documented stdio form passes streams and initialization options directly. For HTTP, use the ASGI application path documented by the low-level guide and API reference.

Common problems and fixes

“ModuleNotFoundError: mcp”

The host is using a different interpreter or virtual environment. Install mcp[cli] into that environment and configure the host with its absolute Python path.

The tool does not appear

Check that on_list_tools=list_tools is passed to the same Server instance that is run. Confirm the handler returns ListToolsResult and that the tool name is stable.

Invocation returns an unknown-tool error

The client is sending a name that does not match your registered name. Compare the exact string in types.Tool(name=...) with params.name.

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

Initialization hangs

Verify that stdio_server() remains inside the asynchronous context and that server.run receives both streams plus server.create_initialization_options(). Also remove stdout logging.

Clients see a generic internal error

An exception escaped the handler. Catch expected validation and domain errors, return an isError=True result, and write the full traceback to server-side logs rather than returning secrets or stack traces.

Schema and implementation disagree

Because the low-level API performs no signature inference, update inputSchema whenever argument names, types, defaults, or required fields change. Test malformed, missing, and extra arguments.

Performance, reliability, and security considerations

  • Keep handlers asynchronous; move blocking work to an executor or an async library so one slow operation does not stall the connection.
  • Set explicit timeouts around network and filesystem operations and return a tool-level error when they expire.
  • Validate URLs, paths, authentication data, and numeric ranges before performing side effects.
  • Return only the data a client needs. Treat both text and structured content as potentially visible to a model.
  • For HTTP deployment, place the ASGI app behind the authentication, TLS, request-size, and observability controls required by your environment.
  • Pin a tested SDK version and review v2 migration notes before upgrading across major versions.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your MCP tool needs website screenshots, ScreenshotNeo provides a website screenshot API and MCP server, so your server does not need to manage a headless browser. A single GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and whether it was billed. AI agents can use its MCP tools: take_screenshot, get_page_info, and capture_pdf.

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

Example cURL request (see the ScreenshotNeo documentation):

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

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every plan includes the features: full-page and element capture, device presets, custom viewport and retina scale, PDF controls, HTML/CSS rendering, JavaScript and CSS injection, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen-TTL caching, signed links, asynchronous webhooks, bulk capture of 100 URLs per call, usage API, OpenAPI, and compatible parameter names used by other screenshot APIs. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000, and yearly billing gives two months free. Create a free ScreenshotNeo account.

Design checklist

  • Confirm the Python and SDK versions before copying code.
  • Write and review the JSON schema manually.
  • Return typed MCP results, including structured content only when useful.
  • Separate protocol errors from recoverable tool errors.
  • Register every capability family you intend to advertise.
  • Select stdio for local subprocesses and Streamable HTTP for remote access.
  • Keep stdout protocol-clean and protect metadata and credentials.

Frequently Asked Questions

Should every Python MCP project use the low-level Server class?

No. The official guide recommends the higher-level MCPServer for ordinary cases; use Server when exact schemas, result metadata, or unsupported protocol methods require direct control.

Can a low-level server expose tools and resources together?

Yes. Register the tool handlers and the corresponding resource handlers on the same Server, then return each method’s matching typed result.

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

What happens if a handler raises an exception?

The SDK turns it into a generic protocol error. Catch expected failures and return isError=True when the model should receive a recoverable explanation.

The Bottom Line

A low-level MCP server is a small asynchronous protocol endpoint: define handlers, schemas, and typed results explicitly, then run those handlers over the transport your host supports. Start with stdio for local development and move to the documented Streamable HTTP ASGI application when clients need a remote connection.

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.