@mcphero/vercel 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @mcphero/vercel@1.1.7 build /Users/atomic/projects/ai/mcphero/packages/vercel
2
+ > @mcphero/vercel@1.2.0 build /Users/atomic/projects/ai/mcphero/packages/vercel
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -9,9 +9,9 @@ CLI Using tsup config: /Users/atomic/projects/ai/mcphero/packages/vercel/tsup.co
9
9
  CLI Target: es2022
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
- ESM build/index.js 6.46 KB
13
- ESM build/index.js.map 11.48 KB
14
- ESM ⚡️ Build success in 7ms
12
+ ESM build/index.js 6.62 KB
13
+ ESM build/index.js.map 12.29 KB
14
+ ESM ⚡️ Build success in 9ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 696ms
17
- DTS build/index.d.ts 593.00 B
16
+ DTS ⚡️ Build success in 666ms
17
+ DTS build/index.d.ts 612.00 B
@@ -1,12 +1,12 @@
1
1
 
2
- > @mcphero/vercel@1.1.7 check /Users/atomic/projects/ai/mcphero/packages/vercel
2
+ > @mcphero/vercel@1.2.0 check /Users/atomic/projects/ai/mcphero/packages/vercel
3
3
  > pnpm lint && pnpm typecheck
4
4
 
5
5
 
6
- > @mcphero/vercel@1.1.7 lint /Users/atomic/projects/ai/mcphero/packages/vercel
6
+ > @mcphero/vercel@1.2.0 lint /Users/atomic/projects/ai/mcphero/packages/vercel
7
7
  > eslint
8
8
 
9
9
 
10
- > @mcphero/vercel@1.1.7 typecheck /Users/atomic/projects/ai/mcphero/packages/vercel
10
+ > @mcphero/vercel@1.2.0 typecheck /Users/atomic/projects/ai/mcphero/packages/vercel
11
11
  > tsc --noEmit
12
12
 
@@ -0,0 +1,5 @@
1
+
2
+ 
3
+ > @mcphero/vercel@1.2.0 lint /Users/atomic/projects/ai/mcphero/packages/vercel
4
+ > eslint
5
+
@@ -1,14 +1,14 @@
1
1
 
2
2
  
3
- > @mcphero/vercel@1.1.7 prepack /Users/atomic/projects/ai/mcphero/packages/vercel
3
+ > @mcphero/vercel@1.2.0 prepack /Users/atomic/projects/ai/mcphero/packages/vercel
4
4
  > pnpm clean && pnpm build
5
5
 
6
6
 
7
- > @mcphero/vercel@1.1.7 clean /Users/atomic/projects/ai/mcphero/packages/vercel
7
+ > @mcphero/vercel@1.2.0 clean /Users/atomic/projects/ai/mcphero/packages/vercel
8
8
  > rimraf build
9
9
 
10
10
 
11
- > @mcphero/vercel@1.1.7 build /Users/atomic/projects/ai/mcphero/packages/vercel
11
+ > @mcphero/vercel@1.2.0 build /Users/atomic/projects/ai/mcphero/packages/vercel
12
12
  > tsup
13
13
 
14
14
  CLI Building entry: src/index.ts
@@ -20,7 +20,7 @@
20
20
  ESM Build start
21
21
  ESM build/index.js 6.46 KB
22
22
  ESM build/index.js.map 11.48 KB
23
- ESM ⚡️ Build success in 9ms
23
+ ESM ⚡️ Build success in 12ms
24
24
  DTS Build start
25
- DTS ⚡️ Build success in 732ms
25
+ DTS ⚡️ Build success in 868ms
26
26
  DTS build/index.d.ts 593.00 B
package/build/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { AdapterGenerator } from '@mcphero/core';
4
4
  interface VercelAdapterOptions {
5
5
  enableJsonResponse?: boolean;
6
6
  auth?: AuthConfig;
7
+ path?: string;
7
8
  }
