ScreenshotNeo

BlogAI agents

How to Run an MCP Server Over HTTP

Build and expose an MCP server over Streamable HTTP, connect a client, and choose the right protocol version, session model, and security controls.

By the ScreenshotNeo team1 October 202611 min read

To run an MCP server over HTTP, implement Streamable HTTP: expose an MCP endpoint, register tools or other capabilities, and connect an MCP client using a transport compatible with the server’s protocol version. In the TypeScript SDK, the familiar server pattern is an McpServer, registered capabilities, a Streamable HTTP transport, and server.connect(transport).

First choose the protocol era your SDK and clients support. The stable 2025-11-25 transport uses POST and may use GET for an SSE stream, with optional sessions. The 2026-07-28 specification describes a newer stateless request model. These are not interchangeable snippets: pin compatible SDK versions and follow their matching documentation.

1. Choose HTTP or stdio

Use Streamable HTTP when clients need to reach a server over a network. Use stdio when a host launches your server as a local child process. HTTP makes it possible to serve multiple remote clients, but it also makes endpoint authentication, origin and host validation, network exposure, and resource limits your responsibility.

2. Build a minimal TypeScript server

The following example uses the TypeScript SDK’s 2025-era Streamable HTTP transport pattern and Express. Keep the SDK packages on mutually compatible versions; the SDK’s server guide documents its server and transport APIs. This sample is intentionally small: add the production protections described below before exposing it publicly.

import { randomUUID } from 'node:crypto';
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';

const app = express();
app.use(express.json());

const sessions = new Map();

function makeServer() {
  const server = new McpServer({ name: 'hello-http', version: '1.0.0' });
  server.tool(
    'greet',
    'Return a greeting for a name',
    { name: z.string().min(1) },
    async ({ name }) => ({
      content: [{ type: 'text', text: `Hello, ${name}!` }]
    })
  );
  return server;
}

app.post('/mcp', async (req, res) => {
  const sessionId = req.header('mcp-session-id');
  let transport = sessionId ? sessions.get(sessionId) : undefined;

  if (sessionId && !transport) {
    res.status(404).json({ error: 'Unknown session; initialize a new connection.' });
    return;
  }

  if (!transport && isInitializeRequest(req.body)) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: id => sessions.set(id, transport)
    });
    transport.onclose = () => {
      if (transport.sessionId) sessions.delete(transport.sessionId);
    };
    await makeServer().connect(transport);
  } else if (!transport) {
    res.status(400).json({ error: 'Expected an initialize request without a session ID.' });
    return;
  }

  await transport.handleRequest(req, res, req.body);
});

app.get('/mcp', async (req, res) => {
  const id = req.header('mcp-session-id');
  const transport = id ? sessions.get(id) : undefined;
  if (!transport) {
    res.sendStatus(404);
    return;
  }
  await transport.handleRequest(req, res);
});

app.delete('/mcp', async (req, res) => {
  const id = req.header('mcp-session-id');
  const transport = id ? sessions.get(id) : undefined;
  if (!transport) {
    res.sendStatus(404);
    return;
  }
  await transport.handleRequest(req, res);
});

const httpServer = app.listen(3000, '127.0.0.1', () => {
  console.log('MCP endpoint listening at http://127.0.0.1:3000/mcp');
});

async function shutdown() {
  httpServer.close();
  await Promise.all([...sessions.values()].map(t => t.close()));
  sessions.clear();
}
process.on('SIGINT', () => void shutdown());
process.on('SIGTERM', () => void shutdown());

Install the SDK, Express, and Zod packages used by the example, then run the file with your TypeScript runtime or compile it to JavaScript. The precise package entry points can change between SDK releases, so use the imports documented for the SDK version you pin. The example keeps transport instances in memory to demonstrate session routing; that has scaling consequences discussed below.

What the endpoint does

  • POST /mcp receives JSON-RPC messages. The first request initializes a connection; later requests route by Mcp-Session-Id.
  • GET /mcp is available for the stable 2025 transport’s optional server-to-client SSE stream.
  • DELETE /mcp lets a client terminate its session.
  • The tool is registered before the server connects to the transport.

For local testing, bind to 127.0.0.1. This example does not implement authentication, origin/host checks, rate limits, or persistent session storage; do not expose it to an untrusted network as-is.

3. Connect with an MCP client

