Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions docs/api-reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ skybridge dev
```

**What it does:**
- Starts a development server at `http://localhost:3000/`
- Opens DevTools for local testing at `http://localhost:3000/`
- Exposes MCP server at `http://localhost:3000/mcp`
- Starts a development server at `http://localhost:3000/` by default
- Opens DevTools for local testing at `http://localhost:3000/` by default
- Exposes MCP server at `http://localhost:3000/mcp` by default
- Enables file watching with automatic server restart
- Enables Hot Module Reloading (HMR) for widgets

#### Flags

| Flag | Description |
|------|-------------|
| `-p, --port <port>` | Port to run the development server on. |
| `--use-forwarded-host` | Uses the forwarded host header to construct widget URLs instead of localhost. Useful when accessing the dev server through a tunnel (e.g., ngrok). |

The `--use-forwarded-host` allows your widgets to work on any device connected to the internet, not just on your local machine. Useful when developing on multiple devices or when you need to test your app from different devices.
Expand All @@ -38,15 +39,15 @@ The `--use-forwarded-host` allows your widgets to work on any device connected

```bash
# Terminal 1: Start Skybridge
skybridge dev --use-forwarded-host
skybridge dev --use-forwarded-host --port 4000

# Terminal 2: Expose your local server
ngrok http 3000
ngrok http 4000
```

**Note:** Only use `--use-forwarded-host` when you need to develop or test on multiple devices. For local-only development, the standard `skybridge dev` command is sufficient.

When using `--use-forwarded-host`, Skybridge will use the ngrok URL (e.g., `https://abc123.ngrok-free.app`) for widget URLs instead of `localhost:3000`.
When using `--use-forwarded-host`, Skybridge will use the ngrok URL (e.g., `https://abc123.ngrok-free.app`) for widget URLs instead of the local `localhost` address.

---

Expand Down Expand Up @@ -83,9 +84,15 @@ skybridge start
**What it does:**
- Runs the compiled server from `dist/index.js`
- Sets `NODE_ENV=production`
- Serves the MCP endpoint at `http://localhost:3000/mcp`
- Serves the MCP endpoint at `http://localhost:3000/mcp` by default
- Serves pre-built widget assets from `/assets`

#### Flags

| Flag | Description |
|------|-------------|
| `-p, --port <port>` | Port to run the production server on. |

---

