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
- Push the project to a GitHub repository.
- Open the dashboard, connect GitHub, and pick the repo.
- On a paid plan (any account with compute added), Dockhold detects the Node project and runs it. Nothing else to do.
- On a free account, add the
Dockerfilebelow 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): run
npx @modelcontextprotocol/inspector, pick "Streamable HTTP", and enter your/mcpURL. - 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
/mcpURL.
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 send
Accept: application/json, text/event-stream. Every real MCP client does this automatically. - URL times out: the server must listen on
0.0.0.0:$PORT, not a fixed port.
Next
- Host a FastMCP server (the Python path: same rules,
mcp.run(transport="http", ...)). - Deploy a plain Node API
- Agent rules: make your AI tool generate Dockhold-ready apps.
- All recipes