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.
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.
#1 Best Overall
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.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →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.
Rank #2
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.
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_resourcesandon_read_resourcefor resource discovery and reads.on_list_promptsandon_get_promptfor prompt templates.on_completionfor 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.
- Install dependencies in the same environment used by the host.
- Run
python server.pymanually to catch import and startup errors. - Configure your MCP host to launch the interpreter and script as a stdio server.
- Ask the host to list tools and verify that
addappears with both required integer fields. - Invoke
addwith values such as2and3; the text content should be5and 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.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
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.
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.
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.
Best Value
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.
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.
Quick Recap
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.

