Skip to main content
Glama
VelixirNET

template-mcp-server

by VelixirNET
README.md
# Remote MCP server

[![Deploy on velixir](https://velixir.net/img/deploy-on-velixir.svg)](https://velixir.net/new?template=mcp-server)

Your own [Model Context Protocol](https://modelcontextprotocol.io) server on a public HTTPS
URL, so Claude Code, Cursor, VS Code and other MCP clients can call tools you write. TypeScript
on the official SDK, served over Streamable HTTP, with a secret token in front.

[Deploy it on velixir](https://velixir.net/new?template=mcp-server), then set one secret and
redeploy to switch it on.

## What it shows

- **The current transport.** Streamable HTTP in stateless mode, on the official
  `@modelcontextprotocol/sdk` 1.x. Every request is answered on its own, so the server runs on
  the Free plan, survives restarts and redeploys without breaking clients, and scales to more
  than one replica.
- **All three MCP primitives.** Four tools (`roll_dice`, `current_time`, `text_stats`,
  `generate_uuid`), a resource (`time://zones`) and a prompt (`plan_meeting`), each with typed
  inputs, and the tools with structured output.
- **Safe example tools.** None of them makes a network request, touches the filesystem or runs
  a command, so nothing a model sends them can reach anything else.
- **Bearer-token auth.** Clients send `Authorization: Bearer <MCP_AUTH_TOKEN>`, compared in
  constant time. Until the token is set, `/mcp` answers 503 and the home page explains what to
  do, while `/healthz` stays green.
- **A landing page with copy-paste setup.** It shows the endpoint and the configuration for each
  client below. It never shows the token.
- **Logs you can read.** One line per MCP request, such as
  `POST /mcp 200 4ms tools/call roll_dice`, and never the body or the token.

## Setting it up on velixir

1. Deploy it from the gallery, or your own copy with `velixir deploy`.
2. Generate a token: `openssl rand -hex 32`, or 32 or more random characters from a password
   manager. Tokens shorter than 16 characters are refused.
3. On the app's Environment tab, set `MCP_AUTH_TOKEN` to the token and tick
   **Treat as secret**.
4. Redeploy, with the Redeploy button on the live release in the Deploys tab.
5. Open the app's URL. The page shows your endpoint, `https://<your-app>.velixir.run/mcp`, and
   the client configuration with your address filled in.

On a custom domain, set `PUBLIC_URL` (for example `https://mcp.example.com`) so the page shows
that address instead.

## Connecting a client

Replace the URL with your endpoint and `YOUR_TOKEN` with the token you set.

### Claude Code

```bash
claude mcp add --transport http velixir https://your-app.velixir.run/mcp \
  --header "Authorization: Bearer YOUR_TOKEN"
```

That adds it for the current project, visible only to you; add `--scope user` to have it in
every project. `/mcp` inside Claude Code shows whether it connected. To share the server with
your team in a committed `.mcp.json`, keep the token out of the file: Claude Code expands
environment variables there.

```json
{
  "mcpServers": {
    "velixir": {
      "type": "http",
      "url": "https://your-app.velixir.run/mcp",
      "headers": { "Authorization": "Bearer ${MCP_AUTH_TOKEN}" }
    }
  }
}
```

### Cursor

In `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:

```json
{
  "mcpServers": {
    "velixir": {
      "url": "https://your-app.velixir.run/mcp",
      "headers": { "Authorization": "Bearer ${env:MCP_AUTH_TOKEN}" }
    }
  }
}
```

`${env:MCP_AUTH_TOKEN}` reads the token from the environment Cursor was started in.

### VS Code

In `.vscode/mcp.json`, or through **MCP: Open User Configuration** for every workspace. VS Code
asks for the token the first time and stores it securely:

```json
{
  "inputs": [
    { "type": "promptString", "id": "mcp-token", "description": "MCP_AUTH_TOKEN", "password": true }
  ],
  "servers": {
    "velixir": {
      "type": "http",
      "url": "https://your-app.velixir.run/mcp",
      "headers": { "Authorization": "Bearer ${input:mcp-token}" }
    }
  }
}
```

### Codex CLI

In `~/.codex/config.toml`, with the token in the `MCP_AUTH_TOKEN` environment variable:

```toml
[mcp_servers.velixir]
url = "https://your-app.velixir.run/mcp"
bearer_token_env_var = "MCP_AUTH_TOKEN"
```

### Clients that only support OAuth

Some clients cannot send a fixed token to a remote server, so they cannot use this one as it
stands:

- **Claude on the web, desktop and mobile** (custom connectors) supports OAuth or no
  authentication. Sending a fixed token in a header is a beta open to a limited set of
  organisations, set up by an organisation Owner. The desktop app's own config file only runs
  local servers.
- **ChatGPT** (developer mode and apps) supports OAuth, no authentication, or a mix of the two.

Connecting those means adding OAuth to the server, which this template does not do.

## Running it locally

```bash
npm install
npm run build
MCP_AUTH_TOKEN=local-dev-token-change-me npm start
```

Then open http://localhost:8080, and point a client at http://localhost:8080/mcp. Or call a tool
with curl:

```bash
curl http://localhost:8080/mcp \
  -H "Authorization: Bearer local-dev-token-change-me" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"roll_dice","arguments":{"notation":"2d6"}}}'
```

The `Accept` header is required: the Streamable HTTP spec has clients accept both JSON and event
streams, and the SDK answers 406 without it.

## Adding your own tools

Tools, the resource and the prompt live in `src/mcp.ts`. A new tool is one `registerTool` call
with a description, a zod schema for its input and an async function that returns its result.
The landing page lists whatever the server serves, so it stays accurate as you change it.

Keep the bar the examples set. A tool runs with arguments a model wrote, so one that fetches a
URL, reads a path or runs a command needs strict limits before it goes on a public endpoint. On
velixir, apps can reach the web (ports 80 and 443) and nothing else.

## Deploying

```bash
velixir deploy
```

## Things to know

**Stateless means no server-to-client messages.** Every POST is answered with one JSON body.
There are no progress notifications, no mid-call questions to the user or the model (sampling,
elicitation) and no list-changed notifications, and `GET /mcp` answers 405, which tells clients
to carry on without a stream. That suits tools that answer at once. Long-running tools that
report progress need the SDK's stateful mode and a plan that does not sleep.

**The Free plan sleeps.** After 15 idle minutes the app stops, and the next request wakes it.
The server itself starts in under a second, but that first call waits for the wake-up, so
expect it to be noticeably slower than the rest. Pick a paid plan if that matters to you.

**It fits the smallest plan with room to spare.** Capped at 256 MB, it measured about 30 MB
idle and peaked under 50 MB under light load (thousands of calls, up to 25 at once).

**One token for everyone.** Anyone holding the token can call every tool, and the server cannot
tell clients apart. To rotate it, set a new `MCP_AUTH_TOKEN`, redeploy, and update every client.
Per-user access needs OAuth.

**"Needs authentication" means the token is wrong.** A missing or wrong token gets a 401, and
some clients read that as a cue to start an OAuth sign-in, which this server does not offer.
Check the `Authorization` header the client sends, and the app log, which records each 401.

**No rate limiting.** A client with the token can call as fast as it likes. Add a limit before
you add a tool that is expensive to run.

**Browser-based clients are refused.** Requests carrying a foreign `Origin` header get a 403,
as the MCP spec asks, and the server sends no CORS headers. Desktop, editor and command-line
clients send no `Origin` and are unaffected.

**The start command lives in `nixpacks.toml`.** It runs `node dist/server.js` directly instead
of `npm start`, so the server receives the stop signal on a redeploy or a sleep and exits
cleanly. If you change the start script, change it there too.

## Endpoints

| Method | Path | Does |
| --- | --- | --- |
| `POST` | `/mcp` | MCP over Streamable HTTP. Needs the bearer token |
| `GET`, `DELETE` | `/mcp` | 405: stateless, so there is no stream to open and no session to end |
| `GET` | `/` | The landing page, or setup instructions until `MCP_AUTH_TOKEN` is set |
| `GET` | `/healthz` | 200 while the process is up, including before the token is set |

## Licence

MIT.