diff --git a/apps/mcp-server/api/index.js b/apps/mcp-server/api/index.js new file mode 100644 index 0000000..4a2d458 --- /dev/null +++ b/apps/mcp-server/api/index.js @@ -0,0 +1,5 @@ +// Thin JS wrapper — imports the pre-compiled handler from dist/ so @vercel/node +// does not have to run TypeScript over the heavy imports in src/vercel-handler.ts. +// The `bun run build` step (executed by Vercel before bundling functions) +// produces dist/vercel-handler.js. +export { default } from "../dist/vercel-handler.js"; diff --git a/apps/mcp-server/api/index.ts b/apps/mcp-server/api/index.ts deleted file mode 100644 index ff4e13f..0000000 --- a/apps/mcp-server/api/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; - -import { authContext } from "../src/auth/context.js"; -import { - buildAuthorizationServerMetadata, - buildProtectedResourceMetadata, -} from "../src/auth/oauth-metadata.js"; -import { - handleAuthorize, - handleCallback, - handleRegister, - handleRevoke, - handleToken, - resolveCalAuthHeaders, - type OAuthConfig, -} from "../src/auth/oauth-handlers.js"; -import { loadConfig, type HttpConfig } from "../src/config.js"; -import { registerTools } from "../src/register-tools.js"; -import { SERVER_INSTRUCTIONS } from "../src/server-instructions.js"; -import { initDb, sql } from "../src/storage/db.js"; -import { countRegisteredClients } from "../src/storage/token-store.js"; - -/** - * Vercel serverless entry point. - * - * Each invocation is stateless — the MCP transport is created per request - * (`sessionIdGenerator: undefined`) and token/OAuth state lives in Postgres. - * There are no setInterval loops, in-memory session maps, or graceful-shutdown - * hooks because the runtime manages lifecycle for us. - */ - -let cachedConfig: HttpConfig | undefined; -function getConfig(): HttpConfig { - if (cachedConfig) return cachedConfig; - const config = loadConfig(); - if (config.transport !== "http") { - throw new Error("MCP_TRANSPORT must be 'http' on Vercel"); - } - cachedConfig = config; - return cachedConfig; -} - -let dbInitPromise: Promise | undefined; -function ensureDb(): Promise { - if (!dbInitPromise) { - dbInitPromise = initDb().catch((err) => { - dbInitPromise = undefined; - throw err; - }); - } - return dbInitPromise; -} - -function oauthConfigFromHttpConfig(config: HttpConfig): OAuthConfig { - return { - serverUrl: config.serverUrl, - calOAuthClientId: config.calOAuthClientId, - calOAuthClientSecret: config.calOAuthClientSecret, - calApiBaseUrl: config.calApiBaseUrl, - calAppBaseUrl: config.calAppBaseUrl, - }; -} - -function setCorsHeaders(res: ServerResponse, corsOrigin: string | undefined): void { - const origin = corsOrigin ?? "*"; - res.setHeader("Access-Control-Allow-Origin", origin); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id"); - res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id"); - if (origin !== "*") res.setHeader("Vary", "Origin"); -} - -function jsonError(res: ServerResponse, status: number, error: string, description?: string): void { - res.writeHead(status, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error, ...(description ? { error_description: description } : {}) })); -} - -export default async function handler(req: IncomingMessage, res: ServerResponse): Promise { - const config = getConfig(); - const oauthConfig = oauthConfigFromHttpConfig(config); - await ensureDb(); - - setCorsHeaders(res, config.corsOrigin); - - if (req.method === "OPTIONS") { - res.writeHead(204); - res.end(); - return; - } - - const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); - - // ── Health check ── - if (url.pathname === "/health") { - let dbOk = false; - try { - await sql`SELECT 1`; - dbOk = true; - } catch { - /* db not healthy */ - } - res.writeHead(dbOk ? 200 : 503, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: dbOk ? "ok" : "degraded", db: dbOk ? "ok" : "error" })); - return; - } - - // ── OAuth metadata ── - if (url.pathname === "/.well-known/oauth-authorization-server" && req.method === "GET") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify(buildAuthorizationServerMetadata({ serverUrl: oauthConfig.serverUrl }))); - return; - } - if (url.pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify(buildProtectedResourceMetadata({ serverUrl: oauthConfig.serverUrl }))); - return; - } - - // ── OAuth endpoints ── - if (url.pathname === "/oauth/register") { - const currentCount = await countRegisteredClients(); - if (currentCount >= config.maxRegisteredClients) { - jsonError(res, 503, "server_error", "Maximum number of registered clients reached"); - return; - } - await handleRegister(req, res); - return; - } - if (url.pathname === "/oauth/authorize" && req.method === "GET") { - await handleAuthorize(req, res, oauthConfig); - return; - } - if (url.pathname === "/oauth/callback" && req.method === "GET") { - await handleCallback(req, res, oauthConfig); - return; - } - if (url.pathname === "/oauth/token") { - await handleToken(req, res); - return; - } - if (url.pathname === "/oauth/revoke") { - await handleRevoke(req, res); - return; - } - - // ── MCP (stateless) ── - if (url.pathname === "/mcp") { - const authHeader = req.headers.authorization; - const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined; - - if (!bearerToken) { - res.writeHead(401, { - "Content-Type": "application/json", - "WWW-Authenticate": `Bearer resource_metadata="${oauthConfig.serverUrl}/.well-known/oauth-protected-resource"`, - }); - res.end(JSON.stringify({ error: "unauthorized", error_description: "Bearer token required" })); - return; - } - - const calAuthHeaders = await resolveCalAuthHeaders(bearerToken, oauthConfig); - if (!calAuthHeaders) { - res.writeHead(401, { - "Content-Type": "application/json", - "WWW-Authenticate": `Bearer resource_metadata="${oauthConfig.serverUrl}/.well-known/oauth-protected-resource"`, - }); - res.end(JSON.stringify({ error: "invalid_token", error_description: "Invalid or expired access token" })); - return; - } - - // DELETE in stateless mode is a no-op — there's no server-side session to terminate. - if (req.method === "DELETE") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "terminated" })); - return; - } - - const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - const server = new McpServer( - { name: "calcom-mcp-server", version: "0.1.0" }, - { instructions: SERVER_INSTRUCTIONS }, - ); - registerTools(server); - await server.connect(transport); - - // Close the transport when the response finishes so the MCP server is - // garbage-collected with the function invocation. - res.on("close", () => { - transport.close().catch(() => {}); - }); - - await authContext.run(calAuthHeaders, async () => { - await transport.handleRequest(req, res); - }); - return; - } - - jsonError(res, 404, "not_found"); -} diff --git a/apps/mcp-server/src/auth/oauth-handlers.ts b/apps/mcp-server/src/auth/oauth-handlers.ts index ce83fa4..f9fda02 100644 --- a/apps/mcp-server/src/auth/oauth-handlers.ts +++ b/apps/mcp-server/src/auth/oauth-handlers.ts @@ -29,6 +29,8 @@ export interface OAuthConfig { calApiBaseUrl: string; /** Cal.com app base URL for authorize redirect (default: https://app.cal.com) */ calAppBaseUrl?: string; + /** Space-separated Cal.com OAuth scopes (e.g. "BOOKING_READ BOOKING_WRITE") */ + calOAuthScopes?: string; } const MAX_BODY_SIZE = 1024 * 1024; // 1 MB @@ -186,6 +188,9 @@ export async function handleAuthorize( calAuthUrl.searchParams.set("code_challenge_method", "S256"); } calAuthUrl.searchParams.set("response_type", "code"); + if (config.calOAuthScopes) { + calAuthUrl.searchParams.set("scope", config.calOAuthScopes); + } res.writeHead(302, { Location: calAuthUrl.toString() }); res.end(); diff --git a/apps/mcp-server/src/auth/oauth-metadata.ts b/apps/mcp-server/src/auth/oauth-metadata.ts index d661501..f4aac0b 100644 --- a/apps/mcp-server/src/auth/oauth-metadata.ts +++ b/apps/mcp-server/src/auth/oauth-metadata.ts @@ -34,6 +34,9 @@ export function buildAuthorizationServerMetadata(config: OAuthServerConfig): Rec export function buildProtectedResourceMetadata(config: OAuthServerConfig): Record { const serverUrl = config.serverUrl.replace(/\/+$/, ""); return { + // resource is the server identifier (base URL). Per RFC 9728 §3 the client constructs + // the discovery URL as "https://host/.well-known/oauth-protected-resource" — no path + // suffix — so resource must stay as the base URL, not the /mcp endpoint path. resource: serverUrl, authorization_servers: [serverUrl], bearer_methods_supported: ["header"], diff --git a/apps/mcp-server/src/config.ts b/apps/mcp-server/src/config.ts index 07d5720..1f542e7 100644 --- a/apps/mcp-server/src/config.ts +++ b/apps/mcp-server/src/config.ts @@ -50,6 +50,11 @@ const httpSchema = baseSchema.extend({ .regex(/^[0-9a-fA-F]+$/, "TOKEN_ENCRYPTION_KEY must be valid hex"), serverUrl: z.string().url("MCP_SERVER_URL must be a valid URL"), databaseUrl: z.string().min(1, "DATABASE_URL is required for HTTP mode"), + calOAuthScopes: z + .string() + .default( + "EVENT_TYPE_READ EVENT_TYPE_WRITE BOOKING_READ BOOKING_WRITE SCHEDULE_READ SCHEDULE_WRITE APPS_READ APPS_WRITE PROFILE_READ PROFILE_WRITE", + ), rateLimitWindowMs: z.coerce.number().int().positive().default(60_000), rateLimitMax: z.coerce.number().int().positive().default(30), maxSessions: z.coerce.number().int().positive().default(10_000), @@ -84,6 +89,7 @@ function readEnv(): Record { tokenEncryptionKey: process.env.TOKEN_ENCRYPTION_KEY || undefined, serverUrl: process.env.MCP_SERVER_URL || undefined, databaseUrl: process.env.DATABASE_URL || undefined, + calOAuthScopes: process.env.CAL_OAUTH_SCOPES || undefined, rateLimitWindowMs: process.env.RATE_LIMIT_WINDOW_MS || undefined, rateLimitMax: process.env.RATE_LIMIT_MAX || undefined, maxSessions: process.env.MAX_SESSIONS || undefined, diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 35c972b..1b30d09 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -26,6 +26,7 @@ async function main(): Promise { calOAuthClientSecret: httpConfig.calOAuthClientSecret, calApiBaseUrl: httpConfig.calApiBaseUrl, calAppBaseUrl: httpConfig.calAppBaseUrl, + calOAuthScopes: httpConfig.calOAuthScopes, }, rateLimitWindowMs: httpConfig.rateLimitWindowMs, rateLimitMax: httpConfig.rateLimitMax, diff --git a/apps/mcp-server/src/storage/db.ts b/apps/mcp-server/src/storage/db.ts index 706d3fd..cf7bb08 100644 --- a/apps/mcp-server/src/storage/db.ts +++ b/apps/mcp-server/src/storage/db.ts @@ -4,7 +4,10 @@ export const pool = createPool({ connectionString: process.env.DATABASE_URL, }); -export const sql = pool.sql; +// Bind so the tagged template keeps its `this` — destructuring `pool.sql` +// loses the binding and @vercel/postgres then reads `connectionString` off +// `undefined` at call time. +export const sql = pool.sql.bind(pool); let initialized = false; diff --git a/apps/mcp-server/src/vercel-handler.ts b/apps/mcp-server/src/vercel-handler.ts new file mode 100644 index 0000000..deaf91d --- /dev/null +++ b/apps/mcp-server/src/vercel-handler.ts @@ -0,0 +1,316 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; + +import { authContext } from "./auth/context.js"; +import { + buildAuthorizationServerMetadata, + buildProtectedResourceMetadata, +} from "./auth/oauth-metadata.js"; +import { + handleAuthorize, + handleCallback, + handleRegister, + handleRevoke, + handleToken, + resolveCalAuthHeaders, + type OAuthConfig, +} from "./auth/oauth-handlers.js"; +import { loadConfig, type HttpConfig } from "./config.js"; +import { registerTools } from "./register-tools.js"; +import { SERVER_INSTRUCTIONS } from "./server-instructions.js"; +import { initDb, sql } from "./storage/db.js"; +import { countRegisteredClients } from "./storage/token-store.js"; + +/** + * Vercel serverless entry point. + * + * Each invocation is stateless — the MCP transport is created per request + * (`sessionIdGenerator: undefined`) and token/OAuth state lives in Postgres. + * There are no setInterval loops, in-memory session maps, or graceful-shutdown + * hooks because the runtime manages lifecycle for us. + * + * This module lives under `src/` (not `api/`) so it gets compiled by our + * `bun run build` step into `dist/vercel-handler.js`. The Vercel function + * at `api/index.js` is a thin JS wrapper re-exporting from the compiled + * output, which avoids @vercel/node having to run TypeScript over the + * heavy `@modelcontextprotocol/sdk` types (which OOMs in practice). + */ + +let cachedConfig: HttpConfig | undefined; +function getConfig(): HttpConfig { + if (cachedConfig) return cachedConfig; + // This handler only runs on Vercel, which is always HTTP mode. Force the + // transport so operators do not have to remember to set MCP_TRANSPORT=http. + process.env.MCP_TRANSPORT = "http"; + const config = loadConfig(); + if (config.transport !== "http") { + throw new Error("MCP_TRANSPORT must be 'http' on Vercel"); + } + cachedConfig = config; + return cachedConfig; +} + +let dbInitPromise: Promise | undefined; +function ensureDb(): Promise { + if (!dbInitPromise) { + dbInitPromise = initDb().catch((err) => { + dbInitPromise = undefined; + throw err; + }); + } + return dbInitPromise; +} + +function oauthConfigFromHttpConfig(config: HttpConfig): OAuthConfig { + return { + serverUrl: config.serverUrl, + calOAuthClientId: config.calOAuthClientId, + calOAuthClientSecret: config.calOAuthClientSecret, + calApiBaseUrl: config.calApiBaseUrl, + calAppBaseUrl: config.calAppBaseUrl, + calOAuthScopes: config.calOAuthScopes, + }; +} + +function setCorsHeaders(req: IncomingMessage, res: ServerResponse, corsOrigin: string | undefined): void { + // Credentialed requests (Authorization header) require an explicit origin, + // not "*". Fall back to echoing the request's Origin header. + const origin = corsOrigin ?? req.headers.origin ?? "*"; + res.setHeader("Access-Control-Allow-Origin", origin); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + // Include mcp-protocol-version and last-event-id: the MCP client adds these custom + // headers on every request after initialization. Without them the browser's CORS + // preflight fails with "header not allowed". + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version, last-event-id", + ); + res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id"); + res.setHeader("Access-Control-Allow-Credentials", "true"); + res.setHeader("Vary", "Origin"); +} + +function jsonError(res: ServerResponse, status: number, error: string, description?: string): void { + res.writeHead(status, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error, ...(description ? { error_description: description } : {}) })); +} + +export default async function handler(req: IncomingMessage, res: ServerResponse): Promise { + const config = getConfig(); + const oauthConfig = oauthConfigFromHttpConfig(config); + await ensureDb(); + + setCorsHeaders(req, res, config.corsOrigin); + + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); + + // ── Health check ── + if (url.pathname === "/health") { + let dbOk = false; + try { + await sql`SELECT 1`; + dbOk = true; + } catch { + /* db not healthy */ + } + res.writeHead(dbOk ? 200 : 503, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: dbOk ? "ok" : "degraded", db: dbOk ? "ok" : "error" })); + return; + } + + // ── OAuth metadata ── + if (url.pathname === "/.well-known/oauth-authorization-server" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(buildAuthorizationServerMetadata({ serverUrl: oauthConfig.serverUrl }))); + return; + } + if (url.pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(buildProtectedResourceMetadata({ serverUrl: oauthConfig.serverUrl }))); + return; + } + + // ── OAuth endpoints ── + if (url.pathname === "/oauth/register") { + const currentCount = await countRegisteredClients(); + if (currentCount >= config.maxRegisteredClients) { + jsonError(res, 503, "server_error", "Maximum number of registered clients reached"); + return; + } + await handleRegister(req, res); + return; + } + if (url.pathname === "/oauth/authorize" && req.method === "GET") { + await handleAuthorize(req, res, oauthConfig); + return; + } + if (url.pathname === "/oauth/callback" && req.method === "GET") { + await handleCallback(req, res, oauthConfig); + return; + } + if (url.pathname === "/oauth/token") { + await handleToken(req, res); + return; + } + if (url.pathname === "/oauth/revoke") { + await handleRevoke(req, res); + return; + } + + // ── MCP (stateless) ── + // Accept both /mcp (canonical) and / (base URL) so that Claude.ai works whether + // the user enters "https://mcp.cal.com" or "https://mcp.cal.com/mcp". + if (url.pathname === "/mcp" || url.pathname === "/") { + const authHeader = req.headers.authorization; + const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined; + + if (!bearerToken) { + res.writeHead(401, { + "Content-Type": "application/json", + "WWW-Authenticate": `Bearer resource_metadata="${oauthConfig.serverUrl.replace(/\/+$/, "")}/.well-known/oauth-protected-resource"`, + }); + res.end(JSON.stringify({ error: "unauthorized", error_description: "Bearer token required" })); + return; + } + + const calAuthHeaders = await resolveCalAuthHeaders(bearerToken, oauthConfig); + if (!calAuthHeaders) { + res.writeHead(401, { + "Content-Type": "application/json", + "WWW-Authenticate": `Bearer resource_metadata="${oauthConfig.serverUrl.replace(/\/+$/, "")}/.well-known/oauth-protected-resource"`, + }); + res.end(JSON.stringify({ error: "invalid_token", error_description: "Invalid or expired access token" })); + return; + } + + // DELETE in stateless mode is a no-op — there's no server-side session to terminate. + if (req.method === "DELETE") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "terminated" })); + return; + } + + // GET opens a long-lived SSE stream for server-initiated messages. Vercel + // serverless functions cannot hold persistent connections, so we return 405. + // The MCP client treats 405 as "SSE not supported" and switches to POST-only + // mode — no error, it just skips the standalone stream. + if (req.method === "GET") { + res.writeHead(405, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "method_not_allowed", error_description: "SSE stream not supported in serverless mode" })); + return; + } + + // ── Minimal in-process JSON-RPC transport ────────────────────────────────── + // + // StreamableHTTPServerTransport uses @hono/node-server's getRequestListener + // under the hood. That adapter converts the Node.js IncomingMessage body to a + // Web ReadableStream and then awaits `reader.closed` before it considers the + // response done. On Vercel, that promise never resolves → 60 s timeout. + // + // We bypass all of that by: + // 1. Reading the raw body ourselves (pure Node.js streams — always works). + // 2. Wiring a trivial Transport object straight to McpServer. + // 3. Driving messages in → collecting responses out → writing JSON directly. + // No ReadableStream, no Hono, no reader.closed, no SSE. + + // -- 1. Read request body -------------------------------------------------- + const bodyText = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on("data", (chunk: Buffer) => chunks.push(chunk)); + req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + req.on("error", reject); + }); + + let rawMessage: unknown; + try { + rawMessage = JSON.parse(bodyText); + } catch { + jsonError(res, 400, "parse_error", "Request body is not valid JSON"); + return; + } + + const messages: JSONRPCMessage[] = (Array.isArray(rawMessage) ? rawMessage : [rawMessage]) as JSONRPCMessage[]; + + // JSON-RPC requests have an `id` field; notifications do not. + const requestIds = messages + .filter((m): m is JSONRPCMessage & { id: string | number } => "id" in m && m.id != null) + .map((m) => (m as { id: string | number }).id); + + // If there are no requests (pure notifications batch), acknowledge with 202. + if (requestIds.length === 0) { + res.writeHead(202); + res.end(); + return; + } + + // -- 2. Build minimal Transport -------------------------------------------- + const collectedResponses = new Map(); + let resolveAll!: () => void; + const allDone = new Promise((r) => { resolveAll = r; }); + + const transport: Transport = { + // These callbacks are set by McpServer.connect() before start() returns. + onmessage: undefined, + onclose: undefined, + onerror: undefined, + + async start() { /* nothing to set up */ }, + async close() { transport.onclose?.(); }, + + async send(msg: JSONRPCMessage) { + // Only capture responses (they have `id` + `result`/`error`, but no `method`); + // ignore server-initiated requests (which have `method` + `id`). + if ("id" in msg && msg.id != null && !("method" in msg)) { + collectedResponses.set((msg as { id: string | number }).id, msg); + if (collectedResponses.size >= requestIds.length) resolveAll(); + } + }, + }; + + // -- 3. Connect McpServer and drive messages -------------------------------- + const server = new McpServer( + { name: "calcom-mcp-server", version: "0.1.0" }, + { instructions: SERVER_INSTRUCTIONS }, + ); + registerTools(server); + await server.connect(transport); + + // 55 s gives a ~5 s buffer before Vercel's 60 s hard limit. + const timeoutMs = 55_000; + + try { + await authContext.run(calAuthHeaders, async () => { + for (const msg of messages) { + transport.onmessage?.(msg, {}); + } + await Promise.race([ + allDone, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`MCP handler timed out after ${timeoutMs} ms`)), timeoutMs), + ), + ]); + }); + } finally { + transport.close().catch(() => {}); + } + + // -- 4. Return JSON -------------------------------------------------------- + const responsePayload = Array.isArray(rawMessage) + ? requestIds.map((id) => collectedResponses.get(id) ?? null) + : collectedResponses.get(requestIds[0]) ?? null; + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(responsePayload)); + return; + } + + jsonError(res, 404, "not_found"); +} diff --git a/apps/mcp-server/tsconfig.check.json b/apps/mcp-server/tsconfig.check.json index 3d846f5..eba18cf 100644 --- a/apps/mcp-server/tsconfig.check.json +++ b/apps/mcp-server/tsconfig.check.json @@ -3,5 +3,5 @@ "compilerOptions": { "skipLibCheck": true }, - "exclude": ["node_modules", "dist", "src/index.ts", "src/register-tools.ts", "src/http-server.ts", "src/**/*.test.ts"] + "exclude": ["node_modules", "dist", "src/index.ts", "src/register-tools.ts", "src/http-server.ts", "src/vercel-handler.ts", "src/**/*.test.ts"] } diff --git a/apps/mcp-server/vercel.json b/apps/mcp-server/vercel.json index 31328e7..22d7946 100644 --- a/apps/mcp-server/vercel.json +++ b/apps/mcp-server/vercel.json @@ -1,13 +1,12 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run build", - "installCommand": "bun install", - "framework": null, + "outputDirectory": ".", "rewrites": [ { "source": "/(.*)", "destination": "/api/index" } ], "functions": { - "api/index.ts": { + "api/index.js": { "maxDuration": 60 } }