@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.
- package/.turbo/turbo-build.log +6 -6
- package/.turbo/turbo-check.log +3 -3
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-prepack.log +5 -5
- package/build/index.d.ts +1 -0
- package/build/index.js +72 -60
- package/build/index.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter/vercel.ts +91 -61
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @mcphero/vercel@1.
|
|
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.
|
|
13
|
-
ESM build/index.js.map
|
|
14
|
-
ESM ⚡️ Build success in
|
|
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
|
|
17
|
-
DTS build/index.d.ts
|
|
16
|
+
DTS ⚡️ Build success in 666ms
|
|
17
|
+
DTS build/index.d.ts 612.00 B
|
package/.turbo/turbo-check.log
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
|
|
2
|
-
> @mcphero/vercel@1.
|
|
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.
|
|
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.
|
|
10
|
+
> @mcphero/vercel@1.2.0 typecheck /Users/atomic/projects/ai/mcphero/packages/vercel
|
|
11
11
|
> tsc --noEmit
|
|
12
12
|
|
package/.turbo/turbo-prepack.log
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @mcphero/vercel@1.
|
|
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.
|
|
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.
|
|
11
|
+
> @mcphero/vercel@1.2.0 build /Users/atomic/projects/ai/mcphero/packages/vercel
|
|
12
12
|
> tsup
|
|
13
13
|
|
|
14
14
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
[34mESM[39m Build start
|
|
21
21
|
[32mESM[39m [1mbuild/index.js [22m[32m6.46 KB[39m
|
|
22
22
|
[32mESM[39m [1mbuild/index.js.map [22m[32m11.48 KB[39m
|
|
23
|
-
[32mESM[39m ⚡️ Build success in
|
|
23
|
+
[32mESM[39m ⚡️ Build success in 12ms
|
|
24
24
|
DTS Build start
|
|
25
|
-
DTS ⚡️ Build success in
|
|
25
|
+
DTS ⚡️ Build success in 868ms
|
|
26
26
|
DTS build/index.d.ts 593.00 B
|
package/build/index.d.ts
CHANGED
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 (
|
|
26
|
-
|
|
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 (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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" });
|
package/build/index.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
18
|
-
"@mcphero/logger": "1.
|
|
19
|
-
"@mcphero/core": "1.
|
|
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",
|
package/src/adapter/vercel.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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' })
|