ScreenshotNeo

BlogAI agents

Simple MCP Server Example in Python

Build a working MCP server in Python, inspect it locally, test it in memory, and understand tools, resources, prompts, errors, and deployment.

By the ScreenshotNeo team1 October 20266 min read

Short answer: install the MCP Python SDK, create an MCPServer, decorate a typed function with @mcp.tool(), and run uv run mcp dev server.py. The command launches MCP Inspector so you can call the tool interactively.

The official Python SDK documentation currently identifies v2 as the stable line and requires Python 3.10 or newer. See the official Python SDK documentation for the current API.

1. Install the SDK

Use either uv or pip. Include the cli extra because it provides the mcp command used by the local development workflow.

uv add "mcp[cli]"
# or
pip install "mcp[cli]"

Confirm that Python is 3.10 or newer:

python --version

2. Create the smallest useful server

Save this complete file as server.py:

from mcp.server import MCPServer

mcp = MCPServer("Demo")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

The type hints let the SDK generate the tool input schema. You do not need to write JSON Schema or protocol parsing for this example. The add function is a tool, while greeting is a URI-template resource.

3. Run and inspect it locally

uv run mcp dev server.py

This starts the server and opens MCP Inspector. In Inspector:

  1. Find the add tool.
  2. Enter 1 for a and 2 for b.
  3. Call the tool and verify that the result is 3.
  4. Open the resource URI greeting://World.
  5. Verify that it returns Hello, World!.

The SDK’s getting-started guide treats these examples as complete working files and documents Inspector as the interactive local workflow.

4. Test the server without a subprocess

For automated tests, connect directly to the server object with the in-memory client. This needs no port, subprocess, or network transport.

import asyncio

from mcp import Client
from server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        result = await client.call_tool("add", {"a": 1, "b": 2})
        assert result.structured_content == {"result": 3}


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

Save it as test_server.py and run:

uv run python test_server.py

This pattern is useful for unit tests because failures stay close to the Python function and do not depend on a running transport. The documented client approach is described in the official SDK getting-started guide.

5. Understand tools, resources, and prompts

Primitive Caller Use it for Example
Tool The model An action with inputs and a result add(a, b)
Resource The application Read-only data addressed by a URI greeting://World
Prompt A person A named message template selected from a menu or slash command A reusable review prompt

These roles are distinct in the SDK. A resource is not a tool, and a prompt is not an action the model automatically calls. The official server reference documents the three primitives.

6. Adapt the example safely

Use explicit types

Type every tool argument and return value. Types become part of the generated input contract and make Inspector and clients show useful validation errors.

@mcp.tool()
def multiply(price: float, quantity: int) -> float:
    """Multiply a unit price by a quantity."""
    if quantity < 0:
        raise ValueError("quantity must be zero or greater")
    return price * quantity

Keep side effects deliberate

Tools can call databases, APIs, or local programs, but validate arguments before performing side effects. Return structured, predictable values and avoid placing secrets in tool results or logs.

Use asynchronous functions for I/O

When an operation spends most of its time waiting on an async HTTP client or database driver, define the tool with async def and await the operation. Keep CPU-heavy work out of the event loop or move it to a worker.

Make resources read-only

Use resources for configuration, documents, or status data that the host reads. Put mutations behind tools so the client can present an explicit action and collect arguments.

7. Troubleshooting

Symptom Likely cause Fix
mcp: command not found The CLI extra is missing or the virtual environment is not active. Install mcp[cli] and run through uv run, or activate the environment containing the package.
Python version error Python is older than 3.10. Use Python 3.10+ and recreate the environment.
Inspector cannot load the file The path or working directory is wrong. Run the command from the directory containing server.py, or pass the correct relative path.
Tool arguments are rejected Input names or types do not match the function signature. Use the generated schema shown in Inspector and provide values matching the annotations.
A resource URI returns an error The URI does not match the template. Use the exact form greeting://{name}, such as greeting://World.
Import errors in the test The test is being run from a different directory or environment. Run it from the project directory with uv run python test_server.py and confirm that server.py is importable.
Requests hang A tool is waiting on network or blocking CPU work without a timeout. Add bounded timeouts to external calls, use async I/O where appropriate, and move long jobs to a background worker.
Secrets appear in output Credentials or full upstream responses are being returned or logged. Redact secrets, return only required fields, and keep credentials in environment or secret-management configuration.

8. Reliability, performance, and deployment notes

  • Timeouts: every outbound request should have a finite timeout and a clear failure result.
  • Retries: retry only idempotent operations, use exponential backoff, and cap the number of attempts.
  • Validation: reject malformed or out-of-range values before touching external systems.
  • Logging: log operation names, durations, and safe identifiers; never log API keys or private payloads.
  • Concurrency: avoid blocking calls inside async tools. Limit concurrent work when an upstream service has quotas.
  • Transport: Inspector is for local development. For a deployed server, follow the SDK guidance for transports, authorization, mounting into FastAPI or Starlette, and deployment.
  • Testing: keep fast in-memory client tests for behavior and use Inspector to manually verify schemas and interaction details.

Or skip the browser setup

If your MCP tool’s job is to capture webpages for an agent, ScreenshotNeo provides a website screenshot API and MCP server. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status with headers.

Use the API directly from Python (see the ScreenshotNeo API documentation):

import requests

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

Equivalent cURL:

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

Equivalent Node.js:

import { writeFile } from "node:fs/promises";

const q = new URLSearchParams({
  access_key: "YOUR_API_KEY",
  url: "https://stripe.com",
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
await writeFile("shot.webp", Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also supports full-page and element captures, dark mode, device presets, custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, async jobs, bulk capture, usage reporting, and an MCP server with take_screenshot, get_page_info, and capture_pdf. Its free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Does this example need an HTTP server?

No. Inspector manages the local development connection, and the in-memory client connects directly to the server object. Choose a deployment transport when you expose the server to a remote host.

Can one server expose several tools?

Yes. Add more decorated functions. Keep each tool focused, typed, validated, and documented with a useful docstring.

When should I use a resource instead of a tool?

Use a resource when the application needs to read data by URI. Use a tool when the model should request an action or a calculation.

Where should authentication be added?

Add it at the transport or application boundary used by your deployment, then enforce authorization inside sensitive tools. The local minimal example intentionally has no production authentication layer.