Deploy an MCP server

A remote MCP server on a live HTTPS URL. Any client can connect over the web, no local install.

MCP servers expose tools to AI clients. Run locally they talk over stdio; to share one over the web you serve the Streamable HTTPtransport on the port Dockhold assigns. Then any MCP client connects to your URL. This recipe is the whole path.

In a hurry? Start from the ready-mademcp-server-starter template: click Use this template on GitHub, thendeploy it. Or follow the steps below.

1. Serve MCP over Streamable HTTP

Use the official SDK and expose a single /mcp endpoint. In stateless mode you build a fresh server per request. It is simple, and it scales with no shared session state:

import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

function buildServer() {
  const server = new McpServer({ name: "my-mcp", version: "1.0.0" });
  server.registerTool(
    "add",
    { title: "Add", description: "Add two numbers.", inputSchema: { a: z.number(), b: z.number() } },
    async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] })
  );
  return server;
}

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

app.post("/mcp", async (req, res) => {
  const server = buildServer();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  res.on("close", () => { transport.close(); server.close(); });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(process.env.PORT || 3000, "0.0.0.0");

Your app must listen on 0.0.0.0 and read its port from thePORT environment variable. Never localhost, never a hardcoded port. Dockhold assigns PORT at runtime; an app that ignores it can't receive traffic.

2. Connect the repo and deploy

  1. Push the project to a GitHub repository.
  2. Open the dashboard, connect GitHub, and pick the repo.
  3. On a paid plan (any account with compute added), Dockhold detects the Node project and runs it. Nothing else to do.
  4. On a free account, add the Dockerfile below at the root of your repo first. Auto-detection is a paid feature, so without one the deploy stops right away and tells you.
# Dockerfile
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

Your server goes live at https://<your-app>.dockhold.app, with the MCP endpoint at /mcp. Every later push redeploys.

3. Connect a client

Point any MCP client at your /mcp URL using the Streamable HTTP transport.

  • MCP Inspector (quickest test): runnpx @modelcontextprotocol/inspector, pick "Streamable HTTP", and enter your /mcp URL.
  • Claude Code:claude mcp add --transport http my-server https://<your-app>.dockhold.app/mcp
  • Other clients: add it as a remote / HTTP MCP server with the /mcp URL.

4. Lock it down

The endpoint is public by default. Anyone with the URL can call your tools. To require a token, make it a private app (Access tab → Private → mint a token); clients that support custom headers then sendAuthorization: Bearer <token>. See theprivate-app pattern, and never expose a tool that acts on secrets without auth.

Troubleshooting

  • Client can't connect: confirm the transport isStreamable HTTP (not stdio or SSE) and the URL ends in/mcp.
  • 406 Not Acceptable: the client must sendAccept: application/json, text/event-stream. Every real MCP client does this automatically.
  • URL times out: the server must listen on0.0.0.0:$PORT, not a fixed port.

Next

Suggested guides
to move to openEsc to closeFrom the Dockhold docs