@mcphero/vercel 1.1.6 → 1.2.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.6 build /Users/atomic/projects/ai/mcphero/packages/vercel
2
+ > @mcphero/vercel@1.1.7 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 2.92 KB
13
- ESM build/index.js.map 5.42 KB
14
- ESM ⚡️ Build success in 5ms
12
+ ESM build/index.js 6.46 KB
13
+ ESM build/index.js.map 11.48 KB
14
+ ESM ⚡️ Build success in 7ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 569ms
17
- DTS build/index.d.ts 526.00 B
16
+ DTS ⚡️ Build success in 696ms
17
+ DTS build/index.d.ts 593.00 B
@@ -1,14 +1,12 @@
1
1
 
2
- > @mcphero/vercel@1.1.6 check /Users/atomic/projects/ai/mcphero/packages/vercel
2
+ > @mcphero/vercel@1.1.7 check /Users/atomic/projects/ai/mcphero/packages/vercel
3
3
  > pnpm lint && pnpm typecheck
4
4
 
5
5
 
6
- > @mcphero/vercel@1.1.6 lint /Users/atomic/projects/ai/mcphero/packages/vercel
6
+ > @mcphero/vercel@1.1.7 lint /Users/atomic/projects/ai/mcphero/packages/vercel
7
7
  > eslint
8
8
 
9
9
 
10
- > @mcphero/vercel@1.1.6 typecheck /Users/atomic/projects/ai/mcphero/packages/vercel
10
+ > @mcphero/vercel@1.1.7 typecheck /Users/atomic/projects/ai/mcphero/packages/vercel
11
11
  > tsc --noEmit
12
12
 
13
-  ELIFECYCLE  Command failed.
14
-  ELIFECYCLE  Command failed.
@@ -1,14 +1,14 @@
1
1
 
2
2
  
3
- > @mcphero/vercel@1.1.6 prepack /Users/atomic/projects/ai/mcphero/packages/vercel
3
+ > @mcphero/vercel@1.1.7 prepack /Users/atomic/projects/ai/mcphero/packages/vercel
4
4
  > pnpm clean && pnpm build
5
5
 
6
6
 
7
- > @mcphero/vercel@1.1.6 clean /Users/atomic/projects/ai/mcphero/packages/vercel
7
+ > @mcphero/vercel@1.1.7 clean /Users/atomic/projects/ai/mcphero/packages/vercel
8
8
  > rimraf build
9
9
 
10
10
 
11
- > @mcphero/vercel@1.1.6 build /Users/atomic/projects/ai/mcphero/packages/vercel
11
+ > @mcphero/vercel@1.1.7 build /Users/atomic/projects/ai/mcphero/packages/vercel
12
12
  > tsup
13
13
 
14
14
  CLI Building entry: src/index.ts
@@ -18,9 +18,9 @@
18
18
  CLI Target: es2022
19
19
  CLI Cleaning output folder
20
20
  ESM Build start
21
- ESM build/index.js 2.92 KB
22
- ESM build/index.js.map 5.42 KB
21
+ ESM build/index.js 6.46 KB
22
+ ESM build/index.js.map 11.48 KB
23
23
  ESM ⚡️ Build success in 9ms
24
24
  DTS Build start