Use an MCP client transport rather than treating the endpoint as an ordinary REST API. The official TypeScript SDK client completes initialization during connect() and then can list and call tools.

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const client = new Client({ name: 'demo-client', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(
  new URL('http://127.0.0.1:3000/mcp')
);

await client.connect(transport);
console.log(await client.listTools());
console.log(await client.callTool({ name: 'greet', arguments: { name: 'Ada' } }));
await client.close();

See the official TypeScript client guide for client transport setup. A browser or framework client may also need CORS configuration; ordinary server-to-server clients generally do not.

4. Match the protocol version

Protocol version affects methods, headers, session behavior, and streaming. Check the specification and SDK documentation that match the client and server you actually deploy.

Behavior Stable 2025-11-25 2026-07-28 specification
HTTP methods One endpoint handles POST; GET may open an SSE stream. One endpoint accepts POST requests.
Responses A POST may return JSON or an SSE stream. Each POST receives a JSON or request-scoped SSE response.
Sessions Optional session IDs; when issued, clients reuse them. Protocol-level sessions removed; request/response model is stateless.
Server interaction Can use SSE for server-originated requests and notifications. Independent server requests on SSE streams are removed; input-required results support multi-round trips.
Version metadata Initialization negotiates a version; clients send the negotiated version on subsequent requests. Follow the version metadata and required headers specified by the selected revision.

The table summarizes protocol-level differences; use the selected specification’s exact requirements rather than hand-constructing headers from memory. The newer protocol specification and SDK are version-sensitive. A server built for one era may not interoperate with a client expecting another. Legacy HTTP+SSE predates Streamable HTTP and is deprecated; the specification says new implementations should not adopt it.

5. Choose a session model

Stateful sessions

In the 2025 transport, supplying a session ID generator creates stateful sessions. The server must keep a transport per active session, route requests using the session header, clean up closed sessions, and decide how sessions survive process restarts. The in-memory map in the sample works for one process only.

For multiple instances, either use a stateless design supported by your chosen SDK and protocol or provide shared state and routing suitable for that SDK’s session model. Session affinity alone does not restore sessions after a process restart. Resumability requires event storage and transport support; it is not automatic merely because the connection uses SSE.

Stateless handling

The TypeScript SDK documents a stateless mode by omitting the session ID generator for its 2025 transport; it is simpler but does not support resumability. The 2026-07-28 protocol also removes protocol-level sessions. Neither choice prevents your application from storing user or workflow state in a database and passing an explicit identifier through tool arguments.

6. Add security before network exposure

  • Validate Origin: the stable specification requires checking the Origin header on incoming connections to prevent DNS rebinding. Reject an invalid present origin with HTTP 403. Origin validation does not replace authentication.
  • Validate Host for local services: use localhost binding, preferably 127.0.0.1, and validate host headers. The TypeScript SDK server guide documents helper applications that enable host validation; if you wire a transport directly, implement the validation in your HTTP framework.
  • Authenticate callers: use an authentication mechanism appropriate for the service and authorize each operation. Do not assume a difficult-to-guess endpoint URL is access control.
  • Use TLS for remote traffic: terminate TLS at the application or trusted proxy and keep credentials out of URLs and logs.
  • Limit resource use: set request body limits, timeouts, concurrency limits, and tool-specific execution bounds. Avoid logging authorization headers or sensitive tool inputs.
  • Configure CORS deliberately: allow only browser origins that need access and expose MCP response headers when browser clients need to read them. CORS is not authentication.

For the normative transport details, consult the stable transport security guidance. TLS termination, authorization design, and operational limits depend on your deployment and are operational recommendations.

7. Test the connection and tool flow

  1. Start the server on loopback and confirm the endpoint is reachable.
  2. Connect with a protocol-matched MCP SDK client. Initialization should complete before tool calls.
  3. List tools, call the sample tool, and verify the returned content.
  4. For stateful 2025 connections, confirm subsequent requests carry the issued session ID and that closing the client removes server-side state.
  5. Test rejected origins, missing authentication, unknown sessions, malformed messages, timeouts, and shutdown while requests are in flight.
  6. Test with the real MCP host or agent runtime that will use the service. Different host versions may support different protocol eras.

To inspect the HTTP layer manually, send a protocol-valid initialization message with the required headers for the version you target, then use the negotiated metadata on subsequent requests. For a working request flow, an MCP SDK client is less error-prone than hand-authoring JSON-RPC and version headers.

8. cURL, Python, and Node.js clients

These examples show the shape of a 2025-era initialization request for diagnostics. MCP is a stateful protocol in this revision, so a complete interaction must preserve the negotiated version and any returned session ID, advertise acceptable response types, and parse either JSON or SSE. For regular application use, prefer an MCP SDK client.

cURL

curl -i -N -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Origin: http://localhost' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -H 'MCP-Method: initialize' \
  -H 'MCP-Name: demo-client' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-demo","version":"1.0.0"}}}'

Exact metadata and request headers depend on the revision and server implementation; consult the selected specification. Do not reuse this illustrative command against a newer protocol endpoint without updating it. cURL does not automatically perform the MCP handshake or track sessions for you.

Python

Use the official Python SDK’s Streamable HTTP client transport for a full client. The SDK API evolves; install a compatible release and follow its current client documentation:

import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client("http://127.0.0.1:3000/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print(tools)
            result = await session.call_tool("greet", {"name": "Ada"})
            print(result)

asyncio.run(main())

See the official Python SDK for installation and version-matched examples.

Node.js

The TypeScript SDK example above is the recommended Node.js approach: it manages MCP framing and initialization. A plain fetch() request can send HTTP, but does not by itself handle session IDs, SSE parsing, protocol negotiation, or subsequent tool-call messages.

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const client = new Client({ name: 'node-demo', version: '1.0.0' });
await client.connect(
  new StreamableHTTPClientTransport(new URL('http://127.0.0.1:3000/mcp'))
);
const result = await client.callTool({
  name: 'greet',
  arguments: { name: 'Ada' }
});
console.log(result);
await client.close();

9. Troubleshooting

Symptom Likely cause Fix
Client cannot initialize Client and server target different protocol versions, or required headers/body metadata do not match. Check both SDK versions and the matching specification. Let the SDK perform negotiation where supported.
HTTP 404 after initialization Unknown or expired session ID, or a request reached a different process with no shared session. Start a new connection, persist and route session state appropriately, or use a supported stateless mode.
HTTP 400 on a tool request Missing session for a sessionful server, malformed JSON-RPC, or request sent before initialization. Use an MCP client SDK and complete initialization; check the server’s transport logs without recording secrets.
Browser reports CORS failure Origin not allowed, preflight is unhandled, or required response headers are not exposed. Configure explicit allowed origins, methods, request headers, and exposed headers for the browser client.
Connection rejected with 403 Origin or host validation rejected the request. Send the expected legitimate origin and configure an allow-list. Do not disable validation as a shortcut.
Tool call hangs Handler is waiting on an upstream operation, response stream is not consumed, or proxy buffering/timeouts interfere with streaming. Add bounded timeouts to tool work, inspect proxy streaming behavior, and use an MCP SDK client that consumes SSE correctly.
Session works on one instance only Transport state is kept in process memory. Use shared session/event storage and suitable routing, or use a stateless architecture supported by the protocol and SDK.
Import path or class not found Code was copied from another SDK generation or package layout. Pin compatible packages and update imports to the exact release’s documentation; do not mix old examples and new APIs.
Server crashes on shutdown Open streams or pending handlers were not closed cleanly. Stop accepting new requests, close transports, apply bounded draining for in-flight work, then exit.

10. Performance, reliability, and cost

The official material reviewed for this guide does not establish a universal fastest runtime or hosting provider. Measure your own tool handlers and deployment. Most response time usually depends on what the tool does, network round trips, and whether the selected transport streams responses; avoid presenting a benchmark without measuring the same workload.

  • Concurrency: bound concurrent expensive tool calls and upstream requests. Set per-tool deadlines and cancellation behavior.
  • Streaming: SSE can deliver progress or server-to-client events in the 2025 transport. Check that reverse proxies do not buffer or prematurely time out long-lived streams.
  • Availability: stateless request handling is easier to distribute. Stateful sessions need a deliberate persistence and recovery plan.
  • Cost: HTTP itself does not determine hosting cost. Account for compute, memory held by sessions, shared storage, network egress, and upstream services. Choose a host based on runtime, geography, traffic, security, and operational needs.
  • Observability: record request IDs, durations, status, tool names, and sanitized errors. Do not log credentials or sensitive payloads by default.

Or skip the browser setup

If the MCP server you need is for website screenshots, ScreenshotNeo provides a screenshot API and MCP server. A single API request can return an image or PDF; the MCP server exposes take_screenshot, get_page_info, and capture_pdf to AI agents.

Example using the ScreenshotNeo API; see the API documentation for parameters:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners, popups, and chat widgets are removed before the shot.
  • Bot checks, blank pages, and failed loads are never billed.
  • An MCP server lets AI agents take screenshots.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can I expose an MCP server on a public URL?

Yes, with a compatible HTTP transport, TLS, authentication, authorization, and the applicable origin and host protections. A reachable URL alone does not make the endpoint safe for public access.

Does an HTTP MCP server need a database?

No. A stateless server may not need protocol session storage. Your tools may still need a database for application data, and sessionful deployments need a session strategy.

Can I keep an existing HTTP+SSE server?

Existing deployments can migrate or maintain compatibility as needed, but HTTP+SSE is the older deprecated transport. New implementations should use Streamable HTTP.

Should every tool be a separate server?

No. Register capabilities that belong together in one server. Separate services when they need distinct access controls, scaling, ownership, or failure boundaries.