mcp-server-deep-dive-deployment
This server exposes MCP tools for basic arithmetic, streaming counts, and greetings, with different tool sets depending on the transport (stdio vs streamable HTTP).
add(x: int, y: int) -> int: sums two integers and returns the result (available on both the stdio and HTTP servers).count_to(n: int) -> str: counts to n, emitting a progress notification per step while streaming (available only on the HTTP server).greeting(name: str) -> str: returns a personalized greeting such asHi Ada(available only on the HTTP server).The HTTP server also demonstrates remote deployment and client connection via streamable HTTP, showing real-time progress updates during long-running calls.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-server-deep-dive-deploymentadd 42 and 58"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 syncuv 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 |
|
| stdio | the client launches the server as a subprocess |
|
| 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 mcpserverstreamable 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-httpThe 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/mcpThe 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 FoundIf 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 |
|
| Interface to bind. Set |
|
| Port to bind. Hosting platforms usually inject this. |
HOST=0.0.0.0 PORT=9000 uv run mcpserver-httpConnecting 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 2In 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.pyInstall 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-httpTo 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 → New → Blueprint →
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 |
|
|
| The console script |
no | Nothing is mounted at |
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/mcpThe 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
running — uv 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 RequestSo 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 |
|
| Description |
|
| ✅ | ✅ | Sums 2 numbers. |
|
| — | ✅ | Counts to n, streaming a progress update per step. |
|
| — | ✅ | 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)returns5on either server.greeting(name="Ada")returnsHi Adaon the HTTP server.count_to(n=5)returnsCounted 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 HTTPStartup 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 * yTo 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
Add a module next to the existing ones with its own
MCPServer(...)and tools.Give it a
main()that returnsserve(mcp, ...)fromrunner.py.Register a console script in
pyproject.tomlunder[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 tooladdA
Sums 2 numbers.
:param x: the first addend :param y: the second addend :return: the sum of x and y
| Name | Required | Description | Default |
|---|---|---|---|
| x | Yes | ||
| y | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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 tool update
v0.1.0- First observed
add
TDQS
Scored across 1 tool
Only one tool exists, so there is no possibility of confusion with other tools.
The single tool name 'add' is simple and conventional, though no broader naming pattern can be assessed.
A single arithmetic 'add' tool is entirely inadequate for a server named 'deep-dive-deployment', which implies a much broader deployment-focused toolset.
The tool surface is severely incomplete for any deployment workflow, lacking all expected operations such as deploy, list, status, rollback, or configuration management.
Maintenance
Related MCP Connectors
Host your MCP tool over streamable HTTP in one command.
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA 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.10MIT
- FlicenseNot gradedqualityCmaintenanceA minimal MCP server that exposes tools for addition, echoing text, time lookup, and URL fetching, with support for HTTP and stdio transports.-
- FlicenseNot gradedqualityCmaintenanceA model-agnostic MCP server exposing example tools (add1, multiply2, greet) for learning purposes, working with any LLM through stdio transport.-
- FlicenseNot gradedqualityCmaintenanceA zero-dependency MCP server that demonstrates the raw MCP protocol over stdio and Streamable HTTP transports, with simple tools like time, echo, and add.-