@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,60 @@
1
+ /** JSON-RPC 2.0 error codes used by MCP. */
2
+ export const JSON_RPC_ERROR = {
3
+ parseError: -32700,
4
+ invalidRequest: -32600,
5
+ methodNotFound: -32601,
6
+ invalidParams: -32602,
7
+ internalError: -32603,
8
+ };
9
+ /**
10
+ * The 500 body the apps' `handleMcpRequest` returns when it throws:
11
+ * `{"jsonrpc":"2.0","error":{"code":-32603,"message":...},"id":null}`.
12
+ * The message is `err.message`, or "Internal server error" for non-Errors.
13
+ */
14
+ export function mcpInternalError(err) {
15
+ const message = err instanceof Error ? err.message : "Internal server error";
16
+ return {
17
+ status: 500,
18
+ body: JSON.stringify({
19
+ jsonrpc: "2.0",
20
+ error: { code: JSON_RPC_ERROR.internalError, message },
21
+ id: null,
22
+ }),
23
+ };
24
+ }
25
+ /** Success response. Key order matches the MCP SDK: result, jsonrpc, id. */
26
+ export function rpcResult(id, result) {
27
+ return { result, jsonrpc: "2.0", id };
28
+ }
29
+ /** Error response. Key order matches the MCP SDK: jsonrpc, id, error. */
30
+ export function rpcError(id, code, message) {
31
+ return { jsonrpc: "2.0", id, error: { code, message } };
32
+ }
33
+ /**
34
+ * Protocol versions the MCP SDK 1.29.0 to 1.32.1 accept (the versions the
35
+ * apps lock today), newest first. `SUPPORTED_PROTOCOL_VERSIONS` in the SDK's
36
+ * `types.js`.
37
+ */
38
+ export const SDK_PROTOCOL_VERSIONS = [
39
+ "2025-11-25",
40
+ "2025-06-18",
41
+ "2025-03-26",
42
+ "2024-11-05",
43
+ "2024-10-07",
44
+ ];
45
+ /**
46
+ * Echo the client's version when supported, else the fallback. The SDK
47
+ * falls back to its latest version; app-e's gateway falls back to
48
+ * "2025-03-26" (`{ versions: ["2025-06-18", "2025-03-26", "2024-11-05"], fallback: "2025-03-26" }`).
49
+ */
50
+ export function negotiateProtocolVersion(requested, options = {}) {
51
+ const versions = options.versions ?? SDK_PROTOCOL_VERSIONS;
52
+ if (typeof requested === "string" && versions.includes(requested)) {
53
+ return requested;
54
+ }
55
+ const fallback = options.fallback ?? versions[0];
56
+ if (fallback === undefined) {
57
+ throw new Error("negotiateProtocolVersion needs at least one version");
58
+ }
59
+ return fallback;
60
+ }
@@ -0,0 +1,59 @@
1
+ import type { EnvSource } from "../convex/index.js";
2
+ import { type CorsOption, type HeaderMap } from "./cors.js";
3
+ import { type EnvUrl } from "./env.js";
4
+ import type { HttpHandler } from "./http.js";
5
+ import type { JsonInput } from "./json.js";
6
+ type CommonOptions = {
7
+ /** CORS on every response. Default none. */
8
+ cors?: CorsOption;
9
+ /** Extra headers on every response (after CORS). */
10
+ headers?: HeaderMap;
11
+ /** Env source, read on each request. Default `process.env`. */
12
+ env?: EnvSource;
13
+ };
14
+ export type AuthorizationServerMetadataProxyOptions = CommonOptions & {
15
+ /** Issuer URL env var. Default `CLERK_FRONTEND_API_URL`. */
16
+ issuer?: EnvUrl;
17
+ /**
18
+ * How the upstream body is returned on 200:
19
+ * - `"json"` (default): `Response.json(await res.json())`, like app-a and app-e.
20
+ * - `"text"`: `await res.text()` as `application/json`, like app-c and app-d.
21
+ * - `"stream"`: `res.body` streamed as `application/json`, like app-b.
22
+ */
23
+ forward?: "json" | "text" | "stream";
24
+ /** Fetch implementation. Default the global `fetch` at request time. */
25
+ fetch?: (url: string) => Promise<Response>;
26
+ };
27
+ /**
28
+ * `GET /.well-known/oauth-authorization-server`: proxy
29
+ * `<issuer>/.well-known/oauth-authorization-server`. On a non-2xx upstream,
30
+ * 502 `{"error":"Failed to fetch authorization server metadata"}`.
31
+ */
32
+ export declare function createAuthorizationServerMetadataProxy(options?: AuthorizationServerMetadataProxyOptions): HttpHandler<object>;
33
+ export type ProtectedResourceMetadataOptions = CommonOptions & {
34
+ /** Resource path on this origin. Default `/mcp` (app-a team: `/mcp/team`). */
35
+ resourcePath?: string;
36
+ /** Authorization server URL env var. Default `CLERK_FRONTEND_API_URL`. */
37
+ authorizationServer?: EnvUrl;
38
+ /** `resource_documentation` URL env var. Default `WEB_APP_URL`. */
39
+ documentation?: EnvUrl;
40
+ };
41
+ /**
42
+ * `GET /.well-known/oauth-protected-resource[/...]`:
43
+ * `{ resource, authorization_servers: [issuer], bearer_methods_supported: ["header"], resource_documentation }`.
44
+ * Env is read on each request, issuer first, so a missing var throws like the apps.
45
+ */
46
+ export declare function createProtectedResourceMetadata(options?: ProtectedResourceMetadataOptions): HttpHandler<object>;
47
+ export type HealthOptions = Omit<CommonOptions, "env"> & {
48
+ /** Body. Default `{ status: "ok" }`. app-e: `{ status: "ok", service: "app-e" }`. */
49
+ body?: JsonInput;
50
+ };
51
+ /** `GET /health`: 200 JSON, `{"status":"ok"}` by default. */
52
+ export declare function createHealthHandler(options?: HealthOptions): HttpHandler<object>;
53
+ export type OptionsHandlerOptions = {
54
+ cors: CorsOption;
55
+ headers?: HeaderMap;
56
+ };
57
+ /** `OPTIONS` preflight: 204, no body, CORS headers only. */
58
+ export declare function createOptionsHandler(options: OptionsHandlerOptions): HttpHandler<object>;
59
+ export {};
@@ -0,0 +1,63 @@
1
+ import { resolveCors } from "./cors.js";
2
+ import { readEnvUrl } from "./env.js";
3
+ function headersFor(options, request) {
4
+ return { ...resolveCors(options.cors, request), ...options.headers };
5
+ }
6
+ /**
7
+ * `GET /.well-known/oauth-authorization-server`: proxy
8
+ * `<issuer>/.well-known/oauth-authorization-server`. On a non-2xx upstream,
9
+ * 502 `{"error":"Failed to fetch authorization server metadata"}`.
10
+ */
11
+ export function createAuthorizationServerMetadataProxy(options = {}) {
12
+ const forward = options.forward ?? "json";
13
+ return async (_ctx, request) => {
14
+ const issuer = readEnvUrl(options.issuer ?? "CLERK_FRONTEND_API_URL", options.env);
15
+ const url = `${issuer}/.well-known/oauth-authorization-server`;
16
+ const res = await (options.fetch ? options.fetch(url) : fetch(url));
17
+ const headers = headersFor(options, request);
18
+ if (!res.ok) {
19
+ return Response.json({ error: "Failed to fetch authorization server metadata" }, { status: 502, headers: { "Content-Type": "application/json", ...headers } });
20
+ }
21
+ if (forward === "json") {
22
+ const metadata = await res.json();
23
+ return Response.json(metadata, {
24
+ headers: { "Content-Type": "application/json", ...headers },
25
+ });
26
+ }
27
+ const body = forward === "text" ? await res.text() : res.body;
28
+ return new Response(body, {
29
+ status: 200,
30
+ headers: { "Content-Type": "application/json", ...headers },
31
+ });
32
+ };
33
+ }
34
+ /**
35
+ * `GET /.well-known/oauth-protected-resource[/...]`:
36
+ * `{ resource, authorization_servers: [issuer], bearer_methods_supported: ["header"], resource_documentation }`.
37
+ * Env is read on each request, issuer first, so a missing var throws like the apps.
38
+ */
39
+ export function createProtectedResourceMetadata(options = {}) {
40
+ const resourcePath = options.resourcePath ?? "/mcp";
41
+ return async (_ctx, request) => {
42
+ const origin = new URL(request.url).origin;
43
+ return Response.json({
44
+ resource: `${origin}${resourcePath}`,
45
+ authorization_servers: [
46
+ readEnvUrl(options.authorizationServer ?? "CLERK_FRONTEND_API_URL", options.env),
47
+ ],
48
+ bearer_methods_supported: ["header"],
49
+ resource_documentation: readEnvUrl(options.documentation ?? "WEB_APP_URL", options.env),
50
+ }, { headers: { "Content-Type": "application/json", ...headersFor(options, request) } });
51
+ };
52
+ }
53
+ /** `GET /health`: 200 JSON, `{"status":"ok"}` by default. */
54
+ export function createHealthHandler(options = {}) {
55
+ const body = options.body ?? { status: "ok" };
56
+ return async (_ctx, request) => Response.json(body, {
57
+ headers: { "Content-Type": "application/json", ...headersFor(options, request) },
58
+ });
59
+ }
60
+ /** `OPTIONS` preflight: 204, no body, CORS headers only. */
61
+ export function createOptionsHandler(options) {
62
+ return async (_ctx, request) => new Response(null, { status: 204, headers: headersFor(options, request) });
63
+ }
@@ -0,0 +1,56 @@
1
+ import { type RpcResult } from "./jsonRpc.js";
2
+ import type { JsonValue, Thrown } from "./json.js";
3
+ /** The part of `WebStandardStreamableHTTPServerTransport` this helper uses. */
4
+ export type SdkTransportLike = {
5
+ handleRequest(request: Request, options?: {
6
+ parsedBody?: JsonValue;
7
+ }): Promise<Response>;
8
+ };
9
+ /** The part of `McpServer` this helper uses. */
10
+ export type SdkServerLike<Transport> = {
11
+ connect(transport: Transport): Promise<void>;
12
+ };
13
+ export type RunSdkMcpRequestOptions<Transport extends SdkTransportLike> = {
14
+ /**
15
+ * Build a fresh, fully registered `McpServer` (one per request, as
16
+ * stateless mode needs). Called inside the error boundary, after the body
17
+ * is parsed, so a registration error also becomes the 500 result.
18
+ */
19
+ createServer: () => NoInfer<SdkServerLike<Transport>> | Promise<NoInfer<SdkServerLike<Transport>>>;
20
+ /** `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js`. */
21
+ Transport: new (options: {
22
+ sessionIdGenerator: undefined;
23
+ enableJsonResponse: boolean;
24
+ }) => Transport;
25
+ /** JSON-RPC body text from `createMcpHttpHandler`. */
26
+ body: string;
27
+ /** URL of the synthetic request. Default `http://localhost/mcp` (app-a team: `http://localhost/mcp/team`). */
28
+ url?: string;
29
+ /**
30
+ * Pass `JSON.parse(body)` as `parsedBody`, like app-a, app-b and app-c
31
+ * (default `true`). app-d passes nothing and lets the transport read the body.
32
+ */
33
+ parsedBody?: boolean;
34
+ /** Called with the error before the 500 result. Default logs "[MCP][handleMcpRequest] threw:". */
35
+ onError?: (message: string, error: Thrown) => void;
36
+ };
37
+ /**
38
+ * The body of the apps' `handleMcpRequest` internalAction: connect a
39
+ * stateless JSON-response transport, post the body, return
40
+ * `{ status, body }`; on a throw, 500 with a JSON-RPC -32603 error.
41
+ * The kit does not import the SDK; pass its classes in.
42
+ *
43
+ * ```ts
44
+ * // nodeActions.ts ("use node")
45
+ * return runSdkMcpRequest({
46
+ * body,
47
+ * Transport: WebStandardStreamableHTTPServerTransport,
48
+ * createServer: () => {
49
+ * const server = new McpServer(serverInfo);
50
+ * registerTools(server, clerkUserId, ctx);
51
+ * return server;
52
+ * },
53
+ * });
54
+ * ```
55
+ */
56
+ export declare function runSdkMcpRequest<Transport extends SdkTransportLike>(options: RunSdkMcpRequestOptions<Transport>): Promise<RpcResult>;
@@ -0,0 +1,52 @@
1
+ import { mcpInternalError } from "./jsonRpc.js";
2
+ /**
3
+ * The body of the apps' `handleMcpRequest` internalAction: connect a
4
+ * stateless JSON-response transport, post the body, return
5
+ * `{ status, body }`; on a throw, 500 with a JSON-RPC -32603 error.
6
+ * The kit does not import the SDK; pass its classes in.
7
+ *
8
+ * ```ts
9
+ * // nodeActions.ts ("use node")
10
+ * return runSdkMcpRequest({
11
+ * body,
12
+ * Transport: WebStandardStreamableHTTPServerTransport,
13
+ * createServer: () => {
14
+ * const server = new McpServer(serverInfo);
15
+ * registerTools(server, clerkUserId, ctx);
16
+ * return server;
17
+ * },
18
+ * });
19
+ * ```
20
+ */
21
+ export async function runSdkMcpRequest(options) {
22
+ try {
23
+ const parsedBody = options.parsedBody === false ? undefined : JSON.parse(options.body);
24
+ const server = await options.createServer();
25
+ const transport = new options.Transport({
26
+ sessionIdGenerator: undefined,
27
+ enableJsonResponse: true,
28
+ });
29
+ await server.connect(transport);
30
+ const request = new Request(options.url ?? "http://localhost/mcp", {
31
+ method: "POST",
32
+ headers: {
33
+ "content-type": "application/json",
34
+ accept: "application/json, text/event-stream",
35
+ },
36
+ body: options.body,
37
+ });
38
+ const response = parsedBody === undefined
39
+ ? await transport.handleRequest(request)
40
+ : await transport.handleRequest(request, { parsedBody });
41
+ return { status: response.status, body: await response.text() };
42
+ }
43
+ catch (err) {
44
+ const result = mcpInternalError(err);
45
+ const message = err instanceof Error ? err.message : "Internal server error";
46
+ if (options.onError)
47
+ options.onError(message, err);
48
+ else
49
+ console.error("[MCP][handleMcpRequest] threw:", message, err);
50
+ return result;
51
+ }
52
+ }
@@ -0,0 +1,176 @@
1
+ import { type JsonInput, type JsonObject, type JsonValue } from "./json.js";
2
+ export type ToolContent = {
3
+ type: "text";
4
+ text: string;
5
+ } | {
6
+ type: "image";
7
+ data: string;
8
+ mimeType: string;
9
+ } | {
10
+ type: "audio";
11
+ data: string;
12
+ mimeType: string;
13
+ };
14
+ /** An MCP `tools/call` result. */
15
+ export type ToolResult = {
16
+ content: ToolContent[];
17
+ isError?: boolean;
18
+ structuredContent?: {
19
+ [key: string]: JsonInput;
20
+ };
21
+ };
22
+ /** `{ content: [{ type: "text", text }] }` (app-d `textResult`). */
23
+ export declare function toolTextResult(text: string): ToolResult;
24
+ /** Pretty JSON text, `JSON.stringify(data, null, 2)` (app-b and app-c `textResult`). */
25
+ export declare function toolJsonResult(data: JsonInput): ToolResult;
26
+ /** `{ content: [{ type: "text", text: message }], isError: true }` (every app's `errorResult`). */
27
+ export declare function toolErrorResult(message: string): ToolResult;
28
+ /**
29
+ * Run `fn` and return its value as pretty JSON, or its error as an error
30
+ * result. `errorLabel` prefixes the message (app-a: "Files get failed: ...").
31
+ * A value with no JSON form becomes "Result is not JSON" (app-d `jsonTool`).
32
+ */
33
+ export declare function runTool(fn: () => Promise<JsonInput | undefined>, options?: {
34
+ errorLabel?: string;
35
+ }): Promise<ToolResult>;
36
+ /** MCP tool annotations (hints only). */
37
+ export type ToolAnnotations = {
38
+ title?: string;
39
+ readOnlyHint?: boolean;
40
+ destructiveHint?: boolean;
41
+ idempotentHint?: boolean;
42
+ openWorldHint?: boolean;
43
+ };
44
+ type Issue = {
45
+ path: ReadonlyArray<PropertyKey>;
46
+ message: string;
47
+ };
48
+ /**
49
+ * The part of a zod schema (3.25+ or 4) the kit uses. The kit never imports
50
+ * zod; any object with this shape works.
51
+ */
52
+ export type ZodLikeSchema<Out> = {
53
+ safeParse(data: JsonValue): {
54
+ success: true;
55
+ data: Out;
56
+ } | {
57
+ success: false;
58
+ error: {
59
+ issues: ReadonlyArray<Issue>;
60
+ };
61
+ };
62
+ /** Present on zod 4 schemas. Used for `tools/list` when no converter is given. */
63
+ toJSONSchema?(params: {
64
+ target: "draft-7";
65
+ io: "input";
66
+ }): object;
67
+ };
68
+ /** A raw JSON Schema object (`{ type: "object", properties: {...} }`). Args are not validated unless `parseArgs` is given. */
69
+ export type JsonSchemaObject = {
70
+ type: "object";
71
+ properties?: object;
72
+ required?: readonly string[];
73
+ } & object;
74
+ /**
75
+ * Turn a zod schema into JSON Schema for `tools/list`. Needed for zod 3,
76
+ * whose schemas have no `toJSONSchema()`:
77
+ * `(schema) => (schema instanceof ZodType ? zodToJsonSchema(schema) : {})`.
78
+ */
79
+ export type JsonSchemaConverter = (schema: ZodLikeSchema<object>) => object;
80
+ /** What a tool handler receives besides its arguments. */
81
+ export type ToolContext<Principal, Ctx, Scope extends string = string> = {
82
+ principal: Principal;
83
+ ctx: Ctx;
84
+ scope: Scope | undefined;
85
+ };
86
+ type ToolMeta<Scope extends string> = {
87
+ name: string;
88
+ title?: string;
89
+ /** A string, or a function of the scope (app-a: personal vs team wording). */
90
+ description: string | ((scope: Scope | undefined) => string);
91
+ annotations?: ToolAnnotations;
92
+ /** Scopes the tool is listed and callable on. Omit for all (app-a `scopes`). */
93
+ scopes?: readonly Scope[];
94
+ };
95
+ export type ZodToolDefinition<Args extends object, Principal, Ctx, Scope extends string> = ToolMeta<Scope> & {
96
+ inputSchema: ZodLikeSchema<Args>;
97
+ /** JSON Schema for `tools/list`. Overrides any converter. */
98
+ jsonSchema?: object;
99
+ handler: (args: Args, context: ToolContext<Principal, Ctx, Scope>) => Promise<ToolResult>;
100
+ };
101
+ export type JsonSchemaToolDefinition<Args extends object, Principal, Ctx, Scope extends string> = ToolMeta<Scope> & {
102
+ inputSchema: JsonSchemaObject;
103
+ /** Validate and normalise raw arguments. Throw to reject; the message becomes the validation error. */
104
+ parseArgs: (args: JsonObject) => Args;
105
+ handler: (args: Args, context: ToolContext<Principal, Ctx, Scope>) => Promise<ToolResult>;
106
+ };
107
+ /** A defined tool. Arguments are validated inside `call`. */
108
+ export type McpTool<Principal, Ctx, Scope extends string = string> = {
109
+ readonly name: string;
110
+ readonly title?: string;
111
+ readonly description: string | ((scope: Scope | undefined) => string);
112
+ readonly annotations?: ToolAnnotations;
113
+ /** Scopes the tool is on (typed as `Scope[]` in `defineTool`; plain strings here so unscoped tools fit any registry). */
114
+ readonly scopes?: readonly string[];
115
+ /** JSON Schema for `tools/list`. */
116
+ inputJsonSchema(convert?: JsonSchemaConverter): object;
117
+ /**
118
+ * Validate `args`, run the handler, and shape errors like the MCP SDK:
119
+ * invalid args become an `isError` result starting
120
+ * "MCP error -32602: Input validation error: Invalid arguments for tool <name>: ",
121
+ * a thrown error becomes an `isError` result with its message.
122
+ */
123
+ call(args: JsonObject, context: ToolContext<Principal, Ctx, Scope>): Promise<ToolResult>;
124
+ };
125
+ /** Format zod issues like MCP SDK 1.30+: "<message> at <path>", one per line. */
126
+ export declare function formatIssues(issues: ReadonlyArray<Issue>): string;
127
+ export declare function invalidArgumentsResult(toolName: string, detail: string): ToolResult;
128
+ /**
129
+ * Define a tool with a zod object schema (3.25+ or 4) or a raw JSON Schema
130
+ * plus `parseArgs`. Type the handler's second parameter to fix
131
+ * `Principal` and `Ctx`:
132
+ *
133
+ * ```ts
134
+ * defineTool({
135
+ * name: "search",
136
+ * description: "Search your notes.",
137
+ * inputSchema: z.object({ query: z.string() }),
138
+ * handler: async ({ query }, { principal, ctx }: ToolContext<Principal, ActionCtx>) =>
139
+ * toolJsonResult(await ctx.runQuery(internal.search, { userId: principal.userId, query })),
140
+ * });
141
+ * ```
142
+ */
143
+ export declare function defineTool<Args extends object, Principal, Ctx, Scope extends string = string>(definition: ZodToolDefinition<Args, Principal, Ctx, Scope> | JsonSchemaToolDefinition<Args, Principal, Ctx, Scope>): McpTool<Principal, Ctx, Scope>;
144
+ /** A `tools/list` entry. */
145
+ export type ToolListEntry = {
146
+ name: string;
147
+ title?: string;
148
+ description: string;
149
+ inputSchema: object;
150
+ annotations?: ToolAnnotations;
151
+ };
152
+ /** Throw when two tools share a name. */
153
+ export declare function assertUniqueToolNames(tools: ReadonlyArray<{
154
+ name: string;
155
+ }>): void;
156
+ export type ToolsToListOptions<Scope extends string> = {
157
+ /** Keep only tools available on this scope. Tools without `scopes` are always kept. */
158
+ scope?: Scope;
159
+ /** zod to JSON Schema converter for schemas without `toJSONSchema()` (zod 3). */
160
+ toJsonSchema?: JsonSchemaConverter;
161
+ };
162
+ /** Build the `tools/list` result array, in definition order. */
163
+ export declare function toolsToList<Principal, Ctx, Scope extends string>(tools: ReadonlyArray<McpTool<Principal, Ctx, Scope>>, options?: ToolsToListOptions<Scope>): ToolListEntry[];
164
+ export type ToolRegistry<Principal, Ctx, Scope extends string> = {
165
+ readonly tools: ReadonlyArray<McpTool<Principal, Ctx, Scope>>;
166
+ /** Tools on a scope (all tools without `scopes`, plus those listing it). */
167
+ forScope(scope: Scope | undefined): ReadonlyArray<McpTool<Principal, Ctx, Scope>>;
168
+ /** Names on a scope, for `assertMcpTools` from server/convex. */
169
+ names(scope?: Scope): string[];
170
+ /** The tool with this name on this scope, or `undefined`. */
171
+ find(name: string, scope: Scope | undefined): McpTool<Principal, Ctx, Scope> | undefined;
172
+ list(options?: ToolsToListOptions<Scope>): ToolListEntry[];
173
+ };
174
+ /** A tool list with a name-uniqueness check (throws at creation) and scope filtering. */
175
+ export declare function createToolRegistry<Principal, Ctx, Scope extends string = string>(tools: ReadonlyArray<McpTool<Principal, Ctx, Scope>>): ToolRegistry<Principal, Ctx, Scope>;
176
+ export {};
@@ -0,0 +1,159 @@
1
+ import { errorMessage } from "./json.js";
2
+ import { JSON_RPC_ERROR } from "./jsonRpc.js";
3
+ /** `{ content: [{ type: "text", text }] }` (app-d `textResult`). */
4
+ export function toolTextResult(text) {
5
+ return { content: [{ type: "text", text }] };
6
+ }
7
+ /** Pretty JSON text, `JSON.stringify(data, null, 2)` (app-b and app-c `textResult`). */
8
+ export function toolJsonResult(data) {
9
+ return toolTextResult(JSON.stringify(data, null, 2));
10
+ }
11
+ /** `{ content: [{ type: "text", text: message }], isError: true }` (every app's `errorResult`). */
12
+ export function toolErrorResult(message) {
13
+ return { content: [{ type: "text", text: message }], isError: true };
14
+ }
15
+ /**
16
+ * Run `fn` and return its value as pretty JSON, or its error as an error
17
+ * result. `errorLabel` prefixes the message (app-a: "Files get failed: ...").
18
+ * A value with no JSON form becomes "Result is not JSON" (app-d `jsonTool`).
19
+ */
20
+ export async function runTool(fn, options = {}) {
21
+ const label = (message) => options.errorLabel ? `${options.errorLabel}: ${message}` : message;
22
+ try {
23
+ const text = JSON.stringify(await fn(), null, 2);
24
+ // JSON.stringify returns undefined for undefined, functions and symbols.
25
+ if (typeof text !== "string")
26
+ return toolErrorResult(label("Result is not JSON"));
27
+ return toolTextResult(text);
28
+ }
29
+ catch (err) {
30
+ return toolErrorResult(label(err instanceof Error ? err.message : "Tool failed"));
31
+ }
32
+ }
33
+ function dotPath(path) {
34
+ if (path.length === 0)
35
+ return "object root";
36
+ return path.reduce((acc, seg, index) => {
37
+ if (index === 0)
38
+ return String(seg);
39
+ if (typeof seg === "number")
40
+ return `${acc}[${seg}]`;
41
+ return `${acc}.${String(seg)}`;
42
+ }, "");
43
+ }
44
+ /** Format zod issues like MCP SDK 1.30+: "<message> at <path>", one per line. */
45
+ export function formatIssues(issues) {
46
+ return issues
47
+ .map((issue) => (issue.path.length ? `${issue.message} at ${dotPath(issue.path)}` : issue.message))
48
+ .join("\n");
49
+ }
50
+ export function invalidArgumentsResult(toolName, detail) {
51
+ return toolErrorResult(`MCP error ${JSON_RPC_ERROR.invalidParams}: Input validation error: Invalid arguments for tool ${toolName}: ${detail}`);
52
+ }
53
+ function isZodTool(definition) {
54
+ return "safeParse" in definition.inputSchema;
55
+ }
56
+ /**
57
+ * Define a tool with a zod object schema (3.25+ or 4) or a raw JSON Schema
58
+ * plus `parseArgs`. Type the handler's second parameter to fix
59
+ * `Principal` and `Ctx`:
60
+ *
61
+ * ```ts
62
+ * defineTool({
63
+ * name: "search",
64
+ * description: "Search your notes.",
65
+ * inputSchema: z.object({ query: z.string() }),
66
+ * handler: async ({ query }, { principal, ctx }: ToolContext<Principal, ActionCtx>) =>
67
+ * toolJsonResult(await ctx.runQuery(internal.search, { userId: principal.userId, query })),
68
+ * });
69
+ * ```
70
+ */
71
+ export function defineTool(definition) {
72
+ if (!definition.name)
73
+ throw new Error("MCP tool name must not be empty");
74
+ const parse = (raw) => {
75
+ if (isZodTool(definition)) {
76
+ const result = definition.inputSchema.safeParse(raw);
77
+ return result.success
78
+ ? { ok: true, args: result.data }
79
+ : { ok: false, detail: formatIssues(result.error.issues) };
80
+ }
81
+ try {
82
+ return { ok: true, args: definition.parseArgs(raw) };
83
+ }
84
+ catch (err) {
85
+ return { ok: false, detail: errorMessage(err) };
86
+ }
87
+ };
88
+ return {
89
+ name: definition.name,
90
+ title: definition.title,
91
+ description: definition.description,
92
+ annotations: definition.annotations,
93
+ scopes: definition.scopes,
94
+ inputJsonSchema(convert) {
95
+ if (!isZodTool(definition))
96
+ return definition.inputSchema;
97
+ if (definition.jsonSchema)
98
+ return definition.jsonSchema;
99
+ if (convert)
100
+ return convert(definition.inputSchema);
101
+ if (definition.inputSchema.toJSONSchema) {
102
+ return definition.inputSchema.toJSONSchema({ target: "draft-7", io: "input" });
103
+ }
104
+ throw new Error(`MCP tool ${definition.name}: schema has no toJSONSchema(); pass jsonSchema or a toJsonSchema converter (zod 3)`);
105
+ },
106
+ async call(args, context) {
107
+ const parsed = parse(args);
108
+ if (!parsed.ok)
109
+ return invalidArgumentsResult(definition.name, parsed.detail);
110
+ try {
111
+ return await definition.handler(parsed.args, context);
112
+ }
113
+ catch (err) {
114
+ return toolErrorResult(errorMessage(err));
115
+ }
116
+ },
117
+ };
118
+ }
119
+ function availableIn(tool, scope) {
120
+ return !tool.scopes || (scope !== undefined && tool.scopes.includes(scope));
121
+ }
122
+ /** Throw when two tools share a name. */
123
+ export function assertUniqueToolNames(tools) {
124
+ const seen = new Set();
125
+ for (const tool of tools) {
126
+ if (seen.has(tool.name))
127
+ throw new Error(`Duplicate MCP tool name: ${tool.name}`);
128
+ seen.add(tool.name);
129
+ }
130
+ }
131
+ /** Build the `tools/list` result array, in definition order. */
132
+ export function toolsToList(tools, options = {}) {
133
+ return tools
134
+ .filter((tool) => availableIn(tool, options.scope))
135
+ .map((tool) => {
136
+ const entry = {
137
+ name: tool.name,
138
+ description: typeof tool.description === "function" ? tool.description(options.scope) : tool.description,
139
+ inputSchema: tool.inputJsonSchema(options.toJsonSchema),
140
+ };
141
+ if (tool.title !== undefined)
142
+ entry.title = tool.title;
143
+ if (tool.annotations !== undefined)
144
+ entry.annotations = tool.annotations;
145
+ return entry;
146
+ });
147
+ }
148
+ /** A tool list with a name-uniqueness check (throws at creation) and scope filtering. */
149
+ export function createToolRegistry(tools) {
150
+ assertUniqueToolNames(tools);
151
+ const forScope = (scope) => tools.filter((tool) => availableIn(tool, scope));
152
+ return {
153
+ tools,
154
+ forScope,
155
+ names: (scope) => forScope(scope).map((tool) => tool.name),
156
+ find: (name, scope) => forScope(scope).find((tool) => tool.name === name),
157
+ list: (options) => toolsToList(tools, options),
158
+ };
159
+ }