Skip to main content
Glama
telvis07

mcp-server-deep-dive-deployment

by telvis07

mcp-server-deep-dive-deployment

Example Model Context Protocol servers, built while working through the course below. The repo ships two of them — one over stdio, one over streamable HTTP — so the two transports can be compared side by side. Both are packaged so they can be installed and run directly from this GitHub repository, with no clone required.

Course: MCP Complete Guide – Build and Connect Tools for LLMs (O'Reilly)

Prerequisites

  • uv 0.12.9 or newer

  • Python 3.14 (uv will download it for you if it isn't already installed)

Related MCP server: node-mcp-poc

Local setup

USERNAME throughout this README stands in for the GitHub account hosting the repo — substitute your own.

git clone https://github.com/USERNAME/mcp-server-deep-dive-deployment.git
cd mcp-server-deep-dive-deployment
uv sync

uv sync creates .venv/ and installs the locked dependencies from uv.lock.

Servers

The repo ships two servers so the two transports can be compared side by side. Each one is a separate console script; a client talks to one or the other, never both at once.

Script

Module

Transport

Use it when

mcpserver

deployment.py

stdio

the client launches the server as a subprocess

mcpserver-http

http_streamable_io.py

streamable HTTP

the server already runs somewhere and you dial a URL

Run the servers

stdio — running it directly just waits for a client on stdin/stdout. That's expected, not a hang:

uv run mcpserver

streamable HTTP — this one listens, so you can curl it or point a client at the URL. It serves MCP at /mcp on 127.0.0.1:8000; set HOST and PORT to move it:

uv run mcpserver-http
# -> http://127.0.0.1:8000/mcp

PORT=9000 uv run mcpserver-http

The streamable HTTP server

Stdio servers are launched by the client as a subprocess. A streamable HTTP server is the opposite: it has to already be listening, and the client dials a URL. That makes it the transport you want once the server lives in a container, on a PaaS, or anywhere across a network.

Endpoint

http://127.0.0.1:8000/mcp

The path is /mcp, not / — that default comes from streamable_http_path. Nothing is mounted at the root, so pointing a client at http://127.0.0.1:8000 gets you:

INFO: 127.0.0.1:63946 - "POST / HTTP/1.1" 404 Not Found

If you see that 404, the path is missing from your URL. Skip the trailing slash too — /mcp/ answers with a 307 redirect.

Configuration

Variable

Default

Purpose

HOST

127.0.0.1

Interface to bind. Set 0.0.0.0 in a container.

PORT

8000

Port to bind. Hosting platforms usually inject this.

HOST=0.0.0.0 PORT=9000 uv run mcpserver-http

Connecting the MCP Inspector

The Inspector's mcp dev mode only speaks stdio, so for this server start it yourself first, then attach the standalone Inspector:

uv run mcpserver-http                      # terminal 1
npx @modelcontextprotocol/inspector@latest # terminal 2

In the Inspector UI set Transport Type to Streamable HTTP and URL to http://127.0.0.1:8000/mcp, then Connect.

For the stdio server, mcp dev handles the launching for you:

uv run mcp dev src/mcp_server_deep_dive_deployment/deployment.py

Install from GitHub

Because the project defines console script entry points, uvx can build and run either server straight from the repo. Nothing needs to be published to PyPI.

Verify it works end to end — swap the trailing script name to pick a server:

uvx --from git+https://github.com/USERNAME/mcp-server-deep-dive-deployment mcpserver
uvx --from git+https://github.com/USERNAME/mcp-server-deep-dive-deployment mcpserver-http

To pin a specific commit, tag, or branch, append it to the URL — git+https://github.com/USERNAME/mcp-server-deep-dive-deployment@main. Without a ref, uvx tracks the default branch, and it caches builds: pass --refresh to pick up new commits.

Deploy to Render

uvx still runs the server on your machine. Deploying puts it somewhere that is already listening, which is the situation streamable HTTP exists for. Only mcpserver-http can be deployed — the stdio server is launched by its client as a subprocess, so there is nothing for a host to run.

render.yaml at the repo root defines the service. Render dashboard → NewBlueprint → select this repo → Apply.

No application code changes were needed. http_streamable_io.main() already reads HOST and PORT from the environment, so deployment is pure configuration: Render injects PORT, and the Blueprint supplies HOST=0.0.0.0.

Three choices in that file are worth understanding, because each one is a trap avoided:

Setting

Why

buildCommand: pip install uv && uv sync --frozen

--frozen installs uv.lock exactly and fails rather than silently re-resolving. Without it, deploys stop being reproducible — which is why the lockfile is committed.

startCommand: ./.venv/bin/mcpserver-http

The console script uv sync installed. Calling it directly avoids uv run, which re-checks the environment on every start and needs uv on PATH at runtime.

no healthCheckPath

Nothing is mounted at /, so a health check there would 404 and Render would restart the service in a loop. Omitted, the service goes live once the port is bound.

The Python version needs no Render-specific setting: Render reads the existing .python-version.

autoDeployTrigger: commit means every merge to main redeploys.

Free tier and the cold start

The service runs on Render's free plan, which spins down after 15 minutes idle and takes about a minute to wake. That matters most for the one tool you actually want to demo:

Call add first to wake the server, then call count_to. Running count_to against a sleeping instance confounds the result — you cannot tell whether missing progress updates mean the stream was buffered or the instance was still booting.

Switch plan: free to plan: starter in render.yaml for an always-on service.

Streaming survives the proxy

Worth confirming rather than assuming, since a proxy that buffers responses would silently reduce streaming to a single lump of output at the end. Against the deployed server, count_to(5) emits its progress notifications about 100 ms apart — matching the asyncio.sleep(0.1) in the tool, so nothing is being buffered between Render's edge and the client.

The MCP endpoint is served with cache-control: no-cache, no-transform, which is what preserves it.

The deployed endpoint is public

Anyone with the URL can call these tools. There is no authentication.

Binding 0.0.0.0 also turns off DNS-rebinding protection. MCPServer enables it automatically only when the host is 127.0.0.1, localhost, or ::1; for any other bind address the transport security settings default to disabled, so no Host or Origin validation runs at all.

That is acceptable here — add, greeting, and count_to touch no state, secrets, or network — but it is a deliberate choice, not a default to inherit. To close it, pass explicit TransportSecuritySettings through serve(), which already forwards **run_kwargs to MCPServer.run():

serve(
    mcp,
    transport="streamable-http",
    host=..., port=...,
    transport_security=TransportSecuritySettings(
        enable_dns_rebinding_protection=True,
        allowed_hosts=[os.environ["RENDER_EXTERNAL_HOSTNAME"]],
    ),
)

Connecting a client

The two servers are registered differently, because stdio is launched and HTTP is dialed.

Claude Code

# stdio — Claude Code launches it
claude mcp add demo -- uvx --from git+https://github.com/USERNAME/mcp-server-deep-dive-deployment mcpserver

# streamable HTTP, local — start the server first, then point at the URL
claude mcp add --transport http demo-http http://127.0.0.1:8000/mcp

# streamable HTTP, deployed — already listening, so nothing to start
claude mcp add --transport http demo-mcp https://<your-service>.onrender.com/mcp

The last one is the payoff of deploying: no local process, no uvx build, no terminal to keep open.

Claude Desktop

Add to claude_desktop_config.json and restart Claude Desktop. Both entries can live side by side:

{
  "mcpServers": {
    "demo": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/USERNAME/mcp-server-deep-dive-deployment",
        "mcpserver"
      ]
    },
    "demo-http": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://127.0.0.1:8000/mcp"
      ]
    }
  }
}

