agentsclimarketplace

Fastmcp stdio http dual transport

Skill kjuhwa/skills-hub/skills/mcp/fastmcp-stdio-http-dual-transport

Single MCP server entry point that supports both STDIO (default) and Streamable HTTP / SSE transports via a --http flag, using FastMCP + Starlette + uvicorn, with localhost-binding-by-default and a warning banner on non-localhost binds.From its SKILL.md

Install
npx -y skills add kjuhwa/skills-hub --skill fastmcp-stdio-http-dual-transport

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

6.7 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it

FastMCP server with STDIO + Streamable HTTP/SSE dual transport

Ship one MCP server binary that runs under STDIO by default (what Claude Desktop & most MCP clients expect) and switches to a local Streamable HTTP + SSE listener with --http. Useful when you want the same server to be reachable via inspector tools, local dockerized sandboxes, and STDIO-launched Claude sessions — without maintaining two codebases.

The shape

from mcp.server.fastmcp import FastMCP
from mcp.server.sse import SseServerTransport
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.types import Scope, Receive, Send
import contextlib
from collections.abc import AsyncIterator
import uvicorn

mcp = FastMCP("my-server")

@mcp.tool()
async def do_thing(uri: str) -> str:
    """Example MCP tool."""
    return f"processed {uri}"

def _starlette_app(mcp_server, *, debug=False) -> Starlette:
    sse = SseServerTransport("/messages/")
    session_manager = StreamableHTTPSessionManager(
        app=mcp_server, event_store=None, json_response=True, stateless=True,
    )

    async def handle_sse(request: Request) -> None:
        async with sse.connect_sse(request.scope, request.receive, request._send) as (r, w):
            await mcp_server.run(r, w, mcp_server.create_initialization_options())

    async def handle_http(scope: Scope, receive: Receive, send: Send) -> None:
        await session_manager.handle_request(scope, receive, send)

    @contextlib.asynccontextmanager
    async def lifespan(app: Starlette) -> AsyncIterator[None]:
        async with session_manager.run():
            yield

    return Starlette(
        debug=debug,
        routes=[
            Route("/sse", endpoint=handle_sse),
            Mount("/mcp", app=handle_http),
            Mount("/messages/", app=sse.handle_post_message),
        ],
        lifespan=lifespan,
    )

def main():
    import argparse, sys
    ap = argparse.ArgumentParser()
    ap.add_argument("--http", action="store_true",
                    help="Run on Streamable HTTP + SSE instead of STDIO")
    ap.add_argument("--host", default=None, help="(HTTP mode) bind host (default: 127.0.0.1)")
    ap.add_argument("--port", type=int, default=None, help="(HTTP mode) port (default: 3001)")
    args = ap.parse_args()

    if not args.http and (args.host or args.port):
        ap.error("--host/--port only valid with --http")

    if args.http:
        host = args.host or "127.0.0.1"
        if args.host and args.host not in ("127.0.0.1", "localhost"):
            # Loud warning — non-localhost bind is a footgun.
            print(
                "\nWARNING: binding to non-localhost interface "
                f"({host}). The server has NO authentication and runs with your privileges. "
                "Any reachable process/user can invoke tools. Only proceed if you understand "
                "the security implications.\n",
                file=sys.stderr,
            )
        uvicorn.run(_starlette_app(mcp._mcp_server, debug=True),
                    host=host, port=args.port or 3001)
    else:
        mcp.run()   # STDIO

if __name__ == "__main__":
    main()

Why the three routes on the HTTP side

RouteTransportWhen to use
GET /sseSSE (legacy MCP)Older clients that speak MCP-over-SSE only.
POST /mcpStreamable HTTP (newer MCP)Modern clients; stateless; works through HTTP proxies and load balancers.
POST /messages/SSE outbound channelCompanion to /sse; clients POST tool results back here.

Mounting all three on one Starlette app gives clients free choice without you running two processes.

Security defaults (important)

  • Default bind: 127.0.0.1. The MCP server has no authentication and runs with the launching user's privileges. Any process on the local machine that can reach the port can invoke every tool.
  • Non-localhost bind is an explicit user choice--host 0.0.0.0 requires the operator to see the WARNING banner on stderr. Do not silently accept it.
  • STDIO is preferred for Claude Desktop — it inherits the client's launch permissions and isn't network-exposed at all.

If you genuinely need remote access, run the server inside a container and require auth at a reverse proxy (nginx + mTLS, or a VPN). Don't build auth into the MCP server itself.

Claude Desktop config

STDIO is the easy path:

{
  "mcpServers": {
    "my-server": {
      "command": "my-mcp",
      "args": []
    }
  }
}

With Docker (for sandboxed local runs):

{
  "mcpServers": {
    "my-server": {
      "command": "docker",
      "args": ["run", "--rm", "-i", "my-mcp:latest"]
    }
  }
}

The -i keeps STDIN connected; --rm cleans the container when the session ends.

Debugging with MCP Inspector

npx @modelcontextprotocol/inspector gives you a UI to list and invoke tools. For STDIO, set command to your server binary. For HTTP, point it at http://127.0.0.1:3001/mcp (Streamable HTTP) or http://127.0.0.1:3001/sse (SSE).

Anti-patterns

  • Defaulting to 0.0.0.0. Exposes the unauthenticated tool surface to every machine on the LAN.
  • One server per transport. Doubles maintenance; two binaries to ship, test, and document.
  • Silencing the non-localhost warning. Users forget; the banner is their last line of defense.
  • Running the HTTP variant as root. The server executes tools with its own privileges; privileged service = privileged tool surface.

Variations

  • Stateful sessions. Set StreamableHTTPSessionManager(stateless=False, event_store=...) when you need session resumption across reconnects.
  • CORS / auth reverse proxy. Put nginx/Caddy in front; MCP server stays bound to localhost, proxy terminates TLS + auth.
  • Environment toggles for plugins. Plugins-on-by-default often surprises users; gate behind MYMCP_ENABLE_PLUGINS=true and read the env inside the tool function.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,764. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.