@mcphero/vercel 1.1.6 → 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 -5
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-prepack.log +8 -8
- package/README.md +126 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +98 -1
- package/build/index.js.map +1 -1
- package/package.json +4 -3
- package/src/adapter/vercel.ts +122 -1
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
|
|
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,14 +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
|
|
|
13
|
-
ELIFECYCLE Command failed.
|
|
14
|
-
ELIFECYCLE Command failed.
|
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
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
[34mCLI[39m Target: es2022
|
|
19
19
|
[34mCLI[39m Cleaning output folder
|
|
20
20
|
[34mESM[39m Build start
|
|
21
|
-
[32mESM[39m [1mbuild/index.js [22m[
|
|
22
|
-
[32mESM[39m [1mbuild/index.js.map [22m[
|
|
23
|
-
[32mESM[39m ⚡️ Build success in
|
|
21
|
+
[32mESM[39m [1mbuild/index.js [22m[32m6.46 KB[39m
|
|
22
|
+
[32mESM[39m [1mbuild/index.js.map [22m[32m11.48 KB[39m
|
|
23
|
+
[32mESM[39m ⚡️ Build success in 12ms
|
|
24
24
|
DTS Build start
|
|
25
|
-
DTS ⚡️ Build success in
|
|
26
|
-
DTS build/index.d.ts
|
|
25
|
+
DTS ⚡️ Build success in 868ms
|
|
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,10 @@
|
|
|
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;
|
|
7
|
+
path?: string;
|
|
5
8
|
}
|
|
6
9
|
interface VercelAdapter {
|
|
7
10
|
adapter: AdapterGenerator;
|
package/build/index.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
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, POST, DELETE, OPTIONS",
|
|
11
|
+
"Access-Control-Allow-Headers": "*",
|
|
12
|
+
"Access-Control-Max-Age": "86400"
|
|
13
|
+
};
|
|
7
14
|
function vercel(options = {}) {
|
|
15
|
+
const mcpPath = options.path ?? "/mcp";
|
|
16
|
+
const callbackPath = options.auth?.callbackPath ?? "/auth/callback";
|
|
8
17
|
let _actions = [];
|
|
9
18
|
let _options = null;
|
|
10
19
|
let _context = null;
|
|
@@ -12,8 +21,61 @@ function vercel(options = {}) {
|
|
|
12
21
|
const _ready = new Promise((resolve) => {
|
|
13
22
|
_readyResolve = resolve;
|
|
14
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;
|
|
15
42
|
const handleRequest = async (request) => {
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
if (!validPaths.has(url.pathname)) {
|
|
45
|
+
return new Response("Not Found", { status: 404 });
|
|
46
|
+
}
|
|
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) {
|
|
59
|
+
if (!methods.includes(request.method)) {
|
|
60
|
+
return new Response("Method not allowed", { status: 405 });
|
|
61
|
+
}
|
|
62
|
+
return handleOAuth(url, request, options.auth.provider);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
16
65
|
await _ready;
|
|
66
|
+
let requestContext = _context;
|
|
67
|
+
if (options.auth) {
|
|
68
|
+
const result = await validateToken(request.headers.get("authorization"), options.auth);
|
|
69
|
+
if (result.error) {
|
|
70
|
+
return new Response(JSON.stringify(result.error.body), {
|
|
71
|
+
status: result.error.statusCode,
|
|
72
|
+
headers: result.error.headers
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (result.auth) {
|
|
76
|
+
requestContext = _context.fork({ auth: result.auth });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
17
79
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
18
80
|
sessionIdGenerator: void 0,
|
|
19
81
|
enableJsonResponse: options.enableJsonResponse
|
|
@@ -51,7 +113,7 @@ function vercel(options = {}) {
|
|
|
51
113
|
});
|
|
52
114
|
}
|
|
53
115
|
});
|
|
54
|
-
return action.run(input,
|
|
116
|
+
return action.run(input, requestContext.fork({ logger, extra })).then((result) => {
|
|
55
117
|
return toolResponse(result);
|
|
56
118
|
}).catch((error) => {
|
|
57
119
|
if (error instanceof Error) {
|
|
@@ -65,6 +127,41 @@ function vercel(options = {}) {
|
|
|
65
127
|
await server.connect(transport);
|
|
66
128
|
return transport.handleRequest(request);
|
|
67
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
|
+
}
|
|
68
165
|
const adapter = (mcpHeroOptions, baseContext) => {
|
|
69
166
|
_options = mcpHeroOptions;
|
|
70
167
|
_context = baseContext.fork({ adapter: "vercel" });
|
package/build/index.js.map
CHANGED
|
@@ -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 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,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/
|
|
18
|
-
"@mcphero/logger": "1.
|
|
17
|
+
"@mcphero/auth": "1.3.0",
|
|
18
|
+
"@mcphero/logger": "1.3.0",
|
|
19
|
+
"@mcphero/core": "1.3.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@eslint/js": "^10.0.1",
|
package/src/adapter/vercel.ts
CHANGED
|
@@ -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,8 @@ import { capitalCase, pascalCase } from 'change-case'
|
|
|
6
7
|
|
|
7
8
|
export interface VercelAdapterOptions {
|
|
8
9
|
enableJsonResponse?: boolean
|
|
10
|
+
auth?: AuthConfig
|
|
11
|
+
path?: string
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export interface VercelAdapter {
|
|
@@ -16,7 +19,17 @@ export interface VercelAdapter {
|
|
|
16
19
|
DELETE: (request: Request) => Promise<Response>
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
const CORS_HEADERS: Record<string, string> = {
|
|
23
|
+
'Access-Control-Allow-Origin': '*',
|
|
24
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
25
|
+
'Access-Control-Allow-Headers': '*',
|
|
26
|
+
'Access-Control-Max-Age': '86400'
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
|
|
30
|
+
const mcpPath = options.path ?? '/mcp'
|
|
31
|
+
const callbackPath = options.auth?.callbackPath ?? '/auth/callback'
|
|
32
|
+
|
|
20
33
|
let _actions: Action[] = []
|
|
21
34
|
let _options: MCPHeroOptions | null = null
|
|
22
35
|
let _context: MCPHeroContext | null = null
|
|
@@ -25,9 +38,79 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
|
|
|
25
38
|
_readyResolve = resolve
|
|
26
39
|
})
|
|
27
40
|
|
|
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
|
+
}
|
|
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
|
+
|
|
28
65
|
const handleRequest = async (request: Request): Promise<Response> => {
|
|
66
|
+
const url = new URL(request.url)
|
|
67
|
+
|
|
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 })
|
|
71
|
+
}
|
|
72
|
+
|
|
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) {
|
|
90
|
+
if (!methods.includes(request.method)) {
|
|
91
|
+
return new Response('Method not allowed', { status: 405 })
|
|
92
|
+
}
|
|
93
|
+
return handleOAuth(url, request, options.auth!.provider!)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MCP transport — wait for mcphero().start() to complete
|
|
29
98
|
await _ready
|
|
30
99
|
|
|
100
|
+
let requestContext = _context!
|
|
101
|
+
if (options.auth) {
|
|
102
|
+
const result = await validateToken(request.headers.get('authorization'), options.auth)
|
|
103
|
+
if (result.error) {
|
|
104
|
+
return new Response(JSON.stringify(result.error.body), {
|
|
105
|
+
status: result.error.statusCode,
|
|
106
|
+
headers: result.error.headers
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
if (result.auth) {
|
|
110
|
+
requestContext = _context!.fork({ auth: result.auth })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
31
114
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
32
115
|
sessionIdGenerator: undefined,
|
|
33
116
|
enableJsonResponse: options.enableJsonResponse
|
|
@@ -65,7 +148,7 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
|
|
|
65
148
|
})
|
|
66
149
|
}
|
|
67
150
|
})
|
|
68
|
-
return action.run(input,
|
|
151
|
+
return action.run(input, requestContext.fork({ logger, extra })).then((result) => {
|
|
69
152
|
return toolResponse(result)
|
|
70
153
|
}).catch((error) => {
|
|
71
154
|
if (error instanceof Error) {
|
|
@@ -81,6 +164,44 @@ export function vercel(options: VercelAdapterOptions = {}): VercelAdapter {
|
|
|
81
164
|
return transport.handleRequest(request)
|
|
82
165
|
}
|
|
83
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
|
+
|
|
84
205
|
const adapter: AdapterGenerator = (mcpHeroOptions: MCPHeroOptions, baseContext: MCPHeroContext) => {
|
|
85
206
|
_options = mcpHeroOptions
|
|
86
207
|
_context = baseContext.fork({ adapter: 'vercel' })
|