Skip to content

fix(mcp-server): make Vercel deployment actually work#79

Merged
volnei merged 17 commits intomainfrom
fix/mcp-server-vercel-build-command
Apr 16, 2026
Merged

fix(mcp-server): make Vercel deployment actually work#79
volnei merged 17 commits intomainfrom
fix/mcp-server-vercel-build-command

Conversation

@volnei
Copy link
Copy Markdown
Contributor

@volnei volnei commented Apr 16, 2026

Summary

Fixes the Vercel deployment that was hanging at TypeScript compilation of api/index.ts. The root cause: @vercel/node runs TypeScript over the function's dep graph, which OOMs on @modelcontextprotocol/sdk's large types.

Fix

  • Move the handler to src/vercel-handler.ts so it's compiled by our existing bun run build step into dist/vercel-handler.js
  • Replace api/index.ts with a one-line JS wrapper (api/index.js) that re-exports from the compiled output
  • @vercel/node now sees only plain JS in api/ — no TypeScript compilation, no OOM
  • Update vercel.json:
    • Add buildCommand: "bun run build" so dist/ exists before functions are bundled
    • Add outputDirectory: "." so Vercel stops demanding a public/ folder
    • Point the function config at api/index.js

Vercel dashboard

With vercel.json now controlling build + output, Framework Settings overrides can all be turned off. Only settings that still matter in the dashboard:

  • Root Directory: apps/mcp-server
  • Environment Variables: DATABASE_URL, CAL_OAUTH_CLIENT_ID, CAL_OAUTH_CLIENT_SECRET, TOKEN_ENCRYPTION_KEY, MCP_SERVER_URL, MCP_TRANSPORT=http

Test plan

  • bun run build — produces dist/vercel-handler.js
  • bun run test — 203 tests pass
  • Vercel preview deploy builds and responds on /health
  • Full OAuth → MCP tool call works against the deploy

🤖 Generated with Claude Code


Open with Devin

volnei and others added 2 commits April 15, 2026 21:35
Vercel was running our Docker-oriented "bun run build" (which produces
dist/) and then erroring with "No Output Directory named 'public'
found" because no public/ exists. The MCP server on Vercel only needs
the serverless function at api/index.ts — the @vercel/node builder
compiles it automatically; we don't need a separate build step.

Removing buildCommand, installCommand, and framework lets Vercel
auto-detect bun (via bun.lock) and treat the project as
serverless-only, which is what we actually want.

This change was part of #78 but was dropped during the squash-merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous setup had api/index.ts with heavy imports from
@modelcontextprotocol/sdk. @vercel/node's TypeScript compilation
hung indefinitely on it (the SDK types OOM tsc even locally with
a 4 GB heap).

Move the handler to src/vercel-handler.ts so it's compiled to plain
JavaScript by our existing `bun run build` step, and replace
api/index.ts with a one-line ESM wrapper that re-exports the
compiled default from ../dist/vercel-handler.js. @vercel/node now
sees only plain JS in api/ and bundles in milliseconds.

vercel.json:
- Add buildCommand: "bun run build" so dist/ is produced before
  functions are bundled
- Add outputDirectory: "." so Vercel stops expecting public/
- Register the function under its new path (api/index.js)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cal-companion-mcp Ready Ready Preview, Comment Apr 16, 2026 11:33am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
cal-companion-chat Ignored Ignored Apr 16, 2026 11:33am

Request Review

The Vercel handler is only ever used for HTTP transport, but
loadConfig() defaults to stdio mode when MCP_TRANSPORT is unset,
which then errors on the missing CAL_API_KEY. Set MCP_TRANSPORT=http
on the process env before loading so operators do not have to
remember to configure it in the Vercel dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`export const sql = pool.sql;` destructured the tagged-template method
off the pool, losing the `this` binding. On Vercel that surfaced as
"Cannot read properties of undefined (reading 'connectionString')"
because @vercel/postgres reads `this.config.connectionString` lazily
at call time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cal.com's OAuthClient (user-facing) rejects authorize requests that
omit the scope parameter: "scope parameter is required for this OAuth
client". Add a new CAL_OAUTH_SCOPES env var (space-separated) that
defaults to the full set of User-level scopes the MCP tools rely on
(event types, bookings, schedules, apps, profile — read + write).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browsers reject Access-Control-Allow-Origin: * when the request
carries an Authorization header (credentialed request). Echo the
request's Origin header instead and add Access-Control-Allow-Credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…meout

