@vvedantb/server 0.0.0-stage → 0.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/convex/authConfig.d.ts +18 -0
  4. package/dist/convex/authConfig.js +19 -0
  5. package/dist/convex/bearerToken.d.ts +2 -0
  6. package/dist/convex/bearerToken.js +11 -0
  7. package/dist/convex/clerk.d.ts +13 -0
  8. package/dist/convex/clerk.js +17 -0
  9. package/dist/convex/convexQueryLimits.d.ts +11 -0
  10. package/dist/convex/convexQueryLimits.js +28 -0
  11. package/dist/convex/cors.d.ts +11 -0
  12. package/dist/convex/cors.js +20 -0
  13. package/dist/convex/env.d.ts +5 -0
  14. package/dist/convex/env.js +12 -0
  15. package/dist/convex/index.d.ts +8 -0
  16. package/dist/convex/index.js +8 -0
  17. package/dist/convex/mcpServerInfo.d.ts +6 -0
  18. package/dist/convex/mcpServerInfo.js +8 -0
  19. package/dist/convex/mcpTestHelpers.d.ts +7 -0
  20. package/dist/convex/mcpTestHelpers.js +22 -0
  21. package/dist/mcp/cors.d.ts +14 -0
  22. package/dist/mcp/cors.js +13 -0
  23. package/dist/mcp/dispatcher.d.ts +39 -0
  24. package/dist/mcp/dispatcher.js +105 -0
  25. package/dist/mcp/env.d.ts +23 -0
  26. package/dist/mcp/env.js +20 -0
  27. package/dist/mcp/explorer.d.ts +98 -0
  28. package/dist/mcp/explorer.js +299 -0
  29. package/dist/mcp/http.d.ts +105 -0
  30. package/dist/mcp/http.js +123 -0
  31. package/dist/mcp/index.d.ts +10 -0
  32. package/dist/mcp/index.js +10 -0
  33. package/dist/mcp/json.d.ts +18 -0
  34. package/dist/mcp/json.js +7 -0
  35. package/dist/mcp/jsonRpc.d.ts +61 -0
  36. package/dist/mcp/jsonRpc.js +60 -0
  37. package/dist/mcp/metadata.d.ts +59 -0
  38. package/dist/mcp/metadata.js +63 -0
  39. package/dist/mcp/sdk.d.ts +56 -0
  40. package/dist/mcp/sdk.js +52 -0
  41. package/dist/mcp/tools.d.ts +176 -0
  42. package/dist/mcp/tools.js +159 -0
  43. package/docs/convex.md +69 -0
  44. package/docs/mcp.md +227 -0
  45. package/docs/migration.md +163 -0
  46. package/package.json +55 -3
