How to Build MCP Server
Build an order lookup MCP server with Python, test it in Inspector, connect Claude Code and Cursor, then add HTTP auth and hosting.
Published

You can let Claude Code or Cursor answer an order-status question from your company's data without copying a record into chat. Build one read-only MCP tool, prove it in the Inspector, then choose a local process or an authenticated HTTP service that your team can share.
The useful first server has a small job: accept an order ID and return the status. Start there. A general database tool makes the model responsible for too many decisions and gives it more access than this workflow needs.
This walkthrough follows the current official server tutorial. As of 7 October 2026, the documentation uses MCP specification 2026-07-28 and the official Python SDK, the library that handles MCP messages, with its MCPServer API. The example pins SDK 2.3.0, so an older tutorial's imports cannot quietly change what you install.
What Your MCP Server Exposes
An MCP server is a controlled service desk between an AI application and your systems. The application can ask what is available, submit a request, and receive a result. Your code decides what the desk can do.
MCP, the Model Context Protocol, gives that conversation a shared format. A host is the application you use, such as Claude Code or Cursor. Its client handles the protocol conversation with your server. These are roles, not three extra applications you need to install.
A tool can be read-only. Calling something a resource does not replace access checks. A prompt gives instructions, not permission. Client support and presentation vary, so verify the features you actually expose. These are the three server primitives; the server below needs only a tool.

