fix(mcp-server): make Vercel deployment actually work#79
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
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>
…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>
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the Vercel deployment that was hanging at TypeScript compilation of
api/index.ts. The root cause:@vercel/noderuns TypeScript over the function's dep graph, which OOMs on@modelcontextprotocol/sdk's large types.Fix
src/vercel-handler.tsso it's compiled by our existingbun run buildstep intodist/vercel-handler.jsapi/index.tswith a one-line JS wrapper (api/index.js) that re-exports from the compiled output@vercel/nodenow sees only plain JS inapi/— no TypeScript compilation, no OOMvercel.json:buildCommand: "bun run build"sodist/exists before functions are bundledoutputDirectory: "."so Vercel stops demanding apublic/folderapi/index.jsVercel dashboard
With
vercel.jsonnow controlling build + output, Framework Settings overrides can all be turned off. Only settings that still matter in the dashboard:apps/mcp-serverDATABASE_URL,CAL_OAUTH_CLIENT_ID,CAL_OAUTH_CLIENT_SECRET,TOKEN_ENCRYPTION_KEY,MCP_SERVER_URL,MCP_TRANSPORT=httpTest plan
bun run build— producesdist/vercel-handler.jsbun run test— 203 tests pass/health🤖 Generated with Claude Code