Skip to main content
Glama

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
NameRequiredDescriptionDefault
nameYesApp name (used as project name and default subdomain)
tierNoDatabase tier: prototype ($0.10/7d), hobby ($5/30d), team ($20/30d)prototype
migrationsNoSQL migrations to run after provisioning (CREATE TABLE statements, etc.)
rlsNoRLS configuration to apply after migrations
secretsNoSecrets to set (e.g. [{key: 'STRIPE_SECRET_KEY', value: 'sk_...'}])
functionsNoFunctions to deploy
siteNoStatic site files to deploy (must include index.html)
subdomainNoCustom subdomain to claim (e.g. 'myapp' → myapp.run402.com)

Implementation Reference

  • 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") }] };
    }
  • 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),
    );
  • 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 };
    }
  • 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);
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kychee-com/run402'

If you have feedback or need assistance with the MCP directory API, please join our Discord server