Expose a local MCP server
Expose a local MCP server over Streamable HTTP with a Horizon tunnel, and connect Claude or the MCP Inspector to it.
Serve a remote Model Context Protocol (MCP) server from your laptop at https://my-app.hrzn.run/mcp, so a client such as Claude can connect to it.
Horizon streams responses unbuffered, so Server-Sent Events (SSE) responses from your MCP server reach the client as they are written.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - Node.js 22.19.0 or later, if you want to test with the MCP Inspector
The server below has no authentication. Anyone who has the URL can call its tools. Horizon has no password protection or IP allowlist. Stop the tunnel when you aren't using it, and don't expose tools that read or change data you care about. The SDK ships requireBearerAuth in @modelcontextprotocol/express if you need to check a token yourself.
Build the server
The official TypeScript SDK is @modelcontextprotocol/server. The Express and Node.js adapters are separate packages.
npm install @modelcontextprotocol/server @modelcontextprotocol/node @modelcontextprotocol/express express zodcreateMcpHandler builds a fresh server for each request. registerTool defines a tool with a Zod input schema. createMcpExpressApp adds JSON body parsing and validates the Host and Origin headers against 127.0.0.1, localhost and ::1.
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { toNodeHandler } from "@modelcontextprotocol/node";
import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import * as z from "zod/v4";
const handler = createMcpHandler(() => {
const server = new McpServer({ name: "notes", version: "1.0.0" });
server.registerTool(
"add-note",
{ description: "Append a note", inputSchema: z.object({ text: z.string() }) },
async ({ text }) => ({
content: [{ type: "text", text: `Saved: ${text}` }],
}),
);
return server;
});
const app = createMcpExpressApp();
const node = toNodeHandler(handler);
app.all("/mcp", (req, res) => void node(req, res, req.body));
app.listen(3000);Start it on port 3000 with a TypeScript runner such as tsx.
npx tsx server.tsStart a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your client would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appYour MCP endpoint is https://my-app.hrzn.run/mcp. Keep this terminal open. Horizon sets the Host header to localhost:3000, so the SDK's default host check accepts the request.
Connect a client
Run the Inspector against the remote URL with the http transport.
npx @modelcontextprotocol/inspector --server-url https://my-app.hrzn.run/mcp --transport httpThe command prints a URL with a per-launch token and opens it in your browser.
Check it works
In the Inspector, connect and list the tools. You should see add-note. Call it with text set to hello. The result contains:
Saved: helloYour Horizon terminal prints one line per call:
POST 200 /mcpTroubleshooting
The client can't connect
- Check that the Horizon terminal is still running.
- Check that the URL ends with
/mcp, the path inapp.all("/mcp", ...). - Check that
npx tsx server.tsis still running on port 3000.
The server rejects requests from the tunnel
createMcpExpressApp validates Host and Origin against localhost values by default. Horizon sends Host: localhost:3000, so the default passes. If you pass allowedHosts, keep localhost in the list.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.
Next steps
- Read the SDK's Express serving guide.
- Read MCP's guide to connecting to remote servers.
Self-hosted and AI
Expose a local MCP server, Ollama, n8n, Home Assistant or WordPress on a public HTTPS URL with Horizon, including behind CGNAT.
Expose a local Ollama API
Expose a local Ollama API on a public URL with a Horizon tunnel, including streaming responses and browser access with OLLAMA_ORIGINS.