Before building, check whether a maintained connector already covers the job. Our best MCP servers for 2026 is the starting point. A custom server earns its keep when your internal data, permission rules or workflow differ from those connectors.
Build a Read-Only Order Lookup in Python
Use the official SDK to handle the protocol and keep your code focused on the lookup. You need Python 3.10 or newer, uv, the Python project manager used in the official tutorial, and Node.js for the Inspector. The current Inspector requires Node 22.19.0 or newer.
In a terminal, run these commands in order:
uv init orders-mcpcd orders-mcpuv venvuv add "mcp[cli]==2.3.0"
Create orders.py in that folder and paste this complete server. The records are fictional training data. There are no customer names, payment details or API credentials.
import json
from mcp.server import MCPServer
mcp = MCPServer("orders")
# Fictional training data. No customer records or credentials.
ORDERS = {
"A100": {"status": "shipped", "carrier": "Demo Courier"},
"A101": {"status": "packing", "carrier": "not assigned"},
}
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up a fictional order by ID, such as A100. Read-only.
Args:
order_id: Exact order ID, for example A100 or A101.
"""
key = order_id.strip().upper()
order = ORDERS.get(key)
if order is None:
return json.dumps({"found": False, "order_id": key})
return json.dumps({"found": True, "order_id": key, **order})
if __name__ == "__main__":
mcp.run(transport="stdio")This uses the tutorial's documented MCPServer, @mcp.tool() and mcp.run(transport="stdio") pattern, with an order lookup in place of its weather tools. The string type hint tells the SDK that order_id is required text. The docstring tells the client when the tool is useful. The SDK creates the tool definition and handles protocol messages.
Run uv run orders.py. A quiet process waiting for input is expected. stdio, standard input and output, is the pipe the client uses to talk to this process. Stop this manual run before letting a client launch its own copy.
Keep application logs out of standard output. Use Python's logging module, which writes to standard error by default. A stray print() can corrupt the protocol stream. This is a documented stdio constraint, not a cosmetic logging preference.
When you replace the dictionary with a database, preserve the same small interface. Use a parameterized lookup, a database account that can only read the necessary fields, and a caller-specific permission check before returning a record. Never take an arbitrary SQL statement from the model for this task.
Test It With MCP Inspector
Prove the tool works before asking a model to use it. From the project folder, run uv run mcp dev orders.py. The SDK development command launches MCP Inspector. Open the browser URL printed by the command and connect the server if it is not already connected.
In Tools, select lookup_order. The form should show a required order_id field. Call it with A100: the result should contain found: true, status: shipped and carrier: Demo Courier. Call A101 for packing. Call DOES-NOT-EXIST: the result should contain found: false.
Also try a request without order_id. It should fail input validation instead of running a lookup. The Inspector's Protocol and Console views help distinguish a malformed request from a server-process failure.
For a repeatable terminal check, use npx @modelcontextprotocol/inspector --cli uv run orders.py --method tools/list. To call the tool, use npx @modelcontextprotocol/inspector --cli uv run orders.py --method tools/call --tool-name lookup_order --tool-arg order_id=A100. These use the documented Inspector CLI.
The pass condition is concrete: one discoverable tool, the expected status for a known order, and an explicit missing-record response. A plausible paragraph generated by a model is not evidence that the lookup ran.
Connect the Same Server to Claude Code and Cursor
Each local client launches its own server process. You do not need to keep the Inspector running. Use absolute paths so the launch does not depend on whichever folder the client happens to open.
Claude Code: run claude mcp add --transport stdio --scope local orders -- /ABSOLUTE/PATH/orders-mcp/.venv/bin/python /ABSOLUTE/PATH/orders-mcp/orders.py. On Windows, the interpreter is .venv\Scripts\python.exe. This follows Claude Code's local-server syntax, including the -- separator before the launch command.
Run claude mcp get orders to check the connection. In a Claude Code session, open /mcp, then ask: “Use lookup_order to check A100. Report only the returned status and carrier.” Inspect the tool call and its arguments.
Cursor: create .cursor/mcp.json in your project. Add this JSON, replacing both absolute paths: {"mcpServers":{"orders":{"type":"stdio","command":"/ABSOLUTE/PATH/orders-mcp/.venv/bin/python","args":["/ABSOLUTE/PATH/orders-mcp/orders.py"]}}}.
Open Customize, enable the server and ask the same question in Agent. Review the call according to your approval settings. The file location, launch fields and controls follow Cursor's MCP configuration. If the connection fails, check the executable path and server stderr before changing the tool.
Choose Local stdio or Remote HTTP
Stay local for a personal workflow. Choose remote HTTP when multiple people or hosted clients need one managed service.
A remote service also needs network access to your company data. Publishing an endpoint does not make a private database reachable or make its permissions correct.
The 2026-07-28 HTTP protocol uses self-contained requests. The current Python SDK can also serve older clients, whose sessions may need sticky routing when you add replicas. Use the documented legacy settings deliberately before scaling; do not assume every connected client speaks the newest revision.

Move the Lookup to HTTP With Authentication
Protect HTTP access with tokens issued for this service. OAuth 2.1 is the authorization framework in the MCP specification: an identity provider signs the user in and issues a token, while your MCP server verifies it. A scope is a named permission, such as orders:read. The audience says which service may accept the token.
The SDK supplies the resource-server integration, not your company's login system. For this example, configure an identity provider with OAuth discovery, client registration for your chosen clients, PKCE for user login, which proves the app completing sign-in is the one that started it, and a token-introspection endpoint. Introspection asks the issuer whether a token is active and what it permits.
This adapter expects HTTPS introspection with HTTP Basic client authentication and a response containing active, aud, exp, client_id and scope. Configure the issuer to include this endpoint's exact public URL in aud and to issue orders:read. If your provider uses a different introspection authentication method, adapt that request to its documentation. If it provides JWTs, signed tokens, instead, implement signature, issuer, expiry and audience verification in the same TokenVerifier interface.
Add uvicorn with uv add uvicorn, then create remote.py beside orders.py. This follows the SDK's official introspection example and its documented HTTP and auth interfaces. It reuses the lookup you just tested.
import os
import time
from urllib.parse import urlsplit
import httpx2
from pydantic import AnyHttpUrl
from mcp.server import MCPServer
from mcp.server.auth.provider import AccessToken, TokenVerifier
from mcp.server.auth.settings import AuthSettings
from mcp.server.transport_security import TransportSecuritySettings
from orders import lookup_order as local_lookup
RESOURCE = os.environ["MCP_RESOURCE_URL"]
ISSUER = os.environ["MCP_ISSUER_URL"]
INTROSPECT = os.environ["MCP_INTROSPECTION_URL"]
if any(urlsplit(url).scheme != "https" for url in (RESOURCE, ISSUER, INTROSPECT)):
raise ValueError("Public auth and resource URLs must use HTTPS")
class OrderTokenVerifier(TokenVerifier):
async def verify_token(self, token: str) -> AccessToken | None:
try:
async with httpx2.AsyncClient(timeout=5.0) as client:
response = await client.post(
INTROSPECT,
data={"token": token},
auth=(os.environ["MCP_INTROSPECTION_CLIENT_ID"],
os.environ["MCP_INTROSPECTION_CLIENT_SECRET"]),
)
response.raise_for_status()
data = response.json()
audiences = data.get("aud", [])
if isinstance(audiences, str):
audiences = [audiences]
expiry = data.get("exp")
if (data.get("active") is not True or RESOURCE not in audiences
or not isinstance(expiry, int) or expiry <= time.time()):
return None
if data.get("iss", ISSUER) != ISSUER:
return None
return AccessToken(
token=token, client_id=data["client_id"],
scopes=data.get("scope", "").split(), expires_at=expiry,
resource=RESOURCE, subject=data.get("sub"),
)
except Exception:
return None
mcp = MCPServer(
"orders",
token_verifier=OrderTokenVerifier(),
auth=AuthSettings(
issuer_url=AnyHttpUrl(ISSUER),
resource_server_url=AnyHttpUrl(RESOURCE),
required_scopes=["orders:read"], validate_token_resource=True,
),
)
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up a fictional order by ID, such as A100. Read-only."""
return local_lookup(order_id)
hostname = urlsplit(RESOURCE).hostname
security = TransportSecuritySettings(
allowed_hosts=[hostname, f"{hostname}:*"],
allowed_origins=[os.environ["MCP_ALLOWED_ORIGIN"]],
)
app = mcp.streamable_http_app(transport_security=security)Set these values in your deployment's environment or secret store:
The host allowlist is explicit because the SDK otherwise accepts localhost by default and rejects a public hostname with 421 Misdirected Request. Browser origins are a separate check; list only the origins you actually use. The returned app already includes its startup and shutdown lifecycle. These details follow the SDK deployment and ASGI app documentation. ASGI is the interface Python web servers use to run this application.
Start it behind your host's HTTPS proxy with uv run uvicorn remote:app --host 0.0.0.0 --port 8000. The public resource URL remains HTTPS even if the proxy talks HTTP to the process. Configure forwarded-header trust for that host's actual proxy boundary.
Before using real records, run these checks against HTTP:
- No token, expired token or token for another audience: access is refused.
- Valid token without
orders:read: access is refused. - Valid token with the correct audience and scope:
lookup_orderreturns the demo status. /.well-known/oauth-protected-resource/mcp: the metadata identifies the correct resource and issuer.
Use npx @modelcontextprotocol/inspector --server-url https://orders.example.com/mcp --transport http to inspect the deployed endpoint and complete its authentication flow. An in-memory tool test skips HTTP authorization, so it cannot prove this boundary works.
For Claude Code, add a separate connection with claude mcp add --transport http orders-remote https://orders.example.com/mcp, then authenticate through /mcp. For Cursor, add a remote entry under mcpServers with "url":"https://orders.example.com/mcp" and complete OAuth. Cursor also documents an auth object with CLIENT_ID and scopes for pre-registered clients. Register the appropriate client callbacks with your issuer. See Claude Code authentication and Cursor's remote OAuth setup.
This is a small authenticated adapter, not the whole production system. Before replacing the demo dictionary, enforce tenant and record permissions using verified identity, add an audit event around the call, reuse HTTP connections, and cap requests. A scope permits the operation; it does not establish ownership of every order.
Host It on Render or Cloudflare Workers
For the Python server above, I would start with Render. A Python web service preserves the app you have already built. Cloudflare Workers is a strong option if you want to implement the same bounded tool in its documented Worker handler.
These are maker-published prices checked on 7 October 2026:
Sources: Cloudflare Workers pricing and Render pricing. Render's Pro workspace adds $25/month plus compute if you choose its team features. Storage, identity services, model usage and other extras must be budgeted separately; these are hosting prices, not the cost of an AI workflow.
Render deployment: put orders.py, remote.py and requirements.txt in your repository. The requirements file needs mcp[cli]==2.3.0 and uvicorn, each on its own line. Create a Python Web Service, use build command pip install -r requirements.txt, and start command uvicorn remote:app --host 0.0.0.0 --port $PORT. Add the environment variables above, then use the assigned hostname or your custom domain in MCP_RESOURCE_URL. This adapts Render's documented Python web-service deployment to the SDK's ASGI app.
Render's free service is useful for a demo, but it sleeps after 15 idle minutes and takes about a minute to wake. I would use paid compute for an interactive shared tool.
Cloudflare deployment: use the current MCP handler documentation and remote-server guide. The current TypeScript path uses createMcpHandler from agents/mcp/server with @modelcontextprotocol/server. Implement the same order lookup there and configure authentication before sharing the URL. The Python uvicorn launch command is for a Python host; it is not a Worker deployment recipe.
Keep the Security Boundary Small
Give the server only the access its tool requires. For order status, that means a read-only backend credential, selected fields and a per-record permission check. Keep refunds, cancellations and address changes behind separate tools and permissions. The model's choice of arguments is never authorization.
Validate HTTP tokens for your issuer, expiry, audience and scope. Use HTTPS and separate credentials when the server calls a downstream API. The MCP security guidance forbids token passthrough: a token presented to your MCP endpoint is not automatically a credential for your order system. For local stdio, restrict the launching process, its environment and its filesystem access.
Log the verified caller, tool name, an appropriately redacted record reference, outcome, latency and request ID. Avoid tokens and complete customer records. Keep logs on stderr for stdio and in your host's log system for HTTP. Treat text retrieved from records as data; a note inside an order must not grant permission for another action.
Put a gateway in front when multiple servers or teams need shared identity policy, rate limits, audit collection or revocation. A gateway can centralize those controls; each backend still needs correct record permissions. Our MCP gateway guide explains that decision.

