MCP server tutorial: how to build an MCP server

By AIPx team · Published 2026-10-14 · Updated 2026-10-14

To build an MCP server, define a small capability with the official SDK, inspect its schema and verify it through a client before connecting a model. The server supplies operations; the host decides when to use them. This tutorial builds a credential-free Python calculator so the first test isolates the protocol interface from account access and upstream API failures.

The example uses the official Python SDK v2 API and was checked with mcp 2.3.0 on October 14, 2026. An isolated in-memory test returned 3 for add(1, 2) and rejected a non-integer input. That validation covers this sample, not a production deployment, remote authentication or every host’s compatibility.

MCP server setup requirements

Use Python 3.11 or newer and a separate project environment. The official installation guide identifies the mcp package and CLI extra. These v2 examples use MCPServer from mcp.server and Client from mcp; older FastMCP examples belong to a different SDK interface.

Create an empty directory and run:

python3 -m venv .venv
.venv/bin/python -m pip install 'mcp[cli]==2.3.0'

The commands assume a Unix-style environment. On Windows, use the corresponding virtual environment executable paths. Pinning the package makes the example reproducible; it is not a recommendation to stay on this version permanently without reviewing future updates.

No model API key, application OAuth token or cloud service is required for this operation. That is deliberate. If the first example fails, you can investigate installation, schema and client interaction without guessing whether an upstream account is authorized.

Keep the exercise outside your production application and dependency tree. A standalone prototype should not silently add Python packages to a product workspace or change package boundaries merely to validate a tutorial.

Build a minimal MCP server in Python

Save the following complete file as server.py:

from mcp.server import MCPServer

