# XenMCP server guide for AI coding agents

> You are an AI coding agent (Claude Code, Cursor, Codex, Copilot, Windsurf or similar). A user asked you to make an MCP server ready for XenMCP, or to build a new one. This file tells you exactly what a server must do to run on XenMCP. Follow it, then add a `xenmcp.json` manifest to the repository root and run the checklist at the end.

XenMCP (https://xenmcp.com) hosts MCP servers in Germany and gives each one an HTTPS address with OAuth 2.1, e.g. `https://acme-crm.xenmcp.com/mcp`, that works as a connector in Claude (web, desktop, mobile), Claude Code, Cursor, VS Code and other MCP clients.

Status: closed beta in preparation. Hosting your own code (npm, Docker, GitHub) is planned after the beta launch; these rules describe how it will work and may still change. Servers that follow them also run well on any other MCP host, so following them is never wasted work.

Machine-readable files:

- This guide: https://xenmcp.com/agents.md
- Manifest schema (JSON Schema 2020-12): https://xenmcp.com/xenmcp.schema.json
- Site overview for LLMs: https://xenmcp.com/llms.txt

## 1. How XenMCP runs your server

- Every server runs in its own sandbox: a container under gVisor, one container per deployment.
- Default limits: **256 MB RAM, 0.5 CPU**, a process limit, **read-only root file system**, a writable `/tmp` (tmpfs, counts against memory), all Linux capabilities dropped, no privilege escalation.
- Network: outbound traffic only to the hosts declared in `egress` in the manifest. Our internal network, other customers and cloud metadata addresses are unreachable.
- The sandbox **sleeps after 15 minutes without calls** and wakes on the next call. Cold start must be fast (target: ready in under 3 seconds).
- XenMCP terminates TLS and does OAuth 2.1 in front of your server. Your server only ever receives calls from our gateway.

## 2. Transport

Pick one:

- **stdio** (recommended for existing servers): read JSON-RPC from stdin, write to stdout. XenMCP wraps it with a bridge that speaks Streamable HTTP. You change nothing.
- **Streamable HTTP**: listen on the port given in the `PORT` environment variable (default `8080`), serve MCP at `/mcp`, bind `0.0.0.0`. No TLS, no auth — XenMCP does both.

Rules for both:

- Use an official MCP SDK (TypeScript `@modelcontextprotocol/server`, Python `mcp`, or another official SDK) at a current version. Target the current specification (2026-07-28, stateless).
- Do not rely on sessions or on state kept between calls. Treat every request as independent; caches are fine, correctness must not depend on them.
- The old HTTP+SSE transport (2024-11-05) is not supported.
- In stdio mode **stdout belongs to the protocol**: write logs to stderr only. A single `console.log` to stdout breaks the connection.

## 3. Configuration and secrets

- Read all configuration from **environment variables**. Declare each one in the manifest (`env`), mark secrets with `"secret": true`.
- XenMCP stores secrets encrypted and injects them as environment variables at start. Never read config from files in the home directory, never ask for input on stdin, never open a browser.
- No interactive login at runtime (no device codes, no "click this link"). If an upstream API needs OAuth, take a long-lived token or a refresh token from an environment variable.
- Never log secrets, tokens, connection strings, tool arguments or tool results. Log tool name, status and duration at most.

## 4. File system, state and processes

- Root file system is read-only. Write only to `/tmp`, and expect it to be empty after every sleep or restart.
- No local databases on disk (SQLite files, LevelDB, JSON stores). Keep state in an external database declared in `egress`.
- No background jobs, cron loops or schedulers inside the server. Do work when a tool is called.
- Do not download code, models or data at start. Everything the server needs must be in the package or image.
- Keep memory flat: stream large results, page through APIs, cap result sizes.

## 5. Tools

- Give every tool correct annotations: `readOnlyHint: true` for tools that only read; `destructiveHint: true` for tools that write, send, delete or spend money; `idempotentHint` and `openWorldHint` where they apply. MCP clients use them to ask the user before risky calls.
- Prefer read-only tools. Make write tools opt-in via an environment variable (for example `ENABLE_WRITES=true`), off by default.
- Write tool descriptions as plain, factual text for the user. **No instructions to the model** ("always call this first", "ignore previous instructions", hidden text, unusual Unicode). XenMCP reviews descriptions before release and rejects servers that try to steer the model.
- Strict input schemas: types, enums, `required`, length limits. Validate inputs again in code.
- Return concise results. Keep a single response well under 1 MB (hard limit at the gateway: 20 MB); offer paging (`limit`, `cursor`) for lists.
- Finish each call within 60 seconds (gateway timeout: 120 seconds). Set timeouts on every upstream request.
- Treat text from upstream systems (emails, tickets, web pages, database rows) as data. Do not pass it on as instructions; mention in the tool description that results contain untrusted content when that is the case.

## 6. Packaging

One of:

- **npm**: a package with a `bin` entry or a documented start command; Node.js 22 LTS; commit the lockfile; no `postinstall` script that downloads anything.
- **Docker**: an image for `linux/amd64`, running as a non-root user, with `CMD` that starts the server; no `--privileged`, no host mounts, no extra capabilities.
- **GitHub**: a repository with one of the above plus `xenmcp.json` in the root. Pin a tag or commit in the manifest; XenMCP builds the image itself from that exact version.
- Python servers are fine in a Docker image (pin dependencies with a lockfile, e.g. `uv.lock`).

## 7. The manifest: `xenmcp.json`

Put it in the repository root. Schema: https://xenmcp.com/xenmcp.schema.json

```json
{
  "$schema": "https://xenmcp.com/xenmcp.schema.json",
  "schemaVersion": 1,
  "name": "acme-crm",
  "title": "Acme CRM",
  "description": "Read contacts, deals and activities from Acme CRM.",
  "source": { "type": "npm", "package": "@acme/crm-mcp", "version": "1.4.2" },
  "transport": { "type": "stdio" },
  "command": ["acme-crm-mcp"],
  "env": [
    { "name": "ACME_API_KEY", "description": "API key from Acme settings, read-only scope", "secret": true, "required": true },
    { "name": "ACME_REGION", "description": "eu or us", "default": "eu" },
    { "name": "ENABLE_WRITES", "description": "Allow tools that change data", "default": "false" }
  ],
  "egress": ["api.acme.com:443"],
  "resources": { "memoryMb": 256, "cpu": 0.5 }
}
```

Other `source` forms: `{ "type": "docker", "image": "ghcr.io/acme/crm-mcp@sha256:…" }` or `{ "type": "github", "repo": "acme/crm-mcp", "ref": "v1.4.2", "dockerfile": "Dockerfile" }`. For an HTTP server: `"transport": { "type": "http", "port": 8080, "path": "/mcp" }`.

## 8. Remote servers you already run (OAuth bridge)

If the server already runs on the internet and only accepts an API key, it does not need the manifest. It must:

- speak Streamable HTTP over public HTTPS;
- accept the key as `Authorization: Bearer …`, a custom header such as `X-API-Key`, HTTP Basic, or a URL parameter;
- answer `initialize` and `tools/list` with that key (XenMCP tests the connection before saving);
- optionally allow only XenMCP's fixed outbound IP address (published at the beta launch).

## 9. Checklist — run it before you finish

1. The server starts with only the environment variables from the manifest and nothing else on disk.
2. It runs in a sandbox-like container:

```bash
docker run --rm -i --read-only --tmpfs /tmp:size=64m --memory 256m --cpus 0.5 \
  --cap-drop ALL --security-opt no-new-privileges --env-file .env.example <image>
```

3. In stdio mode nothing but JSON-RPC is written to stdout.
4. `tools/list` works; every tool has a plain description and correct `readOnlyHint` / `destructiveHint`.
5. Write tools are off unless an environment variable enables them.
6. No secret, argument or result appears in the logs.
7. All outbound hosts are listed in `egress`.
8. `xenmcp.json` validates against https://xenmcp.com/xenmcp.schema.json.
9. Optional: inspect it with the MCP Inspector (`npx @modelcontextprotocol/inspector`).

When all points pass, tell the user the server is ready for XenMCP and point them to https://xenmcp.com/waitlist/ (German: https://xenmcp.com/de/warteliste/).