demo is the whole story for stdio: Claude Desktop runs uvx, which builds from GitHub and speaks MCP over the subprocess pipes.

demo-http needs the extra hop. Claude Desktop's config launches commands, so a remote server is reached through the mcp-remote bridge, which Claude Desktop starts over stdio and which forwards to the HTTP endpoint. The server must already be runninguv run mcpserver-http in its own terminal — or the bridge has nothing to connect to.

Claude Code talks to HTTP servers natively via --transport http, so it needs no bridge there.

Why not Settings → Connectors? (locally)

Claude's Add custom connector dialog rejects anything that is not https:

http://127.0.0.1:8000/mcp
⚠ URL must start with 'https'

A local server has no certificate, so the Connectors UI is not an option during development — mcp-remote is. The bridge runs as a stdio subprocess and speaks plain HTTP to the server, so no TLS is involved.

The obvious workaround is a tunnel, and it does not work either. Binding to localhost turns on DNS-rebinding protection, which only accepts Host headers matching 127.0.0.1:*, localhost:*, or [::1]:*:

Host: 127.0.0.1:8000    -> 200
Host: abc123.ngrok.app  -> 421 Misdirected Request

So a plain tunnel gets refused. Either rewrite the forwarded header (ngrok http 8000 --host-header=rewrite) or pass explicit allowed_hosts via TransportSecuritySettings.

…and why deploying fixes it

A Render URL is https with a real certificate, so the constraint that ruled out Connectors during development no longer applies: the deployed endpoint is eligible for Add custom connector, with no mcp-remote bridge and no tunnel. (The https rejection is what was verified here; the rest of the Connectors flow is left as an exercise. mcp-remote against the deployed URL works regardless.)

This is the clearest argument for the streamable HTTP transport in the whole repo. The same server that needed a stdio bridge to reach Claude Desktop locally is reachable directly once it lives at a public https URL.

Note that the tunnel problem above inverts once deployed, rather than disappearing. On Render the server binds 0.0.0.0, and DNS-rebinding protection is enabled only for localhost binds — so the 421 goes away because nothing is being checked, not because the hostname is now allowed. See The deployed endpoint is public.