## create-skybridge
Expand Down
2 changes: 1 addition & 1 deletion docs/devtools/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ DevTools currently renders widgets using the **Apps SDK runtime** (mocked `windo

## Quick Start

When you run `pnpm dev`, DevTools is automatically available at `http://localhost:3000/`.
When you run `pnpm dev`, DevTools is automatically available at `http://localhost:3000/` by default. You can change the port with `skybridge dev --port <port>` or by setting `PORT`.

<img src="/images/devtools-landing.png" alt="Skybridge DevTools Overview" style={{maxWidth: '100%', borderRadius: '8px', border: '1px solid #e0e0e0'}} />

Expand Down
3 changes: 1 addition & 2 deletions docs/quickstart/build-for-production.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The production server runs your compiled application from the `dist/` directory
- **No dev tools** - DevTools and Vite dev server are excluded in production for better performance
- **Optimized rendering** - Uses production templates that load pre-bundled JavaScript and CSS files

The server listens on port 3000 by default.
The server listens on port 3000 by default. Override it with `skybridge start --port <port>` or by setting `PORT`.

When deployed remotely, the MCP Client (ChatGPT, Claude, etc.) connects to your MCP endpoint (e.g., `https://your-domain.com/mcp`) to access your tools and widgets.

Expand All @@ -78,4 +78,3 @@ When deployed remotely, the MCP Client (ChatGPT, Claude, etc.) connects to your
Browse the complete API documentation
</Card>
</CardGroup>

8 changes: 5 additions & 3 deletions docs/quickstart/create-new-app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ This runs the `skybridge` command, which starts a development server with the fo
### What it does

The `skybridge` command:
- **Starts an Express server** on port 3000 that packages:
- **Starts an Express server** on port 3000 by default that packages:
- An MCP endpoint on `/mcp` - the App Backend
- A React application on Vite HMR dev server - the App Frontend
- **Watches for file changes** using nodemon, automatically restarting the server when you modify server-side code
Expand All @@ -91,11 +91,13 @@ The `skybridge` command:

When you run `skybridge`:
1. The server starts and displays the welcome screen in your terminal
2. You can access **DevTools** at `http://localhost:3000/` to test your app locally
3. The **MCP server** is available at `http://localhost:3000/mcp`
2. You can access **DevTools** at `http://localhost:3000/` by default to test your app locally
3. The **MCP server** is available at `http://localhost:3000/mcp` by default
4. **File watching** is enabled - changes to server code will automatically restart the server
5. **Hot Module Reload (HMR)** is active for Widgets components - changes appear instantly in the host without reconnecting

You can change the port with `skybridge dev --port <port>` or by setting `PORT`.

## Next steps

<Card title="Test Your App" icon="flask-vial" href="/quickstart/test-your-app">
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/cli/resolve-port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const isValidPort = (value: number): boolean =>
Number.isInteger(value) && value >= 0 && value <= 65_535;

export const resolvePort = (
flagPort: number | undefined,
envPort: string | undefined,
fallbackPort: number,
): number => {
if (typeof flagPort === "number" && isValidPort(flagPort)) {
return flagPort;
}

if (typeof envPort === "string") {
const parsedPort = Number.parseInt(envPort, 10);

if (isValidPort(parsedPort)) {
return parsedPort;
}
}

if (!isValidPort(fallbackPort)) {
throw new Error(`Invalid fallbackPort value: ${fallbackPort}`);
}

return fallbackPort;
};
18 changes: 15 additions & 3 deletions packages/core/src/commands/dev.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Command, Flags } from "@oclif/core";
import { Box, render, Text } from "ink";
import { Header } from "../cli/header.js";
import { resolvePort } from "../cli/resolve-port.js";
import { useNodemon } from "../cli/use-nodemon.js";
import { useTypeScriptCheck } from "../cli/use-typescript-check.js";

