bundle_deploy
Deploy full-stack applications by provisioning databases, running migrations, applying security policies, setting secrets, deploying functions, and hosting static sites in a single transaction.
Instructions
One-call full-stack app deployment. Provisions a database and optionally runs migrations, applies RLS, sets secrets, deploys functions, deploys a static site, and claims a subdomain — all in a single x402 payment.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | App name (used as project name and default subdomain) | |
| tier | No | Database tier: prototype ($0.10/7d), hobby ($5/30d), team ($20/30d) | prototype |
| migrations | No | SQL migrations to run after provisioning (CREATE TABLE statements, etc.) | |
| rls | No | RLS configuration to apply after migrations | |
| secrets | No | Secrets to set (e.g. [{key: 'STRIPE_SECRET_KEY', value: 'sk_...'}]) | |
| functions | No | Functions to deploy | |
| site | No | Static site files to deploy (must include index.html) | |
| subdomain | No | Custom subdomain to claim (e.g. 'myapp' → myapp.run402.com) |
Implementation Reference
- src/tools/bundle-deploy.ts:63-167 (handler)The handleBundleDeploy async function executes the bundle_deploy tool logic. It takes deployment configuration (name, tier, migrations, rls, secrets, functions, site, subdomain), makes a POST request to /v1/deploy/{tier}, handles x402 payment responses (HTTP 402), saves project credentials to the local keystore on success, and returns a formatted markdown result with deployment details.
export async function handleBundleDeploy(args: { name: string; tier?: string; migrations?: string; rls?: { template: string; tables: Array<{ table: string; owner_column?: string }> }; secrets?: Array<{ key: string; value: string }>; functions?: Array<{ name: string; code: string; config?: { timeout?: number; memory?: number } }>; site?: Array<{ file: string; data: string; encoding?: string }>; subdomain?: string; }): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> { const tier = args.tier || "prototype"; const res = await apiRequest(`/v1/deploy/${tier}`, { method: "POST", body: { name: args.name, migrations: args.migrations, rls: args.rls, secrets: args.secrets, functions: args.functions, site: args.site, subdomain: args.subdomain, }, }); if (res.is402) { const body = res.body as Record<string, unknown>; const lines = [ `## Payment Required`, ``, `To bundle-deploy **${args.name}** (tier: **${tier}**), an x402 payment is needed.`, ``, ]; if (body.x402) { lines.push(`**Payment details:**`); lines.push("```json"); lines.push(JSON.stringify(body.x402, null, 2)); lines.push("```"); } else { lines.push(`**Server response:**`); lines.push("```json"); lines.push(JSON.stringify(body, null, 2)); lines.push("```"); } lines.push(``); lines.push( `The user's wallet or payment agent must send the required amount. ` + `Once payment is confirmed, retry this tool call.`, ); return { content: [{ type: "text", text: lines.join("\n") }] }; } if (!res.ok) return formatApiError(res, "deploying bundle"); const body = res.body as { project_id: string; anon_key: string; service_key: string; schema_slot: string; tier: string; lease_expires_at: string; site_url?: string; subdomain_url?: string; functions?: Array<{ name: string; url: string }>; }; // Save credentials to local key store saveProject(body.project_id, { anon_key: body.anon_key, service_key: body.service_key, tier: body.tier, expires_at: body.lease_expires_at, }); const lines = [ `## Bundle Deployed: ${args.name}`, ``, `| Field | Value |`, `|-------|-------|`, `| project_id | \`${body.project_id}\` |`, `| tier | ${body.tier} |`, `| schema | ${body.schema_slot} |`, `| expires | ${body.lease_expires_at} |`, ]; if (body.site_url) { lines.push(`| site | ${body.site_url} |`); } if (body.subdomain_url) { lines.push(`| subdomain | ${body.subdomain_url} |`); } if (body.functions && body.functions.length > 0) { lines.push(``); lines.push(`**Functions:**`); for (const fn of body.functions) { lines.push(`- \`${fn.name}\` → ${fn.url}`); } } lines.push(``); lines.push(`Keys saved to local key store.`); return { content: [{ type: "text", text: lines.join("\n") }] }; } - src/tools/bundle-deploy.ts:6-61 (schema)The bundleDeploySchema defines input validation using Zod. It specifies the structure for a full-stack deployment including: name (string, required), tier (enum: prototype/hobby/team, defaults to prototype), migrations (optional SQL), rls configuration (optional RLS template and tables), secrets (array of key-value pairs), functions (array with name, code, and optional config), site files (array with file, data, encoding), and subdomain (optional custom subdomain).
export const bundleDeploySchema = { name: z.string().describe("App name (used as project name and default subdomain)"), tier: z .enum(["prototype", "hobby", "team"]) .default("prototype") .describe("Database tier: prototype ($0.10/7d), hobby ($5/30d), team ($20/30d)"), migrations: z .string() .optional() .describe("SQL migrations to run after provisioning (CREATE TABLE statements, etc.)"), rls: z .object({ template: z.enum(["user_owns_rows", "public_read", "public_read_write"]), tables: z.array( z.object({ table: z.string(), owner_column: z.string().optional(), }), ), }) .optional() .describe("RLS configuration to apply after migrations"), secrets: z .array(z.object({ key: z.string(), value: z.string() })) .optional() .describe("Secrets to set (e.g. [{key: 'STRIPE_SECRET_KEY', value: 'sk_...'}])"), functions: z .array( z.object({ name: z.string(), code: z.string(), config: z .object({ timeout: z.number().optional(), memory: z.number().optional(), }) .optional(), }), ) .optional() .describe("Functions to deploy"), site: z .array( z.object({ file: z.string(), data: z.string(), encoding: z.enum(["utf-8", "base64"]).optional(), }), ) .optional() .describe("Static site files to deploy (must include index.html)"), subdomain: z .string() .optional() .describe("Custom subdomain to claim (e.g. 'myapp' → myapp.run402.com)"), }; - src/index.ts:229-234 (registration)Registration of the bundle_deploy tool with the MCP server. Imports handleBundleDeploy and bundleDeploySchema from ./tools/bundle-deploy.js and registers with server.tool() using the name 'bundle_deploy', a description explaining it's a one-call full-stack deployment, the schema, and the handler function.
server.tool( "bundle_deploy", "One-call full-stack app deployment. Provisions a database and optionally runs migrations, applies RLS, sets secrets, deploys functions, deploys a static site, and claims a subdomain — all in a single x402 payment.", bundleDeploySchema, async (args) => handleBundleDeploy(args), ); - src/client.ts:18-63 (helper)The apiRequest helper function used by handleBundleDeploy to make HTTP requests to the Run402 API. It handles JSON serialization, network errors, and importantly detects HTTP 402 (Payment Required) responses which are returned with is402: true for x402 payment flows.
export async function apiRequest( path: string, opts: ApiRequestOptions = {}, ): Promise<ApiResponse> { const { method = "GET", headers = {}, body, rawBody } = opts; const url = `${getApiBase()}${path}`; const fetchHeaders: Record<string, string> = { ...headers }; let fetchBody: string | undefined; if (rawBody !== undefined) { fetchBody = rawBody; } else if (body !== undefined) { fetchHeaders["Content-Type"] = fetchHeaders["Content-Type"] || "application/json"; fetchBody = JSON.stringify(body); } let res: Response; try { res = await fetch(url, { method, headers: fetchHeaders, body: fetchBody, }); } catch (err) { return { ok: false, status: 0, body: { error: `Network error: ${(err as Error).message}` }, }; } let resBody: unknown; const contentType = res.headers.get("content-type") || ""; if (contentType.includes("application/json")) { resBody = await res.json(); } else { resBody = await res.text(); } if (res.status === 402) { return { ok: false, is402: true, status: 402, body: resBody }; } return { ok: res.ok, status: res.status, body: resBody }; } - src/keystore.ts:50-59 (helper)The saveProject helper function persists project credentials (anon_key, service_key, tier, expires_at) to the local keystore file (~/.config/run402/projects.json). Called by handleBundleDeploy after successful deployment to save the new project's credentials for future use.
export function saveProject( projectId: string, project: StoredProject, path?: string, ): void { const p = path ?? getKeystorePath(); const store = loadKeyStore(p); store.projects[projectId] = project; saveKeyStore(store, p); }