feat(ui-library): bundle the mcp-server-compute block before pushing

The `node` Compute runtime installs nothing server-side: `supabase compute
push` uploads the source directory exactly as it stands. The block handled
that by telling the user to `npm install` and ship `node_modules` in the
upload, which is a vendored dependency tree in the archive and an install
step to remember on every machine that pushes.

Bundle instead. `npm run build` runs `build.mjs`, which esbuilds `main.ts`
into `dist/index.mjs` as one ESM bundle with its dependencies inlined, and
`[compute.mcp-server] source = "supabase/compute/mcp-server/dist"` points the
push at that directory. The uploaded archive is two files and needs no
`node_modules`.

`index.mjs` is deliberate, not incidental: it is the entrypoint name the
`node` runtime looks for, so the bundle satisfies the contract directly. The
build also writes a `dist/package.json` so a push with no `runtime` recorded
still classifies as `node` rather than falling back to the `deno` default;
pinning `runtime` in config.toml stays the documented path.

`deploy` runs the build and the push together and forwards `--project-ref`.
Docs frame it as the two commands in sequence locally, and the same pair in
CI as the setup to aim for. `dist/` is git-ignored.

Left unminified on purpose: a public instance has 50 seconds to accept
connections and parsing this bundle 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.

Verified by building and booting the output with no `node_modules` present:
default export carries a `fetch` function, RFC 9728 metadata answers `200`
with the `/compute/v1/mcp-server` resource URL, an unauthenticated `POST /`
answers `401` with the `resource_metadata` challenge, and
`MCP_AUTHORIZATION_SERVER` still overrides `authorization_servers`. Bundle is
1.76 MB and cold start (import to listening) is ~135 ms. `tsc --noEmit` clean.

The `public/r` artifacts are regenerated by hand and have not been checked
against a real `pnpm build:registry`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbcCtAHAHnTCXNJGFfEcQF
This commit is contained in:
Claude committed 2026-09-16 08:54:05 +00:00
1 parent f947298253
commit 2e8f0f91a1
8 files changed
+149 -32

No files matched your search

@@ -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/<name>/`; 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://<project-ref>.supabase.co/compute/v1/mcp-server`. The name in
`[compute.<name>]` is the last segment of that URL.
`[compute.<name>]` 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 <project-ref>
supabase compute status mcp-server --project-ref <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 <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:
@@ -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 <base>` + `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.<name>] 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/<name>, 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 <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.<name>] 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/<name>, 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 <ref>` (or `npm run deploy -- --project-ref <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)."
}
+6 -1
View File
@@ -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 <ref>` (or `npm run deploy -- --project-ref <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",
@@ -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 <ref>` (or `npm run deploy -- --project-ref <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",
@@ -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 <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
@@ -1,2 +1,3 @@
node_modules/
dist/
.env
@@ -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 <base>` + `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`)
@@ -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"
}
}