Cookie preferences

We use cookies to improve your experience. See our Privacy Policy.

Manufact

How to Deploy xmcp to Production

Enrico Toniato
Enrico ToniatoCTO
How to Deploy xmcp to Production

This guide is the xmcp entry from our seven-framework deploy comparison. We shipped the same example on Manufact Cloud: an echo tool and a greet_widget that returns a MCP Apps view. Below is the deploy path we used; the reference server code is in the second half if you want to reproduce the example.

If you want to run the same deploy pipeline on your repo, connect it in the dashboard. Open the dashboard.

Deploy to Manufact

xmcp deploys cleanly on Manufact Cloud once you set the right port. Because xmcp bakes the port into the build (Zod default 3001), the platform side has to match.

Push the repo to GitHub

git init && git add . && git commit -m "Initial commit" gh repo create my-org/xmcp-greet --private --source=. --push

Open the new-server flow

Go to manufact.com/cloud/<your-org>/servers/new and pick Deploy from GitHub. The framework probe labels the repo xmcp.

Set the port to 3001

The preset fills npm run build / npm start correctly, but the port field defaults to 3000. Change it to 3001 to match xmcp's hardcoded runtime port. This makes Fly route traffic to the right port and sets PORT=3001 for the container (which xmcp ignores anyway, but consistency matters).

Click Deploy

Build takes ~60s (Rspack pulls in @rspack/binding-linux-x64-musl for the Alpine image: Rspack ships musl wheels, so no node-gyp issues). Once you see Server status: running, the URL is live.

Smoke-test the URL

curl -s -X POST -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \ https://<your-slug>.run.mcp-use.com/mcp

greet_widget._meta["ui/resourceUri"] should be ui://app/greet_widget.html, and resources/list returns the auto-generated HTML resource with mimeType: "text/html;profile=mcp-app".

Example repo: manufacts/mcp-detect-xmcp

Live /mcp: keen-steel-ix5iy.run.mcp-use.com/mcpserverInfo.name is xmcp server (example repo mcp-detect-xmcp; Manufact server status: running).

Warning

Why port 3001?

xmcp serializes its config at build time. The Zod schema port: z.number().default(3001) injects 3001 even if you set http: true (no port): process.env.PORT is never read at runtime. Either set the platform port to 3001 (what we did) or set http.port: 3000 in xmcp.config.ts and rebuild for that target.

The steps above are what we used for the live demo. Your repo can use the same GitHub deploy flow. Open the dashboard.

Reference server

Use this section if you are following along with the same example. If you already have an MCP app to deploy, the GitHub steps above are enough.

What we deployed

  • An xmcp project scaffolded from the official mcp-app template
  • An echo tool (plain .ts file: no UI)
  • A greet_widget tool (.tsx: React handler, auto-registered as MCP App view)

Project setup

npx create-xmcp-app@latest my-server --example mcp-app cd my-server npm install

The starter ships with one weather widget; we'll replace it.

Project layout:

my-server/ ├── src/ │ └── tools/ │ └── weather.tsx # delete this ├── globals.css # Tailwind entry ├── postcss.config.mjs ├── xmcp.config.ts ├── tsconfig.json └── package.json

package.json scripts:

{ "scripts": { "build": "xmcp build", "dev": "xmcp dev", "start": "node dist/http.js" } }

Configure the transport

xmcp.config.ts:

import { type XmcpConfig } from "xmcp"; const config: XmcpConfig = { http: { host: "0.0.0.0", // bind on all interfaces for cloud deploy }, paths: { tools: "./src/tools", prompts: false, resources: false, }, }; export default config;

A subtle gotcha: xmcp serializes this config at build time. Its Zod schema has port: z.number().default(3001), so omitting port still bakes 3001 into the build. The deploy target needs to be told port=3001, or you bake a different port at build time. There's no runtime process.env.PORT override path. (More on this in the field report.)

The echo tool

src/tools/echo.ts:

import { z } from "zod"; import { type InferSchema, type ToolMetadata } from "xmcp"; export const schema = { text: z.string().describe("Text to echo"), }; export const metadata: ToolMetadata = { name: "echo", description: "Echo the input back as text.", }; export default async function echo({ text }: InferSchema<typeof schema>) { return text; }

Three exports: schema, metadata, default handler. xmcp turns a string return into a text/content MCP response automatically.

The widget tool

src/tools/greet_widget.tsx:

import { z } from "zod"; import { type InferSchema, type ToolMetadata } from "xmcp"; import "../../globals.css"; export const schema = { name: z.string().describe("Name to greet"), }; export const metadata: ToolMetadata = { name: "greet_widget", description: "Greet someone and render an MCP App view.", _meta: { ui: { csp: { connectDomains: [] }, prefersBorder: true, }, }, }; export default function GreetWidget({ name }: InferSchema<typeof schema>) { return ( <div className="min-h-screen bg-black text-white p-8 flex items-center justify-center"> <div className="max-w-md w-full border border-white/10 bg-white/5 p-8"> <div className="text-sm font-mono text-zinc-500 uppercase tracking-wider mb-2"> Greet </div> <h1 className="text-4xl font-light tracking-tight mb-2"> Hello, {name}! </h1> <p className="text-sm text-zinc-400"> Greeting widget served by my-server. </p> </div> </div> ); }

Three things to notice:

  • The .tsx extension flips xmcp into widget mode: it builds the React component into a self-contained HTML resource, registers a text/html;profile=mcp-app resource, and writes _meta.ui.resourceUri on the tool.
  • _meta.ui.csp.connectDomains declares the widget's allowed network origins for the host's Content Security Policy. Empty means "no outgoing requests." connectDomains: ["https://api.example.com"] would whitelist that origin.
  • prefersBorder: true is a hint to the host to render the widget inside a bordered card.

The widget's name prop is the tool's input: xmcp passes args straight through to the React component.

What the host sees

After npm run build && npm start:

tools/list returns:

{ "name": "greet_widget", "_meta": { "ui/resourceUri": "ui://app/greet_widget.html" } }

resources/list returns the auto-bundled HTML resource:

{ "uri": "ui://app/greet_widget.html", "mimeType": "text/html;profile=mcp-app", "_meta": { "ui/csp/connectDomains": [], "ui/csp/resourceDomains": ["https://esm.sh"], "ui/prefersBorder": true } }

You wrote a single .tsx file. xmcp produced the resource, the metadata, the bundled HTML, and the wiring.

Run it

npm run build npm start

Or in dev with hot reload:

npm run dev

The dev server watches src/tools/, rebuilds on change, and reconnects clients.

When to reach for it

Pick this when file-based routing matters more to you than runtime flexibility, and you're comfortable with the Rspack toolchain and the build-time-baked port. The file-based router and npx xmcp create widget <name> scaffolder genuinely remove decisions if you stay on the happy path. The friction shows up as soon as you don't: Rspack-only build, port hardcoded by Zod default, no runtime process.env.PORT override.

If you want widget ergonomics with fewer baked-in opinions, mcp-use keeps the React-component-per-widget pattern without the build-time port and toolchain lock-in. The full comparison is in Deploying Seven MCP Frameworks.

Share