@silkweave/edge 3.0.0 → 3.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.
- package/README.md +2 -0
- package/build/index.d.mts +12 -1
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +24 -2
- package/build/index.mjs.map +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -92,6 +92,8 @@ const { adapter, handler } = edge({
|
|
|
92
92
|
| `enableJsonResponse` | `boolean` | `false` | Return JSON responses instead of SSE streams |
|
|
93
93
|
| `auth` | `AuthConfig` | - | Bearer-token validation + OAuth routes (see [Auth](#auth)) |
|
|
94
94
|
| `path` | `string` | `/mcp` | The MCP transport path |
|
|
95
|
+
| `filterActions` | `FilterActions` | - | Per-request tool filter, applied before tools are registered on every POST (the stateless transport recomputes the tool list per request, so permission changes apply on the next `tools/list`). Receives the actions (with their `tags`) and a request stand-in `{ headers, url, method, toolName }` - `method` is the JSON-RPC method (skip DB lookups on `initialize`/`ping`), `toolName` is set for `tools/call`. May be async. A throw surfaces as its `SilkweaveError.statusCode` (e.g. 401) with a JSON-RPC error body, or 500 for other errors - never an empty tool list |
|
|
96
|
+
| `onToolCall` | `OnToolCall` | - | Telemetry hook invoked once per tool call, fire-and-forget (never awaited on the result path; errors logged and swallowed). Events carry `{ action, tool, transport: 'mcp', durationMs, ok, errorCode?, errorMessage?, resultBytes?, sideloaded?, context }` |
|
|
95
97
|
|
|
96
98
|
## Compound Return Pattern
|
|
97
99
|
|
package/build/index.d.mts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { AuthConfig } from "@silkweave/auth";
|
|
2
|
-
import { AdapterGenerator } from "@silkweave/core";
|
|
2
|
+
import { AdapterGenerator, OnToolCall } from "@silkweave/core";
|
|
3
|
+
import { FilterActions } from "@silkweave/mcp/tools";
|
|
3
4
|
|
|
4
5
|
//#region src/adapter/edge.d.ts
|
|
5
6
|
interface EdgeAdapterOptions {
|
|
6
7
|
enableJsonResponse?: boolean;
|
|
7
8
|
auth?: AuthConfig;
|
|
8
9
|
path?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Per-request tool filter, applied before `registerTools()` on every POST
|
|
12
|
+
* (the stateless transport recomputes the tool list per request). See
|
|
13
|
+
* `FilterActions` for the request stand-in (`headers`/`url`/`method`/
|
|
14
|
+
* `toolName`) and error semantics (a throw surfaces as its
|
|
15
|
+
* `SilkweaveError.statusCode` or 500 - never an empty tool list).
|
|
16
|
+
*/
|
|
17
|
+
filterActions?: FilterActions;
|
|
18
|
+
/** Telemetry hook invoked once per tool call (fire-and-forget). */
|
|
19
|
+
onToolCall?: OnToolCall;
|
|
9
20
|
}
|
|
10
21
|
interface EdgeAdapter {
|
|
11
22
|
adapter: AdapterGenerator;
|
package/build/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/edge.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/edge.ts"],"mappings":";;;;;UAMiB,kBAAA;EACf,kBAAA;EACA,IAAA,GAAO,UAAA;EACP,IAAA;;;;;;;;EAQA,aAAA,GAAgB,aAAA;EATT;EAWP,UAAA,GAAa,UAAA;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
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
3
|
import { generateProtectedResourceMetadata, validateToken } from "@silkweave/auth";
|
|
4
|
-
import {
|
|
4
|
+
import { validateActionDisposition } from "@silkweave/core";
|
|
5
|
+
import { filterErrorResponse, registerTools, rpcInfo } from "@silkweave/mcp/tools";
|
|
5
6
|
//#region src/adapter/edge.ts
|
|
6
7
|
const CORS_HEADERS = {
|
|
7
8
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -106,6 +107,26 @@ function edge(options = {}) {
|
|
|
106
107
|
});
|
|
107
108
|
if (result.auth) requestContext = _context.fork({ auth: result.auth });
|
|
108
109
|
}
|
|
110
|
+
let activeActions = _actions;
|
|
111
|
+
if (options.filterActions) {
|
|
112
|
+
const body = await request.clone().json().catch(() => void 0);
|
|
113
|
+
try {
|
|
114
|
+
activeActions = await options.filterActions(_actions, {
|
|
115
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
116
|
+
url: request.url,
|
|
117
|
+
...rpcInfo(body)
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const mapped = filterErrorResponse(error, body);
|
|
121
|
+
return new Response(JSON.stringify(mapped.body), {
|
|
122
|
+
status: mapped.status,
|
|
123
|
+
headers: {
|
|
124
|
+
...CORS_HEADERS,
|
|
125
|
+
"Content-Type": "application/json"
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
109
130
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
110
131
|
sessionIdGenerator: void 0,
|
|
111
132
|
enableJsonResponse: options.enableJsonResponse
|
|
@@ -118,7 +139,7 @@ function edge(options = {}) {
|
|
|
118
139
|
tools: {},
|
|
119
140
|
logging: {}
|
|
120
141
|
} });
|
|
121
|
-
registerTools(server,
|
|
142
|
+
registerTools(server, activeActions, requestContext, { onToolCall: options.onToolCall });
|
|
122
143
|
await server.connect(transport);
|
|
123
144
|
return transport.handleRequest(request);
|
|
124
145
|
};
|
|
@@ -128,6 +149,7 @@ function edge(options = {}) {
|
|
|
128
149
|
return {
|
|
129
150
|
context: _context,
|
|
130
151
|
start: async (actions) => {
|
|
152
|
+
actions.forEach(validateActionDisposition);
|
|
131
153
|
_actions = actions;
|
|
132
154
|
_readyResolve();
|
|
133
155
|
},
|
package/build/index.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
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, OnToolCall, SilkweaveContext, SilkweaveOptions, validateActionDisposition } from '@silkweave/core'\nimport { filterErrorResponse, registerTools, rpcInfo, type FilterActions } from '@silkweave/mcp/tools'\n\nexport interface EdgeAdapterOptions {\n enableJsonResponse?: boolean\n auth?: AuthConfig\n path?: string\n /**\n * Per-request tool filter, applied before `registerTools()` on every POST\n * (the stateless transport recomputes the tool list per request). See\n * `FilterActions` for the request stand-in (`headers`/`url`/`method`/\n * `toolName`) and error semantics (a throw surfaces as its\n * `SilkweaveError.statusCode` or 500 - never an empty tool list).\n */\n filterActions?: FilterActions\n /** Telemetry hook invoked once per tool call (fire-and-forget). */\n onToolCall?: OnToolCall\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 let activeActions = _actions\n if (options.filterActions) {\n // Clone so the transport can still consume the original body stream.\n const body: unknown = await request.clone().json().catch(() => undefined)\n try {\n activeActions = await options.filterActions(_actions, {\n headers: Object.fromEntries(request.headers.entries()),\n url: request.url,\n ...rpcInfo(body)\n })\n } catch (error) {\n const mapped = filterErrorResponse(error, body)\n return new Response(JSON.stringify(mapped.body), {\n status: mapped.status,\n headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' }\n })\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, activeActions, requestContext, { onToolCall: options.onToolCall })\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.forEach(validateActionDisposition)\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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweave/edge",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Silkweave Web-Standard edge/serverless adapter (Cloudflare Workers, Vercel, Bun, Deno, Hono)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://www.silkweave.dev",
|
|
@@ -28,10 +28,9 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
30
|
"zod": "^3.25.0",
|
|
31
|
-
"@silkweave/
|
|
32
|
-
"@silkweave/
|
|
33
|
-
"@silkweave/
|
|
34
|
-
"@silkweave/core": "3.0.0"
|
|
31
|
+
"@silkweave/auth": "3.2.0",
|
|
32
|
+
"@silkweave/core": "3.2.0",
|
|
33
|
+
"@silkweave/mcp": "3.2.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
36
|
"@eslint/js": "^10.0.1",
|
|
@@ -42,7 +41,8 @@
|
|
|
42
41
|
"tsdown": "^0.21.8",
|
|
43
42
|
"tsx": "^4.21.0",
|
|
44
43
|
"typescript": "^5.9.3",
|
|
45
|
-
"typescript-eslint": "^8.56.1"
|
|
44
|
+
"typescript-eslint": "^8.56.1",
|
|
45
|
+
"vitest": "^4.1.9"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"clean": "rimraf build",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"watch": "tsdown --watch",
|
|
51
51
|
"typecheck": "tsc --noEmit",
|
|
52
52
|
"lint": "eslint",
|
|
53
|
-
"check": "pnpm lint && pnpm typecheck"
|
|
53
|
+
"check": "pnpm lint && pnpm typecheck",
|
|
54
|
+
"test": "vitest run"
|
|
54
55
|
}
|
|
55
56
|
}
|