# How to Deploy Skybridge to Production

Enrico Toniato·CTO·May 10, 2026

This guide is the **Skybridge** entry from our [seven-framework deploy comparison](/content/blog/deploying-seven-mcp-frameworks/index.html). We shipped the same example on Manufact Cloud: an `echo` tool and a `greet_widget` that returns a [MCP Apps](https://github.com/modelcontextprotocol/ext-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](/content/login/index.html).

## Deploy to Manufact

Manufact Cloud detects skybridge by the `skybridge` dependency. The deploy uses the **repo's own Dockerfile** (which pins `node:24-slim`) instead of the cloud's generated one: leave the build/start commands empty in the form so the cloud knows to use your Dockerfile.

1

### Push the repo to GitHub

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

Make sure `Dockerfile`, `vite.config.ts`, `tsconfig.json`, and the full `src/` (including `views/`) are committed.

2

### Open the new-server flow

Go to [`manufact.com/cloud/<your-org>/servers/new`](/content/site-root.html) and pick **Deploy from GitHub**. The probe labels the repo **skybridge**.

3

### Clear build and start commands

The skybridge preset auto-fills `npm run build` and `npm start`, but leave **both fields empty**. With nothing in the override, Manufact uses the repo's own Dockerfile (which already does `npm run build && skybridge start`).

Why? Skybridge's `dist/server.js` does `import("./vite-manifest.js")` at runtime: that file is produced by `skybridge build` and lives next to the server entry. The repo's Dockerfile keeps build artifacts where the start command expects them. The cloud's generated Dockerfile assumes a flatter layout and would break the manifest import.

4

### Set the port to 3000

Skybridge's `server.run()` reads `process.env.__PORT` (with double underscore) but defaults to 3000. The cloud sets `PORT=3000` and routes the configured internal port to 3000, which matches Skybridge's default fallback even though the env var name doesn't match. So **port: 3000** in the form, and you're good.

5

### Click Deploy

First build is 90–180 seconds: `npm install` pulls down React 19, Vite 7, the Skybridge devtools, plus type packages. Subsequent rebuilds are faster because image layers are cached.

6

### 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
```

You should see `greet_widget` with both `ui/resourceUri` (MCP Apps) and `openai/outputTemplate` (Apps SDK) keys.

**Example repo:** [`manufacts/manufact-for-skybridge`](https://github.com/manufacts/manufact-for-skybridge)

**Live `/mcp`:** [`warm-steel-j4uhd.run.mcp-use.com/mcp`](https://warm-steel-j4uhd.run.mcp-use.com/mcp) — `serverInfo.name` is `manufact-for-skybridge` (Manufact server status: running).

## 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

- A skybridge project scaffolded from `npm create skybridge`
- An `echo` tool (text-only) and a `greet_widget` tool (returns `structuredContent` for the view)
- A React view at `src/views/greet.tsx` rendered by the host

## Project setup

```
npm create skybridge@latest my-server
cd my-server
npm install
```

This produces:

```
my-server/
├── src/
│   ├── server.ts         # registers tools
│   ├── views/
│   │   └── magic-8-ball.tsx
│   ├── components/
│   │   └── ball.tsx
│   ├── helpers.ts        # type-safe useToolInfo hook
│   └── index.css
├── vite.config.ts        # skybridge() + react() plugins
├── tsconfig.json         # extends "skybridge/tsconfig"
├── Dockerfile            # node:24-slim
└── package.json
```

Replace the demo magic-8-ball with `echo` + `greet_widget`.

## The server

`src/server.ts`:

```
import { McpServer } from "skybridge/server";
import { z } from "zod";

const server = new McpServer(
  {
    name: "my-server",
    version: "0.1.0",
  },
  { capabilities: {} },
)
  .registerTool(
    {
      name: "echo",
      description: "Echo the input back as text.",
      inputSchema: { text: z.string() },
    },
    async ({ text }) => ({
      content: [{ type: "text", text }],
      isError: false,
    }),
  )
  .registerTool(
    {
      name: "greet_widget",
      description: "Greet someone and render a Skybridge view.",
      inputSchema: { name: z.string() },
      view: {
        component: "greet", // matches src/views/greet.tsx
        description: "Greeting card View",
      },
    },
    async ({ name }) => ({
      structuredContent: { name },
      content: [{ type: "text", text: `Hello, ${name}!` }],
      isError: false,
    }),
  );

if (process.env.NODE_ENV === "production") {
  const { default: manifest } = await import("./vite-manifest.js");
  server.setViteManifest(manifest);
}

export default await server.run();

export type AppType = typeof server;
```

The view is identified by name (`greet`). At build time `skybridge build` runs `vite build` which scans `src/views/`, bundles each into a self-contained HTML resource, and emits a manifest. At runtime `server.setViteManifest(manifest)` lets Skybridge look up the resource URI for each view.

## The view

`src/views/greet.tsx`:

```
import "@/index.css";

import { useToolInfo } from "../helpers.js";

export default function Greet() {
  const { input, output } = useToolInfo<"greet_widget">();
  const name = (output as { name?: string } | undefined)?.name ?? input?.name;

return (
    <div className="container">
      <div className="card">
        <h1>{name ? `Hello, ${name}!` : "Greeting view loaded."}</h1>
        <p>Greeting widget served by my-server.</p>
      </div>
    </div>
  );
}
```

`useToolInfo<"greet_widget">()` is type-safe: the `helpers.ts` from the starter introspects `AppType` (your server's exported type) and gives the view typed access to that tool's `input` and `output` shapes. Missing tools, mismatched names, wrong field types: all caught at compile.

`src/index.css`: write whatever CSS you want, scoped however you want. Vite handles bundling it into the view's HTML resource.

## What the host sees

`tools/list`:

```
{
  "name": "greet_widget",
  "_meta": {
    "ui": { "resourceUri": "ui://views/ext-apps/greet.html?v=c6f47c45" },
    "ui/resourceUri": "ui://views/ext-apps/greet.html?v=c6f47c45",
    "openai/outputTemplate": "ui://views/apps-sdk/greet.html?v=c6f47c45"
  }
}
```

`resources/list` returns _two_ resources for the same view: one for the MCP Apps profile (Claude/Inspector) and one for the OpenAI Apps SDK profile (ChatGPT). The `?v=…` suffix is the Vite content hash, so cached widgets bust on rebuild.

## Run it locally

```
npm run dev          # vite dev + tsx watch with HMR
```

Visit `http://localhost:3000` for Skybridge's built-in DevTools: call your tool from a UI panel and the view renders inline.

For a production-shaped run:

```
npm run build
npm start            # actually skybridge start, which runs node dist/server.js
```

## When to reach for it

Pick this when your team already lives on Node 24, React 19, and Vite 7 (the runtime image weight is then a non-issue) and you specifically need both the OpenAI Apps SDK metadata and MCP Apps metadata emitted from the same view code with no extra work.

If your team isn't already on that stack, [mcp-use](/content/blog/mcp-app-with-mcp-use/index.html) gives you the same React-component-as-widget ergonomics with a much lighter dependency tree (no Vite manifest at runtime, no Node 24 lower bound). The full comparison is in [Deploying Seven MCP Frameworks](/content/blog/deploying-seven-mcp-frameworks/index.html).
