@silkweave/edge 0.1.0 → 3.1.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/LICENSE +21 -0
- package/README.md +155 -0
- package/build/index.d.mts +20 -0
- package/build/index.d.mts.map +1 -0
- package/build/index.mjs +150 -0
- package/build/index.mjs.map +1 -0
- package/package.json +50 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Silkweave
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @silkweave/edge
|
|
2
|
+
|
|
3
|
+
Web-Standard edge/serverless adapter for [Silkweave](https://github.com/silkweave/silkweave) - deploy your actions as a **stateless** MCP server on any `(Request) => Response` runtime: **Cloudflare Workers, Vercel, Bun, Deno, Hono**, and Next.js.
|
|
4
|
+
|
|
5
|
+
It uses only Web Standard APIs (`Request`/`Response`/`ReadableStream`/Web Crypto) and the SDK's `WebStandardStreamableHTTPServerTransport` - no Express, no port binding - so the same handler drops onto any edge or serverless platform.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @silkweave/core @silkweave/edge
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Vanilla serverless function (Vercel / Bun / Deno)
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// api/mcp.ts
|
|
19
|
+
import { silkweave } from '@silkweave/core'
|
|
20
|
+
import { edge } from '@silkweave/edge'
|
|
21
|
+
import { MyAction } from '../actions/my-action.js'
|
|
22
|
+
|
|
23
|
+
const { adapter, handler } = edge()
|
|
24
|
+
|
|
25
|
+
await silkweave({ name: 'my-tools', description: 'My MCP Server', version: '1.0.0' })
|
|
26
|
+
.adapter(adapter)
|
|
27
|
+
.action(MyAction)
|
|
28
|
+
.start()
|
|
29
|
+
|
|
30
|
+
export default { fetch: handler }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Next.js App Router
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// app/api/mcp/route.ts
|
|
37
|
+
import { silkweave } from '@silkweave/core'
|
|
38
|
+
import { edge } from '@silkweave/edge'
|
|
39
|
+
import { MyAction } from '../../../actions/my-action.js'
|
|
40
|
+
|
|
41
|
+
const { adapter, GET, POST, DELETE } = edge()
|
|
42
|
+
|
|
43
|
+
await silkweave({ name: 'my-tools', description: 'My MCP Server', version: '1.0.0' })
|
|
44
|
+
.adapter(adapter)
|
|
45
|
+
.action(MyAction)
|
|
46
|
+
.start()
|
|
47
|
+
|
|
48
|
+
export { GET, POST, DELETE }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> For Next.js, [`@silkweave/nextjs`](https://www.npmjs.com/package/@silkweave/nextjs) wraps this adapter with catch-all path normalization and end-to-end tRPC types - prefer it over wiring `edge()` by hand.
|
|
52
|
+
|
|
53
|
+
### Cloudflare Workers
|
|
54
|
+
|
|
55
|
+
See the full [`examples/cloudflare`](https://github.com/silkweave/silkweave/tree/master/examples/cloudflare) example - a Worker with stateless MCP + Google Workspace OAuth 2.1 and OAuth state in Cloudflare KV, with a from-scratch setup guide.
|
|
56
|
+
|
|
57
|
+
## How It Works
|
|
58
|
+
|
|
59
|
+
- Uses `WebStandardStreamableHTTPServerTransport` from the MCP SDK in **stateless mode** (`sessionIdGenerator: undefined`)
|
|
60
|
+
- Each request creates a fresh `McpServer` + transport, registers tools, handles the request, and returns a Web Standard `Response`
|
|
61
|
+
- Only `POST` carries JSON-RPC; `GET` (standing SSE stream) and `DELETE` (session teardown) return `405`, since stateless mode has no session to attach a stream to or tear down (a `GET` stream would otherwise hang the request on serverless runtimes like Cloudflare Workers)
|
|
62
|
+
- Actions are registered as MCP tools using `PascalCase` names (same as the stdio and http adapters)
|
|
63
|
+
- Tool results use `smartToolResult()` by default. Large payloads (> 4096 chars) are automatically split into a text summary + embedded resource to reduce LLM context bloat. Actions can override this with a custom `toolResult` hook.
|
|
64
|
+
- Logging goes to `process.stderr` (serverless log drain) and MCP client notifications
|
|
65
|
+
|
|
66
|
+
## Streaming Actions
|
|
67
|
+
|
|
68
|
+
Streaming actions (see [`@silkweave/core`](https://www.npmjs.com/package/@silkweave/core)) work identically to [`@silkweave/mcp`](https://www.npmjs.com/package/@silkweave/mcp)'s `http()` adapter - chunks are delivered as `notifications/progress` over the Streamable HTTP transport when the client sends `_meta.progressToken`, with the JSON-stringified chunk in the `message` field. The tool call resolves with the buffered chunk array as the `CallToolResult`.
|
|
69
|
+
|
|
70
|
+
The same AI-host caveat applies as for stdio/http MCP: chunks reach the wire as standard MCP progress notifications, but most LLM hosts today consume them for UI rendering rather than as incremental data fed into the model's context. See the [`@silkweave/mcp` README](https://www.npmjs.com/package/@silkweave/mcp#what-this-means-for-ai-hosts) for the full discussion.
|
|
71
|
+
|
|
72
|
+
## Auth
|
|
73
|
+
|
|
74
|
+
`edge()` serves the full OAuth 2.1 surface when you pass an `auth` config - protected-resource metadata (RFC 9728), `/authorize`, `/token`, `/register`, and the provider callback - alongside the MCP transport, all from the one handler. Pass a bearer-validating `AuthConfig` from [`@silkweave/auth`](https://www.npmjs.com/package/@silkweave/auth), or a full provider (e.g. `google()`) from `@silkweave/auth/oauth`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { google } from '@silkweave/auth/oauth'
|
|
78
|
+
|
|
79
|
+
const { adapter, handler } = edge({ auth: google({ /* clientId, clientSecret, resourceUrl, store, ... */ }) })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Options
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const { adapter, handler } = edge({
|
|
86
|
+
enableJsonResponse: true // Return JSON instead of SSE streams
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Option | Type | Default | Description |
|
|
91
|
+
|--------|------|---------|-------------|
|
|
92
|
+
| `enableJsonResponse` | `boolean` | `false` | Return JSON responses instead of SSE streams |
|
|
93
|
+
| `auth` | `AuthConfig` | - | Bearer-token validation + OAuth routes (see [Auth](#auth)) |
|
|
94
|
+
| `path` | `string` | `/mcp` | The MCP transport path |
|
|
95
|
+
|
|
96
|
+
## Compound Return Pattern
|
|
97
|
+
|
|
98
|
+
Unlike other Silkweave adapters that are simple `AdapterFactory` functions, `edge()` returns a compound object:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface EdgeAdapter {
|
|
102
|
+
adapter: AdapterGenerator // Pass to silkweave().adapter()
|
|
103
|
+
handler: (request: Request) => Promise<Response> // The request handler
|
|
104
|
+
GET: (request: Request) => Promise<Response> // Alias for handler
|
|
105
|
+
POST: (request: Request) => Promise<Response> // Alias for handler
|
|
106
|
+
DELETE: (request: Request) => Promise<Response> // Alias for handler
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This is because edge/serverless platforms export request handlers rather than starting long-lived servers. The `adapter` property integrates with the Silkweave builder, while `handler`/`GET`/`POST`/`DELETE` are exported from your route file.
|
|
111
|
+
|
|
112
|
+
## Deployment
|
|
113
|
+
|
|
114
|
+
### Vercel configuration
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"framework": null,
|
|
119
|
+
"functions": {
|
|
120
|
+
"api/mcp.ts": {
|
|
121
|
+
"memory": 1024,
|
|
122
|
+
"maxDuration": 60
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"rewrites": [
|
|
126
|
+
{ "source": "/mcp", "destination": "/api/mcp" }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### CORS
|
|
132
|
+
|
|
133
|
+
CORS is not handled by the adapter (beyond OAuth/preflight). Configure it in your host framework - Next.js middleware, `vercel.json` headers, or Worker response headers:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"headers": [
|
|
138
|
+
{
|
|
139
|
+
"source": "/api/mcp",
|
|
140
|
+
"headers": [
|
|
141
|
+
{ "key": "Access-Control-Allow-Origin", "value": "*" },
|
|
142
|
+
{ "key": "Access-Control-Allow-Methods", "value": "POST, OPTIONS" },
|
|
143
|
+
{ "key": "Access-Control-Allow-Headers", "value": "Content-Type, Authorization" }
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## See Also
|
|
151
|
+
|
|
152
|
+
- [Silkweave README](https://github.com/silkweave/silkweave) - Full documentation
|
|
153
|
+
- [`@silkweave/core`](https://www.npmjs.com/package/@silkweave/core) - Core library
|
|
154
|
+
- [`@silkweave/mcp`](https://www.npmjs.com/package/@silkweave/mcp) - MCP stdio and HTTP adapters
|
|
155
|
+
- [`examples/cloudflare`](https://github.com/silkweave/silkweave/tree/master/examples/cloudflare) - Cloudflare Worker + Google OAuth + KV example
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthConfig } from "@silkweave/auth";
|
|
2
|
+
import { AdapterGenerator } from "@silkweave/core";
|
|
3
|
+
|
|
4
|
+
//#region src/adapter/edge.d.ts
|
|
5
|
+
interface EdgeAdapterOptions {
|
|
6
|
+
enableJsonResponse?: boolean;
|
|
7
|
+
auth?: AuthConfig;
|
|
8
|
+
path?: string;
|
|
9
|
+
}
|
|
10
|
+
interface EdgeAdapter {
|
|
11
|
+
adapter: AdapterGenerator;
|
|
12
|
+
handler: (request: Request) => Promise<Response>;
|
|
13
|
+
GET: (request: Request) => Promise<Response>;
|
|
14
|
+
POST: (request: Request) => Promise<Response>;
|
|
15
|
+
DELETE: (request: Request) => Promise<Response>;
|
|
16
|
+
}
|
|
17
|
+
declare function edge(options?: EdgeAdapterOptions): EdgeAdapter;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { EdgeAdapter, EdgeAdapterOptions, edge };
|
|
20
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/edge.ts"],"mappings":";;;;UAMiB,kBAAA;EACf,kBAAA;EACA,IAAA,GAAO,UAAA;EACP,IAAA;AAAA;AAAA,UAGe,WAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA,GAAU,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACvC,GAAA,GAAM,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACnC,IAAA,GAAO,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACpC,MAAA,GAAS,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;AAAA;AAAA,iBA0DxB,IAAA,CAAK,OAAA,GAAS,kBAAA,GAA0B,WAAA"}
|
package/build/index.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
|
+
import { generateProtectedResourceMetadata, validateToken } from "@silkweave/auth";
|
|
4
|
+
import { registerTools } from "@silkweave/mcp/tools";
|
|
5
|
+
//#region src/adapter/edge.ts
|
|
6
|
+
const CORS_HEADERS = {
|
|
7
|
+
"Access-Control-Allow-Origin": "*",
|
|
8
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
9
|
+
"Access-Control-Allow-Headers": "*",
|
|
10
|
+
"Access-Control-Max-Age": "86400"
|
|
11
|
+
};
|
|
12
|
+
async function parseOAuthRequest(url, request) {
|
|
13
|
+
let body;
|
|
14
|
+
if (request.method === "POST") {
|
|
15
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
16
|
+
const text = await request.text();
|
|
17
|
+
if (contentType.includes("json")) body = JSON.parse(text);
|
|
18
|
+
else body = Object.fromEntries(new URLSearchParams(text));
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
method: request.method,
|
|
22
|
+
url,
|
|
23
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
24
|
+
body
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function oauthResponseToResponse(oauthRes) {
|
|
28
|
+
const responseBody = oauthRes.body ? typeof oauthRes.body === "string" ? oauthRes.body : JSON.stringify(oauthRes.body) : null;
|
|
29
|
+
return new Response(responseBody, {
|
|
30
|
+
status: oauthRes.status,
|
|
31
|
+
headers: oauthRes.headers
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function routeOAuth(url, request, provider, callbackPath) {
|
|
35
|
+
const oauthReq = await parseOAuthRequest(url, request);
|
|
36
|
+
let oauthRes;
|
|
37
|
+
if (url.pathname === "/.well-known/oauth-authorization-server") oauthRes = provider.metadata();
|
|
38
|
+
else if (url.pathname === "/authorize") oauthRes = await provider.authorize(oauthReq);
|
|
39
|
+
else if (url.pathname === callbackPath) oauthRes = await provider.callback(oauthReq);
|
|
40
|
+
else if (url.pathname === "/token") oauthRes = await provider.token(oauthReq);
|
|
41
|
+
else oauthRes = await provider.register(oauthReq);
|
|
42
|
+
return oauthResponseToResponse(oauthRes);
|
|
43
|
+
}
|
|
44
|
+
function edge(options = {}) {
|
|
45
|
+
const mcpPath = options.path ?? "/mcp";
|
|
46
|
+
const callbackPath = options.auth?.callbackPath ?? "/auth/callback";
|
|
47
|
+
let _actions = [];
|
|
48
|
+
let _options = null;
|
|
49
|
+
let _context = null;
|
|
50
|
+
let _readyResolve;
|
|
51
|
+
const _ready = new Promise((resolve) => {
|
|
52
|
+
_readyResolve = resolve;
|
|
53
|
+
});
|
|
54
|
+
const validPaths = new Set([mcpPath]);
|
|
55
|
+
if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) validPaths.add("/.well-known/oauth-protected-resource");
|
|
56
|
+
if (options.auth?.provider) {
|
|
57
|
+
validPaths.add("/.well-known/oauth-authorization-server");
|
|
58
|
+
validPaths.add("/authorize");
|
|
59
|
+
validPaths.add(callbackPath);
|
|
60
|
+
validPaths.add("/token");
|
|
61
|
+
validPaths.add("/register");
|
|
62
|
+
}
|
|
63
|
+
const oauthPaths = options.auth?.provider ? {
|
|
64
|
+
"/.well-known/oauth-authorization-server": ["GET"],
|
|
65
|
+
"/authorize": ["GET"],
|
|
66
|
+
[callbackPath]: ["GET"],
|
|
67
|
+
"/token": ["POST"],
|
|
68
|
+
"/register": ["POST"]
|
|
69
|
+
} : null;
|
|
70
|
+
const handleRequest = async (request) => {
|
|
71
|
+
const url = new URL(request.url);
|
|
72
|
+
if (!validPaths.has(url.pathname)) return new Response("Not Found", { status: 404 });
|
|
73
|
+
if (request.method === "OPTIONS") return new Response(null, {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: CORS_HEADERS
|
|
76
|
+
});
|
|
77
|
+
if (url.pathname === "/.well-known/oauth-protected-resource") {
|
|
78
|
+
const metadata = generateProtectedResourceMetadata(options.auth.resourceUrl, options.auth.authorizationServers, options.auth.requiredScopes);
|
|
79
|
+
return new Response(JSON.stringify(metadata), { headers: {
|
|
80
|
+
...CORS_HEADERS,
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
"Cache-Control": "max-age=3600"
|
|
83
|
+
} });
|
|
84
|
+
}
|
|
85
|
+
if (oauthPaths) {
|
|
86
|
+
const methods = oauthPaths[url.pathname];
|
|
87
|
+
if (methods) {
|
|
88
|
+
if (!methods.includes(request.method)) return new Response("Method not allowed", { status: 405 });
|
|
89
|
+
return routeOAuth(url, request, options.auth.provider, callbackPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (request.method !== "POST") return new Response("Method Not Allowed", {
|
|
93
|
+
status: 405,
|
|
94
|
+
headers: {
|
|
95
|
+
...CORS_HEADERS,
|
|
96
|
+
Allow: "POST"
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
await _ready;
|
|
100
|
+
let requestContext = _context;
|
|
101
|
+
if (options.auth) {
|
|
102
|
+
const result = await validateToken(request.headers.get("authorization"), options.auth, _context.fork({ request }));
|
|
103
|
+
if (result.error) return new Response(JSON.stringify(result.error.body), {
|
|
104
|
+
status: result.error.statusCode,
|
|
105
|
+
headers: result.error.headers
|
|
106
|
+
});
|
|
107
|
+
if (result.auth) requestContext = _context.fork({ auth: result.auth });
|
|
108
|
+
}
|
|
109
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
110
|
+
sessionIdGenerator: void 0,
|
|
111
|
+
enableJsonResponse: options.enableJsonResponse
|
|
112
|
+
});
|
|
113
|
+
const server = new McpServer({
|
|
114
|
+
name: _options.name,
|
|
115
|
+
description: _options.description,
|
|
116
|
+
version: _options.version
|
|
117
|
+
}, { capabilities: {
|
|
118
|
+
tools: {},
|
|
119
|
+
logging: {}
|
|
120
|
+
} });
|
|
121
|
+
registerTools(server, _actions, requestContext);
|
|
122
|
+
await server.connect(transport);
|
|
123
|
+
return transport.handleRequest(request);
|
|
124
|
+
};
|
|
125
|
+
const adapter = (silkweaveOptions, baseContext) => {
|
|
126
|
+
_options = silkweaveOptions;
|
|
127
|
+
_context = baseContext.fork({ adapter: "edge" });
|
|
128
|
+
return {
|
|
129
|
+
context: _context,
|
|
130
|
+
start: async (actions) => {
|
|
131
|
+
_actions = actions;
|
|
132
|
+
_readyResolve();
|
|
133
|
+
},
|
|
134
|
+
stop: async () => {
|
|
135
|
+
_actions = [];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
adapter,
|
|
141
|
+
handler: handleRequest,
|
|
142
|
+
GET: handleRequest,
|
|
143
|
+
POST: handleRequest,
|
|
144
|
+
DELETE: handleRequest
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { edge };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter/edge.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'\nimport { AuthConfig, generateProtectedResourceMetadata, OAuthRequest, OAuthResponse, validateToken } from '@silkweave/auth'\nimport { Action, AdapterGenerator, SilkweaveContext, SilkweaveOptions } from '@silkweave/core'\nimport { registerTools } from '@silkweave/mcp/tools'\n\nexport interface EdgeAdapterOptions {\n enableJsonResponse?: boolean\n auth?: AuthConfig\n path?: string\n}\n\nexport interface EdgeAdapter {\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\nasync function parseOAuthRequest(url: URL, request: Request): 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\nfunction oauthResponseToResponse(oauthRes: OAuthResponse): Response {\n const responseBody = oauthRes.body\n ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body))\n : null\n return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })\n}\n\nasync function routeOAuth(\n url: URL,\n request: Request,\n provider: NonNullable<AuthConfig['provider']>,\n callbackPath: string\n): Promise<Response> {\n const oauthReq = await parseOAuthRequest(url, request)\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 return oauthResponseToResponse(oauthRes)\n}\n\nexport function edge(options: EdgeAdapterOptions = {}): EdgeAdapter {\n const mcpPath = options.path ?? '/mcp'\n const callbackPath = options.auth?.callbackPath ?? '/auth/callback'\n\n let _actions: Action[] = []\n let _options: SilkweaveOptions | null = null\n let _context: SilkweaveContext | 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!, options.auth!.requiredScopes)\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 routeOAuth(url, request, options.auth!.provider!, callbackPath)\n }\n }\n\n // MCP transport (stateless): only POST carries JSON-RPC. A standing-stream\n // GET or a session-teardown DELETE has no session to act on - and the SDK's\n // GET handler would open an SSE stream that never closes, hanging the request\n // on serverless runtimes (e.g. Cloudflare Workers). Answer them with 405, which\n // the Streamable HTTP spec explicitly permits for servers without a GET stream.\n if (request.method !== 'POST') {\n return new Response('Method Not Allowed', {\n status: 405,\n headers: { ...CORS_HEADERS, Allow: 'POST' }\n })\n }\n\n // wait for silkweave().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, _context!.fork({ request }))\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 registerTools(server, _actions, requestContext)\n\n await server.connect(transport)\n return transport.handleRequest(request)\n }\n\n const adapter: AdapterGenerator = (silkweaveOptions: SilkweaveOptions, baseContext: SilkweaveContext) => {\n _options = silkweaveOptions\n _context = baseContext.fork({ adapter: 'edge' })\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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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweave/edge",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Silkweave Web-Standard edge/serverless adapter (Cloudflare Workers, Vercel, Bun, Deno, Hono)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://www.silkweave.dev",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/silkweave/silkweave/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+ssh://git@github.com/silkweave/silkweave.git",
|
|
13
|
+
"directory": "packages/edge"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./build/index.mjs",
|
|
17
|
+
"types": "./build/index.d.mts",
|
|
18
|
+
"files": [
|
|
19
|
+
"build"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"@silkweave/source": "./src/index.ts",
|
|
24
|
+
"types": "./build/index.d.mts",
|
|
25
|
+
"default": "./build/index.mjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"zod": "^3.25.0",
|
|
31
|
+
"@silkweave/auth": "3.1.0",
|
|
32
|
+
"@silkweave/core": "3.1.0",
|
|
33
|
+
"@silkweave/mcp": "3.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^10.0.1",
|
|
37
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"eslint": "^10.4.0",
|
|
40
|
+
"rimraf": "^6.1.3",
|
|
41
|
+
"tsdown": "^0.21.8",
|
|
42
|
+
"tsx": "^4.21.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"typescript-eslint": "^8.56.1"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"clean": "rimraf build",
|
|
48
|
+
"build": "tsdown",
|
|
49
|
+
"watch": "tsdown --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "eslint",
|
|
52
|
+
"check": "pnpm lint && pnpm typecheck"
|
|
6
53
|
}
|
|
7
54
|
}
|