25
- DTS ⚡️ Build success in 773ms
26
- DTS build/index.d.ts 526.00 B
25
+ DTS ⚡️ Build success in 732ms
26
+ DTS build/index.d.ts 593.00 B
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @mcphero/vercel
2
+
3
+ Vercel serverless adapter for [MCPHero](https://github.com/atomicbi/mcphero) — deploy your actions as a stateless MCP server on Vercel.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @mcphero/core @mcphero/vercel
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Vanilla Vercel Functions
14
+
15
+ ```typescript
16
+ // api/mcp.ts
17
+ import { mcphero } from '@mcphero/core'
18
+ import { vercel } from '@mcphero/vercel'
19
+ import { MyAction } from '../actions/my-action.js'
20
+
21
+ const { adapter, handler } = vercel()
22
+
23
+ await mcphero({ name: 'my-tools', description: 'My MCP Server', version: '1.0.0' })
24
+ .adapter(adapter)
25
+ .action(MyAction)
26
+ .start()
27
+
28
+ export default { fetch: handler }
29
+ ```
30
+
31
+ ### Next.js App Router
32
+
33
+ ```typescript
34
+ // app/api/mcp/route.ts
35
+ import { mcphero } from '@mcphero/core'
36
+ import { vercel } from '@mcphero/vercel'
37
+ import { MyAction } from '../../../actions/my-action.js'
38
+
39
+ const { adapter, GET, POST, DELETE } = vercel()
40
+
41
+ await mcphero({ name: 'my-tools', description: 'My MCP Server', version: '1.0.0' })
42
+ .adapter(adapter)
43
+ .action(MyAction)
44
+ .start()
45
+
46
+ export { GET, POST, DELETE }
47
+ ```
48
+
49
+ ## How It Works
50
+
51
+ - Uses `WebStandardStreamableHTTPServerTransport` from the MCP SDK in **stateless mode** (`sessionIdGenerator: undefined`)
52
+ - Each request creates a fresh `McpServer` + transport, registers tools, handles the request, and returns a Web Standard `Response`
53
+ - Actions are registered as MCP tools using `PascalCase` names (same as the stdio and http adapters)
54
+ - Logging goes to `process.stderr` (Vercel log drain) and MCP client notifications
55
+
56
+ ## Options
57
+
58
+ ```typescript
59
+ const { adapter, handler } = vercel({
60
+ enableJsonResponse: true // Return JSON instead of SSE streams
61
+ })
62
+ ```
63
+
64
+ | Option | Type | Default | Description |
65
+ |--------|------|---------|-------------|
66
+ | `enableJsonResponse` | `boolean` | `false` | Return JSON responses instead of SSE streams |
67
+
68
+ ## Compound Return Pattern
69
+
70
+ Unlike other MCPHero adapters that are simple `AdapterFactory` functions, `vercel()` returns a compound object:
71
+
72
+ ```typescript
73
+ interface VercelAdapter {
74
+ adapter: AdapterGenerator // Pass to mcphero().adapter()
75
+ handler: (request: Request) => Promise<Response> // The request handler
76
+ GET: (request: Request) => Promise<Response> // Alias for handler
77
+ POST: (request: Request) => Promise<Response> // Alias for handler
78
+ DELETE: (request: Request) => Promise<Response> // Alias for handler
79
+ }
80
+ ```
81
+
82
+ This is because Vercel functions export request handlers rather than starting long-lived servers. The `adapter` property integrates with the MCPHero builder, while `handler`/`GET`/`POST`/`DELETE` are exported from your route file.
83
+
84
+ ## Deployment
85
+
86
+ ### Vercel Configuration
87
+
88
+ ```json
89
+ {
90
+ "framework": null,
91
+ "functions": {
92
+ "api/mcp.ts": {
93
+ "memory": 1024,
94
+ "maxDuration": 60
95
+ }
96
+ },
97
+ "rewrites": [
98
+ { "source": "/mcp", "destination": "/api/mcp" }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ ### CORS
104
+
105
+ CORS is not handled by the adapter. Use Next.js middleware or Vercel headers configuration:
106
+
107
+ ```json
108
+ {
109
+ "headers": [
110
+ {
111
+ "source": "/api/mcp",
112
+ "headers": [
113
+ { "key": "Access-Control-Allow-Origin", "value": "*" },
114
+ { "key": "Access-Control-Allow-Methods", "value": "GET, POST, DELETE, OPTIONS" },
115
+ { "key": "Access-Control-Allow-Headers", "value": "Content-Type, Mcp-Session-Id" }
116
+ ]
117
+ }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ## See Also
123
+
124
+ - [MCPHero README](https://github.com/atomicbi/mcphero) — Full documentation
125
+ - [`@mcphero/core`](https://www.npmjs.com/package/@mcphero/core) — Core library
126
+ - [`@mcphero/mcp`](https://www.npmjs.com/package/@mcphero/mcp) — MCP stdio and HTTP adapters (stateful)
package/build/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { AuthConfig } from '@mcphero/auth';
1
2
  import { AdapterGenerator } from '@mcphero/core';
2
3
 
3
4
  interface VercelAdapterOptions {
4
5
  enableJsonResponse?: boolean;
6
+ auth?: AuthConfig;
5
7
  }
6
8
  interface VercelAdapter {
7
9
  adapter: AdapterGenerator;
package/build/index.js CHANGED
@@ -1,9 +1,16 @@
1
1
  // src/adapter/vercel.ts
2
+ import { generateProtectedResourceMetadata, validateToken } from "@mcphero/auth";
2
3
  import { toolResponse } from "@mcphero/core";
3
4
  import { createLogger } from "@mcphero/logger";
4
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
6
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
7
  import { capitalCase, pascalCase } from "change-case";
8
+ var CORS_HEADERS = {
9
+ "Access-Control-Allow-Origin": "*",
10
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
11
+ "Access-Control-Allow-Headers": "*",
12
+ "Access-Control-Max-Age": "86400"
13
+ };
7
14
  function vercel(options = {}) {
8
15
  let _actions = [];
9
16
  let _options = null;
@@ -14,6 +21,84 @@ function vercel(options = {}) {
14
21
  });
15
22
  const handleRequest = async (request) => {
16
23
  await _ready;
24
+ 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
+ }
35
+ }
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
+ }
51
+ if (!methods.includes(request.method)) {
52
+ return new Response("Method not allowed", { status: 405 });
53
+ }
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 });
87
+ }
88
+ }
89
+ let requestContext = _context;
90
+ if (options.auth) {
91
+ const result = await validateToken(request.headers.get("authorization"), options.auth);
92
+ if (result.error) {
93
+ return new Response(JSON.stringify(result.error.body), {
94
+ status: result.error.statusCode,
95
+ headers: result.error.headers
96
+ });
97
+ }
98
+ if (result.auth) {
99
+ requestContext = _context.fork({ auth: result.auth });
100
+ }
101
+ }
17
102
  const transport = new WebStandardStreamableHTTPServerTransport({
18
103
  sessionIdGenerator: void 0,
19
104
  enableJsonResponse: options.enableJsonResponse
@@ -51,7 +136,7 @@ function vercel(options = {}) {
51
136
  });
52
137
  }
53
138
  });