mcp = MCPServer("Calculator")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers without changing external state."""
    return a + b

The decorator registers the function as a tool. Its name identifies the operation, its docstring describes it, and the type hints allow the SDK to generate the input schema. The body has no credentials or external side effects, making success and failure easy to inspect.

The official first-steps guide describes this v2 registration approach. We keep the example small because a server does not need to provide resources and prompts merely to be useful. Add another primitive only when the host interaction requires it.

A real handler would translate the validated request into an application operation. That introduces another boundary: object ownership, upstream account permissions, API errors and data returned to the host. Do not add an arbitrary network fetch or shell command to this sample as a shortcut to a “real” integration.

See what is an MCP server for the surrounding role and tools, resources and prompts for the other primitives. The calculator demonstrates a tool interface, not a complete agent loop.

Inspect and test your MCP tools

Save this complete file as check.py beside the server:

import anyio
from mcp import Client
from server import mcp


async def main() -> None:
    async with Client(mcp, raise_exceptions=True) as client:
        result = await client.call_tool("add", {"a": 1, "b": 2})
        assert result.content[0].text == "3"
        print(result.content[0].text)

        invalid = await client.call_tool(
            "add", {"a": "not-an-integer", "b": 2}
        )
        assert invalid.is_error
        print("invalid input: rejected")


if __name__ == "__main__":
    anyio.run(main)

Run it in the project directory:

.venv/bin/python check.py

The two application output lines are:

3
invalid input: rejected

The SDK may also emit a diagnostic line for the rejected arguments. The assertions are the verification: a valid call returns the expected result and an invalid integer input produces an error result. We executed this exact behavior in a temporary virtual environment using the pinned SDK.

The official testing guide documents the in-memory client. It avoids a subprocess, port and remote endpoint, so the test focuses on the registered handler. It does not prove HTTP authorization or a desktop client’s configuration.

raise_exceptions=True is useful for investigating failures outside the tool body in this testing path. It does not mean tool errors cease to be normal error results. Keep expected invalid-input behavior separate from an unexpected crash, and inspect the result fields rather than assuming every returned message means success.

For interactive inspection, the official CLI provides:

.venv/bin/mcp dev server.py

Follow the local Inspector URL it prints and inspect the tool description and fields. The interactive Inspector path was not part of this tutorial’s executed in-memory validation. It can require its own local tooling and browser setup; use current official instructions if that environment differs.

Connect the server to Claude or Cursor

Once the handler works without a model, choose a host and the transport it supports. A local stdio setup launches a command, while a remote setup points at a service endpoint. Do not copy one form into the other and assume the host will infer the deployment.

Follow the host-specific guides for Claude Desktop, Claude Code, Claude or Cursor. Codex has its own configuration route. Use the exact executable path and working directory required by that host.

Begin with a request such as adding two integers. Inspect whether the client discovers the expected operation and whether the result is displayed correctly. If discovery fails, check command paths, environment, transport and supported revision before adding permissions or unrelated tools.

A model-connected success is useful evidence about the host path, but keep it separate from the handler test. The model can choose not to call the tool or produce an incorrect explanation of a correct result. Your server’s contract should remain testable without relying on model behavior.

MCP server input validation and errors

Type validation is a starting point. In this sample, a string that cannot be interpreted as an integer is rejected. In an account integration, an apparently valid string might identify another user’s object or an unsafe network destination. Validate permitted meaning as well as shape.

Keep the caller and account boundary explicit. An authenticated request should not gain access to every object the process can reach. Check the relevant actor, account and object before executing a write or returning sensitive data.

Use errors the host can interpret without exposing secrets. Do not include raw credentials, internal exception dumps or unnecessary personal data in tool results. Preserve useful operational evidence in the appropriate protected reporting channel instead.

Design retry behavior per operation. Addition is safe to repeat, but creating an issue or sending a message may not be. If a response is lost after a write, inspect upstream state or use a business idempotency mechanism rather than repeating blindly.

Document limits in the operation description. A lookup tool should state its intended account or project boundary where that is relevant. Clear descriptions help the model choose correctly, but the handler still enforces the access rule. See MCP security for the broader threat paths.

Move from stdio to Streamable HTTP

The v2 CLI supports the following remote-service transport command:

.venv/bin/mcp run server.py --transport streamable-http

The CLI startup was also checked locally with the pinned SDK: it started a service on 127.0.0.1:8000 and shut down after a short timed run. Treat this as a development server invocation. A local HTTP process is not a reviewed public deployment. Before remote exposure, decide where it runs, who can reach it, how callers authenticate and how credentials and records are handled.

Keep protocol revision distinct from transport. The official current revision checked for this article is 2026-07-28, while SDKs and hosts can offer compatibility paths for earlier releases. The specification guide explains why older initialization examples should be labeled instead of copied into a current hand-built exchange.

Use the SDK’s supported interfaces rather than assembling low-level envelopes from mixed tutorials. For remote deployment requirements, read remote MCP servers and the official security guidance. Authentication belongs to the service boundary even when the first tool has no sensitive inputs.

Host securely or connect through a gateway

A server runtime and a gateway are different responsibilities. Somebody must operate the Python service, update dependencies and handle its authorization. AIPx can connect compatible already-hosted remote MCP endpoints; it does not provide verified arbitrary Python code hosting or generic OpenAPI import.

A MCP gateway can add a shared account and tool access boundary. In AIPx, rules attach to tool × account × agent. Allow and Confirm are execution hints, so review depends on the client honoring metadata. Hide is server-enforced and removes the tool from the agent surface. Do not assume adding the gateway turns every write into a mandatory approval flow.

For one narrow tool and a trustworthy direct endpoint, a gateway may add little value. For several supported accounts and agents with different access, shared configuration can help. In either case, keep the upstream handler’s validation and account permissions intact.

Before broadening the prototype, record what has actually been verified: pinned SDK installation, one valid operation and one invalid-input result. Add separate checks for host configuration, HTTP authentication and any upstream side effects you introduce. The small test gives you a reliable starting contract, while each new boundary needs its own evidence.

Further reading

openapi to mcp; what is mcp.

FAQ

How do I build an MCP server?

Use an official SDK to register a bounded operation, inspect its schema and test it through a client before connecting a model. Add authentication and upstream access only when needed.

Which Python SDK API does this tutorial use?

It uses the official v2 interface, with MCPServer imported from mcp.server and Client imported from mcp. The executed example used mcp 2.3.0.

Was the example tested?

Yes. In an isolated temporary environment, the in-memory client returned 3 for add(1, 2) and returned an error for a non-integer input. This is not a production or remote-authentication test.

Can I run the server remotely?

The SDK supports Streamable HTTP, but public deployment also needs reviewed hosting, authorization, credential handling and operational controls.

Can AIPx host my Python server?

No arbitrary code-hosting feature is verified. AIPx can connect a compatible already-hosted remote MCP endpoint, while the upstream server remains separately operated.