How to Deploy tmcp to Production | Manufact Blog

How to Deploy tmcp to Production

Enrico Toniato·CTO·May 10, 2026

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:

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/manufact-for-tmcp

Live /mcp: dark-forge-inqbu.run.mcp-use.com/mcp — serverInfo.name is manufact-for-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 a managed Node runtime; 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

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:

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.