Six Useful Workflows, Ranked by Immediate Payoff
These are possible extensions of the same pattern. Start where a person repeatedly retrieves a bounded fact and can recognize a correct answer.
The business math should start with your own workflow. Illustrative only: 80 lookups a day at 2 minutes each consume 160 minutes. If measurement later shows that the connected workflow saves 1 minute per lookup, that is 80 minutes recovered per day. This is arithmetic, not a performance benchmark. Measure correct answers and time saved before claiming a return from the hosting spend.
Two Things Worth Building
The strongest opportunity is an order-context adapter for support teams. A team could buy a narrow integration that retrieves the right shipment facts inside the assistant it already uses. DataForSEO estimates 260 US Google searches per month for “customer support automation”, checked on 7 October 2026. That is broad interest in the job, not a count of MCP buyers. Intercom publishes Fin at $0.99 per outcome, which shows an existing budget for support automation; this tiny adapter supplies context rather than replacing that product.
The smallest sellable version could cover one order backend, lookup_order, scoped login, an audit trail and a reply draft that cites returned fields. Its advantage would be fitting a specific company's data and access rules. The catch is that existing vendors may already have the connector, and a team's model seats, data cleanup and support still cost money. Validate that gap with a support lead before adding more tools.
A permission-aware policy lookup is the second opportunity. An operator could build access to one company's approved policy collection through resources or a narrow search tool. DataForSEO estimates 390 US monthly searches for “enterprise search”, checked on the same date. The MVP could include one collection, source citations, freshness checks and filtering by the signed-in person's permissions. The catch is that retrieval quality and access control are the product; wrapping a folder in MCP is easy to copy. The search figure is a demand signal for the broader job, not evidence that users will pay for this implementation.
What This Does Not Solve
MCP standardizes access. You still own data quality, authorization, backend reliability and the choice of what a tool can do. A model can misread a valid result, and a connected client can have different capability support or approval policies.
Build this when a shared tool interface will improve a workflow you can measure. For a fixed batch job that never needs an assistant to choose a tool, an ordinary API call or script may be the better implementation.
Your Monday move: pick one recurring lookup with a support operator, use fictional records to get both clients connected, then replace the dataset behind a read-only account and test record permissions. Keep writes out of that first rollout. Promote it to shared HTTP when the workflow and identity boundary are ready.
Is it hard to build an MCP server?
A small read-only server is straightforward with the official SDK: define the function, describe its input and choose a transport. Making company data available safely takes more work because you must enforce permissions, manage credentials and operate the service.
Is there a free MCP server I can use for testing?
The fictional order server in this tutorial can run locally without a hosting charge. MCP Inspector lets you call it without a model subscription. Cloud hosting and the AI client you later choose have their own pricing.
How much does an MCP server cost?
A local process needs no separate hosting plan. Cloudflare Workers publishes a free tier and a $5/month paid minimum. Render publishes $7/month compute for its small paid web service. These figures exclude model usage, identity services, storage and engineering.
Do I need to install an MCP server?
For stdio, the server runs on the client's machine, so its code and runtime must be available there. For remote HTTP, you configure an endpoint and authenticate; the server runs on the host. Use the deployment your chosen client supports.
If you want a company-data MCP service built and operated for your team, our AI production systems service covers the integration and its access controls.
- Published
- Category
- Build
- Language