package/docs/convex.md ADDED
@@ -0,0 +1,69 @@
1
+ # @vvedantb/server/convex
2
+
3
+ Plain Convex backend helpers. No React, no generated Convex imports. Compiled ESM with types. Import it as `@vvedantb/server/convex`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @vvedantb/server
9
+ ```
10
+
11
+ ## Peers
12
+
13
+ None required. `@clerk/backend` is an optional peer: install it only if you use `getClerkClient`, and pass its `createClerkClient`.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ // convex/auth.config.ts
19
+ import { clerkAuthConfig } from "@vvedantb/server/convex";
20
+ export default clerkAuthConfig(); // domain from CLERK_FRONTEND_API_URL, applicationID "convex"
21
+ ```
22
+
23
+ ```ts
24
+ import { createClerkClient } from "@clerk/backend";
25
+ import { getClerkClient, requiredEnvUrl, extractBearerToken } from "@vvedantb/server/convex";
26
+
27
+ const clerk = getClerkClient(createClerkClient); // throws if CLERK_SECRET_KEY or CLERK_PUBLISHABLE_KEY is missing
28
+ const issuer = requiredEnvUrl("CLERK_FRONTEND_API_URL"); // trailing slash removed
29
+ const token = extractBearerToken(request.headers.get("Authorization"));
30
+ ```
31
+
32
+ ```ts
33
+ import { corsPreflight, jsonWithCors, defineMcpServerInfo } from "@vvedantb/server/convex";
34
+
35
+ if (request.method === "OPTIONS") return corsPreflight();
36
+ return jsonWithCors({ error: "Unauthorized" }, { status: 401, extraHeaders: { "WWW-Authenticate": "Bearer" } });
37
+
38
+ const serverInfo = defineMcpServerInfo({ name: "my-mcp", version: "1.0.0" });
39
+ ```
40
+
41
+ ```ts
42
+ import { formatConvexQueryError, isConvexResourceLimitError } from "@vvedantb/server/convex";
43
+ import { assertMcpTools, expectMcpToolCount } from "@vvedantb/server/convex";
44
+
45
+ assertMcpTools(toolNames, ["search", "write"]); // sorted, unique comparison
46
+ expectMcpToolCount(toolNames, 2);
47
+ ```
48
+
49
+ | Export | What it does |
50
+ | --- | --- |
51
+ | `isConvexResourceLimitError`, `formatConvexQueryError` | Detect Convex read, write and time limit failures and add a remedy to the message. |
52
+ | `clerkAuthConfig(env?)` | `auth.config.ts` provider list. |
53
+ | `getClerkClient(factory, env?)` | Clerk backend client from env, with an injected factory. |
54
+ | `extractBearerToken(header)` | Token from an `Authorization` header, or `null`. |
55
+ | `requiredEnv(name, env?)`, `requiredEnvUrl(name, env?)` | Required env value, optionally stripping a trailing slash. |
56
+ | `mcpCorsHeaders`, `corsPreflight`, `jsonWithCors` | CORS for MCP HTTP handlers. Methods `GET,POST,DELETE,OPTIONS`; headers `Authorization,Content-Type`; credentials `false`. |
57
+ | `defineMcpServerInfo({ name, version })` | MCP `serverInfo`. The caller owns both values. |
58
+ | `expectMcpToolCount`, `assertMcpTools` | Test helpers. They throw plain errors, so they work with any runner. |
59
+
60
+ Not included: Clerk user helpers, `ensureUserExists`, rate limits, crypto, MCP scaffolding.
61
+
62
+ ## Develop
63
+
64
+ ```sh
65
+ pnpm --filter @vvedantb/server build
66
+ pnpm --filter @vvedantb/server test
67
+ ```
68
+
69
+ Publish steps for the whole package are in the root `README.md`.
package/docs/mcp.md ADDED
@@ -0,0 +1,227 @@
1
+ # `@vvedantb/server/mcp`
2
+
3
+ MCP server scaffolding for Convex `httpAction`s: the `/mcp` bearer-auth handler, OAuth metadata endpoints, a pure JSON-RPC dispatcher, tool helpers and user-scoped explorer tools. Web-standard `Request`/`Response` only, no Node APIs, no MCP SDK dependency. Compiled ESM with types. Import it as `@vvedantb/server/mcp`.
4
+
5
+ It replaces the near-copies of `convex/mcp/native.ts`, the `handleMcpRequest` body and the explorer tools in app-a, app-b, app-c and app-d. app-e uses only the metadata, CORS and protocol-version helpers. See [docs/migration.md](docs/migration.md) for per-app steps and measured line counts.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @vvedantb/server
11
+ ```
12
+
13
+ ## Dependencies and peers
14
+
15
+ - `@vvedantb/server/convex` ships in the same package. The MCP kit imports its `extractBearerToken`, `requiredEnvUrl`, `mcpCorsHeaders`, `McpServerInfo` and `EnvSource` through relative imports, so there is no second install step and no version to match.
16
+ - `zod` is an optional peer (`^3.25.0 || ^4.0.0`). The kit never imports zod. `defineTool` accepts any object with zod's `safeParse` (zod 3.25+ or 4), and calls the schema's own `toJSONSchema()` (zod 4) for `tools/list`. With zod 3, pass `jsonSchema` per tool or a `toJsonSchema` converter (see below).
17
+ - Nothing else. The MCP SDK is optional and stays in the app (`runSdkMcpRequest` takes its classes as arguments).
18
+
19
+ ## Quick start
20
+
21
+ Today's architecture: the HTTP layer runs in `httpAction`; JSON-RPC runs in a `"use node"` internalAction with the MCP SDK.
22
+
23
+ ```ts
24
+ // convex/mcp/native.ts
25
+ import { httpAction, type ActionCtx } from "../_generated/server";
26
+ import { internal } from "../_generated/api";
27
+ import {
28
+ createAuthorizationServerMetadataProxy,
29
+ createHealthHandler,
30
+ createMcpHttpHandler,
31
+ createProtectedResourceMetadata,
32
+ } from "@vvedantb/server/mcp";
33
+
34
+ export const oauthMetadata = httpAction(createAuthorizationServerMetadataProxy());
35
+ export const protectedResourceMetadata = httpAction(createProtectedResourceMetadata());
36
+ export const health = httpAction(createHealthHandler());
37
+
38
+ export const mcpHandler = httpAction(
39
+ createMcpHttpHandler<{ clerkUserId: string }, ActionCtx>({
40
+ authenticate: ({ ctx, token }) =>
41
+ ctx.runAction(internal.mcp.nodeActions.verifyAccessToken, { token }),
42
+ handleRpc: ({ ctx, principal, body }) =>
43
+ ctx.runAction(internal.mcp.nodeActions.handleMcpRequest, { clerkUserId: principal.clerkUserId, body }),
44
+ }),
45
+ );
46
+ ```
47
+
48
+ ```ts
49
+ // convex/mcp/nodeActions.ts ("use node")
50
+ import { runSdkMcpRequest } from "@vvedantb/server/mcp";
51
+
52
+ export const handleMcpRequest = internalAction({
53
+ args: { clerkUserId: v.string(), body: v.string() },
54
+ returns: v.object({ status: v.number(), body: v.string() }),
55
+ handler: async (ctx, { clerkUserId, body }) =>
56
+ runSdkMcpRequest({
57
+ body,
58
+ Transport: WebStandardStreamableHTTPServerTransport,
59
+ createServer: () => {
60
+ const server = new McpServer(defineMcpServerInfo({ name: "my-mcp", version: "1.0.0" }));
61
+ registerTools(server, clerkUserId, ctx);
62
+ return server;
63
+ },
64
+ }),
65
+ });
66
+ ```
67
+
68
+ Self-contained alternative (no SDK, no `"use node"`): replace `handleRpc` with the kit dispatcher.
69
+
70
+ ```ts
71
+ const dispatcher = createMcpDispatcher<Principal, ActionCtx>({
72
+ serverInfo: defineMcpServerInfo({ name: "my-mcp", version: "1.0.0" }),
73
+ tools: [searchTool, ...createExplorerTools({ tables, execute })],
74
+ });
75
+
76
+ export const mcpHandler = httpAction(
77
+ createMcpHttpHandler<Principal, ActionCtx>({ authenticate, handleRpc: dispatcher.handleRpc }),
78
+ );
79
+ ```
80
+
81
+ ## API
82
+
83
+ ### HTTP layer
84
+
85
+ | Export | What it does |
86
+ | --- | --- |
87
+ | `createMcpHttpHandler(config)` | `/mcp` handler for `httpAction`. Order: CORS preflight (if `cors`), optional early method check, bearer token (`extractBearerToken`), `authenticate`, method check, body parsing, `handleRpc`. |
88
+ | `createAuthorizationServerMetadataProxy(options)` | `GET /.well-known/oauth-authorization-server`: fetches `<issuer>/.well-known/oauth-authorization-server`. 502 `{"error":"Failed to fetch authorization server metadata"}` on a non-2xx upstream. |
89
+ | `createProtectedResourceMetadata(options)` | `{ resource, authorization_servers, bearer_methods_supported: ["header"], resource_documentation }`. |
90
+ | `createHealthHandler(options)` | 200 `{"status":"ok"}` (body configurable). |
91
+ | `createOptionsHandler({ cors })` | 204 preflight with only the CORS headers. |
92
+ | `mcpCors(overrides?, origin?)` | `mcpCorsHeaders(origin)` from `@vvedantb/server/convex` with overrides on top. |
93
+ | `readEnvUrl(source, env?)` | Env URL with optional fallback var, through `requiredEnvUrl` from `@vvedantb/server/convex`. |
94
+ | `consoleMcpLogger` | Default log hook with the app-a strings. |
95
+ | `runSdkMcpRequest(options)` | The apps' `handleMcpRequest` body: stateless JSON-response SDK transport, 500 JSON-RPC -32603 on any throw. SDK classes are passed in. |
96
+ | `mcpInternalError(err)` | `{ status: 500, body: '{"jsonrpc":"2.0","error":{"code":-32603,"message":...},"id":null}' }`. |
97
+
98
+ `createMcpHttpHandler` config:
99
+
100
+ | Key | Default | Notes |
101
+ | --- | --- | --- |
102
+ | `authenticate({ token, request, ctx, scope })` | required | Return the principal or `null` (401 "Invalid or expired token"). A throw escapes (Convex answers 500), as in the apps today; `onAuthenticateError: "unauthorized"` maps it to 401 instead. |
103
+ | `handleRpc({ principal, body, json, request, ctx, scope })` | required | Returns `{ status, body }`, sent with `Content-Type: application/json` plus CORS. `body` is the JSON text, `json` the parsed value. |
104
+ | `scope` | none | Opaque value passed to both callbacks (app-a `"personal"` / `"team"`). |
105
+ | `resourceMetadataPath` / `resourceMetadataUrl(request)` | `/.well-known/oauth-protected-resource` | Used in `WWW-Authenticate: Bearer resource_metadata="<origin><path>"`. |
106
+ | `rejectMethods` | `["GET", "DELETE"]` | 405 "Method not supported in stateless mode". |
107
+ | `methodCheck` | `"after-auth"` | All four apps check after auth; app-e before. |
108
+ | `body` | `"json"` | `"json"`: `JSON.stringify(await request.json())` (app-a, app-b, app-c). `"text"`: raw text checked with `JSON.parse` (app-d). Bad JSON: 400 "Invalid JSON body". |
109
+ | `cors` | none | `false`, a header map, or `(request) => headers`. With CORS, `OPTIONS` returns a 204 preflight. |
110
+ | `headers` | none | Extra headers on every response. |
111
+ | `messages` | apps' strings | `missingToken`, `invalidToken`, `methodNotAllowed`, `invalidJson`. |
112
+ | `log` | `consoleMcpLogger` | Events: `auth-failed`, `invalid-json`, `authenticate-threw`. `false` for none. |
113
+
114
+ Env fallback modes (`EnvUrl = string | { name, fallback?, fallbackMode? }`). They differ only when vars are missing or empty:
115
+
116
+ | `fallbackMode` | Rule | App |
117
+ | --- | --- | --- |
118
+ | `"fallback-if-set"` (default) | `!env[name] && !!env[fallback] ? fallback : name` | app-d |
119
+ | `"prefer-primary"` | `env[name] ? name : fallback` (error names the fallback when both are missing) | app-c |
120
+ | `"nullish"` | `env[name] ?? env[fallback]`, read as `name` (an empty primary does not fall back) | app-b |
121
+ | plain string | no fallback | app-a, app-e |
122
+
123
+ ### JSON-RPC dispatcher
124
+
125
+ | Export | What it does |
126
+ | --- | --- |
127
+ | `createMcpDispatcher(config)` | `{ handleRpc }` for stateless HTTP: `initialize`, `ping`, `tools/list`, `tools/call`, notifications (202, empty body), batches. Other methods: -32601 "Method not found". |
128
+ | `negotiateProtocolVersion(requested, { versions, fallback })` | Echo a supported version, else the fallback. |
129
+ | `SDK_PROTOCOL_VERSIONS` | `["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"]`, the list in MCP SDK 1.29.0 to 1.32.1. |
130
+ | `parseRpcBody(text)` | Parse a body, or the SDK's 400 -32700 "Parse error: Invalid JSON" result. |
131
+ | `rpcResult`, `rpcError`, `JSON_RPC_ERROR` | Response builders with the SDK's key order. |
132
+
133
+ Config: `serverInfo` (or a function of the scope), `tools`, `protocol` (`versions`, `fallback`; default SDK list, fallback `"2025-11-25"` as the SDK), `capabilities` (default `{ tools: { listChanged: true } }` as `McpServer`), `instructions`, `toJsonSchema`.
134
+
135
+ ### Tools
136
+
137
+ | Export | What it does |
138
+ | --- | --- |
139
+ | `defineTool({ name, description, inputSchema, handler, annotations?, scopes?, title?, jsonSchema? })` | zod schema (3.25+ or 4), or a raw JSON Schema plus `parseArgs`. `description` may be a function of the scope. `call()` validates, runs the handler, and shapes errors like MCP SDK 1.30+. |
140
+ | `toolTextResult(text)`, `toolJsonResult(data)`, `toolErrorResult(message)` | `{content:[{type:"text",text}]}`, pretty JSON text, and `{content:[...], isError:true}`, as in the apps. |
141
+ | `runTool(fn, { errorLabel? })` | app-d `jsonTool`: value as pretty JSON, a throw as an error result. |
142
+ | `createToolRegistry(tools)`, `toolsToList`, `assertUniqueToolNames` | Duplicate names throw. Scope filtering (app-a `scopes`). `registry.names(scope)` feeds `assertMcpTools` from `@vvedantb/server/convex`. |
143
+ | `formatIssues`, `invalidArgumentsResult` | SDK-style validation messages. |
144
+
145
+ zod 3 converter, typed without a cast:
146
+
147
+ ```ts
148
+ import { ZodType } from "zod";
149
+ import { zodToJsonSchema } from "zod-to-json-schema";
150
+ const toJsonSchema: JsonSchemaConverter = (schema) => (schema instanceof ZodType ? zodToJsonSchema(schema) : {});
151
+ ```
152
+
153
+ ### Explorer tools
154
+
155
+ `createExplorerTools({ tables, execute, runQuery?, limits?, features?, names?, descriptions?, messages?, scopes? })` returns `list_tables`, `query_table`, `count_table`, `get_document` and, with `runQuery`, `run_query`. All are marked `readOnlyHint`.
156
+
157
+ - `tables`: a list of names or `{ name, fields? }` entries (app-b, app-d), or a predicate (app-c's deny-list). Names must match `^[A-Za-z_][A-Za-z0-9_]*$`.
158
+ - `execute(op, { principal, ctx, scope })`: the app's user-scoped executor. `op` is `list_tables`, `query_table` (`table`, `order`, `limit`, `cursor`, `filters`), `count_table` or `get_document`. Return a string (sent as-is), any other JSON value (pretty JSON) or `null` (denied). A throw becomes an error result with its message.
159
+ - Checked before `execute` runs: table in catalog, argument types, `order`, `limit` (default 100, max 1000, floored and clamped to at least 1), filter field names (identifiers; in `fields` when the entry lists them, plus `_id` and `_creationTime`), document id `^[a-zA-Z0-9_]+$`, `run_query` tables. An unknown table never reaches the executor.
160
+ - `features`: `cursor` (app-b), `filters` (`eq`/`gte`/`lte`, app-d), `getDocument` (default on), `runQueryTables` (app-d `tables`/`limitPerTable`).
161
+ - `createExplorerTools` throws at creation without `execute` or `tables`.
162
+
163
+ ## Configuration per app
164
+
165
+ | | app-a | app-b | app-c | app-d | app-e |
166
+ | --- | --- | --- | --- | --- | --- |
167
+ | MCP routes | `/mcp`, `/mcp/team` | `/mcp` (+OPTIONS) | `/mcp` | `/mcp` (+OPTIONS) | own gateway |
168
+ | `scope` | `personal` / `team` | none | none | none | n/a |
169
+ | `resourceMetadataPath` | default; team `/.well-known/oauth-protected-resource/mcp/team` | default | default | default | default |
170
+ | `cors` | none | `mcpCors({ Allow-Headers: "Content-Type, Authorization, MCP-Protocol-Version", Max-Age: "86400" })` | none | `mcpCors({ Allow-Headers: "Authorization, Content-Type, Accept, MCP-Protocol-Version, MCP-Session-Id", Expose-Headers: "WWW-Authenticate, MCP-Session-Id" })` | function reflecting `Origin` |
171
+ | `body` | `json` | `json` | `json` | `text` | n/a |
172
+ | `log` | default | `false` | `auth-failed` only | `false` | n/a |
173
+ | principal | `{ clerkUserId }` | `{ clerkUserId, isPro }` | `{ clerkUserId }` | `{ clerkUserId }` (Clerk or legacy JWT) | API key / Clerk JWT |
174
+ | AS proxy `forward` | `json` | `stream` | `text` | `text` | `json` + `fetch` hook |
175
+ | issuer env | `CLERK_FRONTEND_API_URL` | + `NEXT_PUBLIC_…` `nullish` | + `NEXT_PUBLIC_…` `prefer-primary` | + `NEXT_PUBLIC_…` `fallback-if-set` | `CLERK_FRONTEND_API_URL` |
176
+ | documentation env | `WEB_APP_URL` | `WEB_APP_URL` | + `SITE_URL` `prefer-primary` | + `SITE_URL` `fallback-if-set` | `WEB_APP_URL` |
177
+ | health | `/health` | none | `/health` | `/mcp/health`, with CORS | `/health`, `{status, service}` |
178
+ | SDK version locked | 1.29.0 | 1.30.0 | 1.30.0 | 1.32.1 | none |
179
+
180
+ The exact configs are in `tests/mcp/fixtures/apps/<app>.kit.ts` and `docs/migrated/<app>/`.
181
+
182
+ ## Security model
183
+
184
+ The kit does not do authentication, authorisation or data access.
185
+
186
+ - `authenticate` is the app's. Clerk verification, app-b's Pro claim, app-d's legacy HS256 tokens, app-e's API keys and scopes, and user creation all stay in the app.
187
+ - Per-user scoping stays in the app: app-b `userScope.ts`/`scoping.ts`/`owned.ts`/`queries.ts`, app-c `scoping.ts`/`owned.ts`/`queries.ts`, app-d `scope.ts`/`owned.ts`/`queries.ts`, app-a `memoryScope.ts`.
188
+ - The explorer `execute` and `runQuery` callbacks MUST enforce per-user scoping. The kit passes the principal through as an opaque value and has no bypass option. It only narrows input: an unknown table or malformed field name never reaches the callback.
189
+ - `run_query` code is never evaluated by the kit.
190
+ - A missing bearer token is rejected before `authenticate` runs. A thrown `authenticate` is not swallowed unless you opt in.
191
+
192
+ ## Compatibility
193
+
194
+ **HTTP layer: byte-for-byte compatible with each app today.** `tests/mcp/appParity.test.ts` runs each app's current `native.ts` (ported verbatim under `tests/mcp/fixtures/apps/*.reference.ts`) and the kit config side by side over 14 request scenarios, 5 env combinations and both upstream outcomes. Status, every header, body bytes, `console.error` calls and the arguments sent to `handleMcpRequest` must be equal. One addition: with `cors` set, the `/mcp` handler also answers `OPTIONS`; the apps route `OPTIONS` to a separate handler, which gives the same response.
195
+
196
+ **`runSdkMcpRequest`: byte-for-byte compatible.** Run with each app's real SDK build (1.29.0, 1.30.0, 1.32.1), it returned exactly the captured responses for all 14 or 15 exchanges per app. This was a one-off check: the SDK is not a dev dependency. `tests/mcp/sdk.test.ts` covers the same behaviour with a fake transport.
197
+
198
+ **Pure dispatcher: protocol-equivalent, not byte-identical.** Compared with `tests/mcp/fixtures/sdk/*.sdk.json`, captured from each app's SDK by `scripts/capture-sdk-fixtures.mjs`:
199
+
200
+ | Exchange | Same bytes? |
201
+ | --- | --- |
202
+ | initialize (supported, latest, unsupported version), notifications/initialized (202, empty), ping, tools/call ok, handler throw, unknown tool, resources/list and unknown methods (-32601), batches, parse error | yes |
203
+ | invalid arguments | yes against SDK 1.30+; SDK 1.29.0 (app-a) dumps raw zod issues instead |
204
+ | tools/list | names, descriptions and annotations yes; the SDK adds `execution: {taskSupport: "forbidden"}`; schemas are equal with zod 4 and depend on the converter with zod 3 |
205
+ | invalid JSON-RPC message | no: the kit answers 400 -32600 "Invalid Request" (JSON-RPC spec); the SDK answers 400 -32700 "Parse error: Invalid JSON-RPC message" |
206
+
207
+ Not implemented in the dispatcher: resources, prompts, completions, `logging/setLevel`, tasks, elicitation, sampling and output-schema validation. app-a registers resources and an MCP app, so app-a keeps the SDK.
208
+
209
+ ## Develop
210
+
211
+ ```sh
212
+ pnpm --filter @vvedantb/server build
213
+ pnpm --filter @vvedantb/server test
214
+ pnpm --filter @vvedantb/server typecheck
215
+ ```
216
+
217
+ Tests and typecheck read `@vvedantb/server/convex` and `@vvedantb/server/mcp` from source (`vitest.config.ts` alias, `tsconfig.test.json` paths), so they run without building first. `zod` (4) and `zod3` (`npm:zod@^3.25.76`) are dev dependencies so both zod majors are tested.
218
+
219
+ To recapture SDK fixtures from an installed SDK (read-only):
220
+
221
+ ```sh
222
+ node scripts/capture-sdk-fixtures.mjs <path>/node_modules/@modelcontextprotocol/sdk app-d-mcp 1.2.0 no tests/mcp/fixtures/sdk/app-d.sdk.json
223
+ ```
224
+
225
+ ## Publish
226
+
227
+ Publish steps for the whole package are in the root `README.md`.
@@ -0,0 +1,163 @@
1
+ # Migrating the apps to `@vvedantb/server/mcp`
2
+
3
+ Read-only sources: `/workspace/cvx/<app>/packages/backend` at origin/main on 12 October 2026 (app-a `8c972b1a`, app-b `f7b61259`, app-c `4d0ab45`, app-d `3bdce79`, app-e `7077eaf`). All paths below are relative to `packages/backend` in each app.
4
+
5
+ There are two phases per app. Ship them as separate PRs.
6
+
7
+ - **Phase 1, HTTP layer and SDK runner.** Replace `convex/mcp/native.ts` with kit factories, shorten `handleMcpRequest` with `runSdkMcpRequest`, and use the kit's result helpers. The MCP SDK stays. Responses are byte-identical: `tests/mcp/appParity.test.ts` checks every route, and the SDK runner was checked against each app's real SDK build.
8
+ - **Phase 2, explorer tools on the pure dispatcher.** Replace `list_tables`, `query_table`, `count_table`, `get_document` and `run_query` with `createExplorerTools`. Kit tools are not SDK `server.tool` registrations, so the whole server must move to `defineTool` + `createMcpDispatcher` (one server answers `tools/list`). After that the SDK and, for MCP, `"use node"` can go. Protocol-equivalent, not byte-identical (see the README compatibility table and the behaviour changes below).
9
+
10
+ Add the dependency in each app's `packages/backend/package.json` (after `@vvedantb/server` is published):
11
+
12
+ ```json
13
+ "@vvedantb/server": "^0.1.0"
14
+ ```
15
+
16
+ `http.ts` route tables do not change in any app: the exported names stay the same.
17
+
18
+ ## app-a
19
+
20
+ Phase 1 only. app-a registers resources (`resources.ts`) and an MCP app (`memoryGraphApp.ts`), which the dispatcher does not support, so the SDK stays and there are no explorer tools.
21
+
22
+ 1. Replace `convex/mcp/native.ts` with [migrated/app-a/native.ts](migrated/app-a/native.ts). Delete `requestOrigin`, `createProtectedResourceMetadataAction`, `unauthorized`, `runMcpEndpoint` and the inline handlers. The two scopes are two `createMcpHttpHandler` instances with `scope` and the team `resourceMetadataPath`.
23
+ 2. In `convex/mcp/nodeActions.ts`, replace `handleMcpRequest` (lines 56-116) with [migrated/app-a/nodeActions.handleMcpRequest.ts](migrated/app-a/nodeActions.handleMcpRequest.ts). The `url` option keeps `http://localhost/mcp/team` for the team scope.
24
+ 3. Keep: `verifyAccessToken` (Clerk `authenticateRequest`, `isMcpAcceptedTokenType`), `serverInfo.ts`, `memoryScope.ts`, `tools.ts`/`toolTypes.ts`/`toolCatalog.ts`/`content.ts` (their `errorLabel` formatting and image content are app-a-specific; `bindToolSpec` stays on the SDK), resources and the graph app.
25
+
26
+ Checklist:
27
+
28
+ - [ ] 401 `{"error":"Missing Authorization header"}` / `{"error":"Invalid or expired token"}`, `Content-Type: application/json`, `WWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource"` (team: `.../oauth-protected-resource/mcp/team`). No CORS headers.
29
+ - [ ] 405 `{"error":"Method not supported in stateless mode"}` for GET/DELETE after auth; 400 `{"error":"Invalid JSON body"}`.
30
+ - [ ] Logs: "[MCP][mcpHandler] token verification failed", "[MCP][mcpHandler] failed to parse JSON body:", "[MCP][handleMcpRequest] threw:" (all kit defaults).
31
+ - [ ] Body forwarded re-encoded (`JSON.stringify(await request.json())`); `scope` passed to `handleMcpRequest`.
32
+ - [ ] Metadata: `resource` `<origin>/mcp` and `<origin>/mcp/team`; `CLERK_FRONTEND_API_URL`, `WEB_APP_URL` (no fallbacks); AS metadata re-encoded JSON; 502 body unchanged.
33
+ - [ ] `serverInfo` `app-a-mcp` / `app-a-mcp-team` `2.0.0`; protocol strings from SDK 1.29.0 (unchanged).
34
+ - [ ] Routes unchanged: `/.well-known/oauth-authorization-server`, `/.well-known/oauth-protected-resource`, `/.well-known/oauth-protected-resource/mcp/team`, `/mcp` and `/mcp/team` (POST/GET/DELETE), `/health`.
35
+ - [ ] Run: `convex/mcp/serverInfo.test.ts`, `tests/mcp/*.test.ts` (toolCatalog, toolPaths, memoryTools, skillResources, contextPack, extreme, retrieveQuality), and `tests/mcp/live.integration.test.ts` against a dev deployment.
36
+
37
+ ## app-b
38
+
39
+ 1. Replace `convex/mcp/native.ts` with [migrated/app-b/native.ts](migrated/app-b/native.ts): `cors = mcpCors({...})` with app-b's two overrides, the `nullish` Clerk fallback, `forward: "stream"`, `log: false`, `createOptionsHandler` for `mcpOptions`. Delete `clerkFrontendApiUrl`, `requestOrigin`, `MCP_CORS_OVERRIDES`, `corsHeaders`, `unauthorized`, `runMcpEndpoint`.
40
+ 2. In `convex/mcp/nodeActions.ts`, replace `handleMcpRequest` (lines 201-261) with [migrated/app-b/nodeActions.handleMcpRequest.ts](migrated/app-b/nodeActions.handleMcpRequest.ts).
41
+ 3. In `convex/mcp/toolShared.ts`, replace `TextContent`, `textContent`, `errorResult`, `textResult` (lines 5-22) with [migrated/app-b/toolShared.resultHelpers.ts](migrated/app-b/toolShared.resultHelpers.ts) (re-export, plus an import for `runMcpTool`). `runMcpTool` stays: it wraps primitives in `{ result }`, which `runTool` does not.
42
+ 4. Phase 2: replace the five explorer registrations in `convex/mcp/tools.ts` (lines 32-169) with [migrated/app-b/tools.explorer.ts](migrated/app-b/tools.explorer.ts), and move the other tools to `defineTool` + `createMcpDispatcher`.
43
+ 5. Keep: `verifyAccessToken` with `claimsFromJwt` + `claimsIndicatePro` (the Pro claim), `runTestQuery`/`runTestQueryRemote`/deploy key lookup, `userScope.ts`, `scoping.ts`, `owned.ts`, `queries.ts`, `catalog.ts`, all product tools, `MCP_SERVER_VERSION`.
44
+
45
+ Checklist:
46
+
47
+ - [ ] Every MCP response carries `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Methods: GET,POST,DELETE,OPTIONS`, `Access-Control-Allow-Headers: Content-Type, Authorization, MCP-Protocol-Version`, `Access-Control-Allow-Credentials: false`, `Access-Control-Max-Age: 86400`; OPTIONS `/mcp` 204 with only those.
48
+ - [ ] 401/405/400 messages and `WWW-Authenticate` as app-a personal; no `console.error` on auth or JSON failure.
49
+ - [ ] `isPro` and `clerkUserId` passed to `handleMcpRequest`; body re-encoded.
50
+ - [ ] AS metadata streamed (`res.body`) as `application/json` with CORS; 502 with CORS.
51
+ - [ ] `CLERK_FRONTEND_API_URL ?? NEXT_PUBLIC_CLERK_FRONTEND_API_URL` (an empty primary does not fall back); `WEB_APP_URL` with no fallback.
52
+ - [ ] Routes unchanged, including `/.well-known/oauth-protected-resource/mcp`; app-b has no health route.
53
+ - [ ] `serverInfo` `app-b-mcp` `1.15.0`; SDK 1.30.0 protocol strings.
54
+ - [ ] Phase 2 behaviour changes: `limit` above 1000 is clamped (was a validation error) and below 1 becomes 1; property descriptions are the kit's; tools gain `readOnlyHint`; `tools/list` has no `execution` field. Messages kept via config: `Cannot query table "<t>". Use list_tables.`, `Cannot count table "<t>".`, `Invalid document ID format.`.
55
+ - [ ] Run: `convex/mcp/tools.test.ts` (in phase 2, list names from `createToolRegistry(...).names()` instead of an SDK client).
56
+
57
+ ## app-c
58
+
59
+ 1. Replace `convex/mcp/native.ts` with [migrated/app-c/native.ts](migrated/app-c/native.ts): `prefer-primary` fallbacks for the Clerk URL and `WEB_APP_URL`→`SITE_URL`, `forward: "text"`, and a log hook that keeps only `auth-failed`.
60
+ 2. In `convex/mcp/nodeActions.ts`, replace `handleMcpRequest` (lines 324-383) with [migrated/app-c/nodeActions.handleMcpRequest.ts](migrated/app-c/nodeActions.handleMcpRequest.ts).
61
+ 3. In `convex/mcp/tools.ts`, replace `textResult`/`errorResult` (lines 12-23) with the import in [migrated/app-c/tools.resultHelpers.ts](migrated/app-c/tools.resultHelpers.ts).
62
+ 4. Phase 2: replace the explorer registrations (lines 55-195) with [migrated/app-c/tools.explorer.ts](migrated/app-c/tools.explorer.ts). `tables: isTableAllowed` keeps the deny-list; `resolveUserId` is the existing `getContext` lookup.
63
+ 5. Keep: `verifyAccessToken`, `runTestQuery`, `listTables` (remote shapes + `FALLBACK_TABLES`), deploy-key bootstrap, `scoping.ts`, `owned.ts`, `queries.ts`, every product tool.
64
+
65
+ Checklist:
66
+
67
+ - [ ] No CORS headers anywhere. 401/405/400 as app-a personal. Logs "[MCP][mcpHandler] token verification failed" only (no JSON-parse log).
68
+ - [ ] AS metadata forwarded as text; 502 unchanged.
69
+ - [ ] Env: `process.env.CLERK_FRONTEND_API_URL ? it : NEXT_PUBLIC_CLERK_FRONTEND_API_URL`; with neither set the error names `NEXT_PUBLIC_CLERK_FRONTEND_API_URL`. Same rule for `WEB_APP_URL`/`SITE_URL`.
70
+ - [ ] Routes unchanged: two well-known routes, `/mcp` (POST/GET/DELETE), `/health`.
71
+ - [ ] `serverInfo` `app-c-mcp` `1.2.4`; SDK 1.30.0 protocol strings.
72
+ - [ ] Phase 2: `limit` clamped to 1..1000; `Access denied: table "<t>" is not available via MCP.` unchanged (kit default); invalid id message unchanged (kit default).
73
+ - [ ] Run: `convex/mcp/tools.test.ts` (its regex counts `server.tool("...")` registrations, so update it in phase 2), `convex/mcpProduct.test.ts`, `convex/mcpWrites.test.ts`, `convex/mcp/content.test.ts`, `convex/mcp/date.test.ts`.
74
+
75
+ ## app-d
76
+
77
+ 1. Replace `convex/mcp/native.ts` with [migrated/app-d/native.ts](migrated/app-d/native.ts). It keeps `legacyRefreshToken`, `readTokenBody` and `refreshBodySchema` unchanged; `json()` becomes a 3-line `Response.json(data, { status, headers: cors })` (same headers as `jsonWithCors` + extras); `getClerkFrontendApiUrl()` becomes `readEnvUrl(clerk)`.
78
+ 2. In `convex/mcp/nodeActions.ts`, replace `handleMcpRequest` (lines 224-277) with [migrated/app-d/nodeActions.handleMcpRequest.ts](migrated/app-d/nodeActions.handleMcpRequest.ts) (`parsedBody: false`, as app-d does not pass it today).
79
+ 3. Replace `convex/mcp/toolResult.ts` with the re-export in [migrated/app-d/toolResult.ts](migrated/app-d/toolResult.ts). `runTool` matches `jsonTool` exactly, including "Result is not JSON" and "Tool failed".
80
+ 4. Phase 2: replace the explorer schemas and registrations in `convex/mcp/tools.ts` (lines 12-43 and 103-238) with [migrated/app-d/tools.explorer.ts](migrated/app-d/tools.explorer.ts). The `run_query` snapshot and sandbox code moves into the `runQuery` callback unchanged.
81
+ 5. Keep: `verifyAccessToken` (legacy HS256 path with `MCP_JWT_SECRET`, then Clerk, then `ensureUser`), `refreshToken`, `scope.ts`, `owned.ts`, `queries.ts`, `sandbox.ts`, `validators.ts`, product tools, `whoami`.
82
+
83
+ Checklist:
84
+
85
+ - [ ] Every MCP response, including `/mcp/health` and the refresh endpoint, carries `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Methods: GET,POST,DELETE,OPTIONS`, `Access-Control-Allow-Headers: Authorization, Content-Type, Accept, MCP-Protocol-Version, MCP-Session-Id`, `Access-Control-Allow-Credentials: false`, `Access-Control-Expose-Headers: WWW-Authenticate, MCP-Session-Id`. OPTIONS `/mcp` and `/mcp/oauth/token`: 204.
86
+ - [ ] Raw request text forwarded after `JSON.parse` validation; no logs on auth or JSON failure.
87
+ - [ ] AS metadata as text with CORS; served on `/.well-known/oauth-authorization-server` and `/.well-known/oauth-authorization-server/mcp`.
88
+ - [ ] Env: `!env[name] && !!env[fallback] ? fallback : name` for the Clerk URL and `WEB_APP_URL`/`SITE_URL`.
89
+ - [ ] Refresh endpoint: `invalid_request` / `unsupported_grant_type` / `invalid_grant` bodies unchanged.
90
+ - [ ] `serverInfo` `app-d-mcp` `1.2.0`; SDK 1.32.1 protocol strings.
91
+ - [ ] Phase 2 behaviour changes: an unknown table becomes an `isError` result (same JSON text `{"error":"Table \"<t>\" is not queryable"}`; it was a non-error text result); `get_document` now rejects ids outside `^[a-zA-Z0-9_]+$` (it returned null); filter field names must be identifiers in the table's `fields` or `_id`/`_creationTime` (unknown fields matched nothing before); `limit` default 50, max `MAX_QUERY_LIMIT` (200), as `clampLimit`.
92
+ - [ ] Run: `tests/mcpOauth.test.ts` (discovery, bearer auth, legacy tokens, refresh), `tests/mcpProduct.test.ts`, `tests/mcpWrites.test.ts`, `tests/scope.test.ts`, `tests/sandbox.test.ts`.
93
+
94
+ ## app-e (partial)
95
+
96
+ app-e's `/mcp` handler stays: it checks methods before auth, reads the body with a 1 MiB limit (413), authenticates API keys and Clerk JWTs inside the node action, maps 401/429 results to `WWW-Authenticate`/`Retry-After`, and answers 202 without a `Content-Type`. The gateway (`gateway.ts`, `protocol.ts`, `proxy.ts`, `oauth.ts`, `clerkAuth.ts`, `clerkJwt.ts`) stays. app-e already took `extractBearerToken` and `requiredEnvUrl` from `@vvedantb/server/convex` in `7077eaf`.
97
+
98
+ 1. In `convex/mcp/native.ts`, replace `optionsHandler`, `oauthMetadata`, `protectedResourceMetadata` and `health` with [migrated/app-e/native.partial.ts](migrated/app-e/native.partial.ts). `corsHeaders(request)` stays and is passed as the `cors` function. `assertOutboundUrl` moves into the proxy's `fetch` hook. It checks protocol, credentials, host and port, which the full metadata URL shares with the issuer base URL.
99
+ 2. Optional: `negotiateProtocolVersion(requested, { versions: SUPPORTED_PROTOCOL_VERSIONS, fallback: PROTOCOL_VERSION })` can replace the 6-line function in `protocol.ts`. It is not counted below.
100
+ 3. Run: `tests/e2e.test.ts`, `tests/mcpApiKeys.test.ts`, `tests/gateway.test.ts`, `tests/hardening.test.ts`, `tests/limits.test.ts`, `tests/keyAccess.test.ts`, `tests/envelope.test.ts`, `tests/twoUsers.test.ts`.
101
+
102
+ ## Measured lines removed
103
+
104
+ Reproduce with `node packages/server/docs/count-loc.mjs /workspace/cvx`.
105
+
106
+ How counted:
107
+
108
+ - Physical lines, blank and comment lines included (`wc -l`). A file without a final newline counts its last line.
109
+ - "removable" is the whole file or the listed blocks in the app today. Blocks run from a start pattern to the line where brackets balance again, plus the comment lines directly above.
110
+ - "remains" is the replacement in `docs/migrated/<app>/`: real code using the kit's real exports and each app's real names, written out in full. It includes moved strings (the `run_query` descriptions and app-d's blocked-table list) and the new import lines.
111
+ - Not counted:
112
+ - imports that become unused, such as `extractBearerToken` in nodeActions (a few more lines saved);
113
+ - phase 2 rewiring of the other tools from `server.tool(name, desc, shape, fn)` to `defineTool({ ... })`, which is roughly line-neutral;
114
+ - `tests/` changes in the apps;
115
+ - `http.ts` (unchanged).
116
+
117
+ | app | phase | file | LOC now | removable | remains (wiring) | net |
118
+ | --- | --- | --- | ---: | ---: | ---: | ---: |
119
+ | app-a | 1 | `convex/mcp/native.ts` | 122 | 122 | 38 | -84 |
120
+ | app-a | 1 | `convex/mcp/nodeActions.ts` | 116 | 61 | 19 | -42 |
121
+ | app-b | 1 | `convex/mcp/native.ts` | 141 | 141 | 44 | -97 |
122
+ | app-b | 1 | `convex/mcp/nodeActions.ts` | 261 | 61 | 18 | -43 |
123
+ | app-b | 1 | `convex/mcp/toolShared.ts` | 61 | 15 | 2 | -13 |
124
+ | app-b | 2 | `convex/mcp/tools.ts` | 170 | 134 | 54 | -80 |
125
+ | app-c | 1 | `convex/mcp/native.ts` | 107 | 107 | 43 | -64 |
126
+ | app-c | 1 | `convex/mcp/nodeActions.ts` | 383 | 60 | 18 | -42 |
127
+ | app-c | 1 | `convex/mcp/tools.ts` | 430 | 11 | 1 | -10 |
128
+ | app-c | 2 | `convex/mcp/tools.ts` | 430 | 137 | 46 | -91 |
129
+ | app-d | 1 | `convex/mcp/native.ts` | 186 | 186 | 116 | -70 |
130
+ | app-d | 1 | `convex/mcp/nodeActions.ts` | 277 | 54 | 17 | -37 |
131
+ | app-d | 1 | `convex/mcp/toolResult.ts` | 23 | 23 | 5 | -18 |
132
+ | app-d | 2 | `convex/mcp/tools.ts` | 241 | 160 | 68 | -92 |
133
+ | app-e | 1 | `convex/mcp/native.ts` | 183 | 41 | 21 | -20 |
134
+
135
+ | app | phase 1 net | phase 2 net | total net |
136
+ | --- | ---: | ---: | ---: |
137
+ | app-a | -126 | 0 | -126 |
138
+ | app-b | -153 | -80 | -233 |
139
+ | app-c | -116 | -91 | -207 |
140
+ | app-d | -125 | -92 | -217 |
141
+ | app-e | -20 | 0 | -20 |
142
+ | **all** | **-540** | **-263** | **-803** |
143
+
144
+ Removed ranges (line numbers in the app file today):
145
+ - app-a convex/mcp/native.ts: whole file 1-122
146
+ - app-a convex/mcp/nodeActions.ts: handleMcpRequest 56-116
147
+ - app-b convex/mcp/native.ts: whole file 1-141
148
+ - app-b convex/mcp/nodeActions.ts: handleMcpRequest 201-261
149
+ - app-b convex/mcp/toolShared.ts: TextContent 5-5; textContent 7-9; errorResult 11-16; textResult 18-22
150
+ - app-b convex/mcp/tools.ts: tool list_tables 32-43; tool query_table 45-86; tool get_document 88-108; tool count_table 110-130; tool run_query 132-169
151
+ - app-c convex/mcp/native.ts: whole file 1-107
152
+ - app-c convex/mcp/nodeActions.ts: handleMcpRequest 324-383
153
+ - app-c convex/mcp/tools.ts: textResult 12-16; errorResult 18-23
154
+ - app-c convex/mcp/tools.ts: tool list_tables 55-66; tool query_table 68-108; tool get_document 110-133; tool run_query 135-168; tool count_table 170-195
155
+ - app-d convex/mcp/native.ts: whole file 1-186
156
+ - app-d convex/mcp/nodeActions.ts: handleMcpRequest 224-277
157
+ - app-d convex/mcp/toolResult.ts: whole file 1-23
158
+ - app-d convex/mcp/tools.ts: filterValueSchema 12-17; queryTableSchema 19-26; countTableSchema 28-33; getDocumentSchema 35-37; runQuerySchema 39-43; tool list_tables 103-129; tool query_table 131-153; tool count_table 155-175; tool get_document 177-194; tool run_query 196-238
159
+ - app-e convex/mcp/native.ts: optionsHandler 22-24; oauthMetadata 26-45; protectedResourceMetadata 47-58; health 105-110
160
+
161
+ ### Package cost
162
+
163
+ `packages/server/src/mcp` is 1,565 physical lines, 1,153 without blank and comment lines. Most of it is JSDoc, explorer validation that the apps did not have, and the pure dispatcher. That is more than the R4 estimate of about 500, so across all repos the line count goes up: -803 in the apps against +1,565 in the package. The gain is one tested copy instead of four drifting ones, not fewer lines overall. On phase 1 alone the apps lose 540 lines. If phase 2 is not adopted, the explorer (408 lines) and dispatcher (151 lines) could be dropped from the package.
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@vvedantb/server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Shared Convex backend helpers and MCP server scaffolding: query-limit errors, Clerk auth config, bearer tokens, env, CORS, a bearer-auth MCP HTTP handler, OAuth metadata, a JSON-RPC dispatcher and explorer tools.",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/vvedantb/packages.git",
13
+ "directory": "packages/server"
14
+ },
15
+ "exports": {
16
+ "./convex": {
17
+ "types": "./dist/convex/index.d.ts",
18
+ "import": "./dist/convex/index.js"
19
+ },
20
+ "./mcp": {
21
+ "types": "./dist/mcp/index.d.ts",
22
+ "import": "./dist/mcp/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "docs/convex.md",
29
+ "docs/mcp.md",
30
+ "docs/migration.md",
31
+ "LICENSE"
32
+ ],
33
+ "sideEffects": false,
34
+ "peerDependencies": {
35
+ "@clerk/backend": ">=1",
36
+ "zod": "^3.25.0 || ^4.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "@clerk/backend": {
40
+ "optional": true
41
+ },
42
+ "zod": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "typescript": "^5.9.0",
49
+ "vitest": "^3.2.0",
50
+ "zod": "^4.0.0",
51
+ "zod3": "npm:zod@^3.25.76"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.json",
55
+ "typecheck": "tsc -p tsconfig.test.json",
56
+ "test": "vitest run"
57
+ }
6
58
  }