deploy_function
InactiveTool of io.github.butterbase-ai/mcp
Deploy or update a serverless function with custom business logic. Example: Input: { app_id: "app_abc123", name: "send-welcome-email", code: "export async function handler(req, ctx) { ... }", trigger: { type: "http", config: { method: "POST", path: "/welcome", auth: "required" } } } Output: { function_id: "fn_xyz789", name: "send-welcome-email", url: "https://api.butterbase.ai/v1/app_abc123/fn/send-welcome-email", status: "deployed" } Function signature: export async function handler(request: Request, context: { db: PostgresClient, // Query your app database env: Record<string, string>, // Access envVars user: { id: string } | null, // Current user (if auth: required) waitUntil: (promise: Promise) => void, // Keep alive for background work after response idempotency: { // Webhook / event dedup primitive claim: (key: string, opts?: { scope?: string; ttlSeconds?: number }) => Promise<boolean> } }): Promise<Response> Console output: console.log(), console.info(), console.warn(), console.error(), and console.debug() calls are captured and stored with invocation logs. View them via manage_function (action: "get_logs"). IMPORTANT: Handlers MUST return a Response object (Web API standard). Do NOT return plain objects like { status: 200, body: "..." }. Idempotent webhook handlers with ctx.idempotency.claim(): Third-party webhook providers (Stripe, Telegram, GitHub, Slack, Twilio, Discord) retry delivery on non-2xx responses with the same event id. Use ctx.idempotency.claim() to atomically dedupe — it returns true if you're the first to see this key, false if another invocation already claimed it. export async function handler(req, ctx) { const event = await req.json(); if (!(await ctx.idempotency.claim(event.id))) { // Already processed — ack the retry without re-doing work. return new Response('duplicate', { status: 200 }); } await processEvent(event); return new Response('ok', { status: 200 }); } Options: - scope: 'stripe' | 'telegram' | ... (default: 'default'). Namespace claims so keys from different providers can never collide. - ttlSeconds: mark the claim with an expiry. Cleanup is your responsibility: DELETE FROM _idempotency_keys WHERE expires_at < now(); Background work with ctx.waitUntil(): Use ctx.waitUntil(promise) to keep the function alive after the response is sent. This is useful for fire-and-forget tasks like sending emails or logging. Background work has a 30-second timeout. ctx.db is available inside waitUntil promises. export async function handler(req, ctx) { ctx.waitUntil(fetch("https://api.email.com/send", { method: "POST", body: "..." })); return new Response(JSON.stringify({ accepted: true }), { headers: { "Content-Type": "application/json" } }); } Example: export async function handler(req, ctx) { const data = { hello: "world" }; return new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json" } }); } Row-Level Security in Functions: Functions respect RLS policies based on how they're invoked: - Invoked with end-user JWT → butterbase_user role (RLS enforced) * ctx.db queries see only the user's data * ctx.user.id contains the authenticated user ID * Use case: User-facing operations - Invoked with platform API key → butterbase_service role (RLS bypassed) * ctx.db queries see all data * ctx.user is null * Use case: Admin operations, background jobs - Invoked by cron trigger → butterbase_service role (RLS bypassed) * ctx.db queries see all data * ctx.user is null * Use case: Scheduled tasks, cleanup jobs Trigger types: - http: Invoke via HTTP request (GET, POST, etc) - cron: Schedule periodic execution (e.g., "0 9 * * *" = daily at 9am) - websocket: Trigger on WebSocket event from client via realtime connection - s3_upload: Trigger on file upload [not yet implemented] - webhook: Receive webhooks from external services [not yet implemented] Common errors: - VALIDATION_INVALID_SCHEMA: Check code exports a handler function - RESOURCE_NOT_FOUND: App doesn't exist - Syntax error: Code must be valid TypeScript/JavaScript Idempotency: Safe to call multiple times (updates existing function with same name). Next steps: Use invoke_function to test, then manage_function (action: "get_logs") to debug.
Parameters schema
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"app_id",
"name",
"code"
],
"properties": {
"code": {
"type": "string",
"description": "TypeScript/JavaScript code that exports a handler function"
},
"name": {
"type": "string",
"description": "Function name (alphanumeric, hyphens, underscores)"
},
"app_id": {
"type": "string",
"description": "The app ID to deploy the function to"
},
"envVars": {
"type": "object",
"description": "Environment variables (will be encrypted)",
"additionalProperties": {
"type": "string"
}
},
"trigger": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"enum": [
"http",
"cron",
"s3_upload",
"webhook",
"websocket"
],
"type": "string",
"description": "Trigger type"
},
"config": {
"description": "Trigger-specific config (see triggers param for shape)."
}
},
"description": "Single-trigger shorthand. Prefer `triggers` for new code; this is kept for back-compat.",
"additionalProperties": false
},
"triggers": {
"type": "array",
"items": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"enum": [
"http",
"cron",
"s3_upload",
"webhook",
"websocket"
],
"type": "string",
"description": "Trigger type: http, cron, s3_upload, webhook, or websocket"
},
"config": {
"description": "Trigger-specific configuration:\n- http: { method?: string, path?: string, auth?: 'required'|'optional'|'none' }\n * auth defaults to 'required' for new HTTP functions — anonymous callers get 401 at the edge.\n * Set auth: 'none' ONLY for intentionally public endpoints. Inside such a function, ctx.db runs\n as butterbase_service (RLS bypassed), so guard every DB access manually or hand-check ctx.user.\n- cron: { schedule: string (cron expression like \"0 9 * * *\"), timezone?: string (default: UTC) }\n- s3_upload: { bucket: string, prefix?: string, contentTypes?: string[] }\n- webhook: { secret_required?: boolean, allowed_sources?: string }\n- websocket: {} Trigger on incoming WebSocket frames"
},
"enabled": {
"type": "boolean",
"description": "Default true."
}
},
"additionalProperties": false
},
"minItems": 1,
"description": "Canonical multi-trigger array. At most one trigger per type."
},
"timeoutMs": {
"type": "number",
"description": "Execution timeout in milliseconds (default: 30000)"
},
"agent_tool": {
"type": "boolean",
"description": "When true, this function is exposed to agents in this app as a tool. Agents must still list its name under their graph_spec.tools.functions[] to call it."
},
"description": {
"type": "string",
"description": "Function description"
},
"memoryLimitMb": {
"type": "number",
"description": "Memory limit in MB (default: 128)"
},
"agent_tool_mode": {
"enum": [
"read_only",
"read_write"
],
"type": "string",
"description": "read_only (default) | read_write. read_write tools require human approval per call (HITL)."
},
"agent_tool_exposed_to": {
"enum": [
"developer_only",
"end_user"
],
"type": "string",
"description": "developer_only (default) — only dashboard/CLI test runs can call it. end_user — also callable by public agent invocations."
},
"agent_tool_description": {
"type": "string",
"maxLength": 500,
"description": "Short imperative description shown to the LLM (max 500 chars). Required for any function the model is expected to choose intelligently."
},
"allow_service_key_impersonation": {
"type": "boolean",
"description": "Default true. Lets an app-scoped service-key caller assert \"act as user X\" via the X-Butterbase-As-User header — the runtime populates ctx.user with the asserted id before invoking. Set to false on admin-only or billing-webhook handlers that must never accept an as-user assertion: control-api will 403 such calls at the edge."
}
},
"additionalProperties": false
}Parent server
io.github.butterbase-ai/mcp
https://github.com/butterbase-ai/butterbase-oss
1/7 registries