Tools

Tool

Signature

mcpserver

mcpserver-http

Description

add

add(x: int, y: int) -> int

Sums 2 numbers.

count_to

count_to(n: int) -> str

Counts to n, streaming a progress update per step.

greeting

greeting(name: str) -> str

Send a greeting.

count_to is the one that shows why streamable HTTP exists: the client receives progress notifications while the call is still running, instead of one lump of output at the end.

Verify the install

Once a server is registered, ask the client to list its tools and check them against the table above: mcpserver should offer add alone, mcpserver-http all three.

Then call a couple:

  • add(x=2, y=3) returns 5 on either server.

  • greeting(name="Ada") returns Hi Ada on the HTTP server.

  • count_to(n=5) returns Counted to 5. and emits 5 progress notifications before it finishes.

Against a deployed server on the free plan, run add first — it doubles as the wake-up call, so count_to is measuring the stream rather than the cold start.

Project layout

render.yaml                 # Render Blueprint for the deployed HTTP server
src/mcp_server_deep_dive_deployment/
├── __init__.py
├── __main__.py             # `python -m ...`, defaults to the stdio server
├── runner.py               # shared serve(): logging, lifecycle, Ctrl-C handling
├── deployment.py           # MCPServer("Demo") over stdio
└── http_streamable_io.py   # MCPServer("HTTP Streamable IO") over streamable HTTP

Startup plumbing lives once in runner.py, so a server module is just its tools plus a main() that hands the server to serve(). Because serve() forwards **run_kwargs to MCPServer.run(), transport options like transport_security can be passed without touching runner.py.

Adding a tool

Tools are plain functions in a server module. Type hints define the input schema and the docstring becomes the tool description the model reads, so both are worth getting right:

@mcp.tool()
def multiply(x: int, y: int) -> int:
    """Multiplies 2 numbers."""
    return x * y

To share one tool across both servers, define it once and register it on the other with mcp.tool()(add) — that is how add appears on both without a second copy.

Restart the client (or --refresh the uvx install) to pick up the change, and add a row to the Tools table above so it stays the one place that lists what these servers expose.

Adding a server

  1. Add a module next to the existing ones with its own MCPServer(...) and tools.

  2. Give it a main() that returns serve(mcp, ...) from runner.py.

  3. Register a console script in pyproject.toml under [project.scripts].

Clients then select it by name: uvx --from git+https://github.com/USERNAME/mcp-server-deep-dive-deployment <script>.

If the new server should also be deployed, add a second entry under services: in render.yaml with its own name and startCommand. Each Render service runs one process, so two deployed servers means two services.

Available Tools

1 tool
addA

Sums 2 numbers.

:param x: the first addend :param y: the second addend :return: the sum of x and y

ParametersJSON Schema
NameRequiredDescriptionDefault
xYes
yYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full behavioral burden. It discloses the operation (sum), the inputs (addends), and the return value (sum). For a pure arithmetic function, this is transparent enough; no side effects or errors are relevant.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-structured, with the core action front-loaded and parameter/return details clearly formatted. Every line earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple two-parameter pure function, the description covers all essential information: operation, parameter meanings, and return value. Even with an output schema present, the description is self-sufficient.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description fully compensates by naming each parameter semantically: 'the first addend' and 'the second addend'. It adds meaningful meaning beyond the schema's bare integer type declarations.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Sums 2 numbers.' This is unambiguous and distinct even without sibling tools. The resource is clearly the two numeric inputs.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly implies when to use it: whenever two numbers need to be summed. There are no sibling tools or exclusions, so no explicit alternative guidance is required. The context is sufficiently clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedadd

TDQS

A4.1/5.0

Scored across 1 tool

Disambiguation5/5

Only one tool exists, so there is no possibility of confusion with other tools.

Naming Consistency4/5

The single tool name 'add' is simple and conventional, though no broader naming pattern can be assessed.

Tool Count1/5

A single arithmetic 'add' tool is entirely inadequate for a server named 'deep-dive-deployment', which implies a much broader deployment-focused toolset.

Completeness1/5

The tool surface is severely incomplete for any deployment workflow, lacking all expected operations such as deploy, list, status, rollback, or configuration management.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A demonstration MCP server supporting both Stdio and SSE transports, providing example tools (echo, add, time, UUID generation) and resources for learning and testing MCP implementations.
    10
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    A minimal MCP server that exposes tools for addition, echoing text, time lookup, and URL fetching, with support for HTTP and stdio transports.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A model-agnostic MCP server exposing example tools (add1, multiply2, greet) for learning purposes, working with any LLM through stdio transport.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A zero-dependency MCP server that demonstrates the raw MCP protocol over stdio and Streamable HTTP transports, with simple tools like time, echo, and add.
    -