@vvedantb/server 0.0.0-stage → 0.1.1

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
@@ -0,0 +1,299 @@
1
+ import { isJsonObject } from "./json.js";
2
+ import { defineTool, toolErrorResult, toolJsonResult, toolTextResult, } from "./tools.js";
3
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
+ const DOCUMENT_ID = /^[a-zA-Z0-9_]+$/;
5
+ const SYSTEM_FIELDS = ["_id", "_creationTime"];
6
+ const DEFAULT_NAMES = {
7
+ listTables: "list_tables",
8
+ queryTable: "query_table",
9
+ countTable: "count_table",
10
+ getDocument: "get_document",
11
+ runQuery: "run_query",
12
+ };
13
+ const DEFAULT_DESCRIPTIONS = {
14
+ listTables: "List the tables you can query.",
15
+ queryTable: "Read a page of documents from a table, scoped to your user.",
16
+ countTable: "Count documents you can access in a table.",
17
+ getDocument: "Get a single document by Convex id. Returns null when it is missing or outside your scope.",
18
+ runQuery: "Run read-only query code against your scoped data.",
19
+ };
20
+ function defaultDenied(op) {
21
+ switch (op.kind) {
22
+ case "query_table":
23
+ return `Cannot query table "${op.table}". Use list_tables.`;
24
+ case "count_table":
25
+ return `Cannot count table "${op.table}".`;
26
+ case "get_document":
27
+ return "Document not found or not accessible.";
28
+ case "list_tables":
29
+ return "Cannot list tables.";
30
+ }
31
+ }
32
+ function toResult(value) {
33
+ return typeof value === "string" ? toolTextResult(value) : toolJsonResult(value);
34
+ }
35
+ class ArgError extends Error {
36
+ }
37
+ function optionalString(args, key) {
38
+ const value = args[key];
39
+ if (value === undefined)
40
+ return undefined;
41
+ if (typeof value !== "string")
42
+ throw new ArgError(`Expected string at ${key}`);
43
+ return value;
44
+ }
45
+ function requiredString(args, key) {
46
+ const value = optionalString(args, key);
47
+ if (value === undefined)
48
+ throw new ArgError(`Required at ${key}`);
49
+ return value;
50
+ }
51
+ function optionalNumber(args, key) {
52
+ const value = args[key];
53
+ if (value === undefined || value === null)
54
+ return undefined;
55
+ if (typeof value !== "number")
56
+ throw new ArgError(`Expected number at ${key}`);
57
+ return value;
58
+ }
59
+ function fieldMap(value, key, accept, expected) {
60
+ if (value === undefined)
61
+ return undefined;
62
+ if (!isJsonObject(value))
63
+ throw new ArgError(`Expected object at ${key}`);
64
+ const out = {};
65
+ for (const [field, fieldValue] of Object.entries(value)) {
66
+ if (!accept(fieldValue))
67
+ throw new ArgError(`Expected ${expected} at ${key}.${field}`);
68
+ out[field] = fieldValue;
69
+ }
70
+ return out;
71
+ }
72
+ const isFilterValue = (v) => v === null || typeof v === "string" || typeof v === "boolean" || (typeof v === "number" && Number.isFinite(v));
73
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
74
+ /**
75
+ * list_tables, query_table, count_table, get_document and (with `runQuery`)
76
+ * run_query, backed by the app's user-scoped `execute` callback. Arguments
77
+ * and table names are checked before the callback runs, so a table outside
78
+ * the catalog never reaches it. The kit never reads the database.
79
+ */
80
+ export function createExplorerTools(config) {
81
+ if (typeof config.execute !== "function") {
82
+ throw new Error("createExplorerTools: execute is required (a user-scoped executor)");
83
+ }
84
+ if (typeof config.tables !== "function" && !Array.isArray(config.tables)) {
85
+ throw new Error("createExplorerTools: tables is required");
86
+ }
87
+ if (config.runQuery !== undefined && typeof config.runQuery !== "function") {
88
+ throw new Error("createExplorerTools: runQuery must be a function");
89
+ }
90
+ const { execute, runQuery, tables, scopes } = config;
91
+ const defaultLimit = config.limits?.default ?? 100;
92
+ const maxLimit = config.limits?.max ?? 1000;
93
+ if (!(defaultLimit >= 1 && maxLimit >= defaultLimit)) {
94
+ throw new Error("createExplorerTools: limits need 1 <= default <= max");
95
+ }
96
+ const features = config.features ?? {};
97
+ const names = { ...DEFAULT_NAMES, ...config.names };
98
+ const descriptions = { ...DEFAULT_DESCRIPTIONS, ...config.descriptions };
99
+ const unknownTable = config.messages?.unknownTable ??
100
+ ((table, _tool) => `Access denied: table "${table}" is not available via MCP.`);
101
+ const denied = config.messages?.denied ?? defaultDenied;
102
+ const invalidId = config.messages?.invalidId ?? "Invalid document ID format. IDs should be alphanumeric.";
103
+ const entries = typeof tables === "function" ? null : tables.map((t) => (typeof t === "string" ? { name: t } : t));
104
+ const entryFor = (table) => {
105
+ if (!IDENTIFIER.test(table))
106
+ return null;
107
+ if (typeof tables === "function")
108
+ return tables(table) ? { name: table } : null;
109
+ return entries?.find((entry) => entry.name === table) ?? null;
110
+ };
111
+ const checkFields = (entry, filters) => {
112
+ const allowed = entry.fields?.map((f) => (typeof f === "string" ? f : f.name));
113
+ for (const map of [filters.eq, filters.gte, filters.lte]) {
114
+ for (const field of Object.keys(map ?? {})) {
115
+ if (!IDENTIFIER.test(field))
116
+ return `Invalid field name "${field}".`;
117
+ if (allowed && !allowed.includes(field) && !SYSTEM_FIELDS.includes(field)) {
118
+ return `Unknown field "${field}" on table "${entry.name}".`;
119
+ }
120
+ }
121
+ }
122
+ return null;
123
+ };
124
+ const parseFilters = (args) => {
125
+ if (!features.filters)
126
+ return null;
127
+ const filters = {};
128
+ const eq = fieldMap(args.eq, "eq", isFilterValue, "string, number, boolean or null");
129
+ const gte = fieldMap(args.gte, "gte", isFiniteNumber, "number");
130
+ const lte = fieldMap(args.lte, "lte", isFiniteNumber, "number");
131
+ if (eq)
132
+ filters.eq = eq;
133
+ if (gte)
134
+ filters.gte = gte;
135
+ if (lte)
136
+ filters.lte = lte;
137
+ return eq || gte || lte ? filters : null;
138
+ };
139
+ const run = async (op, context) => {
140
+ const value = await execute(op, context);
141
+ return value === null ? toolErrorResult(denied(op)) : toResult(value);
142
+ };
143
+ const tableProperty = { type: "string", description: "Table name from list_tables" };
144
+ const filterProperties = features.filters
145
+ ? {
146
+ eq: {
147
+ type: "object",
148
+ additionalProperties: { type: ["string", "number", "boolean", "null"] },
149
+ description: "Top-level fields that must equal these values",
150
+ },
151
+ gte: { type: "object", additionalProperties: { type: "number" }, description: "Top-level fields >= these numbers" },
152
+ lte: { type: "object", additionalProperties: { type: "number" }, description: "Top-level fields <= these numbers" },
153
+ }
154
+ : {};
155
+ const common = { annotations: { readOnlyHint: true }, scopes };
156
+ const tools = [];
157
+ tools.push(defineTool({
158
+ ...common,
159
+ name: names.listTables,
160
+ description: descriptions.listTables,
161
+ inputSchema: { type: "object", properties: {} },
162
+ parseArgs: () => ({}),
163
+ handler: (_args, context) => run({ kind: "list_tables" }, context),
164
+ }));
165
+ const queryProperties = {
166
+ type: "object",
167
+ properties: {
168
+ table: tableProperty,
169
+ order: { type: "string", enum: ["asc", "desc"], default: "desc", description: "Sort order by creation time" },
170
+ limit: {
171
+ type: "number",
172
+ default: defaultLimit,
173
+ maximum: maxLimit,
174
+ description: `Max documents to return (default ${defaultLimit}, max ${maxLimit})`,
175
+ },
176
+ ...(features.cursor
177
+ ? { cursor: { type: "string", description: "Pagination cursor from a previous query" } }
178
+ : {}),
179
+ ...filterProperties,
180
+ },
181
+ required: ["table"],
182
+ };
183
+ tools.push(defineTool({
184
+ ...common,
185
+ name: names.queryTable,
186
+ description: descriptions.queryTable,
187
+ inputSchema: queryProperties,
188
+ parseArgs: (args) => {
189
+ const order = optionalString(args, "order") ?? "desc";
190
+ if (order !== "asc" && order !== "desc")
191
+ throw new ArgError('Expected "asc" or "desc" at order');
192
+ const rawLimit = optionalNumber(args, "limit");
193
+ const limit = rawLimit === undefined || !Number.isFinite(rawLimit)
194
+ ? defaultLimit
195
+ : Math.min(Math.max(1, Math.floor(rawLimit)), maxLimit);
196
+ const cursor = features.cursor ? (optionalString(args, "cursor") ?? null) : null;
197
+ return {
198
+ kind: "query_table",
199
+ table: requiredString(args, "table"),
200
+ order,
201
+ limit,
202
+ cursor,
203
+ filters: parseFilters(args),
204
+ };
205
+ },
206
+ handler: async (op, context) => {
207
+ const entry = entryFor(op.table);
208
+ if (!entry)
209
+ return toolErrorResult(unknownTable(op.table, op.kind));
210
+ const fieldError = op.filters ? checkFields(entry, op.filters) : null;
211
+ if (fieldError)
212
+ return toolErrorResult(fieldError);
213
+ return run(op, context);
214
+ },
215
+ }));
216
+ tools.push(defineTool({
217
+ ...common,
218
+ name: names.countTable,
219
+ description: descriptions.countTable,
220
+ inputSchema: { type: "object", properties: { table: tableProperty, ...filterProperties }, required: ["table"] },
221
+ parseArgs: (args) => ({ kind: "count_table", table: requiredString(args, "table"), filters: parseFilters(args) }),
222
+ handler: async (op, context) => {
223
+ const entry = entryFor(op.table);
224
+ if (!entry)
225
+ return toolErrorResult(unknownTable(op.table, op.kind));
226
+ const fieldError = op.filters ? checkFields(entry, op.filters) : null;
227
+ if (fieldError)
228
+ return toolErrorResult(fieldError);
229
+ return run(op, context);
230
+ },
231
+ }));
232
+ if (features.getDocument ?? true) {
233
+ tools.push(defineTool({
234
+ ...common,
235
+ name: names.getDocument,
236
+ description: descriptions.getDocument,
237
+ inputSchema: {
238
+ type: "object",
239
+ properties: { id: { type: "string", description: "Convex document id" } },
240
+ required: ["id"],
241
+ },
242
+ parseArgs: (args) => ({ id: requiredString(args, "id") }),
243
+ handler: async ({ id }, context) => {
244
+ if (!DOCUMENT_ID.test(id))
245
+ return toolErrorResult(invalidId);
246
+ return run({ kind: "get_document", id }, context);
247
+ },
248
+ }));
249
+ }
250
+ if (runQuery) {
251
+ tools.push(defineTool({
252
+ ...common,
253
+ name: names.runQuery,
254
+ description: descriptions.runQuery,
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ code: { type: "string", description: "Handler body. Must return a value." },
259
+ ...(features.runQueryTables
260
+ ? {
261
+ tables: { type: "array", items: { type: "string" }, description: "Tables to load" },
262
+ limitPerTable: { type: "number", description: "Rows to load per table" },
263
+ }
264
+ : {}),
265
+ },
266
+ required: ["code"],
267
+ },
268
+ parseArgs: (args) => {
269
+ const parsed = { code: requiredString(args, "code") };
270
+ if (!features.runQueryTables)
271
+ return parsed;
272
+ const rawTables = args.tables;
273
+ if (rawTables !== undefined) {
274
+ if (!Array.isArray(rawTables))
275
+ throw new ArgError("Expected array at tables");
276
+ parsed.tables = rawTables.map((t, i) => {
277
+ if (typeof t !== "string")
278
+ throw new ArgError(`Expected string at tables[${i}]`);
279
+ return t;
280
+ });
281
+ }
282
+ const limitPerTable = optionalNumber(args, "limitPerTable");
283
+ if (limitPerTable !== undefined) {
284
+ if (!Number.isFinite(limitPerTable))
285
+ throw new ArgError("Expected finite number at limitPerTable");
286
+ parsed.limitPerTable = limitPerTable;
287
+ }
288
+ return parsed;
289
+ },
290
+ handler: async (args, context) => {
291
+ const missing = args.tables?.find((table) => !entryFor(table));
292
+ if (missing !== undefined)
293
+ return toolErrorResult(unknownTable(missing, "run_query"));
294
+ return runQuery(args, context);
295
+ },
296
+ }));
297
+ }
298
+ return tools;
299
+ }
@@ -0,0 +1,105 @@
1
+ import { type CorsOption, type HeaderMap } from "./cors.js";
2
+ import type { JsonValue, Thrown } from "./json.js";
3
+ import type { RpcResult } from "./jsonRpc.js";
4
+ /** A Convex `httpAction` body: `httpAction(handler)`. */
5
+ export type HttpHandler<Ctx> = (ctx: Ctx, request: Request) => Promise<Response>;
6
+ /** Default `WWW-Authenticate` resource metadata path. */
7
+ export declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
8
+ /** Response messages. Defaults are the strings every app sends today. */
9
+ export type McpHttpMessages = {
10
+ missingToken: string;
11
+ invalidToken: string;
12
+ methodNotAllowed: string;
13
+ invalidJson: string;
14
+ };
15
+ export declare const DEFAULT_MCP_HTTP_MESSAGES: McpHttpMessages;
16
+ export type McpHttpLogEvent = {
17
+ type: "auth-failed";
18
+ } | {
19
+ type: "authenticate-threw";
20
+ error: Thrown;
21
+ } | {
22
+ type: "invalid-json";
23
+ error: Thrown;
24
+ };
25
+ export type McpHttpLogger = (event: McpHttpLogEvent) => void;
26
+ /**
27
+ * Logs with the app-a strings: "[MCP][mcpHandler] token verification failed"
28
+ * and "[MCP][mcpHandler] failed to parse JSON body:". app-c logs only the
29
+ * first (`log: (e) => e.type === "auth-failed" && consoleMcpLogger(e)`);
30
+ * app-b and app-d log nothing (`log: false`).
31
+ */
32
+ export declare const consoleMcpLogger: McpHttpLogger;
33
+ export type AuthenticateArgs<Ctx, Scope extends string> = {
34
+ /** Bearer token from the `Authorization` header (never empty). */
35
+ token: string;
36
+ request: Request;
37
+ ctx: Ctx;
38
+ scope: Scope | undefined;
39
+ };
40
+ export type HandleRpcArgs<Principal, Ctx, Scope extends string> = {
41
+ principal: Principal;
42
+ /** JSON-RPC body text: re-encoded (`body: "json"`) or as received (`body: "text"`). */
43
+ body: string;
44
+ /** The parsed body. */
45
+ json: JsonValue;
46
+ request: Request;
47
+ ctx: Ctx;
48
+ scope: Scope | undefined;
49
+ };
50
+ export type McpHttpHandlerConfig<Principal, Ctx, Scope extends string = string> = {
51
+ /**
52
+ * Verify the bearer token. Return the principal, or `null` to answer 401
53
+ * "Invalid or expired token". Authorisation (Pro claims, legacy JWTs, API
54
+ * key scopes, user lookup) belongs here, in the app.
55
+ */
56
+ authenticate: (args: AuthenticateArgs<Ctx, Scope>) => Promise<Principal | null>;
57
+ /**
58
+ * Run the JSON-RPC request. Either the app's own (for example an
59
+ * internalAction that runs the MCP SDK) or `createMcpDispatcher(...).handleRpc`.
60
+ * The result is sent with `Content-Type: application/json` plus CORS.
61
+ */
62
+ handleRpc: (args: HandleRpcArgs<Principal, Ctx, Scope>) => Promise<RpcResult>;
63
+ /** Opaque value passed to `authenticate` and `handleRpc` (app-a: "personal" | "team"). */
64
+ scope?: Scope;
65
+ /** Path for `WWW-Authenticate: Bearer resource_metadata="<origin><path>"`. Default `/.well-known/oauth-protected-resource`. */
66
+ resourceMetadataPath?: string;
67
+ /** Full resource metadata URL. Overrides `resourceMetadataPath`. */
68
+ resourceMetadataUrl?: (request: Request) => string;
69
+ /** Methods answered 405 (stateless server). Default `["GET", "DELETE"]`. */
70
+ rejectMethods?: readonly string[];
71
+ /** When to reject those methods. Default `"after-auth"` (all four apps); app-e checks before auth. */
72
+ methodCheck?: "after-auth" | "before-auth";
73
+ /**
74
+ * `"json"` (default): `JSON.stringify(await request.json())`, like app-a, app-b, app-c.
75
+ * `"text"`: raw text, checked with `JSON.parse`, like app-d.
76
+ */
77
+ body?: "json" | "text";
78
+ /** CORS on every response. Default none. With CORS set, `OPTIONS` returns a 204 preflight. */
79
+ cors?: CorsOption;
80
+ /** Extra headers on every response (after CORS). */
81
+ headers?: HeaderMap;
82
+ messages?: Partial<McpHttpMessages>;
83
+ /** Log hook. Default `consoleMcpLogger`; `false` for none. */
84
+ log?: McpHttpLogger | false;
85
+ /**
86
+ * When `authenticate` throws. `"throw"` (default, as the apps today): the
87
+ * error escapes, so Convex answers 500. `"unauthorized"`: log
88
+ * `authenticate-threw` and answer 401 "Invalid or expired token".
89
+ */
90
+ onAuthenticateError?: "throw" | "unauthorized";
91
+ };
92
+ /**
93
+ * Build the `/mcp` handler: bearer check, `authenticate`, method check,
94
+ * body parsing, then `handleRpc`.
95
+ *
96
+ * ```ts
97
+ * export const mcpHandler = httpAction(
98
+ * createMcpHttpHandler<Credentials, ActionCtx>({
99
+ * authenticate: ({ ctx, token }) => ctx.runAction(internal.mcp.nodeActions.verifyAccessToken, { token }),
100
+ * handleRpc: ({ ctx, principal, body }) => ctx.runAction(internal.mcp.nodeActions.handleMcpRequest, { clerkUserId: principal.clerkUserId, body }),
101
+ * }),
102
+ * );
103
+ * ```
104
+ */
105
+ export declare function createMcpHttpHandler<Principal, Ctx, Scope extends string = string>(config: McpHttpHandlerConfig<Principal, Ctx, Scope>): HttpHandler<Ctx>;
@@ -0,0 +1,123 @@
1
+ import { extractBearerToken } from "../convex/index.js";
2
+ import { resolveCors } from "./cors.js";
3
+ /** Default `WWW-Authenticate` resource metadata path. */
4
+ export const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
5
+ export const DEFAULT_MCP_HTTP_MESSAGES = {
6
+ missingToken: "Missing Authorization header",
7
+ invalidToken: "Invalid or expired token",
8
+ methodNotAllowed: "Method not supported in stateless mode",
9
+ invalidJson: "Invalid JSON body",
10
+ };
11
+ /**
12
+ * Logs with the app-a strings: "[MCP][mcpHandler] token verification failed"
13
+ * and "[MCP][mcpHandler] failed to parse JSON body:". app-c logs only the
14
+ * first (`log: (e) => e.type === "auth-failed" && consoleMcpLogger(e)`);
15
+ * app-b and app-d log nothing (`log: false`).
16
+ */
17
+ export const consoleMcpLogger = (event) => {
18
+ switch (event.type) {
19
+ case "auth-failed":
20
+ console.error("[MCP][mcpHandler] token verification failed");
21
+ return;
22
+ case "authenticate-threw":
23
+ console.error("[MCP][mcpHandler] authenticate threw:", event.error);
24
+ return;
25
+ case "invalid-json":
26
+ console.error("[MCP][mcpHandler] failed to parse JSON body:", event.error);
27
+ return;
28
+ }
29
+ };
30
+ /**
31
+ * Build the `/mcp` handler: bearer check, `authenticate`, method check,
32
+ * body parsing, then `handleRpc`.
33
+ *
34
+ * ```ts
35
+ * export const mcpHandler = httpAction(
36
+ * createMcpHttpHandler<Credentials, ActionCtx>({
37
+ * authenticate: ({ ctx, token }) => ctx.runAction(internal.mcp.nodeActions.verifyAccessToken, { token }),
38
+ * handleRpc: ({ ctx, principal, body }) => ctx.runAction(internal.mcp.nodeActions.handleMcpRequest, { clerkUserId: principal.clerkUserId, body }),
39
+ * }),
40
+ * );
41
+ * ```
42
+ */
43
+ export function createMcpHttpHandler(config) {
44
+ const messages = { ...DEFAULT_MCP_HTTP_MESSAGES, ...config.messages };
45
+ const rejectMethods = config.rejectMethods ?? ["GET", "DELETE"];
46
+ const methodCheck = config.methodCheck ?? "after-auth";
47
+ const bodyMode = config.body ?? "json";
48
+ const log = config.log === false ? undefined : (config.log ?? consoleMcpLogger);
49
+ const onAuthenticateError = config.onAuthenticateError ?? "throw";
50
+ const scope = config.scope;
51
+ return async (ctx, request) => {
52
+ const baseHeaders = {
53
+ ...resolveCors(config.cors, request),
54
+ ...config.headers,
55
+ };
56
+ const json = (body, status, extra = {}) => Response.json(body, {
57
+ status,
58
+ headers: { "Content-Type": "application/json", ...baseHeaders, ...extra },
59
+ });
60
+ const resourceMetadataUrl = config.resourceMetadataUrl
61
+ ? config.resourceMetadataUrl(request)
62
+ : `${new URL(request.url).origin}${config.resourceMetadataPath ?? PROTECTED_RESOURCE_METADATA_PATH}`;
63
+ const unauthorized = (message) => json({ error: message }, 401, {
64
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`,
65
+ });
66
+ const rejectsMethod = () => rejectMethods.includes(request.method);
67
+ const methodNotAllowed = () => json({ error: messages.methodNotAllowed }, 405);
68
+ if (request.method === "OPTIONS" && config.cors) {
69
+ return new Response(null, { status: 204, headers: baseHeaders });
70
+ }
71
+ if (methodCheck === "before-auth" && rejectsMethod()) {
72
+ return methodNotAllowed();
73
+ }
74
+ const token = extractBearerToken(request.headers.get("Authorization"));
75
+ if (!token)
76
+ return unauthorized(messages.missingToken);
77
+ let principal;
78
+ try {
79
+ principal = await config.authenticate({ token, request, ctx, scope });
80
+ }
81
+ catch (error) {
82
+ if (onAuthenticateError === "throw")
83
+ throw error;
84
+ log?.({ type: "authenticate-threw", error });
85
+ principal = null;
86
+ }
87
+ if (principal === null || principal === undefined) {
88
+ log?.({ type: "auth-failed" });
89
+ return unauthorized(messages.invalidToken);
90
+ }
91
+ if (methodCheck === "after-auth" && rejectsMethod()) {
92
+ return methodNotAllowed();
93
+ }
94
+ let body;
95
+ let parsed;
96
+ try {
97
+ if (bodyMode === "text") {
98
+ body = await request.text();
99
+ parsed = JSON.parse(body);
100
+ }
101
+ else {
102
+ parsed = await request.json();
103
+ body = JSON.stringify(parsed);
104
+ }
105
+ }
106
+ catch (error) {
107
+ log?.({ type: "invalid-json", error });
108
+ return json({ error: messages.invalidJson }, 400);
109
+ }
110
+ const result = await config.handleRpc({
111
+ principal,
112
+ body,
113
+ json: parsed,
114
+ request,
115
+ ctx,
116
+ scope,
117
+ });
118
+ return new Response(result.body, {
119
+ status: result.status,
120
+ headers: { "Content-Type": "application/json", ...baseHeaders },
121
+ });
122
+ };
123
+ }
@@ -0,0 +1,10 @@
1
+ export { mcpCors, type CorsOption, type HeaderMap } from "./cors.js";
2
+ export { createMcpDispatcher, parseRpcBody, type McpDispatcher, type McpDispatcherConfig, } from "./dispatcher.js";
3
+ export { readEnvUrl, type EnvFallbackMode, type EnvUrl } from "./env.js";
4
+ export { createExplorerTools, type ExplorerFilters, type ExplorerOperation, type ExplorerToolKey, type ExplorerToolsConfig, type FilterValue, type RunQueryArgs, type TableCatalog, type TableEntry, } from "./explorer.js";
5
+ export { consoleMcpLogger, createMcpHttpHandler, DEFAULT_MCP_HTTP_MESSAGES, PROTECTED_RESOURCE_METADATA_PATH, type AuthenticateArgs, type HandleRpcArgs, type HttpHandler, type McpHttpHandlerConfig, type McpHttpLogEvent, type McpHttpLogger, type McpHttpMessages, } from "./http.js";
6
+ export { errorMessage, isJsonObject, type JsonInput, type JsonObject, type JsonValue, type Thrown, } from "./json.js";
7
+ export { JSON_RPC_ERROR, mcpInternalError, negotiateProtocolVersion, rpcError, rpcResult, SDK_PROTOCOL_VERSIONS, type JsonRpcFailure, type JsonRpcId, type JsonRpcSuccess, type ProtocolVersionOptions, type RpcResult, } from "./jsonRpc.js";
8
+ export { createAuthorizationServerMetadataProxy, createHealthHandler, createOptionsHandler, createProtectedResourceMetadata, type AuthorizationServerMetadataProxyOptions, type HealthOptions, type OptionsHandlerOptions, type ProtectedResourceMetadataOptions, } from "./metadata.js";
9
+ export { runSdkMcpRequest, type RunSdkMcpRequestOptions, type SdkServerLike, type SdkTransportLike, } from "./sdk.js";
10
+ export { assertUniqueToolNames, createToolRegistry, defineTool, formatIssues, invalidArgumentsResult, runTool, toolErrorResult, toolJsonResult, toolsToList, toolTextResult, type JsonSchemaConverter, type JsonSchemaObject, type JsonSchemaToolDefinition, type McpTool, type ToolAnnotations, type ToolContent, type ToolContext, type ToolListEntry, type ToolRegistry, type ToolResult, type ToolsToListOptions, type ZodLikeSchema, type ZodToolDefinition, } from "./tools.js";
@@ -0,0 +1,10 @@
1
+ export { mcpCors } from "./cors.js";
2
+ export { createMcpDispatcher, parseRpcBody, } from "./dispatcher.js";
3
+ export { readEnvUrl } from "./env.js";
4
+ export { createExplorerTools, } from "./explorer.js";
5
+ export { consoleMcpLogger, createMcpHttpHandler, DEFAULT_MCP_HTTP_MESSAGES, PROTECTED_RESOURCE_METADATA_PATH, } from "./http.js";
6
+ export { errorMessage, isJsonObject, } from "./json.js";
7
+ export { JSON_RPC_ERROR, mcpInternalError, negotiateProtocolVersion, rpcError, rpcResult, SDK_PROTOCOL_VERSIONS, } from "./jsonRpc.js";
8
+ export { createAuthorizationServerMetadataProxy, createHealthHandler, createOptionsHandler, createProtectedResourceMetadata, } from "./metadata.js";
9
+ export { runSdkMcpRequest, } from "./sdk.js";
10
+ export { assertUniqueToolNames, createToolRegistry, defineTool, formatIssues, invalidArgumentsResult, runTool, toolErrorResult, toolJsonResult, toolsToList, toolTextResult, } from "./tools.js";
@@ -0,0 +1,18 @@
1
+ /** A JSON value as produced by `JSON.parse`. */
2
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ /** A JSON object as produced by `JSON.parse`. */
6
+ export type JsonObject = {
7
+ [key: string]: JsonValue;
8
+ };
9
+ /**
10
+ * Anything `JSON.stringify` turns into JSON text. Wider than `JsonValue` so
11
+ * Convex documents with optional fields can be passed without a cast.
12
+ */
13
+ export type JsonInput = object | string | number | boolean | null;
14
+ export declare function isJsonObject(value: JsonValue | undefined): value is JsonObject;
15
+ /** Any thrown value. `catch` bindings are assignable to it without a cast. */
16
+ export type Thrown = {} | null | undefined;
17
+ /** `err.message` for an `Error`, otherwise `String(err)`, like the MCP SDK. */
18
+ export declare function errorMessage(err: Thrown): string;
@@ -0,0 +1,7 @@
1
+ export function isJsonObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ /** `err.message` for an `Error`, otherwise `String(err)`, like the MCP SDK. */
5
+ export function errorMessage(err) {
6
+ return err instanceof Error ? err.message : String(err);
7
+ }
@@ -0,0 +1,61 @@
1
+ import type { JsonValue, Thrown } from "./json.js";
2
+ export type JsonRpcId = string | number | null;
3
+ /** JSON-RPC 2.0 error codes used by MCP. */
4
+ export declare const JSON_RPC_ERROR: {
5
+ readonly parseError: -32700;
6
+ readonly invalidRequest: -32600;
7
+ readonly methodNotFound: -32601;
8
+ readonly invalidParams: -32602;
9
+ readonly internalError: -32603;
10
+ };
11
+ /** A JSON-RPC 2.0 success response. */
12
+ export type JsonRpcSuccess = {
13
+ result: JsonValue | object;
14
+ jsonrpc: "2.0";
15
+ id: JsonRpcId;
16
+ };
17
+ /** A JSON-RPC 2.0 error response. */
18
+ export type JsonRpcFailure = {
19
+ jsonrpc: "2.0";
20
+ id: JsonRpcId;
21
+ error: {
22
+ code: number;
23
+ message: string;
24
+ };
25
+ };
26
+ /**
27
+ * What `handleRpc` returns: an HTTP status and the response body text.
28
+ * Same shape as the apps' `handleMcpRequest` internalAction result.
29
+ */
30
+ export type RpcResult = {
31
+ status: number;
32
+ body: string;
33
+ };
34
+ /**
35
+ * The 500 body the apps' `handleMcpRequest` returns when it throws:
36
+ * `{"jsonrpc":"2.0","error":{"code":-32603,"message":...},"id":null}`.
37
+ * The message is `err.message`, or "Internal server error" for non-Errors.
38
+ */
39
+ export declare function mcpInternalError(err: Thrown): RpcResult;
40
+ /** Success response. Key order matches the MCP SDK: result, jsonrpc, id. */
41
+ export declare function rpcResult(id: JsonRpcId, result: JsonValue | object): JsonRpcSuccess;
42
+ /** Error response. Key order matches the MCP SDK: jsonrpc, id, error. */
43
+ export declare function rpcError(id: JsonRpcId, code: number, message: string): JsonRpcFailure;
44
+ /**
45
+ * Protocol versions the MCP SDK 1.29.0 to 1.32.1 accept (the versions the
46
+ * apps lock today), newest first. `SUPPORTED_PROTOCOL_VERSIONS` in the SDK's
47
+ * `types.js`.
48
+ */
49
+ export declare const SDK_PROTOCOL_VERSIONS: readonly string[];
50
+ export type ProtocolVersionOptions = {
51
+ /** Versions the server accepts. Default `SDK_PROTOCOL_VERSIONS`. */
52
+ versions?: readonly string[];
53
+ /** Answer for an unsupported or missing request. Default `versions[0]`, like the SDK. */
54
+ fallback?: string;
55
+ };
56
+ /**
57
+ * Echo the client's version when supported, else the fallback. The SDK
58
+ * falls back to its latest version; app-e's gateway falls back to
59
+ * "2025-03-26" (`{ versions: ["2025-06-18", "2025-03-26", "2024-11-05"], fallback: "2025-03-26" }`).
60
+ */
61
+ export declare function negotiateProtocolVersion(requested: JsonValue | undefined, options?: ProtocolVersionOptions): string;