The default StreamableHTTP transport uses SSE streaming (ReadableStream that stays
open until all events are flushed). On Vercel serverless, that stream never closes
naturally, causing every POST /mcp to time out after the 60-second function limit.

Setting enableJsonResponse: true switches to plain-JSON mode: the transport resolves
immediately after processing each JSON-RPC request and writes a single JSON body,
which completes well within the function time limit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…GET SSE on Vercel

Two issues caused the MCP client to get stuck after OAuth:

1. CORS preflight failure: the MCP client sends `mcp-protocol-version` and
   `last-event-id` as custom headers on every request after initialization.
   These were missing from Access-Control-Allow-Headers, so the browser's
   preflight rejected any request that included them (CORS error in DevTools).

2. Pending GET /mcp forever: after the `initialized` notification is
   acknowledged, the MCP client opens a GET SSE stream for server-initiated
   messages. On Vercel, that stream can never close (no persistent connections),
   so it stayed pending until the 60 s function timeout. Per the MCP spec, a
   405 response to GET tells the client "SSE not supported here" and it falls
   back gracefully to POST-only mode — no error thrown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tadata URL

Claude.ai sends MCP requests directly to the URL the user enters. If the user
enters "https://mcp.cal.com" (no /mcp suffix), all MCP POSTs land at "/" and
our handler returned 404 because it only matched "/mcp".

Two fixes:
1. The /mcp handler now also matches "/" so both base URL and explicit /mcp
   endpoint work transparently.
2. buildProtectedResourceMetadata now sets resource to "${serverUrl}/mcp"
   (the canonical MCP endpoint) so that clients that derive the endpoint from
   the resource field also get the correct path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tadata

Changing resource to "${serverUrl}/mcp" was wrong. Per RFC 9728 §3,
the client constructs the discovery URL by inserting
/.well-known/oauth-protected-resource before any path segment:
  resource "https://mcp.cal.com/mcp"
  → discovery URL: https://mcp.cal.com/.well-known/oauth-protected-resource/mcp

That path doesn't exist, so Claude.ai got a 404 and authorization failed.

Keep resource as the plain base URL so discovery stays at the working path:
  https://mcp.cal.com/.well-known/oauth-protected-resource

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s Vercel timeout

StreamableHTTPServerTransport internally uses @hono/node-server's adapter,
which converts the response body to a Web ReadableStream and then awaits
`reader.closed`. On Vercel serverless functions that promise never resolves,
causing every POST /mcp request to hang until the 60 s hard limit.

Replace it with a minimal in-process Transport: read the body with plain
Node.js streams, wire a trivial Transport object directly to McpServer,
drive messages in and collect responses out, then write JSON to `res` with
res.writeHead/res.end. No ReadableStream, no Hono adapter, no reader.closed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@volnei volnei enabled auto-merge (squash) April 16, 2026 11:00
devin-ai-integration[bot]

This comment was marked as resolved.

The "Apply suggestions from code review" commit (5370168) accidentally
added an extra `},` after the `send()` method's closing brace, producing
a syntax error that broke Lint, Type Check and the Vercel build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The combination of the MCP SDK types and zod-schema tool definitions
pushes tsc past the default 2 GB heap on CI's 2-vCPU Blacksmith runner,
failing Type Check with 'Ineffective mark-compacts near heap limit'.

Raising the limit via NODE_OPTIONS keeps the check green without
restructuring the type graph.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Vercel handler is an entry point that imports registerTools, which
transitively drags in the MCP SDK + every zod tool schema. Running tsc
over it blows past even a 4 GB heap on CI, same reason src/index.ts,
src/register-tools.ts and src/http-server.ts are already excluded.

Treating vercel-handler.ts the same way fixes the CI OOM without
touching the type graph. Reverts the NODE_OPTIONS heap bump from the
previous commit since it wasn't enough anyway.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 20 additional findings in Devin Review.

Open in Devin Review

Comment thread apps/mcp-server/src/vercel-handler.ts
Copy link
Copy Markdown
Member

@sahitya-chandra sahitya-chandra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@volnei volnei merged commit 7688f4a into main Apr 16, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants