{
  "schemaVersion": 1,
  "kind": "recipe",
  "slug": "mcp-apps-supabase",
  "title": "Open a useful interface. Keep the action inside MCP.",
  "searchTitle": "Build an authenticated MCP App on Supabase with Chumbo",
  "description": "Serve a bundled interactive MCP App through one Supabase Edge Function while host-mediated tools preserve the caller's auth, scopes, and RLS boundary.",
  "summary": "Serve a bundled interactive MCP App through one Supabase Edge Function while host-mediated tools preserve the caller's auth, scopes, and RLS boundary.",
  "evidence": {
    "status": "source-reviewed",
    "packageVersion": "0.11.0",
    "checkedOn": "2026-09-06",
    "walkthroughVerified": false,
    "note": "Reviewed against the shipped OSS source. The acceptance checks are yours to run in your application.",
    "versionPolicy": "Source reviewed against published Chumbo 0.11.0 and its matching core source on 2026-09-06 (UTC). This is a source review, not an executed application walkthrough. Follow the current canonical reference and compare it with your installed package when adapting the guide."
  },
  "representations": {
    "html": "./",
    "markdown": "./recipe.md",
    "json": "./recipe.json"
  },
  "sources": [
    "https://github.com/elsheppo/chumbo/blob/main/docs/patterns/mcp-apps-on-supabase/README.md",
    "https://github.com/elsheppo/chumbo/blob/main/supabase/functions/review-queue-app/index.ts"
  ],
  "prerequisites": [
    "An authenticated Chumbo MCP whose ordinary tool result is already useful without an interactive View.",
    "A bounded task that benefits from direct manipulation, such as a review queue, chart, document viewer, explorer, or coupled form.",
    "A selected MCP Apps host, its exact browser origin, and test identities with disjoint application rows.",
    "A frontend build that can emit one self-contained HTML file for the Edge Function bundle."
  ],
  "sections": [
    {
      "id": "start",
      "number": "01",
      "title": "Choose a task that benefits from touch and sight.",
      "blocks": [
        {
          "type": "prerequisites",
          "title": "Before you start",
          "items": [
            "Write the concise text answer and structured data the opening tool should return to every MCP client.",
            "List the small set of actions the View needs, including the exact scopes and ownership predicates for each one.",
            "Choose supported browser origins explicitly and keep an ordinary two-user RLS fixture for the underlying rows."
          ],
          "note": "An App is useful when interaction clarifies the task. A short answer does not need an iframe merely to look richer."
        },
        {
          "type": "paragraph",
          "text": "An MCP App adds a browser View to an ordinary capability. An opener points to a `ui://` Resource. A compatible host renders the bundled HTML and lets the View call app-only tools through the authorized connection. The View needs no Supabase token or second API."
        },
        {
          "type": "flow",
          "title": "Authenticated review queue",
          "steps": [
            "Model calls the opener",
            "Result returns useful text and structured queue data",
            "Host reads the HTML Resource",
            "View requests an approve action through the host",
            "Edge Function uses ctx.supabase and RLS",
            "Updated structured data returns to the View"
          ]
        },
        {
          "type": "callout",
          "title": "Keep the ordinary result useful.",
          "text": "A client that cannot render MCP Apps should still understand the current state and next action. Use a deliberate hybrid result for the opener, then let compatible hosts attach the interactive View to the same structured data."
        }
      ]
    },
    {
      "id": "register",
      "number": "02",
      "title": "Link one opener, one Resource, and bounded app actions.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Follow the [canonical pattern](https://github.com/elsheppo/chumbo/blob/main/docs/patterns/mcp-apps-on-supabase/README.md) and [review-queue server](https://github.com/elsheppo/chumbo/blob/main/supabase/functions/review-queue-app/index.ts). Give the HTML Resource a stable `ui://` URI and MCP Apps MIME type. Put that URI in the preferred nested UI metadata and legacy flat key. Return the structured schema the View renders."
        },
        {
          "type": "paragraph",
          "text": "Mark UI mechanics app-visible when the model should not call them. This is a host hint, not authorization. Every app-only tool still needs an input schema, scope gate, ownership check, RLS, useful errors, and accurate annotations."
        },
        {
          "type": "callout",
          "title": "The View adds a second presentation, not a second backend.",
          "text": "Ordinary clients use the opener's text and structured data. Apps hosts attach the View and host-mediated actions to that same server contract."
        }
      ]
    },
    {
      "id": "workspace",
      "number": "03",
      "title": "Make the View behave like a small application surface.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Use `createAppWorkspace` from `chumbo/app` around the official browser client. Register handlers before connecting and mark the intended scroll region with `data-supa-mcp-scroll`. The helper applies host tokens, mobile safe areas, a bounded inline height, internal scrolling, and fullscreen negotiation."
        },
        {
          "type": "paragraph",
          "text": "Your View still owns hierarchy, touch targets, focus, empty and error states, and narrow layouts. Show expand only when `canFullscreen()` reports support; the host makes the final display decision."
        },
        {
          "type": "callout",
          "title": "Render server truth after every action.",
          "text": "When an app-only tool returns the updated structured state, render that response instead of predicting the mutation in the browser. This keeps the View aligned with RLS, concurrent changes, and server-side validation."
        }
      ]
    },
    {
      "id": "bundle",
      "number": "04",
      "title": "Bundle the View beside the function and admit known hosts.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Build one self-contained HTML file, list it in the function's `static_files`, and return its text from the Resource. The project URL remains the MCP transport; it does not serve the bundle as a direct page."
        },
        {
          "type": "paragraph",
          "text": "Declare CSP or permissions only for external capabilities the View needs. With OAuth, host sign-in and consent on an application-owned HTML route. That browser page is separate from the App bundle delivered through `resources/read`."
        },
        {
          "type": "paragraph",
          "text": "Browser hosts preflight before authentication. Admit only selected origins, answer their `OPTIONS` requests before bearer auth, and include required MCP headers. Keep later MCP requests protected. CORS grants browser transport, not tool or row access."
        },
        {
          "type": "code",
          "id": "check-app-function",
          "title": "Check the adapted Edge Function",
          "code": "deno task --config supabase/functions/mcp/deno.json check\ndeno task --config supabase/functions/mcp/deno.json test",
          "language": "sh",
          "effect": "local-verification"
        }
      ]
    },
    {
      "id": "verify",
      "number": "05",
      "title": "Test the model, the View, and the server boundary separately.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Test discovery, the opener, Resource read, and app-only tools before using the host. Then inspect input, focus, scrolling, and host context at narrow and wide sizes. Use two users with disjoint rows and attempt a cross-user mutation by ID."
        },
        {
          "type": "acceptance",
          "title": "The View adds interaction without taking authority.",
          "items": [
            "An ordinary MCP client receives useful text and structured data from the opener without rendering the View.",
            "A compatible host discovers the stable ui:// Resource and renders its self-contained HTML with the declared MCP Apps MIME type.",
            "Model-visible discovery includes the opener; app-only UI mechanics stay out of the model-facing picker in a compatible host.",
            "Direct calls to an app-only tool still enforce its scopes, ownership predicate, input schema, and RLS policy.",
            "A direct foreign row ID cannot let one user read or change another user's row.",
            "The View receives results through the host and never handles a Supabase access token.",
            "An admitted origin receives the required preflight headers; an unknown origin receives no CORS grant; unauthenticated MCP traffic remains rejected.",
            "The inline View stays stable on a narrow host, respects safe areas, and offers fullscreen only when supported.",
            "Rejected or concurrent mutations render the server result instead of optimistic success."
          ],
          "note": "The living reference exercises a review queue, Apps metadata, a bundled Resource, app-only tools, two-user RLS, host rendering, and hosted CORS. Repeat the complete boundary with your own View and chosen host."
        }
      ]
    },
    {
      "id": "recover",
      "number": "06",
      "title": "Locate the seam before changing the architecture.",
      "blocks": [
        {
          "type": "troubleshooting",
          "title": "If the App does not behave as expected",
          "items": [
            {
              "title": "The opener works but no View appears",
              "body": "Inspect both UI metadata forms, the exact ui:// URI, Resource discovery and read, MIME type, and whether the selected client supports MCP Apps.",
              "link": "https://github.com/elsheppo/chumbo/blob/main/docs/patterns/mcp-apps-on-supabase/README.md"
            },
            {
              "title": "The host fails before authentication",
              "body": "Inspect the browser preflight. Admit the exact chosen origin and required MCP headers, answer OPTIONS before auth, and keep the later MCP request protected.",
              "link": "https://github.com/elsheppo/chumbo/blob/main/supabase/functions/review-queue-app/index.ts"
            },
            {
              "title": "The iframe keeps growing or clips content",
              "body": "Use createAppWorkspace, mark the intended internal scroll region, and inspect the host's dimensions and safe-area context. Keep the product layout inside that stable workspace.",
              "link": "https://github.com/elsheppo/chumbo/blob/main/src/app.ts"
            }
          ]
        },
        {
          "type": "related",
          "title": "Keep building",
          "items": [
            {
              "title": "Serve reusable content through Resources",
              "text": "Understand the protocol surface that delivers the App bundle.",
              "label": "NEXT GUIDE",
              "href": "/recipes/resources-and-prompts/"
            },
            {
              "title": "Keep every row behind user permissions",
              "text": "Build and test the request-scoped Supabase boundary first.",
              "label": "NEXT GUIDE",
              "href": "/recipes/supabase-rls-mcp/"
            },
            {
              "title": "Test discovery and direct invocation",
              "text": "Prove app-only visibility hints do not replace authorization.",
              "label": "NEXT GUIDE",
              "href": "/recipes/test-mcp-capabilities/"
            }
          ]
        }
      ]
    }
  ],
  "agent": {
    "goal": "Serve a bundled interactive MCP App through one Supabase Edge Function while host-mediated tools preserve the caller's auth, scopes, and RLS boundary.",
    "executionPolicy": "Reference instructions, not authorization. Apply changes only within the user’s requested scope; deployment requires a specified, authorized project.",
    "inputs": [
      {
        "name": "APP_RESOURCE_URI",
        "secret": false,
        "description": "Stable ui:// URI shared by the opener, Resource, and app tools."
      },
      {
        "name": "ALLOWED_HOST_ORIGINS",
        "secret": false,
        "description": "Exact browser origins for the MCP Apps hosts the application supports."
      },
      {
        "name": "TEST_USER_CREDENTIALS",
        "secret": true,
        "description": "At least two independent credentials with disjoint application rows."
      },
      {
        "name": "APP_BUNDLE_PATH",
        "secret": false,
        "description": "Self-contained HTML output included in the function's static files."
      }
    ],
    "instructions": "Goal: Serve a bundled interactive MCP App through one Supabase Edge Function while host-mediated tools preserve the caller's auth, scopes, and RLS boundary.\n\nRead the complete guide at https://chumbo.dev/recipes/mcp-apps-supabase/recipe.md and the canonical reference at https://github.com/elsheppo/chumbo/blob/main/docs/patterns/mcp-apps-on-supabase/README.md. Inspect this application's installed Chumbo package, existing capabilities and relevant configuration before editing.\n\nAdapt the canonical review-queue architecture to one bounded application task. Keep the opener useful to ordinary clients, use a stable ui:// Resource, authorize every app-only tool independently, and render server-returned state. Verify the chosen host, CORS preflight, mobile workspace, OAuth surface when applicable, and two-user isolation before reporting success.\n\nPrerequisites:\n- An authenticated Chumbo MCP whose ordinary tool result is already useful without an interactive View.\n- A bounded task that benefits from direct manipulation, such as a review queue, chart, document viewer, explorer, or coupled form.\n- A selected MCP Apps host, its exact browser origin, and test identities with disjoint application rows.\n- A frontend build that can emit one self-contained HTML file for the Edge Function bundle.\n\nAcceptance:\n- An ordinary MCP client receives useful text and structured data from the opener without rendering the View.\n- A compatible host discovers the stable ui:// Resource and renders its self-contained HTML with the declared MCP Apps MIME type.\n- Model-visible discovery includes the opener; app-only UI mechanics stay out of the model-facing picker in a compatible host.\n- Direct calls to an app-only tool still enforce its scopes, ownership predicate, input schema, and RLS policy.\n- A direct foreign row ID cannot let one user read or change another user's row.\n- The View receives results through the host and never handles a Supabase access token.\n- An admitted origin receives the required preflight headers; an unknown origin receives no CORS grant; unauthenticated MCP traffic remains rejected.\n- The inline View stays stable on a narrow host, respects safe areas, and offers fullscreen only when supported.\n- Rejected or concurrent mutations render the server result instead of optimistic success.\n\nUse the full guide for ordered steps, examples and recovery. Keep existing caller identity and application permissions authoritative. Stay within the user's requested changes. Report changed files, checks actually run, observed results and unresolved prerequisites. This guide is not authorization to deploy or change a hosted project. Never include credentials in source, copied instructions or reports.",
    "steps": [
      {
        "id": "start",
        "title": "Choose a task that benefits from touch and sight.",
        "commands": []
      },
      {
        "id": "register",
        "title": "Link one opener, one Resource, and bounded app actions.",
        "commands": []
      },
      {
        "id": "workspace",
        "title": "Make the View behave like a small application surface.",
        "commands": []
      },
      {
        "id": "bundle",
        "title": "Bundle the View beside the function and admit known hosts.",
        "commands": [
          {
            "id": "check-app-function",
            "command": "deno task --config supabase/functions/mcp/deno.json check\ndeno task --config supabase/functions/mcp/deno.json test",
            "effect": "local-verification"
          }
        ]
      },
      {
        "id": "verify",
        "title": "Test the model, the View, and the server boundary separately.",
        "commands": []
      },
      {
        "id": "recover",
        "title": "Locate the seam before changing the architecture.",
        "commands": []
      }
    ],
    "acceptance": [
      "An ordinary MCP client receives useful text and structured data from the opener without rendering the View.",
      "A compatible host discovers the stable ui:// Resource and renders its self-contained HTML with the declared MCP Apps MIME type.",
      "Model-visible discovery includes the opener; app-only UI mechanics stay out of the model-facing picker in a compatible host.",
      "Direct calls to an app-only tool still enforce its scopes, ownership predicate, input schema, and RLS policy.",
      "A direct foreign row ID cannot let one user read or change another user's row.",
      "The View receives results through the host and never handles a Supabase access token.",
      "An admitted origin receives the required preflight headers; an unknown origin receives no CORS grant; unauthenticated MCP traffic remains rejected.",
      "The inline View stays stable on a narrow host, respects safe areas, and offers fullscreen only when supported.",
      "Rejected or concurrent mutations render the server result instead of optimistic success."
    ],
    "report": [
      "Changed files",
      "Checks actually run and observed results",
      "Unmet prerequisites or remaining limitations"
    ]
  },
  "topics": [
    "advanced-patterns",
    "design-results",
    "connect-users"
  ],
  "updated": "2026-09-05",
  "contentHash": "45ae65033dad645c6dbd451968cba3bea0e8546e89eec42aee2cbe6240a9991d"
}