/**
* Development server command.
*/
export default class Dev extends Command {
static override description = "Start development server";
static override examples = ["skybridge"];
static override flags = {
port: Flags.integer({
char: "p",
description: "Port to run the dev server on.",
min: 0,
max: 65535,
}),
"use-forwarded-host": Flags.boolean({
description:
"Uses the forwarded host header to construct widget URLs instead of localhost, useful when accessing the dev server through a tunnel (e.g., ngrok)",
Expand All @@ -16,9 +26,11 @@ export default class Dev extends Command {

public async run(): Promise<void> {
const { flags } = await this.parse(Dev);
const port = resolvePort(flags.port, process.env.PORT, 3000);

const env = {
...process.env,
PORT: String(port),
...(flags["use-forwarded-host"]
? { SKYBRIDGE_USE_FORWARDED_HOST: "true" }
: {}),
Expand All @@ -36,13 +48,13 @@ export default class Dev extends Command {
<Text color="white" bold>
Open DevTools to test your app locally:{" "}
</Text>
<Text color="green">http://localhost:3000/</Text>
<Text color="green">{`http://localhost:${port}/`}</Text>
</Box>
<Box marginBottom={1}>
<Text color="#20a832">→{" "}</Text>
<Text>MCP server running at:{" "}</Text>
<Text color="white" bold>
http://localhost:3000/mcp
{`http://localhost:${port}/mcp`}
</Text>
</Box>
<Text color="white" underline>
Expand All @@ -52,7 +64,7 @@ export default class Dev extends Command {
<Text color="#20a832">→{" "}</Text>
<Text color="grey">Make your local server accessible with </Text>
<Text color="white" bold>
ngrok http 3000
{`ngrok http ${port}`}
</Text>
</Box>
<Box marginBottom={1}>
Expand Down
21 changes: 17 additions & 4 deletions packages/core/src/commands/start.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { Command } from "@oclif/core";
import { Command, Flags } from "@oclif/core";
import { resolvePort } from "../cli/resolve-port.js";
import { runCommand } from "../cli/run-command.js";

/**
* Production server command.
*/
export default class Start extends Command {
static override description = "Start production server";
static override examples = ["skybridge start"];
static override flags = {};
static override flags = {
port: Flags.integer({
char: "p",
description: "Port to run the production server on.",
min: 0,
max: 65535,
}),
};

public async run(): Promise<void> {
console.clear();
const { flags } = await this.parse(Start);
const port = resolvePort(flags.port, process.env.PORT, 3000);

const distPath = resolve(process.cwd(), "dist/index.js");
if (!existsSync(distPath)) {
Expand All @@ -25,12 +38,12 @@ export default class Start extends Command {
`\x1b[36m\x1b[1m⛰ Welcome to Skybridge\x1b[0m \x1b[36mv${this.config.version}\x1b[0m`,
);
console.log(
`Server running at: \x1b[32m\x1b[1mhttp://localhost:3000/mcp\x1b[0m`,
`Server running at: \x1b[32m\x1b[1mhttp://localhost:${port}/mcp\x1b[0m`,
);

await runCommand("node dist/index.js", {
stdio: ["ignore", "inherit", "inherit"],
env: { ...process.env, NODE_ENV: "production" },
env: { ...process.env, NODE_ENV: "production", PORT: String(port) },
});
}
}
4 changes: 3 additions & 1 deletion packages/core/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,8 @@ export class McpServer<
process.env.SKYBRIDGE_USE_FORWARDED_HOST === "true";
const isClaude =
extra?.requestInfo?.headers?.["user-agent"] === "Claude-User";
const parsedPort = Number.parseInt(process.env.PORT ?? "", 10);
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;

const hostFromHeaders =
extra?.requestInfo?.headers?.["x-forwarded-host"] ??
Expand All @@ -407,7 +409,7 @@ export class McpServer<

const serverUrl = useExternalHost
? `https://${hostFromHeaders}`
: "http://localhost:3000";
: `http://localhost:${port}`;

const html = isProduction
? templateHelper.renderProduction({
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/server/widgetsDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { assetBaseUrlTransformPlugin } from "./asset-base-url-transform-plugin.j
*/
export const widgetsDevServer = async (): Promise<Router> => {
const router = express.Router();
const parsedPort = Number.parseInt(process.env.PORT ?? "", 10);
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;

const { createServer, searchForWorkspaceRoot, loadConfigFromFile } =
await import("vite");
Expand Down Expand Up @@ -64,7 +66,9 @@ export const widgetsDevServer = async (): Promise<Router> => {
},
plugins: [
...userPlugins,
assetBaseUrlTransformPlugin({ devServerOrigin: "http://localhost:3000" }),
assetBaseUrlTransformPlugin({
devServerOrigin: `http://localhost:${port}`,
}),
],
});

Expand Down
5 changes: 4 additions & 1 deletion packages/create-skybridge/template/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ app.use(express.json());

app.use(mcp(server));

const parsedPort = Number.parseInt(process.env.PORT ?? "", 10);
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;

const env = process.env.NODE_ENV || "development";

if (env !== "production") {
Expand All @@ -29,7 +32,7 @@ if (env === "production") {
app.use("/assets", express.static(path.join(__dirname, "assets")));
}

app.listen(3000, (error) => {
app.listen(port, (error) => {
if (error) {
console.error("Failed to start server:", error);
process.exit(1);
Expand Down
12 changes: 11 additions & 1 deletion packages/devtools/src/lib/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ import { McpClient } from "./client.js";

const client = new McpClient();

client.connect("http://localhost:3000/mcp").then(() => {
const resolveMcpServerUrl = (): string => {
if (typeof window !== "undefined") {
return `${window.location.origin}/mcp`;
}

const parsedPort = Number.parseInt(process.env.PORT ?? "", 10);
const port = Number.isFinite(parsedPort) ? parsedPort : 3000;
return `http://localhost:${port}/mcp`;
};

client.connect(resolveMcpServerUrl()).then(() => {
console.info("Connected to MCP server");
});

Expand Down