# MCP Transports That Still Matter: stdio vs Streamable HTTP (and Why SSE Is a Trap)

> Source: <https://dev.to/yong_yu_f98e15562e9b120a0/mcp-transports-that-still-matter-stdio-vs-streamable-http-and-why-sse-is-a-trap-1ji>
> Published: 2026-09-28 15:05:56+00:00

**Attributed Chinese → English compile (not original authorship)**

**Original title:** MCP Server 开发入门：手把手写一个能跑的 Server，三种协议怎么选

**Author:** 晚安code

**URL:** [https://juejin.cn/post/7673880140422955046](https://juejin.cn/post/7673880140422955046)

**Date:** 2026-08-15

**Why this matters for English readers:** Most English MCP intros still paste SSE configs from early 2025. This post is short, version-aware, and mechanism-first: it separates *what an MCP Server is* from *how the bytes move*, flags HTTP+SSE as deprecated, and shows the one-line transport switch in the Python FastMCP path. Useful if you are wiring Cursor / Claude Desktop locally and deciding whether a remote Streamable HTTP deploy is worth the auth/ops cost.

If you have ever watched an LLM “want” to check a package status and then fail because it has no eyes and no hands, you already understand why MCP exists. The interesting part is not the metaphor (“USB-C for AI”). The interesting part is the boundary: a small process you control that **exposes typed tools and read-only resources**, so the model can call out without owning your database credentials or your shell.

This compile re-explains 晚安code’s Juejin walkthrough in English: a minimal FastMCP server, how to debug it, and how to pick a transport without learning an obsolete one first.

**MCP (Model Context Protocol)** is an open protocol for connecting model hosts to external tools and data. A **host** (Claude Desktop, Cursor, your own agent runtime) talks to one or more **MCP Servers**. Each server is ordinary application code—Python or Node—that wraps APIs, files, or internal services behind a stable interface.

Why not let the model call HTTP and SQL directly? Because unconstrained tools are how you get “buy ten refrigerators” demos. The server is the policy layer: allowlists, auth, rate limits, and “this tool may read but not write.” That is the product value, not the JSON-RPC ceremony.

Minimal mental model:

`uv` (skip the fragile pip folklore)
The source uses **uv** (Astral) as a fast env + dependency manager. On Linux/macOS the install path differs from the Windows PowerShell one-liner in the article; the important sequence is:

```
# install uv from https://docs.astral.sh/uv/ if needed
uv python install 3.11
uv init . -p 3.11
uv add "mcp[cli]"
```

`mcp[cli]` pulls the official Python SDK **and** the Inspector CLI helpers. Activate the `.venv` uv creates before you debug in VS Code / Cursor. The article notes SDK ~1.27.x at time of writing—pin what you ship; tutorial snippets rot fast in this ecosystem.

FastMCP (in `mcp.server.fastmcp`) is the high-level API: decorators instead of hand-rolled JSON-RPC handlers.

``` python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("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}!"

if __name__ == "__main__":
    mcp.run()  # default transport: stdio
```

Three details that burn hours when omitted:

| Requirement | Why it matters | 
|---|---|
| Type annotations on tool args | FastMCP builds JSON Schema ( `inputSchema` ) from them. No types → weak or empty schemas → worse model calls. | 
| Docstrings | Become the tool/resource description the model reads when deciding whether to call. Empty docstring ≈ invisible tool. | 
| `if __name__ == "__main__":` | Import-only modules never start the stdio loop; “nothing happens” is usually this. | 

**Tool vs resource** is a semantic contract, not a style preference:

|  | `@mcp.tool()` | `@mcp.resource()` | 
|---|---|---|
| Intent | Perform an action | Expose read-only data | 
| Side effects | Expected (write, call API, mutate) | Should be none | 
| Discovery | `tools/list` + model-driven call | URI / URI template | 
| Examples | `add` , send email, query order | `greeting://{name}` , config blob | 

Treat tools as verbs and resources as nouns. Mixing “read file” into a write-capable god-tool is how local servers become shell-shaped.

```
python server.py          # smoke: process starts, no immediate crash
mcp dev server.py         # Inspector UI: list tools, invoke, inspect errors
```

Version pitfall called out in the source: older blog posts import `MCPServer` from paths that only exist on pre-alpha SDK v2 sketches. On the stable 1.x line you want `from mcp.server.fastmcp import FastMCP`. If an import fails on day one, assume **tutorial/SDK skew**, not “MCP is broken.”

Transport is not a fashion label. It decides whether the server is a **local child process** or a **network service**.

`command` + `args` from config.
This is the right default while you are learning and while tools only need the developer’s laptop.

Switching in FastMCP is intentionally boring:

```
mcp.run()                                 # local stdio
mcp.run(transport="streamable-http")      # remote Streamable HTTP
```

Once you leave stdio, **you** own authentication, rate limits, observability, and deployment. The protocol does not magically make a public URL safe.

SSE-over-HTTP was an early remote option. Per the article’s reading of the ecosystem (including SEP-2596 era deprecation notes around March 2025), **new work should not start on SSE**. Some TypeScript SDK paths have already removed SSE server support. Old Chinese and English tutorials still teach it; treat those snippets as historical.

Cheat sheet:

| Transport | Where it runs | How it talks | Use when | Status | 
|---|---|---|---|---|
| stdio | Same machine as host | stdin/stdout | Cursor, Claude Desktop, local CLI | Prefer for local | 
| Streamable HTTP | Your server / cluster | HTTP bidirectional streaming | Shared/prod remote tools | Prefer for remote | 
| SSE | Remote | HTTP unidirectional push | Legacy clients only | Deprecated | 

Hosts differ in file names, but the stdio shape is the same idea: name → command → args → optional env. Conceptually:

```
{
  "mcpServers": {
    "demo": {
      "command": "uv",
      "args": ["run", "python", "/absolute/path/to/server.py"]
    }
  }
}
```

Prefer absolute paths and the same interpreter that has `mcp` installed. After restart, confirm the host lists `add` and can call it with integers—not with natural language stuffed into a single string field.

The source’s closing advice is the right engineering posture: the barrier to a first MCP Server is low; the barrier to a *correct remote* deployment is ops, not decorators. Read the official specification when you outgrow demos—secondary tutorials lag the SDK by months.

**Compiler byline:** YongBo Yu, Toronto · [https://yongbo-yu.vercel.app](https://yongbo-yu.vercel.app) · [https://github.com/YongBoYu1](https://github.com/YongBoYu1)

**Attribution:** Mechanisms and examples adapted from 晚安code, [MCP Server 开发入门：手把手写一个能跑的 Server，三种协议怎么选](https://juejin.cn/post/7673880140422955046) (2026-08-15). Re-explained in original English wording; not a verbatim translation.
