@silkweave/nextjs 2.4.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 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,161 @@
1
+ # @silkweave/nextjs
2
+
3
+ Next.js **App Router** adapter for Silkweave. Define a set of Actions once and
4
+ project them onto Next.js route handlers - **MCP tools** for agents and a
5
+ **tRPC endpoint** for your own frontend - from a single source of truth.
6
+
7
+ This package is action-first and additive: it adds route files to an existing
8
+ Next.js app, it doesn't restructure anything. Under the hood it wraps
9
+ [`@silkweave/vercel`](../vercel) (MCP over Web-Standard Streamable HTTP) and
10
+ [`@silkweave/trpc`](../trpc) (fetch handler), adding the Next.js glue -
11
+ catch-all path normalization, ergonomic route factories, and end-to-end tRPC
12
+ types.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @silkweave/nextjs @silkweave/core
18
+ # for the typed tRPC client on the frontend:
19
+ pnpm add @trpc/client
20
+ ```
21
+
22
+ Requires Next.js 13.4+ (App Router). The package itself has no `next`/`react`
23
+ dependency - it only deals with Web-Standard `Request`/`Response`.
24
+
25
+ ## Usage
26
+
27
+ ### 1. Define your actions and the app (single source of truth)
28
+
29
+ ```ts
30
+ // silkweave/actions.ts
31
+ import { createAction } from '@silkweave/core'
32
+ import { z } from 'zod'
33
+
34
+ export const listUsers = createAction({
35
+ name: 'list-users',
36
+ kind: 'query',
37
+ description: 'List users',
38
+ input: z.object({ activeOnly: z.boolean().optional() }),
39
+ run: async ({ activeOnly }) => ({ users: activeOnly ? [] : [{ id: '1' }] })
40
+ })
41
+
42
+ export const banUser = createAction({
43
+ name: 'ban-user',
44
+ description: 'Ban a user',
45
+ input: z.object({ id: z.string(), reason: z.string().min(3) }),
46
+ run: async ({ id, reason }) => ({ banned: id, reason })
47
+ })
48
+ ```
49
+
50
+ ```ts
51
+ // silkweave/server.ts
52
+ import { defineSilkweave } from '@silkweave/nextjs'
53
+ import { banUser, listUsers } from './actions'
54
+
55
+ export const app = defineSilkweave({
56
+ name: 'my-app',
57
+ description: 'My app exposed to agents + frontend',
58
+ version: '1.0.0',
59
+ actions: [listUsers, banUser]
60
+ })
61
+
62
+ // Type-only export for the tRPC client:
63
+ export type AppRouter = typeof app.Router
64
+ ```
65
+
66
+ ### 2. Mount the MCP route (one catch-all file)
67
+
68
+ ```ts
69
+ // app/api/mcp/[[...mcp]]/route.ts
70
+ import { app } from '@/silkweave/server'
71
+
72
+ export const { GET, POST, DELETE, OPTIONS } = app.mcp({ basePath: '/api/mcp' })
73
+
74
+ export const dynamic = 'force-dynamic'
75
+ export const runtime = 'nodejs'
76
+ ```
77
+
78
+ The single `[[...mcp]]` catch-all file serves the MCP transport (`/api/mcp`)
79
+ plus any OAuth and protected-resource-metadata sub-paths
80
+ (`/api/mcp/.well-known/...`, `/api/mcp/authorize`, ...). `basePath` is stripped
81
+ from incoming requests so the underlying MCP handler - which matches absolute
82
+ paths - resolves correctly regardless of where you mount it.
83
+
84
+ ### 3. Mount the tRPC route
85
+
86
+ ```ts
87
+ // app/api/trpc/[trpc]/route.ts
88
+ import { app } from '@/silkweave/server'
89
+
90
+ export const { GET, POST, OPTIONS } = app.trpc({ endpoint: '/api/trpc' })
91
+
92
+ export const dynamic = 'force-dynamic'
93
+ export const runtime = 'nodejs'
94
+ ```
95
+
96
+ ### 4. Call it from your frontend with full type safety
97
+
98
+ ```ts
99
+ // lib/trpc.ts
100
+ import { createTRPCClient, httpBatchLink } from '@trpc/client'
101
+ import type { AppRouter } from '@/silkweave/server'
102
+
103
+ export const trpc = createTRPCClient<AppRouter>({
104
+ links: [httpBatchLink({ url: '/api/trpc' })]
105
+ })
106
+
107
+ // const { users } = await trpc.listUsers.query({ activeOnly: true })
108
+ ```
109
+
110
+ ## API
111
+
112
+ ### `defineSilkweave(options)`
113
+
114
+ | Option | Type | Description |
115
+ |--------|------|-------------|
116
+ | `name` / `description` / `version` | `string` | Server identity (`SilkweaveOptions`). |
117
+ | `actions` | `Action[]` | The action set projected onto every surface. |
118
+
119
+ Returns a `SilkweaveApp` with `.mcp()`, `.trpc()`, and a type-only `Router`
120
+ phantom for client inference (`typeof app.Router`).
121
+
122
+ ### `app.mcp(options)` → `{ GET, POST, DELETE, OPTIONS }`
123
+
124
+ | Option | Type | Default | Description |
125
+ |--------|------|---------|-------------|
126
+ | `basePath` | `string` | - (required) | URL prefix; **must equal the route file's directory** (e.g. `/api/mcp`). |
127
+ | `auth` | `AuthConfig` | - | Bearer-token / OAuth 2.1 config (see `@silkweave/auth`). |
128
+ | `enableJsonResponse` | `boolean` | `false` | Return a single JSON response instead of an SSE stream when possible. |
129
+
130
+ ### `app.trpc(options)` → `{ GET, POST, OPTIONS }`
131
+
132
+ | Option | Type | Default | Description |
133
+ |--------|------|---------|-------------|
134
+ | `endpoint` | `string` | - (required) | tRPC prefix; **must equal the route file's directory minus `[trpc]`** (e.g. `/api/trpc`). |
135
+ | `auth` | `AuthConfig` | - | Bearer-token / OAuth 2.1 config. |
136
+ | `cors` | `boolean` | `false` | Add permissive CORS + an `OPTIONS` handler. Enable only for cross-origin clients - a same-origin Next.js frontend needs none. |
137
+
138
+ Lower-level building blocks are also exported (`buildMcpRoute`,
139
+ `buildTrpcRoute`, `normalizeBasePath`, `rewriteRequestPath`) for custom wiring.
140
+
141
+ ## Notes & gotchas
142
+
143
+ - **`basePath` must match the file location.** There's no reliable way to read
144
+ the mounted URL at module load, so you pass it explicitly and it must equal
145
+ the route directory (`/api/mcp` ⇄ `app/api/mcp/[[...mcp]]/route.ts`).
146
+ - **Runtime.** Use `runtime = 'nodejs'` (the MCP transport needs Node APIs) and
147
+ `dynamic = 'force-dynamic'` (these handlers are never statically cached).
148
+ - **RFC 9728 well-known location.** Mounting under `/api/mcp` serves the
149
+ protected-resource metadata under that prefix. MCP discovery is driven by the
150
+ URLs you advertise in `auth`, so self-consistent mounting works. For strict
151
+ spec compliance you can additionally serve
152
+ `/.well-known/oauth-protected-resource` from a root route file.
153
+ - **Streaming actions over tRPC** register as subscriptions; consuming them
154
+ needs an SSE/WS tRPC link, not `httpBatchLink`. MCP streams via progress
155
+ notifications as usual.
156
+ - **One source, many surfaces.** `.mcp()` and `.trpc()` each build their own
157
+ internal Silkweave instance, so mounting both from the same `app` is safe.
158
+
159
+ ## License
160
+
161
+ ISC
@@ -0,0 +1,141 @@
1
+ import { Action, Silkweave, SilkweaveOptions } from "@silkweave/core";
2
+ import { InferTrpcRouter } from "@silkweave/trpc";
3
+ import { AuthConfig } from "@silkweave/auth";
4
+
5
+ //#region src/types.d.ts
6
+ /** A Next.js App Router route handler: receives a Web `Request`, returns a `Response`. */
7
+ type NextRouteHandler = (request: Request) => Promise<Response>;
8
+ /** Options for `app.mcp()` - mounts MCP Streamable HTTP under a catch-all route. */
9
+ interface McpRouteOptions {
10
+ /**
11
+ * The URL prefix this route is mounted at - it MUST equal the route file's
12
+ * directory. For `app/api/mcp/[[...mcp]]/route.ts` this is `'/api/mcp'`.
13
+ *
14
+ * The adapter strips this prefix from incoming requests so the MCP transport
15
+ * (`/api/mcp`), OAuth routes (`/api/mcp/authorize`, `/api/mcp/token`, ...) and
16
+ * the protected-resource metadata (`/api/mcp/.well-known/...`) all resolve
17
+ * from this single catch-all file.
18
+ */
19
+ basePath: string;
20
+ /** Optional bearer-token / OAuth 2.1 configuration (see `@silkweave/auth`). */
21
+ auth?: AuthConfig;
22
+ /** Return a single JSON response instead of an SSE stream when possible. */
23
+ enableJsonResponse?: boolean;
24
+ }
25
+ /** Options for `app.trpc()` - mounts a tRPC endpoint under a `[trpc]` route. */
26
+ interface TrpcRouteOptions {
27
+ /**
28
+ * The tRPC endpoint prefix - it MUST equal the route file's directory minus
29
+ * the `[trpc]` segment. For `app/api/trpc/[trpc]/route.ts` this is
30
+ * `'/api/trpc'`. tRPC strips this prefix itself when routing procedures.
31
+ */
32
+ endpoint: string;
33
+ /** Optional bearer-token / OAuth 2.1 configuration (see `@silkweave/auth`). */
34
+ auth?: AuthConfig;
35
+ /**
36
+ * Add permissive CORS headers + an `OPTIONS` preflight handler. Default
37
+ * `false` - a Next.js frontend calling its own `/api/trpc` is same-origin and
38
+ * needs no CORS. Enable only for cross-origin tRPC clients.
39
+ */
40
+ cors?: boolean;
41
+ }
42
+ /** Handlers to re-export from `app/api/mcp/[[...mcp]]/route.ts`. */
43
+ interface McpRouteHandlers {
44
+ GET: NextRouteHandler;
45
+ POST: NextRouteHandler;
46
+ DELETE: NextRouteHandler;
47
+ OPTIONS: NextRouteHandler;
48
+ }
49
+ /** Handlers to re-export from `app/api/trpc/[trpc]/route.ts`. */
50
+ interface TrpcRouteHandlers {
51
+ GET: NextRouteHandler;
52
+ POST: NextRouteHandler;
53
+ OPTIONS: NextRouteHandler;
54
+ }
55
+ //#endregion
56
+ //#region src/lib/defineSilkweave.d.ts
57
+ /** Maps the actions tuple into the `Record<name, Action>` shape `Silkweave` carries. */
58
+ type ActionsRecord<Arr extends readonly Action[]> = { [K in Arr[number] as K['name']]: K };
59
+ /** The typed `Silkweave` instance equivalent to `silkweave(...).actions(arr)`. */
60
+ type AppServer<Arr extends readonly Action[]> = Silkweave<ActionsRecord<Arr>>;
61
+ /** Configuration for {@link defineSilkweave}: server identity + the action set. */
62
+ type DefineSilkweaveOptions<Arr extends readonly Action[]> = SilkweaveOptions & {
63
+ actions: Arr;
64
+ };
65
+ /**
66
+ * A Silkweave app projected onto Next.js App Router route handlers. Define it
67
+ * once, then mount the surfaces you need from their respective route files.
68
+ */
69
+ interface SilkweaveApp<Arr extends readonly Action[]> {
70
+ /** Build handlers for `app/<basePath>/[[...slug]]/route.ts` (MCP tools). */
71
+ mcp(options: McpRouteOptions): McpRouteHandlers;
72
+ /** Build handlers for `app/<endpoint>/[trpc]/route.ts` (tRPC procedures). */
73
+ trpc(options: TrpcRouteOptions): TrpcRouteHandlers;
74
+ /**
75
+ * Type-only phantom for the tRPC client. Use as
76
+ * `export type AppRouter = typeof app.Router`. Accessing the value at runtime
77
+ * returns `undefined` - it exists purely to carry the inferred router type.
78
+ */
79
+ readonly Router: InferTrpcRouter<AppServer<Arr>>;
80
+ }
81
+ /**
82
+ * Define a Silkweave app from a single set of Actions and project it onto
83
+ * Next.js App Router route handlers - MCP (for agents) and tRPC (for your
84
+ * frontend) - from one source of truth.
85
+ *
86
+ * ```ts
87
+ * // silkweave/server.ts
88
+ * export const app = defineSilkweave({
89
+ * name: 'my-app', description: '...', version: '1.0.0',
90
+ * actions: [listUsers, getUser]
91
+ * })
92
+ * export type AppRouter = typeof app.Router
93
+ * ```
94
+ */
95
+ declare function defineSilkweave<const Arr extends readonly Action[]>(options: DefineSilkweaveOptions<Arr>): SilkweaveApp<Arr>;
96
+ //#endregion
97
+ //#region src/lib/mcpRoute.d.ts
98
+ /**
99
+ * Build the MCP route handlers for a single catch-all Next.js route file.
100
+ *
101
+ * Wires the actions through `@silkweave/vercel` (stateless MCP over Web Standard
102
+ * Streamable HTTP) and wraps its handler with a prefix-stripping rewrite so the
103
+ * transport, OAuth routes and protected-resource metadata all resolve from one
104
+ * `app/<basePath>/[[...slug]]/route.ts` file.
105
+ */
106
+ declare function buildMcpRoute(identity: SilkweaveOptions, actions: readonly Action[], options: McpRouteOptions): McpRouteHandlers;
107
+ //#endregion
108
+ //#region src/lib/trpcRoute.d.ts
109
+ /**
110
+ * Build the tRPC route handlers for a `[trpc]` Next.js route file.
111
+ *
112
+ * Wires the actions through `@silkweave/trpc`'s fetch handler. tRPC strips the
113
+ * `endpoint` prefix itself, so no URL rewriting is needed. CORS is opt-in
114
+ * (`options.cors`) since a Next.js frontend hitting its own `/api/trpc` is
115
+ * same-origin.
116
+ */
117
+ declare function buildTrpcRoute(identity: SilkweaveOptions, actions: readonly Action[], options: TrpcRouteOptions): TrpcRouteHandlers;
118
+ //#endregion
119
+ //#region src/lib/stripPrefix.d.ts
120
+ /**
121
+ * Normalize a user-supplied mount path: ensure a single leading slash and no
122
+ * trailing slash. `'api/mcp/'` -> `'/api/mcp'`, `'/'` -> `''`.
123
+ */
124
+ declare function normalizeBasePath(basePath: string): string;
125
+ /**
126
+ * Rewrite a Web `Request`'s URL so the inner `@silkweave/vercel` MCP handler -
127
+ * which matches absolute pathnames (`/mcp`, `/authorize`, `/.well-known/...`) -
128
+ * sees canonical paths regardless of where the Next.js route is mounted.
129
+ *
130
+ * Given `basePath = '/api/mcp'`:
131
+ * `/api/mcp` -> `/mcp` (the transport root)
132
+ * `/api/mcp/authorize` -> `/authorize`
133
+ * `/api/mcp/.well-known/oauth-...` -> `/.well-known/oauth-...`
134
+ *
135
+ * The method, headers, body and abort signal are preserved. A streaming body
136
+ * requires `duplex: 'half'` under Node's undici `fetch` implementation.
137
+ */
138
+ declare function rewriteRequestPath(request: Request, basePath: string, fallback?: string): Request;
139
+ //#endregion
140
+ export { type DefineSilkweaveOptions, type McpRouteHandlers, type McpRouteOptions, type NextRouteHandler, type SilkweaveApp, type TrpcRouteHandlers, type TrpcRouteOptions, buildMcpRoute, buildTrpcRoute, defineSilkweave, normalizeBasePath, rewriteRequestPath };
141
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/lib/defineSilkweave.ts","../src/lib/mcpRoute.ts","../src/lib/trpcRoute.ts","../src/lib/stripPrefix.ts"],"mappings":";;;;;;KAGY,gBAAA,IAAoB,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;;UAG5C,eAAA;EAHL;;;;;;;;;EAaV,QAAA;EAbmD;EAenD,IAAA,GAAO,UAAA;EAf4D;EAiBnE,kBAAA;AAAA;;UAIe,gBAAA;EANE;;;;;EAYjB,QAAA;EAVkB;EAYlB,IAAA,GAAO,UAAA;EARwB;;;;;EAc/B,IAAA;AAAA;;UAIe,gBAAA;EACf,GAAA,EAAK,gBAAA;EACL,IAAA,EAAM,gBAAA;EACN,MAAA,EAAQ,gBAAA;EACR,OAAA,EAAS,gBAAA;AAAA;;UAIM,iBAAA;EACf,GAAA,EAAK,gBAAA;EACL,IAAA,EAAM,gBAAA;EACN,OAAA,EAAS,gBAAA;AAAA;;;;KC9CN,aAAA,sBAAmC,MAAA,cAChC,GAAA,YAAe,CAAA,WAAY,CAAA;;KAI9B,SAAA,sBAA+B,MAAA,MAAY,SAAA,CAAU,aAAA,CAAc,GAAA;;KAG5D,sBAAA,sBAA4C,MAAA,MAAY,gBAAA;EAClE,OAAA,EAAS,GAAA;AAAA;;;;;UAOM,YAAA,sBAAkC,MAAA;EDpBkB;ECsBnE,GAAA,CAAI,OAAA,EAAS,eAAA,GAAkB,gBAAA;EDnBD;ECqB9B,IAAA,CAAK,OAAA,EAAS,gBAAA,GAAmB,iBAAA;EDThB;;;;;EAAA,SCeR,MAAA,EAAQ,eAAA,CAAgB,SAAA,CAAU,GAAA;AAAA;ADT7C;;;;;;;;;;AAkBA;;;;AAlBA,iBC0BgB,eAAA,4BAA2C,MAAA,GAAA,CACzD,OAAA,EAAS,sBAAA,CAAuB,GAAA,IAC/B,YAAA,CAAa,GAAA;;;;;;ADjDhB;;;;;iBEUgB,aAAA,CACd,QAAA,EAAU,gBAAA,EACV,OAAA,WAAkB,MAAA,IAClB,OAAA,EAAS,eAAA,GACR,gBAAA;;;;;;AFdH;;;;;iBGyBgB,cAAA,CACd,QAAA,EAAU,gBAAA,EACV,OAAA,WAAkB,MAAA,IAClB,OAAA,EAAS,gBAAA,GACR,iBAAA;;;;;;;iBC5Ba,iBAAA,CAAkB,QAAA;AJDlC;;;;;;;;;;;;;AAAA,iBIoBgB,kBAAA,CAAmB,OAAA,EAAS,OAAA,EAAS,QAAA,UAAkB,QAAA,YAAoB,OAAA"}
@@ -0,0 +1,149 @@
1
+ import { silkweave } from "@silkweave/core";
2
+ import { vercel } from "@silkweave/vercel";
3
+ import { trpcFetch } from "@silkweave/trpc";
4
+ //#region src/lib/stripPrefix.ts
5
+ /**
6
+ * Normalize a user-supplied mount path: ensure a single leading slash and no
7
+ * trailing slash. `'api/mcp/'` -> `'/api/mcp'`, `'/'` -> `''`.
8
+ */
9
+ function normalizeBasePath(basePath) {
10
+ const trimmed = basePath.trim().replace(/\/+$/, "");
11
+ if (trimmed === "" || trimmed === "/") return "";
12
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
13
+ }
14
+ /**
15
+ * Rewrite a Web `Request`'s URL so the inner `@silkweave/vercel` MCP handler -
16
+ * which matches absolute pathnames (`/mcp`, `/authorize`, `/.well-known/...`) -
17
+ * sees canonical paths regardless of where the Next.js route is mounted.
18
+ *
19
+ * Given `basePath = '/api/mcp'`:
20
+ * `/api/mcp` -> `/mcp` (the transport root)
21
+ * `/api/mcp/authorize` -> `/authorize`
22
+ * `/api/mcp/.well-known/oauth-...` -> `/.well-known/oauth-...`
23
+ *
24
+ * The method, headers, body and abort signal are preserved. A streaming body
25
+ * requires `duplex: 'half'` under Node's undici `fetch` implementation.
26
+ */
27
+ function rewriteRequestPath(request, basePath, fallback = "/mcp") {
28
+ const base = normalizeBasePath(basePath);
29
+ const url = new URL(request.url);
30
+ let rest = url.pathname;
31
+ if (base !== "") {
32
+ if (rest === base) rest = fallback;
33
+ else if (rest.startsWith(`${base}/`)) rest = rest.slice(base.length);
34
+ }
35
+ if (rest === "" || rest === "/") rest = fallback;
36
+ url.pathname = rest;
37
+ const init = {
38
+ method: request.method,
39
+ headers: request.headers,
40
+ signal: request.signal
41
+ };
42
+ if (request.method !== "GET" && request.method !== "HEAD") {
43
+ init.body = request.body;
44
+ init.duplex = "half";
45
+ }
46
+ return new Request(url.toString(), init);
47
+ }
48
+ //#endregion
49
+ //#region src/lib/mcpRoute.ts
50
+ /**
51
+ * Build the MCP route handlers for a single catch-all Next.js route file.
52
+ *
53
+ * Wires the actions through `@silkweave/vercel` (stateless MCP over Web Standard
54
+ * Streamable HTTP) and wraps its handler with a prefix-stripping rewrite so the
55
+ * transport, OAuth routes and protected-resource metadata all resolve from one
56
+ * `app/<basePath>/[[...slug]]/route.ts` file.
57
+ */
58
+ function buildMcpRoute(identity, actions, options) {
59
+ const basePath = normalizeBasePath(options.basePath);
60
+ const { adapter, handler } = vercel({
61
+ auth: options.auth,
62
+ enableJsonResponse: options.enableJsonResponse
63
+ });
64
+ silkweave(identity).actions(actions).adapter(adapter).start();
65
+ const route = (request) => handler(rewriteRequestPath(request, basePath));
66
+ return {
67
+ GET: route,
68
+ POST: route,
69
+ DELETE: route,
70
+ OPTIONS: route
71
+ };
72
+ }
73
+ //#endregion
74
+ //#region src/lib/trpcRoute.ts
75
+ const CORS_HEADERS = {
76
+ "Access-Control-Allow-Origin": "*",
77
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
78
+ "Access-Control-Allow-Headers": "*",
79
+ "Access-Control-Max-Age": "86400"
80
+ };
81
+ function withCors(handler) {
82
+ return async (request) => {
83
+ const response = await handler(request);
84
+ const headers = new Headers(response.headers);
85
+ for (const [key, value] of Object.entries(CORS_HEADERS)) headers.set(key, value);
86
+ return new Response(response.body, {
87
+ status: response.status,
88
+ statusText: response.statusText,
89
+ headers
90
+ });
91
+ };
92
+ }
93
+ /**
94
+ * Build the tRPC route handlers for a `[trpc]` Next.js route file.
95
+ *
96
+ * Wires the actions through `@silkweave/trpc`'s fetch handler. tRPC strips the
97
+ * `endpoint` prefix itself, so no URL rewriting is needed. CORS is opt-in
98
+ * (`options.cors`) since a Next.js frontend hitting its own `/api/trpc` is
99
+ * same-origin.
100
+ */
101
+ function buildTrpcRoute(identity, actions, options) {
102
+ const { adapter, GET, POST } = trpcFetch({
103
+ endpoint: options.endpoint,
104
+ auth: options.auth
105
+ });
106
+ silkweave(identity).actions(actions).adapter(adapter).start();
107
+ const optionsHandler = () => Promise.resolve(new Response(null, {
108
+ status: 204,
109
+ headers: options.cors ? CORS_HEADERS : {}
110
+ }));
111
+ if (!options.cors) return {
112
+ GET,
113
+ POST,
114
+ OPTIONS: optionsHandler
115
+ };
116
+ return {
117
+ GET: withCors(GET),
118
+ POST: withCors(POST),
119
+ OPTIONS: optionsHandler
120
+ };
121
+ }
122
+ //#endregion
123
+ //#region src/lib/defineSilkweave.ts
124
+ /**
125
+ * Define a Silkweave app from a single set of Actions and project it onto
126
+ * Next.js App Router route handlers - MCP (for agents) and tRPC (for your
127
+ * frontend) - from one source of truth.
128
+ *
129
+ * ```ts
130
+ * // silkweave/server.ts
131
+ * export const app = defineSilkweave({
132
+ * name: 'my-app', description: '...', version: '1.0.0',
133
+ * actions: [listUsers, getUser]
134
+ * })
135
+ * export type AppRouter = typeof app.Router
136
+ * ```
137
+ */
138
+ function defineSilkweave(options) {
139
+ const { actions, ...identity } = options;
140
+ return {
141
+ mcp: (mcpOptions) => buildMcpRoute(identity, actions, mcpOptions),
142
+ trpc: (trpcOptions) => buildTrpcRoute(identity, actions, trpcOptions),
143
+ Router: void 0
144
+ };
145
+ }
146
+ //#endregion
147
+ export { buildMcpRoute, buildTrpcRoute, defineSilkweave, normalizeBasePath, rewriteRequestPath };
148
+
149
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/stripPrefix.ts","../src/lib/mcpRoute.ts","../src/lib/trpcRoute.ts","../src/lib/defineSilkweave.ts"],"sourcesContent":["/**\n * Normalize a user-supplied mount path: ensure a single leading slash and no\n * trailing slash. `'api/mcp/'` -> `'/api/mcp'`, `'/'` -> `''`.\n */\nexport function normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim().replace(/\\/+$/, '')\n if (trimmed === '' || trimmed === '/') { return '' }\n return trimmed.startsWith('/') ? trimmed : `/${trimmed}`\n}\n\n/**\n * Rewrite a Web `Request`'s URL so the inner `@silkweave/vercel` MCP handler -\n * which matches absolute pathnames (`/mcp`, `/authorize`, `/.well-known/...`) -\n * sees canonical paths regardless of where the Next.js route is mounted.\n *\n * Given `basePath = '/api/mcp'`:\n * `/api/mcp` -> `/mcp` (the transport root)\n * `/api/mcp/authorize` -> `/authorize`\n * `/api/mcp/.well-known/oauth-...` -> `/.well-known/oauth-...`\n *\n * The method, headers, body and abort signal are preserved. A streaming body\n * requires `duplex: 'half'` under Node's undici `fetch` implementation.\n */\nexport function rewriteRequestPath(request: Request, basePath: string, fallback = '/mcp'): Request {\n const base = normalizeBasePath(basePath)\n const url = new URL(request.url)\n\n let rest = url.pathname\n if (base !== '') {\n if (rest === base) {\n rest = fallback\n } else if (rest.startsWith(`${base}/`)) {\n rest = rest.slice(base.length)\n }\n }\n if (rest === '' || rest === '/') { rest = fallback }\n url.pathname = rest\n\n const init: RequestInit & { duplex?: 'half' } = {\n method: request.method,\n headers: request.headers,\n signal: request.signal\n }\n if (request.method !== 'GET' && request.method !== 'HEAD') {\n init.body = request.body\n init.duplex = 'half'\n }\n return new Request(url.toString(), init)\n}\n","import { Action, silkweave, SilkweaveOptions } from '@silkweave/core'\nimport { vercel } from '@silkweave/vercel'\nimport { McpRouteHandlers, McpRouteOptions } from '../types.js'\nimport { normalizeBasePath, rewriteRequestPath } from './stripPrefix.js'\n\n/**\n * Build the MCP route handlers for a single catch-all Next.js route file.\n *\n * Wires the actions through `@silkweave/vercel` (stateless MCP over Web Standard\n * Streamable HTTP) and wraps its handler with a prefix-stripping rewrite so the\n * transport, OAuth routes and protected-resource metadata all resolve from one\n * `app/<basePath>/[[...slug]]/route.ts` file.\n */\nexport function buildMcpRoute(\n identity: SilkweaveOptions,\n actions: readonly Action[],\n options: McpRouteOptions\n): McpRouteHandlers {\n const basePath = normalizeBasePath(options.basePath)\n const { adapter, handler } = vercel({\n auth: options.auth,\n enableJsonResponse: options.enableJsonResponse\n })\n\n // Resolve the adapter's `_ready` promise. The handler awaits it internally, so\n // we don't need to await here - a floating start is safe and avoids forcing a\n // top-level `await` into the consumer's route module.\n void silkweave(identity).actions(actions).adapter(adapter).start()\n\n const route = (request: Request): Promise<Response> => handler(rewriteRequestPath(request, basePath))\n\n return { GET: route, POST: route, DELETE: route, OPTIONS: route }\n}\n","import { Action, silkweave, SilkweaveOptions } from '@silkweave/core'\nimport { trpcFetch } from '@silkweave/trpc'\nimport { NextRouteHandler, TrpcRouteHandlers, TrpcRouteOptions } from '../types.js'\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': '*',\n 'Access-Control-Max-Age': '86400'\n}\n\nfunction withCors(handler: NextRouteHandler): NextRouteHandler {\n return async (request) => {\n const response = await handler(request)\n const headers = new Headers(response.headers)\n for (const [key, value] of Object.entries(CORS_HEADERS)) { headers.set(key, value) }\n return new Response(response.body, { status: response.status, statusText: response.statusText, headers })\n }\n}\n\n/**\n * Build the tRPC route handlers for a `[trpc]` Next.js route file.\n *\n * Wires the actions through `@silkweave/trpc`'s fetch handler. tRPC strips the\n * `endpoint` prefix itself, so no URL rewriting is needed. CORS is opt-in\n * (`options.cors`) since a Next.js frontend hitting its own `/api/trpc` is\n * same-origin.\n */\nexport function buildTrpcRoute(\n identity: SilkweaveOptions,\n actions: readonly Action[],\n options: TrpcRouteOptions\n): TrpcRouteHandlers {\n const { adapter, GET, POST } = trpcFetch({ endpoint: options.endpoint, auth: options.auth })\n\n void silkweave(identity).actions(actions).adapter(adapter).start()\n\n const optionsHandler: NextRouteHandler = () =>\n Promise.resolve(new Response(null, { status: 204, headers: options.cors ? CORS_HEADERS : {} }))\n\n if (!options.cors) {\n return { GET, POST, OPTIONS: optionsHandler }\n }\n return { GET: withCors(GET), POST: withCors(POST), OPTIONS: optionsHandler }\n}\n","import { Action, Silkweave, SilkweaveOptions } from '@silkweave/core'\nimport type { InferTrpcRouter } from '@silkweave/trpc'\nimport { McpRouteHandlers, McpRouteOptions, TrpcRouteHandlers, TrpcRouteOptions } from '../types.js'\nimport { buildMcpRoute } from './mcpRoute.js'\nimport { buildTrpcRoute } from './trpcRoute.js'\n\n/** Maps the actions tuple into the `Record<name, Action>` shape `Silkweave` carries. */\ntype ActionsRecord<Arr extends readonly Action[]> = {\n [K in Arr[number] as K['name']]: K\n}\n\n/** The typed `Silkweave` instance equivalent to `silkweave(...).actions(arr)`. */\ntype AppServer<Arr extends readonly Action[]> = Silkweave<ActionsRecord<Arr>>\n\n/** Configuration for {@link defineSilkweave}: server identity + the action set. */\nexport type DefineSilkweaveOptions<Arr extends readonly Action[]> = SilkweaveOptions & {\n actions: Arr\n}\n\n/**\n * A Silkweave app projected onto Next.js App Router route handlers. Define it\n * once, then mount the surfaces you need from their respective route files.\n */\nexport interface SilkweaveApp<Arr extends readonly Action[]> {\n /** Build handlers for `app/<basePath>/[[...slug]]/route.ts` (MCP tools). */\n mcp(options: McpRouteOptions): McpRouteHandlers\n /** Build handlers for `app/<endpoint>/[trpc]/route.ts` (tRPC procedures). */\n trpc(options: TrpcRouteOptions): TrpcRouteHandlers\n /**\n * Type-only phantom for the tRPC client. Use as\n * `export type AppRouter = typeof app.Router`. Accessing the value at runtime\n * returns `undefined` - it exists purely to carry the inferred router type.\n */\n readonly Router: InferTrpcRouter<AppServer<Arr>>\n}\n\n/**\n * Define a Silkweave app from a single set of Actions and project it onto\n * Next.js App Router route handlers - MCP (for agents) and tRPC (for your\n * frontend) - from one source of truth.\n *\n * ```ts\n * // silkweave/server.ts\n * export const app = defineSilkweave({\n * name: 'my-app', description: '...', version: '1.0.0',\n * actions: [listUsers, getUser]\n * })\n * export type AppRouter = typeof app.Router\n * ```\n */\nexport function defineSilkweave<const Arr extends readonly Action[]>(\n options: DefineSilkweaveOptions<Arr>\n): SilkweaveApp<Arr> {\n const { actions, ...identity } = options\n\n return {\n mcp: (mcpOptions) => buildMcpRoute(identity, actions, mcpOptions),\n trpc: (trpcOptions) => buildTrpcRoute(identity, actions, trpcOptions),\n Router: undefined as unknown as InferTrpcRouter<AppServer<Arr>>\n }\n}\n"],"mappings":";;;;;;;;AAIA,SAAgB,kBAAkB,UAA0B;CAC1D,MAAM,UAAU,SAAS,MAAM,CAAC,QAAQ,QAAQ,GAAG;AACnD,KAAI,YAAY,MAAM,YAAY,IAAO,QAAO;AAChD,QAAO,QAAQ,WAAW,IAAI,GAAG,UAAU,IAAI;;;;;;;;;;;;;;;AAgBjD,SAAgB,mBAAmB,SAAkB,UAAkB,WAAW,QAAiB;CACjG,MAAM,OAAO,kBAAkB,SAAS;CACxC,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI;CAEhC,IAAI,OAAO,IAAI;AACf,KAAI,SAAS;MACP,SAAS,KACX,QAAO;WACE,KAAK,WAAW,GAAG,KAAK,GAAG,CACpC,QAAO,KAAK,MAAM,KAAK,OAAO;;AAGlC,KAAI,SAAS,MAAM,SAAS,IAAO,QAAO;AAC1C,KAAI,WAAW;CAEf,MAAM,OAA0C;EAC9C,QAAQ,QAAQ;EAChB,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EACjB;AACD,KAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ;AACzD,OAAK,OAAO,QAAQ;AACpB,OAAK,SAAS;;AAEhB,QAAO,IAAI,QAAQ,IAAI,UAAU,EAAE,KAAK;;;;;;;;;;;;AClC1C,SAAgB,cACd,UACA,SACA,SACkB;CAClB,MAAM,WAAW,kBAAkB,QAAQ,SAAS;CACpD,MAAM,EAAE,SAAS,YAAY,OAAO;EAClC,MAAM,QAAQ;EACd,oBAAoB,QAAQ;EAC7B,CAAC;AAKG,WAAU,SAAS,CAAC,QAAQ,QAAQ,CAAC,QAAQ,QAAQ,CAAC,OAAO;CAElE,MAAM,SAAS,YAAwC,QAAQ,mBAAmB,SAAS,SAAS,CAAC;AAErG,QAAO;EAAE,KAAK;EAAO,MAAM;EAAO,QAAQ;EAAO,SAAS;EAAO;;;;AC3BnE,MAAM,eAAuC;CAC3C,+BAA+B;CAC/B,gCAAgC;CAChC,gCAAgC;CAChC,0BAA0B;CAC3B;AAED,SAAS,SAAS,SAA6C;AAC7D,QAAO,OAAO,YAAY;EACxB,MAAM,WAAW,MAAM,QAAQ,QAAQ;EACvC,MAAM,UAAU,IAAI,QAAQ,SAAS,QAAQ;AAC7C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,aAAa,CAAI,SAAQ,IAAI,KAAK,MAAM;AAClF,SAAO,IAAI,SAAS,SAAS,MAAM;GAAE,QAAQ,SAAS;GAAQ,YAAY,SAAS;GAAY;GAAS,CAAC;;;;;;;;;;;AAY7G,SAAgB,eACd,UACA,SACA,SACmB;CACnB,MAAM,EAAE,SAAS,KAAK,SAAS,UAAU;EAAE,UAAU,QAAQ;EAAU,MAAM,QAAQ;EAAM,CAAC;AAEvF,WAAU,SAAS,CAAC,QAAQ,QAAQ,CAAC,QAAQ,QAAQ,CAAC,OAAO;CAElE,MAAM,uBACJ,QAAQ,QAAQ,IAAI,SAAS,MAAM;EAAE,QAAQ;EAAK,SAAS,QAAQ,OAAO,eAAe,EAAE;EAAE,CAAC,CAAC;AAEjG,KAAI,CAAC,QAAQ,KACX,QAAO;EAAE;EAAK;EAAM,SAAS;EAAgB;AAE/C,QAAO;EAAE,KAAK,SAAS,IAAI;EAAE,MAAM,SAAS,KAAK;EAAE,SAAS;EAAgB;;;;;;;;;;;;;;;;;;ACO9E,SAAgB,gBACd,SACmB;CACnB,MAAM,EAAE,SAAS,GAAG,aAAa;AAEjC,QAAO;EACL,MAAM,eAAe,cAAc,UAAU,SAAS,WAAW;EACjE,OAAO,gBAAgB,eAAe,UAAU,SAAS,YAAY;EACrE,QAAQ,KAAA;EACT"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@silkweave/nextjs",
3
+ "version": "2.4.0",
4
+ "description": "Silkweave Next.js App Router Adapter",
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/nextjs"
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
+ "@trpc/server": "^11.7.1",
30
+ "zod": "^3.25.0",
31
+ "@silkweave/auth": "2.4.0",
32
+ "@silkweave/core": "2.4.0",
33
+ "@silkweave/trpc": "2.4.0",
34
+ "@silkweave/vercel": "2.4.0"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "@stylistic/eslint-plugin": "^5.10.0",
39
+ "@types/node": "^22.0.0",
40
+ "eslint": "^10.4.0",
41
+ "rimraf": "^6.1.3",
42
+ "tsdown": "^0.21.8",
43
+ "tsx": "^4.21.0",
44
+ "typescript": "^5.9.3",
45
+ "typescript-eslint": "^8.56.1"
46
+ },
47
+ "scripts": {
48
+ "clean": "rimraf build",
49
+ "build": "tsdown",
50
+ "watch": "tsdown --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "lint": "eslint",
53
+ "check": "pnpm lint && pnpm typecheck"
54
+ }
55
+ }