Cookie preferences

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

Manufact

How to Deploy tmcp to Production

Enrico Toniato
Enrico ToniatoCTO
How to Deploy tmcp to Production

This guide is the tmcp 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

Manufact Cloud detects tmcp by the tmcp dependency and labels the repo accordingly. The deploy is the standard Node.js path (npm run build, npm start, port 3000) with no framework-specific quirks.

Push the repo to GitHub

git init && git add . && git commit -m "Initial commit" gh repo create my-org/tmcp-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 tmcp.

Confirm build and start

Preset fills:

  • Build command: npm run build
  • Start command: npm start
  • Port: 3000

The cloud's generated Dockerfile clones the repo, runs npm install, runs tsc, and starts node dist/index.js.

Click Deploy

First build is fast (~30s); tmcp has minimal dependencies. Once you see Server status: running, the URL is live.

Smoke-test the URL

# tmcp is stateless, so tools/list works directly 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

Confirm greet_widget._meta.ui.resourceUri and _meta["ui/resourceUri"] both point at your view URI, and resources/list returns it with mimeType: "text/html;profile=mcp-app".

Example repo: manufacts/mcp-detect-tmcp

Live /mcp: fast-wave-zubi2.run.mcp-use.com/mcpserverInfo.name is mcp-detect-tmcp (Manufact server status: running).

Tip

Switch to Workers later

Because tmcp's HTTP transport returns a fetch Response, you can move the same handler to Cloudflare Workers without rewriting tool registration; only the listener changes. The Manufact deployment uses Node + Fly today; the tmcp code itself is portable.

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 MCP server with McpServer from tmcp and a Zod-v4 adapter
  • An echo tool
  • A greet_widget tool that follows the MCP Apps protocol manually: your own HTML, your own resource, your own metadata

Project setup

mkdir my-server && cd my-server npm init -y npm pkg set type=module npm pkg set scripts.build="tsc" npm pkg set scripts.start="node dist/index.js" npm install tmcp @tmcp/adapter-zod @tmcp/transport-http \ @remix-run/node-fetch-server zod npm install -D typescript @types/node

@tmcp/transport-http returns a Response; @remix-run/node-fetch-server adapts that into a Node http.createServer listener. Here zod is v4, which matches what the tmcp Zod adapter expects.

tsconfig.json:

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "rootDir": "src", "outDir": "dist", "strict": true, "skipLibCheck": true, "esModuleInterop": true }, "include": ["src/**/*.ts"] }

The server

src/index.ts:

import * as http from "node:http"; import { McpServer } from "tmcp"; import { ZodJsonSchemaAdapter } from "@tmcp/adapter-zod"; import { HttpTransport } from "@tmcp/transport-http"; import { createRequestListener } from "@remix-run/node-fetch-server"; import { z } from "zod"; const VIEW_URI = "ui://my-server/greet.html"; const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; const VIEW_HTML = `<!doctype html> <html><body style="font:16px/1.4 system-ui;padding:24px"> <h1 id="greeting">Greeting view loaded.</h1> <script type="module"> import { App } from "https://esm.sh/@modelcontextprotocol/ext-apps@1"; const app = new App({ name: "greet", version: "0.1.0" }); app.ontoolresult = (r) => { const text = (r?.content ?? []).find(c => c.type === "text")?.text; if (text) document.getElementById("greeting").textContent = text; }; app.connect(); </script> </body></html>`; const server = new McpServer( { name: "my-server", version: "0.1.0", description: "tmcp + MCP Apps" }, { adapter: new ZodJsonSchemaAdapter(), capabilities: { tools: { listChanged: true }, resources: { listChanged: true } }, }, ); // Register the view resource at a stable URI server.resource( { name: "Greet view", description: "MCP Apps view for greet_widget.", uri: VIEW_URI, mimeType: RESOURCE_MIME_TYPE, }, async (uri) => ({ contents: [{ uri, mimeType: RESOURCE_MIME_TYPE, text: VIEW_HTML }], }), ); // Plain text-only tool server.tool( { name: "echo", description: "Echo the input back as text.", schema: z.object({ text: z.string() }), }, async ({ text }) => ({ content: [{ type: "text", text }] }), ); // Widget tool: wire _meta.ui.resourceUri yourself server.tool( { name: "greet_widget", description: "Greet someone and render an MCP App view.", schema: z.object({ name: z.string() }), outputSchema: z.object({ name: z.string() }), _meta: { ui: { resourceUri: VIEW_URI }, "ui/resourceUri": VIEW_URI, // legacy key for older hosts }, }, async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }], structuredContent: { name }, }), ); const transport = new HttpTransport(server, { path: "/mcp" }); const port = Number(process.env.PORT ?? "3000"); http .createServer( createRequestListener(async (req) => { const response = await transport.respond(req); return response ?? new Response(null, { status: 404 }); }), ) .listen(port, "0.0.0.0", () => console.log(`tmcp on :${port}/mcp`));

A few details worth noting:

  • outputSchema is required when you return structuredContent. tmcp validates that the response shape matches what you declared. Hosts that read outputSchema use it to type the widget's useToolInfo hook (where applicable).
  • Both metadata keys. _meta.ui.resourceUri is the modern key; _meta["ui/resourceUri"] is the legacy key. Newer hosts read the first; older hosts read the second. Set both.
  • The transport handler returns Response | null. Null means "not my path." Your HTTP layer decides what to do with non-MCP routes.

What the host sees

curl -s -X POST -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \ http://localhost:3000/mcp

Returns:

{ "name": "greet_widget", "_meta": { "ui": { "resourceUri": "ui://my-server/greet.html" }, "ui/resourceUri": "ui://my-server/greet.html" } }

And resources/list returns the view at the matching URI with the right MIME type.

Run it

npm run build npm start

Then probe:

curl -s -X POST -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"greet_widget","arguments":{"name":"World"}}}' \ http://localhost:3000/mcp

You should see { structuredContent: { name: "World" }, content: [{ type: "text", text: "Hello, World!" }] }.

When to reach for it

Pick this when you're embedding MCP into a host you already have (a Worker, a Hono app, a Fastify plugin), you want a small schema-agnostic core, and you're fine writing the MCP Apps wiring by hand. The fetch-shaped transport is portable across Node, Bun, Deno, and Workers without a rewrite, which matters when the MCP server is one piece of a larger system.

For a standalone MCP server that ships widgets as its main job, mcp-use gives you the same protocol fidelity with a widget() helper that auto-registers the resource so you don't repeat the wiring on every tool. The full comparison is in Deploying Seven MCP Frameworks.

Share