Host a remote MCP server
Deploy an MCP server as an HTTP application when clients need a remote URL. This example serves one arithmetic tool, validates a bearer token, and exposes a health endpoint. It uses Streamable HTTP; a local stdio server cannot serve remote clients by itself.
Before you start
You need Node.js 22, Lizard CLI, a project you can deploy to, and an MCP client that accepts a configured bearer token. The example uses @modelcontextprotocol/sdk 1.30.0 in stateless mode. It does not provide OAuth login, browser access, or an identity provider.
The runnable files live in remote-mcp-node. Use the checked-in lockfile. The local smoke test checks initialization, tool discovery, a tool call, and rejection without a valid token. A deployment check must also confirm the public proxy and TLS path.
Run locally
From the example directory:
npm ci
npm test
node issue-token.mjs
export MCP_PUBLIC_KEY="$(cat .mcp-public-key.pem)"
export MCP_ALLOWED_HOSTS=127.0.0.1,localhost
npm startThe generated token expires after one hour. The private signing key is not saved. Use your own token issuer and rotation process for a lasting deployment; keep its private key outside the server.
In another terminal:
export MCP_URL=http://127.0.0.1:8000/mcp
export MCP_TOKEN="$(cat .mcp-token)"
node client.mjsThe client checks initialization, tool discovery and the result 5, then prints MCP initialize, tools/list and tools/call passed: 5. /health returns ok; a request to /mcp without a valid token returns 401.
Deploy the application
Keep the example’s Dockerfile and lockfile. From the directory containing them:
lizard init --name mcp-example
lizard add --service mcp
lizard domain --service mcp --jsonSign in with lizard login if a command reports that authentication is required. init links the project; add creates the named service. The domain command assigns its hostname before deployment. Use the returned hostname below, without https:// or a path:
lizard secrets set MCP_PUBLIC_KEY="$(cat .mcp-public-key.pem)" --service mcp
lizard secrets set MCP_ALLOWED_HOSTS=YOUR_SERVICE_HOSTNAME --service mcpSet both values before the first deployment. Then run:
lizard up --service mcp --port 8000
lizard logs --build --service mcp --json
lizard logs --service mcp --json
lizard ps --jsonOn macOS with Lizard CLI 0.3.92, use COPYFILE_DISABLE=1 lizard up --service mcp --port 8000 to exclude AppleDouble metadata from the archive. A later CLI release may include the archive fix. Check the final deployment event and public endpoint; do not rely on this CLI version’s exit code alone after a failed build. server.mjs listens on 0.0.0.0 and reads PORT, with 8000 as its default. The deploy command sets the service port to 8000. Configure the public key as a multiline environment value; do not upload the private signing key or the token file.
Verify the public endpoint
Set MCP_URL to https://YOUR_SERVICE_HOSTNAME/mcp, keep a valid token in the client environment, and run node client.mjs. Verify all three results:
/healthreturns200over HTTPS./mcprejects a client with no valid token.- The authenticated client lists tools and calls
add, returning5.
A healthy process alone does not prove the MCP handshake or streamed response works through the public proxy.
Troubleshooting
| Result | Check |
|---|---|
| Process exits during startup | MCP_PUBLIC_KEY must contain the public PEM key; MCP_ALLOWED_HOSTS must contain the service hostname. |
401 | The token must use RS256, match issuer and audience mcp-example, and not have expired. |
403 | Match the request hostname to MCP_ALLOWED_HOSTS. Browser origins are not enabled by this example. |
405 with a valid token | This stateless MCP endpoint accepts protocol requests through POST. Use an MCP client. A browser GET without a token returns 401 first. |
| Local test passes but remote call fails | Check port, HTTPS, response streaming, and proxy timeouts. |
Limits and cost
This example does not keep user sessions or durable files in server memory. Add a database for lasting application state and check access per user before exposing private tools. For clients that require OAuth discovery or interactive login, add a supported OAuth provider instead of distributing this test token.
An HTTP process may remain active between calls. Check pricing, limits, and measured CPU/memory; an empty request queue does not imply no charges.
Next steps
See scenario test results for checked versions, cloud results and remaining limits.
Updated