Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesSome links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Build an HTTP MCP server in three stages: create an McpServer and register its tools, resources, and prompts; create a transport; then connect the server to that transport. For a new remote service, use Streamable HTTP rather than the legacy HTTP+SSE transport. The example below uses TypeScript, Express, and the MCP SDK, exposes one /mcp endpoint, keeps protocol responses under SDK control, and adds origin and bearer-token checks.
What an HTTP MCP server actually provides
Model Context Protocol (MCP) defines a JSON-RPC contract that lets a client discover and invoke capabilities exposed by your server. Your application supplies the capability contract; the HTTP transport carries initialization messages, tool calls, resource reads, prompts, notifications, and errors.
The official TypeScript server guide reduces implementation to three operations:
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →- Create an
McpServer, then register tools, resources, and prompts with explicit input schemas. - Create an HTTP transport.
- Call
server.connect(transport).
For remote deployments, the SDK describes Streamable HTTP as the modern, fully featured transport. It uses HTTP POST for client JSON-RPC messages, can use server-sent events (SSE) for server-to-client notifications, supports JSON-only responses, and can support sessions and resumability. HTTP+SSE remains for backward compatibility, but it is not the preferred starting point for a new implementation.
#1 Best Overall
Choose the protocol and state model before writing code
Streamable HTTP versus HTTP+SSE
| Decision | New Streamable HTTP implementation | Legacy HTTP+SSE compatibility |
|---|---|---|
| Primary use | New remote servers | Existing clients or servers that still require the older transport |
| Endpoint shape | One endpoint that supports POST and GET | Legacy SSE-oriented endpoint arrangement |
| Responses | JSON-only or SSE-enabled, according to negotiation | SSE-centered streaming behavior |
| Sessions | Can be stateless or session-capable | Depends on the legacy implementation |
| Recommendation | Use for new work | Use only when compatibility requires it |
Each client message is sent as a new POST. Clients normally advertise both application/json and text/event-stream in the Accept header so the server can choose an allowed response mode. A GET on the same MCP endpoint is used when the negotiated transport needs a server-to-client event stream.
Pin the wire version
The 2025-11-25 transport format defines one MCP endpoint supporting both POST and GET, session identifiers such as Mcp-Session-Id, and resumability behavior. A draft dated 2026-07-28 changes the model: it removes the GET stream endpoint and protocol-level sessions and describes a stateless core. Those are materially different wire contracts. Pin the protocol version your clients support, document it, and test initialization, tool calls, errors, and streaming against that exact version. Do not combine 2025 session assumptions with a 2026 wire implementation without a compatibility layer.
Stateless or stateful?
| Model | Use it when | What you must implement |
|---|---|---|
| Stateless | The server behaves like an API and every request contains everything needed to execute it. | Create a transport without a session ID generator, avoid per-client memory, and make requests independently routable. |
| Stateful | You need resumability, richer server-to-client behavior, or conversation-specific state. | Issue an Mcp-Session-Id during initialization, store the transport and server for that session, require the ID on later requests, and remove state when the transport closes. |
The example uses stateful sessions because it demonstrates the complete 2025-11-25 behavior. For a stateless API, remove the session map and configure the transport with no session ID generator, then verify that your selected SDK release supports that mode.
Free tools Windows power users keep installed
One-click scans. No signup required.
Prerequisites and project setup
- Node.js with a current LTS release.
- TypeScript and a package manager such as npm.
- The MCP TypeScript SDK, Express, and Zod for boundary validation.
- A client that supports the same Streamable HTTP protocol version as your server.
- An HTTPS reverse proxy and an identity system before exposing the endpoint publicly.
Create a project and install dependencies:
mkdir http-mcp-server
cd http-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk express zod
npm install --save-dev typescript tsx @types/express @types/node
npx tsc --init
Set the module target and run script appropriate to your installed SDK release. SDK exports can change between releases, so keep the package version pinned in your lockfile and check the release documentation if an import name differs.
Register tools, resources, and prompts
Keep the server contract separate from HTTP routing. Tool arguments are untrusted input; Zod schemas make the accepted shape explicit and reject malformed calls before business logic runs.
import express from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/nodeStreamableHttp.js';
import { z } from 'zod';
const app = express();
const port = Number(process.env.PORT ?? '3000');
const allowedOrigins = new Set(
(process.env.ALLOWED_ORIGINS ?? 'http://localhost:3000')
.split(',')
.map(value => value.trim())
.filter(Boolean)
);
const bearerToken = process.env.MCP_BEARER_TOKEN;
app.use(express.json({ limit: '1mb' }));
function createServer() {
const server = new McpServer({
name: 'example-http-mcp',
version: '1.0.0'
});
server.tool(
'add',
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }]
})
);
server.resource('status', 'status://service', async uri => ({
contents: [{
uri: uri.href,
mimeType: 'text/plain',
text: 'ok'
}]
}));
server.prompt(
'explain',
'Explain a value',
{ value: z.string() },
async ({ value }) => ({
messages: [{
role: 'user',
content: { type: 'text', text: `Explain ${value}` }
}]
})
);
return server;
}
const sessions = new Map();
function protectMcpEndpoint(req, res, next) {
const origin = req.get('origin');
if (origin && !allowedOrigins.has(origin)) {
res.status(403).send('Forbidden origin');
return;
}
if (process.env.REQUIRE_ORIGIN === '1' && !origin) {
res.status(403).send('Origin required');
return;
}
if (bearerToken && req.get('authorization') !== `Bearer ${bearerToken}`) {
res.status(401).send('Unauthorized');
return;
}
next();
}
app.use('/mcp', protectMcpEndpoint);
app.post('/mcp', async (req, res) => {
try {
const sessionId = req.get('mcp-session-id');
let entry = sessionId ? sessions.get(sessionId) : undefined;
if (!entry) {
const server = createServer();
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});
await server.connect(transport);
entry = { server, transport };
}
await entry.transport.handleRequest(req, res, req.body);
const assignedId = entry.transport.sessionId;
if (assignedId) {
sessions.set(assignedId, entry);
}
} catch (error) {
console.error(error);
if (!res.headersSent) {
res.status(500).json({ error: 'MCP request failed' });
}
}
});
app.get('/mcp', async (req, res) => {
const sessionId = req.get('mcp-session-id');
const entry = sessionId ? sessions.get(sessionId) : undefined;
if (!entry) {
res.status(400).send('Missing or unknown Mcp-Session-Id');
return;
}
try {
await entry.transport.handleRequest(req, res);
} catch (error) {
console.error(error);
if (!res.headersSent) {
res.status(500).send('MCP stream failed');
}
}
});
app.listen(port, '127.0.0.1', () => {
console.log(`MCP server listening on http://127.0.0.1:${port}/mcp`);
});
This is a stateful 2025-style arrangement: the first POST creates a server and transport, initialization causes the transport to obtain a session ID, and later POST or GET requests look up that ID. The SDK transport owns MCP protocol responses; Express supplies routing, body parsing, authentication, and error boundaries around it. If your SDK release exposes a slightly different transport class or handler signature, use its matching Node Streamable HTTP adapter rather than mixing APIs from another release.
Run and exercise the endpoint
Start the server
export ALLOWED_ORIGINS=http://localhost:3000
export MCP_BEARER_TOKEN=replace-with-a-long-random-value
npx tsx src/server.ts
Bind to 127.0.0.1 while developing. Put a TLS-terminating reverse proxy in front of it for remote use, and pass the authorization header through to the application.
Initialize with HTTP
A client should advertise both supported response types. The following request targets the 2025-11-25 format; save the Mcp-Session-Id response header for subsequent calls.
Rank #2
curl -i http://127.0.0.1:3000/mcp
-H 'Content-Type: application/json'
-H 'Accept: application/json, text/event-stream'
-H 'Origin: http://localhost:3000'
-H 'Authorization: Bearer replace-with-a-long-random-value'
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
After initialization, send the returned session ID on every later POST. A compliant client will perform capability negotiation and then call tools, read resources, or request prompts using the JSON-RPC methods defined by MCP. For an SSE response, keep the GET connection open and handle reconnect or resumability according to the pinned protocol version.
Secure an MCP HTTP endpoint
Validate Origin
Check the Origin header on every incoming MCP request against an explicit allowlist. Return HTTP 403 for an invalid value. This blocks DNS-rebinding attempts against local or private services. The sample allows a missing Origin for non-browser clients; set REQUIRE_ORIGIN=1 when your deployment requires the header on every connection.
Bind narrowly during development
Use 127.0.0.1 for a local server. Do not bind to 0.0.0.0 unless the exposure is intentional, firewalled, authenticated, and monitored.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Authenticate and authorize
Authenticate every connection. A bearer token is only a minimal example; production deployments should use an identity provider or equivalent mechanism, rotate credentials, and authorize each tool by caller identity and scope. A user allowed to read a status resource should not automatically be allowed to execute an administrative tool.
Treat all data as hostile
- Validate every tool argument at the boundary.
- Apply request-body limits and per-operation timeouts.
- Rate-limit expensive tools and bound concurrency.
- Redact tokens, cookies, authorization headers, and sensitive tool arguments in logs.
- Sanitize data returned from remote resources before displaying or passing it onward.
- Keep dependencies patched and disable unused tools.
Session lifecycle and deployment design
Reject missing or unknown IDs when sessions are required
Once a stateful server has issued Mcp-Session-Id, clients must send it on later requests. Return HTTP 400 for a missing or unknown ID rather than silently creating a second conversation. The sample does this for GET; add the same strict check to POST if your deployment must reject every non-initial request without a valid ID.
Clean up session state
Keep a bounded session map and remove entries when the transport closes. Add an idle expiration so abandoned clients cannot consume memory indefinitely. If a process restarts, in-memory sessions disappear; clients must initialize again.
Scale horizontally only with a state plan
Stateless mode can be routed to any instance. Stateful mode requires sticky routing or a shared session store that can safely coordinate transport state and resumability. Do not place a stateful transport map behind round-robin load balancing without one of those mechanisms.
Separate protocol errors from application errors
Let the SDK produce valid JSON-RPC errors for malformed MCP messages and unknown methods. Use HTTP errors for failures before the transport can answer, such as rejected origins, missing authentication, oversized bodies, or an unavailable session. Log an internal correlation ID rather than returning stack traces.
Performance, reliability, and cost considerations
No universal throughput figure applies to an MCP HTTP server: tool execution, downstream APIs, payload sizes, and the selected SDK release dominate performance. Measure your own workload with realistic concurrent clients.
- Keep handlers asynchronous and avoid blocking the Node.js event loop.
- Set explicit timeouts on network calls and cancel work when a client disconnects.
- Cache safe, read-only resources, but never cache responses containing credentials or user-specific data without an isolation policy.
- Use compression and response-size limits where they help, while preserving SSE framing.
- Emit structured metrics for request count, latency, status, tool name, timeout count, active sessions, and rejected authentication or Origin checks.
- Test reconnects, duplicate request IDs, concurrent calls, malformed JSON, oversized bodies, and downstream outages.
The transport itself has no published universal price or performance guarantee. Your cost comes from the host, bandwidth, observability, and the services your tools call.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting common failures
HTTP 403 on every request
Cause: The request Origin is absent or is not in ALLOWED_ORIGINS while strict checking is enabled. Fix: Use the exact scheme, host, and port sent by the client, or deliberately set a policy for non-browser clients. Do not replace the check with a wildcard on a private endpoint.
HTTP 401 Unauthorized
Cause: The Authorization header does not match the configured bearer token. Fix: Send Authorization: Bearer ..., verify that the reverse proxy forwards it, and rotate a leaked token.
HTTP 400 Missing or unknown session
Cause: A stateful endpoint received a GET or later POST without the issued Mcp-Session-Id, or the process restarted and lost its in-memory map. Fix: Preserve the response header, send it on every subsequent request, or initialize a new session after a restart.
The client reports an unsupported protocol version
Cause: Client and server are negotiating different wire versions, commonly the 2025-11-25 session-capable format versus the 2026-07-28 draft behavior. Fix: Pin one version, configure the client for it, and use an adapter if both versions must be supported.
Initialization succeeds but tools are missing
Cause: Registration code did not run for the connected server instance, the tool name is duplicated, or the client cached an earlier capability list. Fix: Register tools before server.connect(transport), use unique names, restart the session, and inspect the server’s capability response.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →GET returns an error or closes immediately
Cause: The request has no active session, the client did not negotiate text/event-stream, or the selected 2026 draft transport does not define a GET stream. Fix: Confirm the pinned protocol, send the session ID for a 2025-style server, and ensure the client’s Accept header includes the response mode you intend to use.
Requests hang until a proxy timeout
Cause: A tool is blocking, a downstream call has no timeout, or a proxy is buffering SSE. Fix: Make handlers asynchronous, set downstream and proxy timeouts, disable buffering for event streams, and emit heartbeats or reconnect behavior supported by your pinned transport.
Rank #4
Or skip the browser setup
If your MCP tools need reliable website images, you can call ScreenshotNeo instead of maintaining browser automation. It is a website screenshot API and MCP server: a GET request returns a PNG, JPEG, WebP, or PDF. Before capture it accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers.
One call is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo documentation for authentication and options. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. You can also set viewport and device presets, load lazy images in full-page captures, select one element by CSS selector, set dark mode, retina scale, custom CSS or JavaScript, click before capture, hide selectors, wait for a selector, delay, or network idle, block ads or resource types, provide headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, cache TTLs, signed image links, asynchronous signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification.
The free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan. Create a free ScreenshotNeo account.
FAQ
Can the MCP endpoint use a path other than /mcp?
Yes. The path is an application routing choice. Whatever path you choose must implement the transport’s required methods and be documented for clients.
Is SSE required for every tool call?
No. Streamable HTTP can return JSON-only responses. SSE is an optional response mode for server-to-client notifications and streaming behavior supported by the negotiated protocol.
What should happen after a server restart?
In-memory stateful sessions are gone. Return a clear session error and have the client initialize again; use a shared state design only when you have a defined persistence and scaling requirement.
Recommended Free Tools
Frequently Asked Questions
Can the MCP endpoint use a path other than /mcp?
Yes. The path is an application routing choice, provided that the chosen transport methods are implemented at that path and the client is configured to use it.
Is SSE required for every tool call?
No. Streamable HTTP also supports JSON-only responses; SSE is used when the negotiated protocol needs server-to-client notifications or streaming.
What should a client do after a server restart?
A process-local stateful session no longer exists. The client should discard the old session ID and perform initialization again.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

