Skip to content

feat: add dual-era package serving entries - #358

Open
barryroodt wants to merge 5 commits into
raulb/spike-mcp-v2from
barryroodt/ai-1044-dual-era-serving
Open

feat: add dual-era package serving entries#358
barryroodt wants to merge 5 commits into
raulb/spike-mcp-v2from
barryroodt/ai-1044-dual-era-serving

Conversation

@barryroodt

@barryroodt barryroodt commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

This is a stacked PR. Its parent is #327, please review that one first.

The diff here is only this branch's own commits. The base is raulb/spike-mcp-v2 rather than main on purpose, because #327 is still in review; once it lands, this retargets to main and the diff against main shrinks accordingly.

There is no test check on this page. tests.yml only triggers on pull requests into main, so GitHub skips it here instead of running or failing it. The checks that do run are biome and check-management-api-types, so a short green list is expected rather than a sign of missing coverage. The test output under Verification came from a local run, since CI cannot produce it while this PR is stacked.

At merge time: retarget to main, let tests.yml run green there, then merge.

What kind of change does this PR introduce?

The CLI serves both protocol eras from one tool definition, and the package exports one modern HTTP handler for the hosted /mcp endpoint to mount.

PR 2 of 3 for AI-1044 (plan).

What is the current behavior?

src/transports/stdio.ts connects a raw StdioServerTransport, so only 2025-era clients are served, and there's no HTTP entry for hosted to consume.

What is the new behavior?

serveStdio owns the transport and picks the era per connection. createSupabaseMcpHandler wraps the SDK's createMcpHandler with legacy: 'reject', so hosted keeps authentication, ABAC, era dispatch, and logging.

tools/list is unchanged, so the frozen ChatGPT contract is unaffected. mcp-server-postgrest keeps its single-era entry, and nothing here adds Elicitation machinery, tool policies, or telemetry.

Verification

  • mcp-server-supabase unit + integration: 222 passed. Both eras run against the real built dist/transports/stdio.js, spawned as a child process.
  • mcp-utils: 12 passed. pnpm build and pnpm format:check clean.
  • pnpm test:packed-platform-consumer (new): 3/3 on packed 0.10.0. Installs the tarballs with plain npm outside the repo on Platform's exact zod pin, then checks the CJS entry, a tsc typecheck of the packed declarations, and one modern call that imports the ESM entry.
  • CLI behavior unchanged: --version exits 0, a missing access token exits 1.

Move the CLI's stdio entry from a one-shot server plus raw
StdioServerTransport onto the SDK's serveStdio, which owns transport
startup, connection-pinned era selection, and teardown. The default
legacy: 'serve' is the dual-era behavior we want, so no option is passed.
CLI parsing, token handling, platform construction, error output, and
exit codes are unchanged.

Export createSupabaseMcpHandler, a thin wrapper over the SDK's
createMcpHandler with legacy: 'reject'. Hosted owns authentication,
ABAC, era dispatch, logging, and request lifetime, so the package
interface carries only the strict modern entry and server construction.

The stdio wire goldens now run as an era matrix. Both eras assert the
same 29-tool set, the same server identity and capabilities, and the
same list_projects content. Modern additionally carries the protocol's
mandatory _meta['io.modelcontextprotocol/serverInfo'] stamp, which
legacy asserts is absent.

Add test:packed-platform-consumer, which packs mcp-server-supabase and
mcp-utils, installs both with plain npm into a throwaway project pinned
to the exact zod version Platform's catalog carries, then asserts the
ESM entry, the CJS entry, the type declarations, and one modern
2026-07-28 call. Registering the account tool group keeps the zod-built
tool surface under test, which an empty catalog would skip.
serveStdio routes transport startup and out-of-band wire errors only
through options.onerror, swallowing them otherwise, so without it a
startup failure was silent. The previous awaited server.connect() surfaced
it through main().catch(console.error).
@barryroodt
barryroodt force-pushed the barryroodt/ai-1044-dual-era-serving branch from 8a775c8 to e464513 Compare August 11, 2026 14:10
Restore eager --features validation. Moving server construction into
serveStdio's lazy factory deferred parseFeatureGroups until the first valid
opening message, so an invalid --features value no longer reported and
terminated at startup. Validate the explicitly provided list before serving,
leaving platform-dependent default resolution inside createSupabaseMcpServer.
Covered by a new startup-failure test.

Make the packed-consumer gate prove what it claimed. Its only schema check was
a truthiness test on list_projects, a zero-argument tool whose healthy schema
already carries no properties, so a zod regression emitting property-less
schemas passed. Assert a parameterised witness instead, and drop skipLibCheck
so the packed declarations are really type-checked. Pin the fixture's dev
dependencies exactly and install without lifecycle scripts.

Move the fixture's source out of JS string constants into real files, collapse
the per-check pass-throughs into one runner plus a table, and drop the
esm-entry check that modern-call.mjs already covers by importing and calling
the package ESM entry. The script goes from 429 lines to 240.

Share the msw lifecycle between the two suites that had copied it, assert
status and error code rather than the SDK's exact envelope wording, guard the
integration suite against a stale dist build, and document createSupabaseMcpHandler
with the required per-request mounting pattern.
The modern validation test asserted the SDK's exact envelope message, so an
upstream rewording would fail a test whose contract held. Assert the status,
the JSON-RPC error code, and the structured envelope data instead. The
message is upstream text this package does not own; {key, problem} is
machine-readable and does not churn on rewording.
handler.close() returns a Promise that aborts in-flight exchanges, and the
example discarded it inside an event callback while also making the
createServer callback async, so a cleanup or serving failure surfaced as an
unhandled rejection. Attach a catch to both, and keep the close on res
finishing rather than on the handler resolving, since the latter would cut
streaming responses short.
@barryroodt
barryroodt marked this pull request as ready for review August 12, 2026 09:56
@barryroodt
barryroodt requested a review from a team as a code owner August 12, 2026 09:56
@barryroodt barryroodt added the publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/ label Aug 12, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@supabase/mcp-server-postgrest@ce9e313
pnpm add https://pkg.pr.new/@supabase/mcp-server-supabase@ce9e313
pnpm add https://pkg.pr.new/@supabase/mcp-utils@ce9e313

commit: ce9e313

@barryroodt barryroodt self-assigned this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant