diff --git a/apps/ui-library/content/docs/headless/mcp-server-compute.mdx b/apps/ui-library/content/docs/headless/mcp-server-compute.mdx index b07b89dccf0..d6fdb3f96f0 100644 --- a/apps/ui-library/content/docs/headless/mcp-server-compute.mdx +++ b/apps/ui-library/content/docs/headless/mcp-server-compute.mdx @@ -20,26 +20,37 @@ Installs Node Compute files into a Supabase project or empty directory. No `components.json` is required. Backend files stay in `supabase/` at the project root even when the frontend uses `src/`. -Then install the service's dependencies, in the service directory: +The block ships sources and a build script, not a build. Install the +dependencies and bundle them, in the service directory: ```bash cd supabase/compute/mcp-server npm install +npm run build ``` -The `node` runtime does no server-side install. `supabase compute push` uploads -the directory as it stands, so `node_modules` has to already be there. The block -ships a `.gitignore` that keeps `node_modules` and `.env` out of version -control; re-run `npm install` on any machine that pushes. +`npm run build` writes `dist/index.mjs`: one ESM bundle with every dependency +inlined. That bundle is what gets deployed, because `[compute.mcp-server] +source` points at `dist/`. + +This is worth doing because the `node` runtime installs nothing server-side. +`supabase compute push` uploads the source directory exactly as it stands, so +the alternative is committing to keep a full `node_modules` in the directory you +push. Bundling makes that upload two files instead. + +Build on every machine that pushes, and before every deploy: the block's +`.gitignore` keeps `dist/`, `node_modules/`, and `.env` out of version control, +so a fresh checkout has no bundle yet. The tool files are a copy of the Edge Function block's, with the `npm:` prefixes removed from their imports. The Edge Function block resolves -`npm:@modelcontextprotocol/server@2.0.0` at runtime; node resolves -`@modelcontextprotocol/server` from `node_modules` and cannot read a `npm:` -specifier, and node has no import map to bridge the two. So the two blocks hold -the same four tool modules separately, and a tool added to one has to be added -to the other. Relative imports carry their `.ts` extension in both: that is what -Deno wants and what node's TypeScript stripping requires. +`npm:@modelcontextprotocol/server@2.0.0` at runtime; here esbuild resolves +`@modelcontextprotocol/server` from `node_modules` at build time, and it cannot +read a `npm:` specifier either. The two blocks also install separately, into +different directories. So they hold the same four tool modules separately, and a +tool added to one has to be added to the other. Relative imports carry their +`.ts` extension in both: that is what Deno resolves at runtime and what esbuild +resolves at build time. ## Folder structure @@ -58,15 +69,24 @@ compute = true runtime = "node" size = "2gb" exposure = "public" +source = "supabase/compute/mcp-server/dist" ``` -`runtime = "node"` serves `main.ts`'s default-exported `fetch` handler with no -build step: node runs the TypeScript directly, stripping the types. It does not -resolve `npm:` or `jsr:` specifiers, and it installs nothing at deploy time, so -dependencies come from the `node_modules` you install before pushing. +`source` is the directory that gets packaged and uploaded, relative to the +project root. It defaults to `supabase/compute//`; pointing it at `dist/` +is what deploys the bundle rather than the sources and a `node_modules` beside +them. + +`runtime = "node"` serves `dist/index.mjs`'s default-exported `fetch` handler. +Keep it set. With no `runtime` the CLI guesses from marker files in the source +directory and defaults to `deno`, which will not run a bundle built for node. +The build writes a `dist/package.json` so that guess lands on `node` anyway, but +pinning `runtime` is what makes the deploy independent of the guess. + `exposure = "public"` publishes the service at `https://.supabase.co/compute/v1/mcp-server`. The name in -`[compute.]` is the last segment of that URL. +`[compute.]` is the last segment of that URL; `source` does not change +it. The project must sign JWTs with an asymmetric key. `withSupabase` verifies user tokens against the project JWKS and rejects legacy HS256 tokens, so a project @@ -203,21 +223,40 @@ way an Edge Function is. Set them yourself. The block includes cp supabase/compute/mcp-server/.env.example supabase/compute/mcp-server/.env ``` -The block's `.gitignore` already covers `supabase/compute/mcp-server/.env` and -`node_modules/`. +The block's `.gitignore` already covers `supabase/compute/mcp-server/.env`, +`node_modules/`, and `dist/`. ## Deploy ```bash supabase config push supabase secrets set --env-file supabase/compute/mcp-server/.env -npm install --prefix supabase/compute/mcp-server -supabase compute push mcp-server -supabase compute status mcp-server +npm run build --prefix supabase/compute/mcp-server +supabase compute push mcp-server --project-ref +supabase compute status mcp-server --project-ref ``` -`npm install` is not optional. Push a directory without `node_modules` and the -instance fails to start on `import { createMcpHandler } from '@modelcontextprotocol/server'`. +The block also ships a `deploy` script that runs the build and the push +together: + +```bash +cd supabase/compute/mcp-server +npm run deploy -- --project-ref +``` + +`supabase compute push` needs a project either way: pass `--project-ref`, or run +`supabase link` once in the repo and drop the flag. Arguments after `--` are +forwarded to the push. + +Build before pushing. `push` refuses an empty `source` directory, so a first +deploy with no build fails outright — but a stale `dist/` deploys the previous +code without complaining. + +Running the two locally is fine, and it is why they are separate scripts. The +setup worth aiming for is the same pair in CI, so the bundle is built from the +committed sources and pushed on merge rather than from whatever is in someone's +working tree. `SUPABASE_ACCESS_TOKEN` and `--project-ref` are all the push needs +to run unattended. Then check the advertised metadata matches the URL clients call: diff --git a/apps/ui-library/public/r/mcp-server-compute.json b/apps/ui-library/public/r/mcp-server-compute.json index f1613efa0c3..31868f758f1 100644 --- a/apps/ui-library/public/r/mcp-server-compute.json +++ b/apps/ui-library/public/r/mcp-server-compute.json @@ -13,10 +13,16 @@ }, { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/package.json", - "content": "{\n \"name\": \"mcp-server-compute\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"check\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/server\": \"2.0.0\",\n \"@supabase/middleware\": \"0.5.0\",\n \"@supabase/server\": \"1.6.0\",\n \"@supabase/supabase-js\": \"2.108.2\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.15.3\",\n \"typescript\": \"^5.8.3\"\n }\n}\n", + "content": "{\n \"name\": \"mcp-server-compute\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"check\": \"tsc --noEmit\",\n \"build\": \"node build.mjs\",\n \"deploy\": \"npm run build && supabase compute push mcp-server\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/server\": \"2.0.0\",\n \"@supabase/middleware\": \"0.5.0\",\n \"@supabase/server\": \"1.6.0\",\n \"@supabase/supabase-js\": \"2.108.2\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.15.3\",\n \"esbuild\": \"^0.25.10\",\n \"typescript\": \"^5.8.3\"\n }\n}\n", "type": "registry:file", "target": "~/supabase/compute/mcp-server/package.json" }, + { + "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/build.mjs", + "content": "// Bundles this service into `dist/index.mjs`, the entrypoint the `node` Compute\n// runtime imports and serves.\n//\n// Nothing is installed server-side: `supabase compute push` uploads the source\n// directory as-is and the platform synthesizes `FROM ` + `COPY` with no\n// install step. So either `node_modules/` ships in the upload, or the\n// dependencies are in the bundle. This block does the latter, and points\n// `[compute.mcp-server] source` at `dist/`, so the uploaded directory is two\n// files instead of a vendored dependency tree.\nimport { mkdir, rm, stat, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { build } from 'esbuild'\n\nconst OUT_DIR = 'dist'\n\n// `index.mjs` is the name the `node` runtime looks for: the archive root must\n// contain the entrypoint, and it must default-export an object with a `fetch`\n// method. Renaming it deploys a directory the runtime cannot start.\nconst ENTRY_OUT = join(OUT_DIR, 'index.mjs')\n\n// A `package.json` next to the bundle, so a push with no\n// `[compute.mcp-server] runtime` still classifies as `node`. The CLI guesses\n// the runtime from marker files and falls back to `deno` when it finds none,\n// and a bundle built for node does not run under deno. Pinning `runtime` in\n// config.toml is still the documented path; this is the safety net.\nconst OUT_MANIFEST = { private: true, type: 'module' }\n\nawait rm(OUT_DIR, { recursive: true, force: true })\nawait mkdir(OUT_DIR, { recursive: true })\n\nawait build({\n entryPoints: ['main.ts'],\n outfile: ENTRY_OUT,\n bundle: true,\n // The platform owns the listener: it imports the default export and serves it\n // on $PORT. This is a library, not a program, so there is no shebang or\n // top-level `listen`.\n platform: 'node',\n format: 'esm',\n target: 'node22',\n // Everything except `node:` builtins goes in the bundle. This is the whole\n // point of the build: `dist/` has no `node_modules/` to resolve against.\n packages: 'bundle',\n // Left unminified on purpose. A public instance has 50 seconds to accept\n // connections and parsing a bundle this size is a few milliseconds of that,\n // so the trade buys nothing \u2014 while readable frames in `supabase compute\n // logs` are what makes a failed request diagnosable.\n minify: false,\n logLevel: 'info',\n})\n\nawait writeFile(join(OUT_DIR, 'package.json'), `${JSON.stringify(OUT_MANIFEST, null, 2)}\\n`)\n\n// Printed because packaged size is the number that moves when a dependency is\n// added, and the cost lands on cold start.\nconst { size } = await stat(ENTRY_OUT)\nconsole.log(`${ENTRY_OUT} ${(size / 1024).toFixed(0)} KiB`)\n", + "type": "registry:file", + "target": "~/supabase/compute/mcp-server/build.mjs" + }, { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/tsconfig.json", "content": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2023\"],\n \"module\": \"nodenext\",\n \"moduleResolution\": \"nodenext\",\n \"types\": [\"node\"],\n \"strict\": true,\n \"noUncheckedIndexedAccess\": true,\n \"verbatimModuleSyntax\": true,\n \"allowImportingTsExtensions\": true,\n \"erasableSyntaxOnly\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"main.ts\", \"tools/**/*.ts\"]\n}\n", @@ -25,13 +31,13 @@ }, { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.env.example", - "content": "# A Compute instance receives its configuration as environment variables. Copy\n# this file and set the values as project secrets before pushing:\n# cp supabase/compute/mcp-server/.env.example supabase/compute/mcp-server/.env\n# supabase secrets set --env-file supabase/compute/mcp-server/.env\n# npm install --prefix supabase/compute/mcp-server\n# supabase compute push mcp-server\n\n# Required. The project URL and a publishable key, the same values passed to\n# createClient(). Unlike an Edge Function, a Compute instance is not given\n# these automatically.\nSUPABASE_URL=https://your-project-ref.supabase.co\nSUPABASE_PUBLISHABLE_KEY=your-publishable-key\n\n# Must match [compute.] in supabase/config.toml. It is the last segment\n# of this service's public URL, so the advertised OAuth resource identifier\n# depends on it.\nMCP_COMPUTE_SERVICE_NAME=mcp-server\n\n# Optional. Set only when clients reach this service somewhere other than\n# SUPABASE_URL + /compute/v1/, for example behind a custom domain. It\n# must be the exact URL clients call, which RFC 9728 requires to match.\n# MCP_RESOURCE_SERVER=https://mcp.example.com\n\n# Optional. A third-party OAuth 2.1 issuer for external clients. Unset, this\n# project's Supabase Auth is the authorization server.\n# MCP_AUTHORIZATION_SERVER=https://example.clerk.accounts.dev\n\n# Keep the protocol-level server name short and project-specific.\nMCP_SERVER_NAME=supabase-mcp\nMCP_SERVER_DESCRIPTION=\"MCP access to this Supabase project for the signed-in user.\"\n", + "content": "# A Compute instance receives its configuration as environment variables. Copy\n# this file and set the values as project secrets before pushing:\n# cp supabase/compute/mcp-server/.env.example supabase/compute/mcp-server/.env\n# supabase secrets set --env-file supabase/compute/mcp-server/.env\n# npm run build --prefix supabase/compute/mcp-server\n# supabase compute push mcp-server --project-ref \n\n# Required. The project URL and a publishable key, the same values passed to\n# createClient(). Unlike an Edge Function, a Compute instance is not given\n# these automatically.\nSUPABASE_URL=https://your-project-ref.supabase.co\nSUPABASE_PUBLISHABLE_KEY=your-publishable-key\n\n# Must match [compute.] in supabase/config.toml. It is the last segment\n# of this service's public URL, so the advertised OAuth resource identifier\n# depends on it.\nMCP_COMPUTE_SERVICE_NAME=mcp-server\n\n# Optional. Set only when clients reach this service somewhere other than\n# SUPABASE_URL + /compute/v1/, for example behind a custom domain. It\n# must be the exact URL clients call, which RFC 9728 requires to match.\n# MCP_RESOURCE_SERVER=https://mcp.example.com\n\n# Optional. A third-party OAuth 2.1 issuer for external clients. Unset, this\n# project's Supabase Auth is the authorization server.\n# MCP_AUTHORIZATION_SERVER=https://example.clerk.accounts.dev\n\n# Keep the protocol-level server name short and project-specific.\nMCP_SERVER_NAME=supabase-mcp\nMCP_SERVER_DESCRIPTION=\"MCP access to this Supabase project for the signed-in user.\"\n", "type": "registry:file", "target": "~/supabase/compute/mcp-server/.env.example" }, { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.gitignore", - "content": "node_modules/\n.env\n", + "content": "node_modules/\ndist/\n.env\n", "type": "registry:file", "target": "~/supabase/compute/mcp-server/.gitignore" }, @@ -60,5 +66,5 @@ "target": "~/supabase/compute/mcp-server/tools/index.ts" } ], - "docs": "Set `[compute.mcp-server]` with `runtime = \"node\"` in `supabase/config.toml`. The `node` runtime does no server-side install, so run `npm install` in `supabase/compute/mcp-server` before `supabase compute push` \u2014 the directory is uploaded as-is and `node_modules` has to be in it. Keep `node_modules` and `.env` out of version control. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` as secrets: a Compute instance is not given them automatically. A trusted product backend can call it with the signed-in user's access token. For external clients, install the [OAuth Consent block](https://supabase.com/library/docs/nextjs/oauth-consent), enable OAuth and dynamic registration, and set the Auth Site URL to the consent app. Every call runs through the user's RLS-scoped client. OAuth tokens include `client_id`; product sessions do not, so define policies for both paths. See [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) and [token security](https://supabase.com/docs/guides/auth/oauth-server/token-security)." + "docs": "Set `[compute.mcp-server]` in `supabase/config.toml` with `runtime = \"node\"` and `source = \"supabase/compute/mcp-server/dist\"`. The block ships sources and a build script, not a build: run `npm install` then `npm run build` in `supabase/compute/mcp-server` to write `dist/index.mjs`, a single ESM bundle with its dependencies inlined, then `supabase compute push mcp-server --project-ref ` (or `npm run deploy -- --project-ref `, which does both). Nothing is installed server-side and the `source` directory is uploaded as-is, so the bundle is what removes the need for a vendored `node_modules`. Build on every machine that pushes: `dist`, `node_modules` and `.env` are git-ignored. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` as secrets: a Compute instance is not given them automatically. A trusted product backend can call it with the signed-in user's access token. For external clients, install the [OAuth Consent block](https://supabase.com/library/docs/nextjs/oauth-consent), enable OAuth and dynamic registration, and set the Auth Site URL to the consent app. Every call runs through the user's RLS-scoped client. OAuth tokens include `client_id`; product sessions do not, so define policies for both paths. See [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) and [token security](https://supabase.com/docs/guides/auth/oauth-server/token-security)." } \ No newline at end of file diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json index 062582d19a5..2ca0b7593d6 100644 --- a/apps/ui-library/public/r/registry.json +++ b/apps/ui-library/public/r/registry.json @@ -1800,7 +1800,7 @@ "type": "registry:item", "title": "MCP Server (Compute)", "description": "Add a user-scoped MCP server to your product, running on Supabase Compute.", - "docs": "Set `[compute.mcp-server]` with `runtime = \"node\"` in `supabase/config.toml`. The `node` runtime does no server-side install, so run `npm install` in `supabase/compute/mcp-server` before `supabase compute push` \u2014 the directory is uploaded as-is and `node_modules` has to be in it. Keep `node_modules` and `.env` out of version control. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` as secrets: a Compute instance is not given them automatically. A trusted product backend can call it with the signed-in user's access token. For external clients, install the [OAuth Consent block](https://supabase.com/library/docs/nextjs/oauth-consent), enable OAuth and dynamic registration, and set the Auth Site URL to the consent app. Every call runs through the user's RLS-scoped client. OAuth tokens include `client_id`; product sessions do not, so define policies for both paths. See [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) and [token security](https://supabase.com/docs/guides/auth/oauth-server/token-security).", + "docs": "Set `[compute.mcp-server]` in `supabase/config.toml` with `runtime = \"node\"` and `source = \"supabase/compute/mcp-server/dist\"`. The block ships sources and a build script, not a build: run `npm install` then `npm run build` in `supabase/compute/mcp-server` to write `dist/index.mjs`, a single ESM bundle with its dependencies inlined, then `supabase compute push mcp-server --project-ref ` (or `npm run deploy -- --project-ref `, which does both). Nothing is installed server-side and the `source` directory is uploaded as-is, so the bundle is what removes the need for a vendored `node_modules`. Build on every machine that pushes: `dist`, `node_modules` and `.env` are git-ignored. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` as secrets: a Compute instance is not given them automatically. A trusted product backend can call it with the signed-in user's access token. For external clients, install the [OAuth Consent block](https://supabase.com/library/docs/nextjs/oauth-consent), enable OAuth and dynamic registration, and set the Auth Site URL to the consent app. Every call runs through the user's RLS-scoped client. OAuth tokens include `client_id`; product sessions do not, so define policies for both paths. See [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) and [token security](https://supabase.com/docs/guides/auth/oauth-server/token-security).", "files": [ { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/main.ts", @@ -1812,6 +1812,11 @@ "type": "registry:file", "target": "~/supabase/compute/mcp-server/package.json" }, + { + "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/build.mjs", + "type": "registry:file", + "target": "~/supabase/compute/mcp-server/build.mjs" + }, { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/tsconfig.json", "type": "registry:file", diff --git a/apps/ui-library/registry/default/blocks/mcp-server-compute/registry-item.json b/apps/ui-library/registry/default/blocks/mcp-server-compute/registry-item.json index dbdafc9f4e7..cc847ddb74b 100644 --- a/apps/ui-library/registry/default/blocks/mcp-server-compute/registry-item.json +++ b/apps/ui-library/registry/default/blocks/mcp-server-compute/registry-item.json @@ -3,7 +3,7 @@ "type": "registry:item", "title": "MCP Server (Compute)", "description": "Add a user-scoped MCP server to your product, running on Supabase Compute.", - "docs": "Set `[compute.mcp-server]` with `runtime = \"node\"` in `supabase/config.toml`. The `node` runtime does no server-side install, so run `npm install` in `supabase/compute/mcp-server` before `supabase compute push` \u2014 the directory is uploaded as-is and `node_modules` has to be in it. Keep `node_modules` and `.env` out of version control. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` as secrets: a Compute instance is not given them automatically. A trusted product backend can call it with the signed-in user's access token. For external clients, install the [OAuth Consent block](https://supabase.com/library/docs/nextjs/oauth-consent), enable OAuth and dynamic registration, and set the Auth Site URL to the consent app. Every call runs through the user's RLS-scoped client. OAuth tokens include `client_id`; product sessions do not, so define policies for both paths. See [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) and [token security](https://supabase.com/docs/guides/auth/oauth-server/token-security).", + "docs": "Set `[compute.mcp-server]` in `supabase/config.toml` with `runtime = \"node\"` and `source = \"supabase/compute/mcp-server/dist\"`. The block ships sources and a build script, not a build: run `npm install` then `npm run build` in `supabase/compute/mcp-server` to write `dist/index.mjs`, a single ESM bundle with its dependencies inlined, then `supabase compute push mcp-server --project-ref ` (or `npm run deploy -- --project-ref `, which does both). Nothing is installed server-side and the `source` directory is uploaded as-is, so the bundle is what removes the need for a vendored `node_modules`. Build on every machine that pushes: `dist`, `node_modules` and `.env` are git-ignored. Set `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` as secrets: a Compute instance is not given them automatically. A trusted product backend can call it with the signed-in user's access token. For external clients, install the [OAuth Consent block](https://supabase.com/library/docs/nextjs/oauth-consent), enable OAuth and dynamic registration, and set the Auth Site URL to the consent app. Every call runs through the user's RLS-scoped client. OAuth tokens include `client_id`; product sessions do not, so define policies for both paths. See [MCP authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication) and [token security](https://supabase.com/docs/guides/auth/oauth-server/token-security).", "files": [ { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/main.ts", @@ -15,6 +15,11 @@ "type": "registry:file", "target": "~/supabase/compute/mcp-server/package.json" }, + { + "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/build.mjs", + "type": "registry:file", + "target": "~/supabase/compute/mcp-server/build.mjs" + }, { "path": "registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/tsconfig.json", "type": "registry:file", diff --git a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.env.example b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.env.example index 73fcbb321d7..e18071eceb8 100644 --- a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.env.example +++ b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.env.example @@ -2,8 +2,8 @@ # this file and set the values as project secrets before pushing: # cp supabase/compute/mcp-server/.env.example supabase/compute/mcp-server/.env # supabase secrets set --env-file supabase/compute/mcp-server/.env -# npm install --prefix supabase/compute/mcp-server -# supabase compute push mcp-server +# npm run build --prefix supabase/compute/mcp-server +# supabase compute push mcp-server --project-ref # Required. The project URL and a publishable key, the same values passed to # createClient(). Unlike an Edge Function, a Compute instance is not given diff --git a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.gitignore b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.gitignore index 713d5006dab..deed335be2e 100644 --- a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.gitignore +++ b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/.gitignore @@ -1,2 +1,3 @@ node_modules/ +dist/ .env diff --git a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/build.mjs b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/build.mjs new file mode 100644 index 00000000000..63ccd52b98f --- /dev/null +++ b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/build.mjs @@ -0,0 +1,58 @@ +// Bundles this service into `dist/index.mjs`, the entrypoint the `node` Compute +// runtime imports and serves. +// +// Nothing is installed server-side: `supabase compute push` uploads the source +// directory as-is and the platform synthesizes `FROM ` + `COPY` with no +// install step. So either `node_modules/` ships in the upload, or the +// dependencies are in the bundle. This block does the latter, and points +// `[compute.mcp-server] source` at `dist/`, so the uploaded directory is two +// files instead of a vendored dependency tree. +import { mkdir, rm, stat, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +import { build } from 'esbuild' + +const OUT_DIR = 'dist' + +// `index.mjs` is the name the `node` runtime looks for: the archive root must +// contain the entrypoint, and it must default-export an object with a `fetch` +// method. Renaming it deploys a directory the runtime cannot start. +const ENTRY_OUT = join(OUT_DIR, 'index.mjs') + +// A `package.json` next to the bundle, so a push with no +// `[compute.mcp-server] runtime` still classifies as `node`. The CLI guesses +// the runtime from marker files and falls back to `deno` when it finds none, +// and a bundle built for node does not run under deno. Pinning `runtime` in +// config.toml is still the documented path; this is the safety net. +const OUT_MANIFEST = { private: true, type: 'module' } + +await rm(OUT_DIR, { recursive: true, force: true }) +await mkdir(OUT_DIR, { recursive: true }) + +await build({ + entryPoints: ['main.ts'], + outfile: ENTRY_OUT, + bundle: true, + // The platform owns the listener: it imports the default export and serves it + // on $PORT. This is a library, not a program, so there is no shebang or + // top-level `listen`. + platform: 'node', + format: 'esm', + target: 'node22', + // Everything except `node:` builtins goes in the bundle. This is the whole + // point of the build: `dist/` has no `node_modules/` to resolve against. + packages: 'bundle', + // Left unminified on purpose. A public instance has 50 seconds to accept + // connections and parsing a bundle this size is a few milliseconds of that, + // so the trade buys nothing — while readable frames in `supabase compute + // logs` are what makes a failed request diagnosable. + minify: false, + logLevel: 'info', +}) + +await writeFile(join(OUT_DIR, 'package.json'), `${JSON.stringify(OUT_MANIFEST, null, 2)}\n`) + +// Printed because packaged size is the number that moves when a dependency is +// added, and the cost lands on cold start. +const { size } = await stat(ENTRY_OUT) +console.log(`${ENTRY_OUT} ${(size / 1024).toFixed(0)} KiB`) diff --git a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/package.json b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/package.json index 2c4e2c7bc05..9c020073ab9 100644 --- a/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/package.json +++ b/apps/ui-library/registry/default/blocks/mcp-server-compute/supabase/compute/mcp-server/package.json @@ -3,7 +3,9 @@ "private": true, "type": "module", "scripts": { - "check": "tsc --noEmit" + "check": "tsc --noEmit", + "build": "node build.mjs", + "deploy": "npm run build && supabase compute push mcp-server" }, "dependencies": { "@modelcontextprotocol/server": "2.0.0", @@ -13,6 +15,7 @@ }, "devDependencies": { "@types/node": "^22.15.3", + "esbuild": "^0.25.10", "typescript": "^5.8.3" } }