On this page
This tutorial builds a small read-only inventory server using the official MCP Python SDK. A client can discover one tool and ask for the count of a known demonstration SKU. Unknown identifiers return an error rather than pretending the inventory is zero.
The example was executed on 9 November 2026 using Python 3.14 and mcp 2.3.0. Both the in-memory client and a real stdio subprocess passed discovery, positive-stock, zero-stock, unknown-SKU and missing-argument checks. No HTTP deployment, third-party host compatibility or performance benchmark is claimed.
The inventory is synthetic. There is no model call, customer database, API key or paid service. This makes protocol behavior observable without mixing it with model quality or external-system access.
Understand the version before copying code
The current official SDK documentation describes the v2 release line and Python 3.11 or later. This example pins the version actually tested instead of assuming future releases preserve every interface.
In particular, this tutorial imports MCPServer from mcp.server. Older SDK tutorials can use different imports and connection patterns. Follow the migration documentation when adapting an existing deployment; do not mechanically combine v1 and v2 snippets.
The MCP architecture guide explains the distinction between the host, client and server. Here the Python check acts as a client, and the inventory process supplies the capability.
Create an isolated environment
Create a directory for the example, open a terminal in it and use a virtual environment. On Linux or macOS:
python3 -m venv .venv
.venv/bin/python -m pip install mcp==2.3.0On Windows, the corresponding interpreter is normally .venv\Scripts\python.exe; create the environment with your installed Python interpreter and use that path for the commands below. The documented minimum is not a claim that this article's test ran on every supported Python or operating-system version.
This repository includes the exact example in examples/blog/mcp-python/. requirements.txt pins the SDK, and requirements.lock.txt records the dependency versions resolved in the executed environment. Use the lock for reproduction in a compatible environment and reassess dependencies before a production upgrade.
The example uses no CLI extra or Inspector, so Node.js is not needed for its checks. A real host can be added later after the protocol behavior has been verified.
Implement the server
Save this as server.py:
"""Read-only illustrative inventory server; no external services or credentials."""
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
mcp = MCPServer("Example inventory")
STOCK = {"demo-cable": 12, "demo-adapter": 0}
@mcp.tool()
def stock_count(sku: str) -> int:
"""Return available units for an exact demo SKU; never places an order."""
if sku not in STOCK:
raise ToolError("Unknown SKU")
return STOCK[sku]
if __name__ == "__main__":
mcp.run(transport="stdio")The name identifies the server. The decorated function is the public capability. Its type annotation and description communicate that the input is an exact SKU and the output is a count. It deliberately has no order-placement or arbitrary database operation.
The two known identifiers distinguish positive stock from zero stock. An unknown identifier raises ToolError. That distinction matters operationally: an unavailable item and a failed lookup should not be presented as the same business fact.
The main guard starts stdio only when the file is executed. Importing it for an in-memory test does not launch a blocking process. Keep protocol output clean; do not add ordinary diagnostic print calls to server stdout. Use an appropriate logging destination such as stderr.
Exercise the protocol rather than only the Python function
Calling stock_count directly would test the arithmetic and dictionary access, but would not establish that a client can discover or invoke the exposed tool. The check below goes through the SDK client and inspects the typed result.
Save this as check.py alongside server.py:
import asyncio
import sys
from pathlib import Path
from mcp import Client, StdioServerParameters
from server import mcp
async def check(target):
async with Client(target) as client:
listed = await client.list_tools()
assert any(t.name == "stock_count" for t in listed.tools)
for sku, expected in [("demo-cable", 12), ("demo-adapter", 0)]:
result = await client.call_tool("stock_count", {"sku": sku})
assert not result.is_error
assert result.structured_content == {"result": expected}
missing = await client.call_tool("stock_count", {"sku": "unknown"})
assert missing.is_error
invalid = await client.call_tool("stock_count", {})
assert invalid.is_error
async def main():
await check(mcp)
await check(StdioServerParameters(command=sys.executable, args=[str(Path(__file__).with_name("server.py"))]))
print("PASS: discovery, positive stock, zero stock, unknown SKU and invalid input over memory and stdio")
asyncio.run(main())The official client documentation describes connecting the same client abstraction to server objects and subprocess transports. This check uses both. sys.executable selects the interpreter running the check, so the child uses the same environment and installed SDK rather than an unrelated system Python.
The absolute script path is derived from the check file. This avoids relying on a particular current working directory when the child starts. Your eventual host configuration should likewise use an unambiguous interpreter and script location.
Run and inspect the result
Run:
.venv/bin/python check.pyThe final success line is:
PASS: discovery, positive stock, zero stock, unknown SKU and invalid input over memory and stdioThe two deliberately failing operations can also produce diagnostic messages about the unknown SKU and missing argument. Those are expected negative-case logs. The assertions require their protocol results to identify failure; the check is not suppressing a broken case to obtain a pass.
In the executed run, valid calls returned structured results containing result: 12 and result: 0. The unknown identifier and missing required argument were errors. Discovery exposed stock_count. These are the limits of the reported execution evidence; no production database or remote host was involved.
Why test zero separately?
A check that only tests a positive count can miss truthiness bugs. If application code treats zero as failure, it may report a valid out-of-stock result as a broken integration. Conversely, converting every failure to zero hides missing or invalid identifiers.
Keep the distinction in both the protocol result and the host's presentation. An application should inspect error state before interpreting structured content as a successful business result.
For a real database, add tests for tenant isolation, denied permissions, unavailable dependencies and stale data. The demonstration dictionary cannot establish those properties.
Start the process manually
You can run:
.venv/bin/python server.pyA stdio server waits for protocol input; it is not a web page and does not print a browser address. A quiet process is expected. Use the provided client to communicate with it rather than typing natural-language questions into the terminal.
For an actual host, consult that host's current local-server configuration instructions. Specify the absolute interpreter path and absolute server.py path. Configuration keys and supported capabilities vary by host, so this article does not invent a universal configuration block.
Troubleshooting
| Symptom | Check |
|---|---|
Import of MCPServer fails | Confirm mcp==2.3.0 is installed in the interpreter actually running the file |
| Child process cannot import the SDK | Use the environment's Python, not an unrelated global executable |
| Server appears to hang when run manually | Stdio waits for protocol messages; run the client check |
Missing sku is an error | Supply the required argument; this is an intentional schema check |
| Unknown identifier fails | Use a documented demo SKU or implement an explicit real-data lookup |
| Structured result assertion changes after upgrade | Inspect release notes and the new contract before updating the test |
When reporting a problem, include the Python version, SDK version, transport and sanitized exception. Do not include private credentials or database records in a public issue.
Security boundaries before adding real data
Read-only design limits actions but does not automatically protect confidential data. A real inventory integration must authenticate its caller where required and authorize access to the requested account or catalogue. Never trust a supplied tenant identifier as proof of ownership.
Keep database credentials outside the exported code. Use the least permissions needed for lookup. Bound input size, execution time and concurrency. If you later add writes, design duplicate prevention and recovery before allowing automatic retries.
Treat returned descriptions and external records as untrusted content. The host must not follow embedded instructions that attempt to change its permissions or disclose secrets. Tool schemas are useful interfaces, not authorization policies.
What changes for HTTP deployment?
Remote deployment adds a network endpoint, authorization, hosting and operating requirements. The current transport specification distinguishes Streamable HTTP from stdio. Read the appropriate SDK deployment and authorization guidance before exposing this example.
This article's tests do not validate an HTTP deployment. Do not describe a proposed remote setup as tested merely because the in-memory and local subprocess checks passed. Add integration checks against the real deployed configuration and an appropriate authorized client.
Extend only after the contract is stable
A useful next change is replacing the dictionary with a narrowly scoped data adapter while preserving the distinction between success, missing data and denied access. Keep the protocol checks and add adapter-specific tests. Introduce a model or an agent only when the host's task actually requires it.
For distribution, read where to publish an MCP server. For an importable workflow, use agent publishing guidance. Theaimart's developer documentation describes its verified product flows without implying that a workflow listing hosts your MCP process.