8
9
  interface VercelAdapter {
9
10
  adapter: AdapterGenerator;
package/build/index.js CHANGED
@@ -7,11 +7,13 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
7
7
  import { capitalCase, pascalCase } from "change-case";
8
8
  var CORS_HEADERS = {
9
9
  "Access-Control-Allow-Origin": "*",
10
- "Access-Control-Allow-Methods": "GET, OPTIONS",
10
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
11
11
  "Access-Control-Allow-Headers": "*",
12
12
  "Access-Control-Max-Age": "86400"
13
13
  };
14
14
  function vercel(options = {}) {
15
+ const mcpPath = options.path ?? "/mcp";
16
+ const callbackPath = options.auth?.callbackPath ?? "/auth/callback";
15
17
  let _actions = [];
16
18
  let _options = null;
17
19
  let _context = null;
@@ -19,73 +21,48 @@ function vercel(options = {}) {
19
21
  const _ready = new Promise((resolve) => {
20
22
  _readyResolve = resolve;
21
23
  });
24
+ const validPaths = /* @__PURE__ */ new Set([mcpPath]);
25
+ if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {
26
+ validPaths.add("/.well-known/oauth-protected-resource");
27
+ }
28
+ if (options.auth?.provider) {
29
+ validPaths.add("/.well-known/oauth-authorization-server");
30
+ validPaths.add("/authorize");
31
+ validPaths.add(callbackPath);
32
+ validPaths.add("/token");
33
+ validPaths.add("/register");
34
+ }
35
+ const oauthPaths = options.auth?.provider ? {
36
+ "/.well-known/oauth-authorization-server": ["GET"],
37
+ "/authorize": ["GET"],
38
+ [callbackPath]: ["GET"],
39
+ "/token": ["POST"],
40
+ "/register": ["POST"]
41
+ } : null;
22
42
  const handleRequest = async (request) => {
23
- await _ready;
24
43
  const url = new URL(request.url);
25
- if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {
26
- if (url.pathname === "/.well-known/oauth-protected-resource" || url.pathname.endsWith("/.well-known/oauth-protected-resource")) {
27
- if (request.method === "OPTIONS") {
28
- return new Response(null, { status: 200, headers: CORS_HEADERS });
29
- }
30
- const metadata = generateProtectedResourceMetadata(options.auth.resourceUrl, options.auth.authorizationServers);
31
- return new Response(JSON.stringify(metadata), {
32
- headers: { ...CORS_HEADERS, "Content-Type": "application/json", "Cache-Control": "max-age=3600" }
33
- });
34
- }
44
+ if (!validPaths.has(url.pathname)) {
45
+ return new Response("Not Found", { status: 404 });
35
46
  }
36
- if (options.auth?.provider) {
37
- const provider = options.auth.provider;
38
- const oauthPaths = {
39
- "/.well-known/oauth-authorization-server": ["GET"],
40
- "/authorize": ["GET"],
41
- "/auth/callback": ["GET"],
42
- "/token": ["POST"],
43
- "/register": ["POST"]
44
- };
45
- const match = Object.entries(oauthPaths).find(([path]) => url.pathname === path || url.pathname.endsWith(path));
46
- if (match) {
47
- const [, methods] = match;
48
- if (request.method === "OPTIONS") {
49
- return new Response(null, { status: 200, headers: CORS_HEADERS });
50
- }
47
+ if (request.method === "OPTIONS") {
48
+ return new Response(null, { status: 200, headers: CORS_HEADERS });
49
+ }
50
+ if (url.pathname === "/.well-known/oauth-protected-resource") {
51
+ const metadata = generateProtectedResourceMetadata(options.auth.resourceUrl, options.auth.authorizationServers);
52
+ return new Response(JSON.stringify(metadata), {
53
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json", "Cache-Control": "max-age=3600" }
54
+ });
55
+ }
56
+ if (oauthPaths) {
57
+ const methods = oauthPaths[url.pathname];
58
+ if (methods) {
51
59
  if (!methods.includes(request.method)) {
52
60
  return new Response("Method not allowed", { status: 405 });
53
61
  }
54
- const toOAuthReq = async () => {
55
- let body;
56
- if (request.method === "POST") {
57
- const contentType = request.headers.get("content-type") ?? "";
58
- const text = await request.text();
59
- if (contentType.includes("json")) {
60
- body = JSON.parse(text);
61
- } else {
62
- body = Object.fromEntries(new URLSearchParams(text));
63
- }
64
- }
65
- return {
66
- method: request.method,
67
- url,
68
- headers: Object.fromEntries(request.headers.entries()),
69
- body
70
- };
71
- };
72
- const oauthReq = await toOAuthReq();
73
- let oauthRes;
74
- if (url.pathname.endsWith("/.well-known/oauth-authorization-server")) {
75
- oauthRes = provider.metadata();
76
- } else if (url.pathname.endsWith("/authorize")) {
77
- oauthRes = await provider.authorize(oauthReq);
78
- } else if (url.pathname.endsWith("/auth/callback")) {
79
- oauthRes = await provider.callback(oauthReq);
80
- } else if (url.pathname.endsWith("/token")) {
81
- oauthRes = await provider.token(oauthReq);
82
- } else {
83
- oauthRes = await provider.register(oauthReq);
84
- }
85
- const responseBody = oauthRes.body ? typeof oauthRes.body === "string" ? oauthRes.body : JSON.stringify(oauthRes.body) : null;
86
- return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers });
62
+ return handleOAuth(url, request, options.auth.provider);
87
63
  }
88
64
  }
65
+ await _ready;
89
66
  let requestContext = _context;
90
67
  if (options.auth) {
91
68
  const result = await validateToken(request.headers.get("authorization"), options.auth);
@@ -150,6 +127,41 @@ function vercel(options = {}) {
150
127
  await server.connect(transport);
151
128
  return transport.handleRequest(request);
152
129
  };
130
+ async function handleOAuth(url, request, provider) {
131
+ const toOAuthReq = async () => {
132
+ let body;
133
+ if (request.method === "POST") {
134
+ const contentType = request.headers.get("content-type") ?? "";
135
+ const text = await request.text();
136
+ if (contentType.includes("json")) {
137
+ body = JSON.parse(text);
138
+ } else {
139
+ body = Object.fromEntries(new URLSearchParams(text));
140
+ }
141
+ }
142
+ return {
143
+ method: request.method,
144
+ url,
145
+ headers: Object.fromEntries(request.headers.entries()),
146
+ body
147
+ };
148
+ };
149
+ const oauthReq = await toOAuthReq();
150
+ let oauthRes;
151
+ if (url.pathname === "/.well-known/oauth-authorization-server") {
152
+ oauthRes = provider.metadata();
153
+ } else if (url.pathname === "/authorize") {
154
+ oauthRes = await provider.authorize(oauthReq);
155
+ } else if (url.pathname === callbackPath) {
156
+ oauthRes = await provider.callback(oauthReq);
157
+ } else if (url.pathname === "/token") {
158
+ oauthRes = await provider.token(oauthReq);
159
+ } else {
160
+ oauthRes = await provider.register(oauthReq);
161
+ }
162
+ const responseBody = oauthRes.body ? typeof oauthRes.body === "string" ? oauthRes.body : JSON.stringify(oauthRes.body) : null;
163
+ return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers });
164
+ }
153
165
  const adapter = (mcpHeroOptions, baseContext) => {
154
166
  _options = mcpHeroOptions;
155
167
  _context = baseContext.fork({ adapter: "vercel" });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapter/vercel.ts"],"sourcesContent":["import { AuthConfig, generateProtectedResourceMetadata, OAuthRequest, validateToken } from '@mcphero/auth'\nimport { Action, AdapterGenerator, MCPHeroContext, MCPHeroOptions, toolResponse } from '@mcphero/core'\nimport { createLogger } from '@mcphero/logger'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'\nimport { capitalCase, pascalCase } from 'change-case'\n\nexport interface VercelAdapterOptions {\n enableJsonResponse?: boolean\n auth?: AuthConfig\n}\n\nexport interface VercelAdapter {\n adapter: AdapterGenerator\n handler: (request: Request) => Promise<Response>\n GET: (request: Request) => Promise<Response>\n POST: (request: Request) => Promise<Response>\n DELETE: (request: Request) => Promise<Response>\n}\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, OPTIONS',\n 'Access-Control-Allow-Headers': '*',\n 'Access-Control-Max-Age': '86400'\n}\n\nexport function vercel(options: VercelAdapterOptions = {}): VercelAdapter {\n let _actions: Action[] = []\n let _options: MCPHeroOptions | null = null\n let _context: MCPHeroContext | null = null\n let _readyResolve: () => void\n const _ready = new Promise<void>((resolve) => {\n _readyResolve = resolve\n })\n\n const handleRequest = async (request: Request): Promise<Response> => {\n await _ready\n\n const url = new URL(request.url)\n\n if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {\n if (url.pathname === '/.well-known/oauth-protected-resource' || url.pathname.endsWith('/.well-known/oauth-protected-resource')) {\n if (request.method === 'OPTIONS') {\n return new Response(null, { status: 200, headers: CORS_HEADERS })\n }\n const metadata = generateProtectedResourceMetadata(options.auth.resourceUrl, options.auth.authorizationServers)\n return new Response(JSON.stringify(metadata), {\n headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' }\n })\n }\n }\n\n if (options.auth?.provider) {\n const provider = options.auth.provider\n const oauthPaths: Record<string, string[]> = {\n '/.well-known/oauth-authorization-server': ['GET'],\n '/authorize': ['GET'],\n '/auth/callback': ['GET'],\n '/token': ['POST'],\n '/register': ['POST']\n }\n const match = Object.entries(oauthPaths).find(([path]) => url.pathname === path || url.pathname.endsWith(path))\n if (match) {\n const [, methods] = match\n if (request.method === 'OPTIONS') {\n return new Response(null, { status: 200, headers: CORS_HEADERS })\n }\n if (!methods.includes(request.method)) {\n return new Response('Method not allowed', { status: 405 })\n }\n const toOAuthReq = async (): Promise<OAuthRequest> => {\n let body: Record<string, string> | undefined\n if (request.method === 'POST') {\n const contentType = request.headers.get('content-type') ?? ''\n const text = await request.text()\n if (contentType.includes('json')) {\n body = JSON.parse(text)\n } else {\n body = Object.fromEntries(new URLSearchParams(text))\n }\n }\n return {\n method: request.method,\n url,\n headers: Object.fromEntries(request.headers.entries()),\n body\n }\n }\n const oauthReq = await toOAuthReq()\n let oauthRes\n if (url.pathname.endsWith('/.well-known/oauth-authorization-server')) {\n oauthRes = provider.metadata()\n } else if (url.pathname.endsWith('/authorize')) {\n oauthRes = await provider.authorize(oauthReq)\n } else if (url.pathname.endsWith('/auth/callback')) {\n oauthRes = await provider.callback(oauthReq)\n } else if (url.pathname.endsWith('/token')) {\n oauthRes = await provider.token(oauthReq)\n } else {\n oauthRes = await provider.register(oauthReq)\n }\n const responseBody = oauthRes.body ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body)) : null\n return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })\n }\n }\n\n let requestContext = _context!\n if (options.auth) {\n const result = await validateToken(request.headers.get('authorization'), options.auth)\n if (result.error) {\n return new Response(JSON.stringify(result.error.body), {\n status: result.error.statusCode,\n headers: result.error.headers\n })\n }\n if (result.auth) {\n requestContext = _context!.fork({ auth: result.auth })\n }\n }\n\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: options.enableJsonResponse\n })\n\n const server = new McpServer({\n name: _options!.name,\n description: _options!.description,\n version: _options!.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n\n for (const action of _actions) {\n server.registerTool(pascalCase(action.name), {\n title: capitalCase(action.name),\n description: action.description,\n inputSchema: action.input\n }, async (input, extra) => {\n const logger = createLogger({\n stream: process.stderr,\n onLog: (level, data) => {\n extra.sendNotification({ method: 'notifications/message', params: { level, data } })\n },\n onProgress: ({ progress, total, message }) => {\n if (!extra._meta?.progressToken) { return }\n extra.sendNotification({\n method: 'notifications/progress',\n params: {\n progress,\n total,\n message,\n progressToken: extra._meta.progressToken\n }\n })\n }\n })\n return action.run(input, requestContext.fork({ logger, extra })).then((result) => {\n return toolResponse(result)\n }).catch((error) => {\n if (error instanceof Error) {\n return toolResponse({ success: false, name: error.name, message: error.message, stack: error.stack })\n } else {\n return toolResponse({ success: false, name: 'Unknown Error', message: 'An unknown error occured', error })\n }\n })\n })\n }\n\n await server.connect(transport)\n return transport.handleRequest(request)\n }\n\n const adapter: AdapterGenerator = (mcpHeroOptions: MCPHeroOptions, baseContext: MCPHeroContext) => {\n _options = mcpHeroOptions\n _context = baseContext.fork({ adapter: 'vercel' })\n return {\n context: _context,\n start: async (actions) => {\n _actions = actions\n _readyResolve()\n },\n stop: async () => {\n _actions = []\n }\n }\n }\n\n return {\n adapter,\n handler: handleRequest,\n GET: handleRequest,\n POST: handleRequest,\n DELETE: handleRequest\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/adapter/vercel.ts"],"sourcesContent":["import { AuthConfig, generateProtectedResourceMetadata, OAuthRequest, validateToken } from '@mcphero/auth'\nimport { Action, AdapterGenerator, MCPHeroContext, MCPHeroOptions, toolResponse } from '@mcphero/core'\nimport { createLogger } from '@mcphero/logger'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'\nimport { capitalCase, pascalCase } from 'change-case'\n\nexport interface VercelAdapterOptions {\n enableJsonResponse?: boolean\n auth?: AuthConfig\n path?: string\n}\n\nexport interface VercelAdapter {\n adapter: AdapterGenerator\n handler: (request: Request) => Promise<Response>\n GET: (request: Request) => Promise<Response>\n POST: (request: Request) => Promise<Response>\n DELETE: (request: Request) => Promise<Response>\n}\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',\n 'Access-Control-Allow-Headers': '*',\n 'Access-Control-Max-Age': '86400'\n}\n\nexport function vercel(options: VercelAdapterOptions = {}): VercelAdapter {\n const mcpPath = options.path ?? '/mcp'\n const callbackPath = options.auth?.callbackPath ?? '/auth/callback'\n\n let _actions: Action[] = []\n let _options: MCPHeroOptions | null = null\n let _context: MCPHeroContext | null = null\n let _readyResolve: () => void\n const _ready = new Promise<void>((resolve) => {\n _readyResolve = resolve\n })\n\n // Pre-compute valid paths for fast rejection of bogus requests\n const validPaths = new Set<string>([mcpPath])\n if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {\n validPaths.add('/.well-known/oauth-protected-resource')\n }\n if (options.auth?.provider) {\n validPaths.add('/.well-known/oauth-authorization-server')\n validPaths.add('/authorize')\n validPaths.add(callbackPath)\n validPaths.add('/token')\n validPaths.add('/register')\n }\n\n // OAuth path → allowed methods (built once, not per-request)\n const oauthPaths: Record<string, string[]> | null = options.auth?.provider\n ? {\n '/.well-known/oauth-authorization-server': ['GET'],\n '/authorize': ['GET'],\n [callbackPath]: ['GET'],\n '/token': ['POST'],\n '/register': ['POST']\n }\n : null\n\n const handleRequest = async (request: Request): Promise<Response> => {\n const url = new URL(request.url)\n\n // Fast rejection — no async work, no allocations for unknown paths\n if (!validPaths.has(url.pathname)) {\n return new Response('Not Found', { status: 404 })\n }\n\n // CORS preflight\n if (request.method === 'OPTIONS') {\n return new Response(null, { status: 200, headers: CORS_HEADERS })\n }\n\n // Protected resource metadata (RFC 9728)\n if (url.pathname === '/.well-known/oauth-protected-resource') {\n const metadata = generateProtectedResourceMetadata(options.auth!.resourceUrl!, options.auth!.authorizationServers!)\n return new Response(JSON.stringify(metadata), {\n headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' }\n })\n }\n\n // OAuth provider routes\n if (oauthPaths) {\n const methods = oauthPaths[url.pathname]\n if (methods) {\n if (!methods.includes(request.method)) {\n return new Response('Method not allowed', { status: 405 })\n }\n return handleOAuth(url, request, options.auth!.provider!)\n }\n }\n\n // MCP transport — wait for mcphero().start() to complete\n await _ready\n\n let requestContext = _context!\n if (options.auth) {\n const result = await validateToken(request.headers.get('authorization'), options.auth)\n if (result.error) {\n return new Response(JSON.stringify(result.error.body), {\n status: result.error.statusCode,\n headers: result.error.headers\n })\n }\n if (result.auth) {\n requestContext = _context!.fork({ auth: result.auth })\n }\n }\n\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: options.enableJsonResponse\n })\n\n const server = new McpServer({\n name: _options!.name,\n description: _options!.description,\n version: _options!.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n\n for (const action of _actions) {\n server.registerTool(pascalCase(action.name), {\n title: capitalCase(action.name),\n description: action.description,\n inputSchema: action.input\n }, async (input, extra) => {\n const logger = createLogger({\n stream: process.stderr,\n onLog: (level, data) => {\n extra.sendNotification({ method: 'notifications/message', params: { level, data } })\n },\n onProgress: ({ progress, total, message }) => {\n if (!extra._meta?.progressToken) { return }\n extra.sendNotification({\n method: 'notifications/progress',\n params: {\n progress,\n total,\n message,\n progressToken: extra._meta.progressToken\n }\n })\n }\n })\n return action.run(input, requestContext.fork({ logger, extra })).then((result) => {\n return toolResponse(result)\n }).catch((error) => {\n if (error instanceof Error) {\n return toolResponse({ success: false, name: error.name, message: error.message, stack: error.stack })\n } else {\n return toolResponse({ success: false, name: 'Unknown Error', message: 'An unknown error occured', error })\n }\n })\n })\n }\n\n await server.connect(transport)\n return transport.handleRequest(request)\n }\n\n async function handleOAuth(url: URL, request: Request, provider: NonNullable<AuthConfig['provider']>): Promise<Response> {\n const toOAuthReq = async (): Promise<OAuthRequest> => {\n let body: Record<string, string> | undefined\n if (request.method === 'POST') {\n const contentType = request.headers.get('content-type') ?? ''\n const text = await request.text()\n if (contentType.includes('json')) {\n body = JSON.parse(text)\n } else {\n body = Object.fromEntries(new URLSearchParams(text))\n }\n }\n return {\n method: request.method,\n url,\n headers: Object.fromEntries(request.headers.entries()),\n body\n }\n }\n\n const oauthReq = await toOAuthReq()\n let oauthRes\n if (url.pathname === '/.well-known/oauth-authorization-server') {\n oauthRes = provider.metadata()\n } else if (url.pathname === '/authorize') {\n oauthRes = await provider.authorize(oauthReq)\n } else if (url.pathname === callbackPath) {\n oauthRes = await provider.callback(oauthReq)\n } else if (url.pathname === '/token') {\n oauthRes = await provider.token(oauthReq)\n } else {\n oauthRes = await provider.register(oauthReq)\n }\n\n const responseBody = oauthRes.body ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body)) : null\n return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })\n }\n\n const adapter: AdapterGenerator = (mcpHeroOptions: MCPHeroOptions, baseContext: MCPHeroContext) => {\n _options = mcpHeroOptions\n _context = baseContext.fork({ adapter: 'vercel' })\n return {\n context: _context,\n start: async (actions) => {\n _actions = actions\n _readyResolve()\n },\n stop: async () => {\n _actions = []\n }\n }\n }\n\n return {\n adapter,\n handler: handleRequest,\n GET: handleRequest,\n POST: handleRequest,\n DELETE: handleRequest\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcphero/vercel",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP Hero Vercel Serverless Adapter",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,9 +14,9 @@
14
14
  "@modelcontextprotocol/sdk": "^1.29.0",
15
15
  "change-case": "^5.4.4",
16
16
  "zod": "^4.3.6",
17
- "@mcphero/auth": "1.2.0",
18
- "@mcphero/logger": "1.2.0",
19
- "@mcphero/core": "1.2.0"
17
+ "@mcphero/auth": "1.3.0",
18
+ "@mcphero/logger": "1.3.0",
19
+ "@mcphero/core": "1.3.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@eslint/js": "^10.0.1",
@@ -8,6 +8,7 @@ import { capitalCase, pascalCase } from 'change-case'
8
8
  export interface VercelAdapterOptions {
9
9
  enableJsonResponse?: boolean
10
10
  auth?: AuthConfig
11
+ path?: string
11
12
  }
12
13
 
13
14
  export interface VercelAdapter {
@@ -20,12 +21,15 @@ export interface VercelAdapter {
20
21
 
21
22
  const CORS_HEADERS: Record<string, string> = {
22
23
  'Access-Control-Allow-Origin': '*',
23
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
24
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
24
25
  'Access-Control-Allow-Headers': '*',
25
26
  'Access-Control-Max-Age': '86400'
26
27
  }
27
28
 
28
29
  export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
30
+ const mcpPath = options.path ?? '/mcp'
31
+ const callbackPath = options.auth?.callbackPath ?? '/auth/callback'
32
+
29
33
  let _actions: Action[] = []
30
34
  let _options: MCPHeroOptions | null = null
31
35
  let _context: MCPHeroContext | null = null
@@ -34,77 +38,65 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
34
38
  _readyResolve = resolve
35
39
  })
36
40
 
37
- const handleRequest = async (request: Request): Promise<Response> => {
38
- await _ready
41
+ // Pre-compute valid paths for fast rejection of bogus requests
42
+ const validPaths = new Set<string>([mcpPath])
43
+ if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {
44
+ validPaths.add('/.well-known/oauth-protected-resource')
45
+ }
46
+ if (options.auth?.provider) {
47
+ validPaths.add('/.well-known/oauth-authorization-server')
48
+ validPaths.add('/authorize')
49
+ validPaths.add(callbackPath)
50
+ validPaths.add('/token')
51
+ validPaths.add('/register')
52
+ }
39
53
 
54
+ // OAuth path → allowed methods (built once, not per-request)
55
+ const oauthPaths: Record<string, string[]> | null = options.auth?.provider
56
+ ? {
57
+ '/.well-known/oauth-authorization-server': ['GET'],
58
+ '/authorize': ['GET'],
59
+ [callbackPath]: ['GET'],
60
+ '/token': ['POST'],
61
+ '/register': ['POST']
62
+ }
63
+ : null
64
+
65
+ const handleRequest = async (request: Request): Promise<Response> => {
40
66
  const url = new URL(request.url)
41
67
 
42
- if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {
43
- if (url.pathname === '/.well-known/oauth-protected-resource' || url.pathname.endsWith('/.well-known/oauth-protected-resource')) {
44
- if (request.method === 'OPTIONS') {
45
- return new Response(null, { status: 200, headers: CORS_HEADERS })
46
- }
47
- const metadata = generateProtectedResourceMetadata(options.auth.resourceUrl, options.auth.authorizationServers)
48
- return new Response(JSON.stringify(metadata), {
49
- headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' }
50
- })
51
- }
68
+ // Fast rejection no async work, no allocations for unknown paths
69
+ if (!validPaths.has(url.pathname)) {
70
+ return new Response('Not Found', { status: 404 })
52
71
  }
53
72
 
54
- if (options.auth?.provider) {
55
- const provider = options.auth.provider
56
- const oauthPaths: Record<string, string[]> = {
57
- '/.well-known/oauth-authorization-server': ['GET'],
58
- '/authorize': ['GET'],
59
- '/auth/callback': ['GET'],
60
- '/token': ['POST'],
61
- '/register': ['POST']
62
- }
63
- const match = Object.entries(oauthPaths).find(([path]) => url.pathname === path || url.pathname.endsWith(path))
64
- if (match) {
65
- const [, methods] = match
66
- if (request.method === 'OPTIONS') {
67
- return new Response(null, { status: 200, headers: CORS_HEADERS })
68
- }
73
+ // CORS preflight
74
+ if (request.method === 'OPTIONS') {
75
+ return new Response(null, { status: 200, headers: CORS_HEADERS })
76
+ }
77
+
78
+ // Protected resource metadata (RFC 9728)
79
+ if (url.pathname === '/.well-known/oauth-protected-resource') {
80
+ const metadata = generateProtectedResourceMetadata(options.auth!.resourceUrl!, options.auth!.authorizationServers!)
81
+ return new Response(JSON.stringify(metadata), {
82
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' }
83
+ })
84
+ }
85
+
86
+ // OAuth provider routes
87
+ if (oauthPaths) {
88
+ const methods = oauthPaths[url.pathname]
89
+ if (methods) {
69
90
  if (!methods.includes(request.method)) {
70
91
  return new Response('Method not allowed', { status: 405 })
71
92
  }
72
- const toOAuthReq = async (): Promise<OAuthRequest> => {
73
- let body: Record<string, string> | undefined
74
- if (request.method === 'POST') {
75
- const contentType = request.headers.get('content-type') ?? ''
76
- const text = await request.text()
77
- if (contentType.includes('json')) {
78
- body = JSON.parse(text)
79
- } else {
80
- body = Object.fromEntries(new URLSearchParams(text))
81
- }
82
- }
83
- return {
84
- method: request.method,
85
- url,
86
- headers: Object.fromEntries(request.headers.entries()),
87
- body
88
- }
89
- }
90
- const oauthReq = await toOAuthReq()
91
- let oauthRes
92
- if (url.pathname.endsWith('/.well-known/oauth-authorization-server')) {
93
- oauthRes = provider.metadata()
94
- } else if (url.pathname.endsWith('/authorize')) {
95
- oauthRes = await provider.authorize(oauthReq)
96
- } else if (url.pathname.endsWith('/auth/callback')) {
97
- oauthRes = await provider.callback(oauthReq)
98
- } else if (url.pathname.endsWith('/token')) {
99
- oauthRes = await provider.token(oauthReq)
100
- } else {
101
- oauthRes = await provider.register(oauthReq)
102
- }
103
- const responseBody = oauthRes.body ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body)) : null
104
- return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })
93
+ return handleOAuth(url, request, options.auth!.provider!)
105
94
  }
106
95
  }
107
96
 
97
+ // MCP transport — wait for mcphero().start() to complete
98
+ await _ready
99
+
108
100
  let requestContext = _context!
109
101
  if (options.auth) {
110
102
  const result = await validateToken(request.headers.get('authorization'), options.auth)
@@ -172,6 +164,44 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
172
164
  return transport.handleRequest(request)
173
165
  }
174
166
 
167
+ async function handleOAuth(url: URL, request: Request, provider: NonNullable<AuthConfig['provider']>): Promise<Response> {
168
+ const toOAuthReq = async (): Promise<OAuthRequest> => {
169
+ let body: Record<string, string> | undefined
170
+ if (request.method === 'POST') {
171
+ const contentType = request.headers.get('content-type') ?? ''
172
+ const text = await request.text()
173
+ if (contentType.includes('json')) {
174
+ body = JSON.parse(text)
175
+ } else {
176
+ body = Object.fromEntries(new URLSearchParams(text))
177
+ }
178
+ }
179
+ return {
180
+ method: request.method,
181
+ url,
182
+ headers: Object.fromEntries(request.headers.entries()),
183
+ body
184
+ }
185
+ }
186
+
187
+ const oauthReq = await toOAuthReq()
188
+ let oauthRes
189
+ if (url.pathname === '/.well-known/oauth-authorization-server') {
190
+ oauthRes = provider.metadata()
191
+ } else if (url.pathname === '/authorize') {
192
+ oauthRes = await provider.authorize(oauthReq)
193
+ } else if (url.pathname === callbackPath) {
194
+ oauthRes = await provider.callback(oauthReq)
195
+ } else if (url.pathname === '/token') {
196
+ oauthRes = await provider.token(oauthReq)
197
+ } else {
198
+ oauthRes = await provider.register(oauthReq)
199
+ }
200
+
201
+ const responseBody = oauthRes.body ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body)) : null
202
+ return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })
203
+ }
204
+
175
205
  const adapter: AdapterGenerator = (mcpHeroOptions: MCPHeroOptions, baseContext: MCPHeroContext) => {
176
206
  _options = mcpHeroOptions
177
207
  _context = baseContext.fork({ adapter: 'vercel' })