DEV Community

SRKDAN
SRKDAN

Posted on

What is MCP? Build and call your first remote MCP tool in 15 minutes

AI-assisted draft. I wrote and verified the code against the current MCP docs and Python SDK.

MCP (Model Context Protocol) is an open standard for connecting AI apps to external systems. Think of it like USB-C for AI: one shape of plug for tools, data, and workflows, instead of a custom integration for every host.

If you have used Claude Desktop, Cursor, VS Code, or ChatGPT with connectors, you have already seen the client side. This post focuses on the server side: a tiny remote tool you can run locally, then call from a real MCP client.

Three building blocks

An MCP server can expose:

  1. Tools - functions the model can call (search, create a ticket, generate an image).
  2. Resources - read-only data the app can pull in as context (files, schemas, docs).
  3. Prompts - reusable templates the user picks on purpose.

Most first servers start with tools. That is what we will build.

Local vs remote transport

How the bytes move is the transport:

Transport What it is When to use it
stdio Host launches your process and talks over stdin/stdout Local servers on one machine
streamable-http A normal HTTP endpoint (default path /mcp) Anything you deploy or call over the network
sse (old HTTP+SSE) Deprecated remote transport Only for older clients. Do not build new servers on it

Streamable HTTP replaced the older HTTP+SSE transport in the 2025-03-26 protocol revision. New remote servers should expose one MCP endpoint (for example https://your-host/mcp) that accepts client POSTs.

What you need

  • Python 3.11+
  • uv (or pip)
  • About 15 minutes

1. Scaffold a tiny server

uv init greeter
cd greeter
uv venv
source .venv/bin/activate
uv add "mcp[cli]"
Enter fullscreen mode Exit fullscreen mode

Create server.py:

from mcp.server import MCPServer

mcp = MCPServer("greeter")


@mcp.tool()
def greet(name: str = "World") -> str:
    """Return a short greeting for the given name."""
    return f"Hello, {name}!"


if __name__ == "__main__":
    # Streamable HTTP: clients hit http://127.0.0.1:3001/mcp
    mcp.run(
        transport="streamable-http",
        host="127.0.0.1",
        port=3001,
        # Stateless + JSON is a simple default for a first remote server
        stateless_http=True,
        json_response=True,
    )
Enter fullscreen mode Exit fullscreen mode

Notes that trip people up:

  • Transport options (host, port, stateless_http, …) go on run(), not on MCPServer(...).
  • For stdio servers, never write to stdout (print is unsafe). Use logging to stderr. For HTTP servers, normal stdout logging is fine.
  • The default MCP path is /mcp.

Start it:

uv run server.py
Enter fullscreen mode Exit fullscreen mode

You should now have http://127.0.0.1:3001/mcp.

2. Call the tool from a Python client

In another terminal (same venv, or another project with mcp installed):

import asyncio
from mcp import Client


async def main() -> None:
    async with Client("http://127.0.0.1:3001/mcp") as client:
        listed = await client.list_tools()
        print("tools:", [t.name for t in listed.tools])

        result = await client.call_tool("greet", {"name": "Dev.to"})
        # Simple string tools often expose structured_content;
        # otherwise inspect result.content for text blocks.
        print(result.structured_content or result.content)


asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

A URL string is treated as Streamable HTTP by the Python client. That is the remote path. For local stdio servers you would pass launch parameters instead of a URL.

Expected shape of the happy path: list_tools shows greet, and call_tool returns something like Hello, Dev.to!.

3. Point a real host at it

Hosts differ in UI, but the idea is the same: give them the MCP URL.

Claude (Custom Connector): Settings → Connectors → Add custom connector → paste http://127.0.0.1:3001/mcp (for a public server, use your HTTPS URL). Complete any auth the server asks for.

Cursor / VS Code-style config often looks like:

{
  "mcpServers": {
    "greeter": {
      "url": "http://127.0.0.1:3001/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Exact keys vary by client version. Prefer the client's current docs over screenshots from early 2025 tutorials, which still show /sse endpoints.

For a local stdio server instead, the config uses command + args so the host can spawn your process. Remotes use a URL.

4. Smoke-test with the Inspector

The MCP Inspector is the fastest way to see tools without wiring a full chat UI:

uv run mcp dev server.py
Enter fullscreen mode Exit fullscreen mode

That path is aimed at stdio servers. For an already-running Streamable HTTP server, open the Inspector UI and connect to http://127.0.0.1:3001/mcp as an HTTP / Streamable HTTP target. List tools, call greet, confirm the text result.

Common mistakes

  1. Building on SSE-only /sse URLs. That is the deprecated remote transport. Prefer /mcp Streamable HTTP.
  2. Putting port= on MCPServer(...). It belongs on run(...).
  3. Printing to stdout on a stdio server. Corrupts JSON-RPC.
  4. Copying 2024 client snippets. Many still assume initialize/session headers that newer protocol revisions changed. Check the current MCP docs and the Python SDK run guide.

What to build next

Swap greet for a real tool: wrap an internal API, return a resource URI for docs, or add a prompt template for a workflow your team repeats. Keep tools small and typed. Prefer one clear job per tool over a mega-tool with a dozen modes.

If you ship the server beyond localhost, put it behind HTTPS, require auth (API key or OAuth), and turn on the SDK's transport security options before you expose it.

Sources

Questions or corrections welcome in the comments. Protocol details move; if something here diverges from the live docs, trust the docs.

Top comments (0)