54
- return action.run(input, _context.fork({ logger, extra })).then((result) => {
139
+ return action.run(input, requestContext.fork({ logger, extra })).then((result) => {
55
140
  return toolResponse(result);
56
141
  }).catch((error) => {
57
142
  if (error instanceof Error) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapter/vercel.ts"],"sourcesContent":["import { 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}\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\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 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, _context!.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":";AAAA,SAAmE,oBAAoB;AACvF,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,gDAAgD;AACzD,SAAS,aAAa,kBAAkB;AAcjC,SAAS,OAAO,UAAgC,CAAC,GAAkB;AACxE,MAAI,WAAqB,CAAC;AAC1B,MAAI,WAAkC;AACtC,MAAI,WAAkC;AACtC,MAAI;AACJ,QAAM,SAAS,IAAI,QAAc,CAAC,YAAY;AAC5C,oBAAgB;AAAA,EAClB,CAAC;AAED,QAAM,gBAAgB,OAAO,YAAwC;AACnE,UAAM;AAEN,UAAM,YAAY,IAAI,yCAAyC;AAAA,MAC7D,oBAAoB;AAAA,MACpB,oBAAoB,QAAQ;AAAA,IAC9B,CAAC;AAED,UAAM,SAAS,IAAI,UAAU;AAAA,MAC3B,MAAM,SAAU;AAAA,MAChB,aAAa,SAAU;AAAA,MACvB,SAAS,SAAU;AAAA,IACrB,GAAG;AAAA,MACD,cAAc,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,EAAE;AAAA,IACzC,CAAC;AAED,eAAW,UAAU,UAAU;AAC7B,aAAO,aAAa,WAAW,OAAO,IAAI,GAAG;AAAA,QAC3C,OAAO,YAAY,OAAO,IAAI;AAAA,QAC9B,aAAa,OAAO;AAAA,QACpB,aAAa,OAAO;AAAA,MACtB,GAAG,OAAO,OAAO,UAAU;AACzB,cAAM,SAAS,aAAa;AAAA,UAC1B,QAAQ,QAAQ;AAAA,UAChB,OAAO,CAAC,OAAO,SAAS;AACtB,kBAAM,iBAAiB,EAAE,QAAQ,yBAAyB,QAAQ,EAAE,OAAO,KAAK,EAAE,CAAC;AAAA,UACrF;AAAA,UACA,YAAY,CAAC,EAAE,UAAU,OAAO,QAAQ,MAAM;AAC5C,gBAAI,CAAC,MAAM,OAAO,eAAe;AAAE;AAAA,YAAO;AAC1C,kBAAM,iBAAiB;AAAA,cACrB,QAAQ;AAAA,cACR,QAAQ;AAAA,gBACN;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,eAAe,MAAM,MAAM;AAAA,cAC7B;AAAA,YACF,CAAC;AAAA,UACH;AAAA,QACF,CAAC;AACD,eAAO,OAAO,IAAI,OAAO,SAAU,KAAK,EAAE,QAAQ,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW;AAC3E,iBAAO,aAAa,MAAM;AAAA,QAC5B,CAAC,EAAE,MAAM,CAAC,UAAU;AAClB,cAAI,iBAAiB,OAAO;AAC1B,mBAAO,aAAa,EAAE,SAAS,OAAO,MAAM,MAAM,MAAM,SAAS,MAAM,SAAS,OAAO,MAAM,MAAM,CAAC;AAAA,UACtG,OAAO;AACL,mBAAO,aAAa,EAAE,SAAS,OAAO,MAAM,iBAAiB,SAAS,4BAA4B,MAAM,CAAC;AAAA,UAC3G;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAEA,UAAM,OAAO,QAAQ,SAAS;AAC9B,WAAO,UAAU,cAAc,OAAO;AAAA,EACxC;AAEA,QAAM,UAA4B,CAAC,gBAAgC,gBAAgC;AACjG,eAAW;AACX,eAAW,YAAY,KAAK,EAAE,SAAS,SAAS,CAAC;AACjD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,OAAO,YAAY;AACxB,mBAAW;AACX,sBAAc;AAAA,MAChB;AAAA,MACA,MAAM,YAAY;AAChB,mBAAW,CAAC;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS;AAAA,IACT,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,EACV;AACF;","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}\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcphero/vercel",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "description": "MCP Hero Vercel Serverless Adapter",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,8 +14,9 @@
14
14
  "@modelcontextprotocol/sdk": "^1.29.0",
15
15
  "change-case": "^5.4.4",
16
16
  "zod": "^4.3.6",
17
- "@mcphero/core": "1.1.6",
18
- "@mcphero/logger": "1.1.6"
17
+ "@mcphero/auth": "1.2.0",
18
+ "@mcphero/logger": "1.2.0",
19
+ "@mcphero/core": "1.2.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@eslint/js": "^10.0.1",
@@ -1,3 +1,4 @@
1
+ import { AuthConfig, generateProtectedResourceMetadata, OAuthRequest, validateToken } from '@mcphero/auth'
1
2
  import { Action, AdapterGenerator, MCPHeroContext, MCPHeroOptions, toolResponse } from '@mcphero/core'
2
3
  import { createLogger } from '@mcphero/logger'
3
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -6,6 +7,7 @@ import { capitalCase, pascalCase } from 'change-case'
6
7
 
7
8
  export interface VercelAdapterOptions {
8
9
  enableJsonResponse?: boolean
10
+ auth?: AuthConfig
9
11
  }
10
12
 
11
13
  export interface VercelAdapter {
@@ -16,6 +18,13 @@ export interface VercelAdapter {
16
18
  DELETE: (request: Request) => Promise<Response>
17
19
  }
18
20
 
21
+ const CORS_HEADERS: Record<string, string> = {
22
+ 'Access-Control-Allow-Origin': '*',
23
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
24
+ 'Access-Control-Allow-Headers': '*',
25
+ 'Access-Control-Max-Age': '86400'
26
+ }
27
+
19
28
  export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
20
29
  let _actions: Action[] = []
21
30
  let _options: MCPHeroOptions | null = null
@@ -28,6 +37,88 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
28
37
  const handleRequest = async (request: Request): Promise<Response> => {
29
38
  await _ready
30
39
 
40
+ const url = new URL(request.url)
41
+
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
+ }
52
+ }
53
+
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
+ }
69
+ if (!methods.includes(request.method)) {
70
+ return new Response('Method not allowed', { status: 405 })
71
+ }
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 })
105
+ }
106
+ }
107
+
108
+ let requestContext = _context!
109
+ if (options.auth) {
110
+ const result = await validateToken(request.headers.get('authorization'), options.auth)
111
+ if (result.error) {
112
+ return new Response(JSON.stringify(result.error.body), {
113
+ status: result.error.statusCode,
114
+ headers: result.error.headers
115
+ })
116
+ }
117
+ if (result.auth) {
118
+ requestContext = _context!.fork({ auth: result.auth })
119
+ }
120
+ }
121
+
31
122
  const transport = new WebStandardStreamableHTTPServerTransport({
32
123
  sessionIdGenerator: undefined,
33
124
  enableJsonResponse: options.enableJsonResponse
@@ -65,7 +156,7 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
65
156
  })
66
157
  }
67
158
  })
68
- return action.run(input, _context!.fork({ logger, extra })).then((result) => {
159
+ return action.run(input, requestContext.fork({ logger, extra })).then((result) => {
69
160
  return toolResponse(result)
70
161
  }).catch((error) => {
71
162
  if (error instanceof Error) {