{
  "schemaVersion": 1,
  "kind": "recipe",
  "slug": "supabase-rls-mcp",
  "title": "Let agents into your app. Keep your permissions.",
  "searchTitle": "Build a Supabase MCP server with your existing RLS policies",
  "description": "Let your users’ agents work with their projects. Chumbo connects the MCP layer to Supabase Auth, so your existing row-level security stays in charge.",
  "summary": "Add one useful, authenticated tool to your Supabase app, prove it locally, then deploy the same Edge Function.",
  "evidence": {
    "status": "contract-tested",
    "check": "rls-projects",
    "packageVersion": "0.11.3",
    "checkedOn": "2026-09-18",
    "walkthroughVerified": false,
    "note": "Example typechecked and exercised with simulated query results. Run the two-user checks below against your own Supabase app; these checks do not prove your RLS policies.",
    "versionPolicy": "Example checked with Chumbo 0.11.3 on 2026-09-18 (UTC). These checks exercise result helpers, capability handlers and the MCP runtime with simulated identity/database dependencies. They do not prove your application’s real auth, RLS or deployment. Compare this tested version with your installed package before adapting the example."
  },
  "representations": {
    "html": "./",
    "markdown": "./recipe.md",
    "json": "./recipe.json"
  },
  "sources": [
    "https://github.com/elsheppo/chumbo/tree/main/docs/patterns/authenticated-tools",
    "https://github.com/elsheppo/chumbo/tree/main/supabase/functions/authenticated-tools"
  ],
  "prerequisites": [
    "An existing Supabase app with supabase/config.toml.",
    "A projects table with id, name, and owner_id columns, appropriate grants, and tested owner-based SELECT policies.",
    "Node 22+, Supabase CLI, Deno, and Docker for the local stack.",
    "Two local test users, each with a project and a valid Supabase access token."
  ],
  "sections": [
    {
      "id": "the-result",
      "number": "01",
      "title": "One tool. Each user's projects.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Your users already have projects in your app. Give their agents a way to find them, using the same identity and access rules. Choose a caller below to see the intended behavior."
        },
        {
          "type": "permissions",
          "title": "Same tool, different caller",
          "tool": "list_projects",
          "request": "Show me my projects.",
          "evidence": "Illustrative data for two example callers.",
          "rule": "Each caller sees only their own projects. Alice cannot see Bob’s projects; Bob cannot see Alice’s projects. The user’s token travels with the query. Postgres decides which rows come back.",
          "cases": [
            {
              "id": "alice",
              "name": "Alice",
              "role": "Product designer",
              "projects": [
                "Website refresh",
                "Design system"
              ]
            },
            {
              "id": "bob",
              "name": "Bob",
              "role": "Developer",
              "projects": [
                "API integration",
                "Release checklist"
              ]
            }
          ]
        },
        {
          "type": "flow",
          "title": "Request flow",
          "steps": [
            "Your user’s agent",
            "Chumbo MCP",
            "Supabase + RLS"
          ]
        },
        {
          "type": "paragraph",
          "text": "Chumbo creates a fresh Supabase client for each caller. In bearer and OAuth modes, that client carries the connected user’s access token. Your database grants and RLS policies govern the query."
        }
      ]
    },
    {
      "id": "start",
      "number": "02",
      "title": "Start with your existing app.",
      "blocks": [
        {
          "type": "prerequisites",
          "title": "Before you start",
          "items": [
            "An existing Supabase app with supabase/config.toml.",
            "A projects table with id, name, and owner_id columns, appropriate grants, and tested owner-based SELECT policies.",
            "Node 22+, Supabase CLI, Deno, and Docker for the local stack.",
            "Two local test users, each with a project and a valid Supabase access token."
          ],
          "note": "Already have a Chumbo MCP? Keep its auth configuration and add the capability to your existing registration."
        },
        {
          "type": "paragraph",
          "text": "From your app’s repository, generate one Edge Function. This recipe uses bearer auth to make a local user-token test explicit. The installed skill gives your coding agent the matching Chumbo instructions."
        },
        {
          "type": "code",
          "id": "setup-code",
          "title": "In your app’s repository",
          "code": "npx chumbo setup --auth bearer\nnpx chumbo skill install",
          "language": "sh"
        },
        {
          "type": "callout",
          "title": "Start small. Keep your app.",
          "text": "Setup previews its file changes and preserves existing application-authored capabilities. You supply the operation; Chumbo supplies the MCP connection around it."
        }
      ]
    },
    {
      "id": "capability",
      "number": "03",
      "title": "Give your app a useful capability.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Replace the generated `whoami` starter with `list_projects`. This example assumes your existing `projects` table and its owner-based policies. Adapt the query to the operation your app already exposes."
        },
        {
          "type": "code",
          "id": "capability-code",
          "title": "supabase/functions/mcp/capabilities.ts",
          "code": "import {\n  errorResult, textResult,\n  type SupabaseMcpContext,\n  type SupabaseMcpServer,\n} from \"chumbo\";\nimport { z } from \"zod\";\n\nexport function registerCapabilities(\n  server: SupabaseMcpServer,\n  ctx: SupabaseMcpContext,\n) {\n  server.registerTool(\"list_projects\", {\n    description: \"Show up to 10 of your projects in ID order.\",\n    inputSchema: z.object({}),\n    annotations: { readOnlyHint: true },\n  }, async () => {\n    const { data, error } = await ctx.supabase\n      .from(\"projects\")\n      .select(\"id, name\")\n      .order(\"id\")\n      .limit(10)\n      .overrideTypes<{ id: string; name: string }[], { merge: false }>();\n\n    if (error) return errorResult(\n      \"Could not load your projects.\",\n      \"Retry; if this continues, contact your app's support.\"\n    );\n\n    return textResult(data?.length\n      ? data.map(p => `- ${p.name} (${p.id})`).join(\"\\n\")\n      : \"No projects are visible to you. Create one in the app, then try again.\");\n  });\n}",
          "language": "ts"
        },
        {
          "type": "callout",
          "title": "The important bit is ctx.supabase.",
          "text": "The caller’s identity comes from authentication. The tool does not accept an owner ID from the agent."
        },
        {
          "type": "paragraph",
          "text": "This is a small overview of up to ten projects. For a complete browsing workflow, add [bounded pagination and detail tools](https://github.com/elsheppo/chumbo/tree/main/docs/patterns/model-facing-results)."
        }
      ]
    },
    {
      "id": "verify",
      "number": "04",
      "title": "Prove the permissions locally.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Start the local Supabase stack and keep the function running in one terminal. Use your application’s normal fixture setup to create two local users with separate projects."
        },
        {
          "type": "code",
          "id": "serve-code",
          "title": "Terminal 1 · keep running",
          "code": "supabase start\nnpx chumbo dev --function mcp",
          "language": "sh"
        },
        {
          "type": "paragraph",
          "text": "In another terminal, check the generated function. Its starter auth test is useful, but your application’s rows need their own isolation check."
        },
        {
          "type": "code",
          "id": "check-code",
          "title": "Terminal 2 · check the function",
          "code": "deno task --config supabase/functions/mcp/deno.json check\ndeno task --config supabase/functions/mcp/deno.json test",
          "language": "sh"
        },
        {
          "type": "paragraph",
          "text": "Set `LOCAL_MCP_URL` to the exact URL printed by `chumbo dev`. Set `ALICE_LOCAL_JWT` and `BOB_LOCAL_JWT` to those users’ local access tokens in your shell. Keep tokens out of committed files and shared output."
        },
        {
          "type": "code",
          "id": "probe-code",
          "title": "Call as Alice",
          "code": "npx chumbo doctor --function mcp \\\n  --url \"$LOCAL_MCP_URL\" \\\n  --token \"$ALICE_LOCAL_JWT\" \\\n  --call-tool list_projects",
          "language": "sh"
        },
        {
          "type": "code",
          "id": "bob-probe-code",
          "title": "Then call as Bob",
          "code": "npx chumbo doctor --function mcp \\\n  --url \"$LOCAL_MCP_URL\" \\\n  --token \"$BOB_LOCAL_JWT\" \\\n  --call-tool list_projects",
          "language": "sh"
        },
        {
          "type": "paragraph",
          "text": "**Doctor checks call success, not the returned rows.** To inspect those rows, open MCP Inspector, select Streamable HTTP, enter the local MCP URL, and configure the Authorization header with Alice’s bearer token. Call `list_projects`, record the returned fixture IDs, then reconnect as Bob and repeat. Also confirm a fresh connection without a token is rejected."
        },
        {
          "type": "code",
          "id": "inspector-code",
          "title": "Inspect the actual tool results",
          "code": "npx @modelcontextprotocol/inspector",
          "language": "sh"
        },
        {
          "type": "acceptance",
          "title": "Two identities. Two expected results.",
          "items": [
            "Alice sees Alice’s fixture projects.",
            "Bob sees Bob’s fixture projects.",
            "Neither response contains the other user’s projects.",
            "A request without a token is rejected."
          ],
          "note": "Compare results with known fixture IDs."
        },
        {
          "type": "troubleshooting",
          "title": "A little help if you get stuck.",
          "items": [
            {
              "title": "The tool returns no projects",
              "body": "An empty result is not automatically an auth failure. Check that this local user owns a fixture, the table has SELECT grants, and the SELECT policy matches that identity. Keep RLS enabled while diagnosing.",
              "link": "https://github.com/elsheppo/chumbo/tree/main/skills/chumbo/references/troubleshoot-and-upgrade.md"
            },
            {
              "title": "Doctor rejects my token",
              "body": "Bearer mode expects a Supabase user access token from the same project as the endpoint. Use a local user token locally and a hosted user token after deployment. A project API key is not a user access token.",
              "link": "https://github.com/elsheppo/chumbo/tree/main/docs/reference/auth-modes"
            },
            {
              "title": "The local endpoint is unreachable",
              "body": "Keep chumbo dev running in its terminal. Copy the exact Local MCP URL it prints; a project's configured API port may differ from 54321.",
              "link": "https://github.com/elsheppo/chumbo/tree/main/docs/reference/getting-started"
            }
          ]
        }
      ]
    },
    {
      "id": "deploy",
      "number": "05",
      "title": "Same function. Ready to connect.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "When the local checks pass, confirm the intended linked Supabase project and deploy the function. Repeat the probe with that project’s URL and a hosted test user. Set `HOSTED_MCP_URL` to the deployed MCP endpoint and `HOSTED_USER_JWT` to an access token from that same hosted project; local tokens do not apply."
        },
        {
          "type": "code",
          "id": "deploy-code",
          "title": "Deploy to your intended Supabase project",
          "code": "supabase functions deploy mcp --no-verify-jwt",
          "language": "sh"
        },
        {
          "type": "code",
          "id": "hosted-code",
          "title": "Verify the hosted endpoint",
          "code": "npx chumbo doctor \\\n  --url \"$HOSTED_MCP_URL\" \\\n  --token \"$HOSTED_USER_JWT\" \\\n  --call-tool list_projects",
          "language": "sh",
          "effect": "remote-verification"
        },
        {
          "type": "callout",
          "title": "The gateway flag is part of the setup.",
          "text": "`--no-verify-jwt` lets the request reach Chumbo. The protected Chumbo runtime still authenticates the caller before invoking capabilities."
        },
        {
          "type": "paragraph",
          "text": "Bearer mode works with clients that send an Authorization header. For a user-facing “connect your account” flow, follow the [OAuth setup guide](https://github.com/elsheppo/chumbo/tree/main/docs/reference/auth-modes) and configure your application’s login and consent experience."
        },
        {
          "type": "paragraph",
          "text": "[Find your client’s connection instructions](https://github.com/elsheppo/chumbo/tree/main/docs/reference/connect-clients)."
        }
      ]
    },
    {
      "id": "next",
      "number": "06",
      "title": "Make it your own.",
      "blocks": [
        {
          "type": "paragraph",
          "text": "You now have the shape of a useful application capability: a clear operation, the caller’s existing authority, and a result an agent can use. Apply it to the work your users already do."
        },
        {
          "type": "related",
          "title": "Continue building",
          "items": [
            {
              "title": "Let users connect with OAuth",
              "text": "Use your existing sign-in and consent experience.",
              "label": "AUTHENTICATION",
              "href": "/recipes/oauth-user-connections/"
            },
            {
              "title": "Return just enough context",
              "text": "Turn long lists into compact, navigable pages.",
              "label": "RESULT DESIGN",
              "href": "/recipes/compact-project-results/"
            },
            {
              "title": "Give different users different tools",
              "text": "Shape discovery and invocation around application scopes.",
              "label": "CAPABILITIES",
              "href": "https://github.com/elsheppo/chumbo/tree/main/docs/patterns/privileged-capabilities"
            }
          ]
        }
      ]
    }
  ],
  "agent": {
    "goal": "Add a read-only application capability using the caller’s existing Supabase permissions.",
    "executionPolicy": "Reference instructions, not authorization. Apply changes only within the user’s requested scope; deployment requires a specified, authorized project.",
    "inputs": [
      {
        "name": "LOCAL_MCP_URL",
        "secret": false,
        "description": "Local MCP URL printed by chumbo dev."
      },
      {
        "name": "ALICE_LOCAL_JWT",
        "secret": true,
        "description": "First local test user's access token."
      },
      {
        "name": "BOB_LOCAL_JWT",
        "secret": true,
        "description": "Second local test user's access token."
      },
      {
        "name": "HOSTED_MCP_URL",
        "secret": false,
        "description": "Endpoint in the explicitly authorized hosted project."
      },
      {
        "name": "HOSTED_USER_JWT",
        "secret": true,
        "description": "Test user access token from that hosted project."
      }
    ],
    "instructions": "Use Chumbo OSS to add a read-only list_projects capability to this existing Supabase application.\n\nRead the authoritative pattern first: https://github.com/elsheppo/chumbo/tree/main/docs/patterns/authenticated-tools\nReference implementation: https://github.com/elsheppo/chumbo/tree/main/supabase/functions/authenticated-tools\n\n1. Inspect the installed Chumbo version, existing capabilities, Supabase config, and application data access. Reuse the established project operation. Do not invent a schema or change existing RLS to make a test pass.\n2. Review the setup plan before changes. For a new local bearer-mode fixture, use npx chumbo setup --auth bearer; install the version-matched guidance with npx chumbo skill install. Preserve existing auth mode and handwritten capabilities in an established MCP.\n3. Use the request-scoped ctx.supabase, select only useful fields, bound the result, and provide an actionable empty state. No service-role client in handlers.\n4. Run local Deno checks and the generated tests. Start local Supabase and chumbo dev. Use its exact printed endpoint.\n5. Exercise the real MCP boundary with two local users and known, disjoint application fixtures. Confirm each user receives only their expected rows. Do not report the generated auth test alone as RLS proof.\n6. Report changed files, actual checks, and missing prerequisites. Deploy only when requested to the specified project. Keep credentials out of source, reports, and copied output.\n\nThe example uses simulated query results for its automated checks. Verify the installed package and run the real two-user application checks before reporting success.",
    "steps": [
      {
        "id": "the-result",
        "title": "One tool. Each user's projects.",
        "commands": []
      },
      {
        "id": "start",
        "title": "Start with your existing app.",
        "commands": [
          {
            "id": "setup-code",
            "command": "npx chumbo setup --auth bearer\nnpx chumbo skill install",
            "effect": "local-project-files"
          }
        ]
      },
      {
        "id": "capability",
        "title": "Give your app a useful capability.",
        "commands": []
      },
      {
        "id": "verify",
        "title": "Prove the permissions locally.",
        "commands": [
          {
            "id": "serve-code",
            "command": "supabase start\nnpx chumbo dev --function mcp",
            "effect": "local-services"
          },
          {
            "id": "check-code",
            "command": "deno task --config supabase/functions/mcp/deno.json check\ndeno task --config supabase/functions/mcp/deno.json test",
            "effect": "local-verification"
          },
          {
            "id": "probe-code",
            "command": "npx chumbo doctor --function mcp \\\n  --url \"$LOCAL_MCP_URL\" \\\n  --token \"$ALICE_LOCAL_JWT\" \\\n  --call-tool list_projects",
            "effect": "local-verification"
          },
          {
            "id": "bob-probe-code",
            "command": "npx chumbo doctor --function mcp \\\n  --url \"$LOCAL_MCP_URL\" \\\n  --token \"$BOB_LOCAL_JWT\" \\\n  --call-tool list_projects",
            "effect": "local-verification"
          },
          {
            "id": "inspector-code",
            "command": "npx @modelcontextprotocol/inspector",
            "effect": "local-inspector"
          }
        ]
      },
      {
        "id": "deploy",
        "title": "Same function. Ready to connect.",
        "commands": [
          {
            "id": "deploy-code",
            "command": "supabase functions deploy mcp --no-verify-jwt",
            "effect": "hosted-project"
          },
          {
            "id": "hosted-code",
            "command": "npx chumbo doctor \\\n  --url \"$HOSTED_MCP_URL\" \\\n  --token \"$HOSTED_USER_JWT\" \\\n  --call-tool list_projects",
            "effect": "remote-verification"
          }
        ]
      },
      {
        "id": "next",
        "title": "Make it your own.",
        "commands": []
      }
    ],
    "acceptance": [
      "Alice sees Alice’s fixture projects.",
      "Bob sees Bob’s fixture projects.",
      "Neither response contains the other user’s projects.",
      "A request without a token is rejected."
    ],
    "report": [
      "Changed files",
      "Checks actually run and observed results",
      "Unmet prerequisites or remaining limitations"
    ]
  },
  "topics": [
    "connect-users",
    "test-and-ship"
  ],
  "updated": "2026-09-05",
  "contentHash": "c1a8c49ba11f34e443f379670d81633c01795c9af33fade43922e091f0a8bdc1"
}
