Created
February 5, 2026 16:37
-
-
Save lassiter/8f67bea27d8b588506f532c6f37ba325 to your computer and use it in GitHub Desktop.
Add component support to Convex MCP tools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/src/cli/lib/mcp/tools/componentResolver.ts b/src/cli/lib/mcp/tools/componentResolver.ts | |
| new file mode 100644 | |
| index 0000000..b56b5f9 | |
| --- /dev/null | |
| +++ b/src/cli/lib/mcp/tools/componentResolver.ts | |
| @@ -0,0 +1,54 @@ | |
| +import { Context } from "../../../../bundler/context.js"; | |
| +import { runSystemQuery } from "../../run.js"; | |
| + | |
| +export type ComponentInfo = { | |
| + id: string; | |
| + name: string | null; | |
| + path: string; | |
| + state: string; | |
| +}; | |
| + | |
| +/** | |
| + * Resolves a component path or ID to a full ComponentInfo object. | |
| + * Accepts either: | |
| + * - A component path (e.g., "widget", "parent/child") | |
| + * - A component ID (e.g., "kh72fgh3j4...") | |
| + * | |
| + * Returns the matched component and the full list of available components. | |
| + */ | |
| +export async function resolveComponent( | |
| + ctx: Context, | |
| + credentials: { url: string; adminKey: string }, | |
| + componentPathOrId: string | undefined, | |
| +): Promise<{ component: ComponentInfo | null; allComponents: ComponentInfo[] }> { | |
| + // Fetch all components from the deployment | |
| + const allComponents = (await runSystemQuery(ctx, { | |
| + deploymentUrl: credentials.url, | |
| + adminKey: credentials.adminKey, | |
| + functionName: "_system/frontend/components:list", | |
| + componentPath: undefined, | |
| + args: {}, | |
| + })) as ComponentInfo[]; | |
| + | |
| + if (!componentPathOrId) { | |
| + return { component: null, allComponents }; | |
| + } | |
| + | |
| + // Try to match by path first (more common), then by ID | |
| + const component = | |
| + allComponents.find((c) => c.path === componentPathOrId) || | |
| + allComponents.find((c) => c.id === componentPathOrId); | |
| + | |
| + return { component: component || null, allComponents }; | |
| +} | |
| + | |
| +/** | |
| + * Formats a helpful error message when a component is not found. | |
| + */ | |
| +export function formatComponentError( | |
| + componentPathOrId: string, | |
| + allComponents: ComponentInfo[], | |
| +): string { | |
| + const available = allComponents.map((c) => c.path || "(root)").join(", "); | |
| + return `Component '${componentPathOrId}' not found. Available components: ${available}`; | |
| +} | |
| diff --git a/src/cli/lib/mcp/tools/components.ts b/src/cli/lib/mcp/tools/components.ts | |
| new file mode 100644 | |
| index 0000000..2443673 | |
| --- /dev/null | |
| +++ b/src/cli/lib/mcp/tools/components.ts | |
| @@ -0,0 +1,72 @@ | |
| +import { z } from "zod"; | |
| +import { ConvexTool } from "./index.js"; | |
| +import { loadSelectedDeploymentCredentials } from "../../api.js"; | |
| +import { getDeploymentSelection } from "../../deploymentSelection.js"; | |
| +import { resolveComponent } from "./componentResolver.js"; | |
| + | |
| +const inputSchema = z.object({ | |
| + deploymentSelector: z | |
| + .string() | |
| + .describe( | |
| + "Deployment selector (from the status tool) to list components from.", | |
| + ), | |
| +}); | |
| + | |
| +const outputSchema = z.object({ | |
| + components: z.array( | |
| + z.object({ | |
| + id: z.string().describe("Component ID (can be used as componentPath)"), | |
| + name: z | |
| + .string() | |
| + .nullable() | |
| + .describe("Component name (null for root)"), | |
| + path: z | |
| + .string() | |
| + .describe( | |
| + "Component path (e.g., 'widget') - use this with other tools", | |
| + ), | |
| + state: z | |
| + .string() | |
| + .describe("Component state ('active' or 'unmounted')"), | |
| + }), | |
| + ), | |
| +}); | |
| + | |
| +const description = ` | |
| +List all components in a Convex deployment. | |
| + | |
| +Use the returned 'path' values as the componentPath parameter in other tools | |
| +(tables, data, functionSpec, run). You can also use the 'id' if needed. | |
| + | |
| +The root component has an empty path (""). | |
| +`.trim(); | |
| + | |
| +export const ComponentsTool: ConvexTool<typeof inputSchema, typeof outputSchema> = | |
| + { | |
| + name: "components", | |
| + description, | |
| + inputSchema, | |
| + outputSchema, | |
| + handler: async (ctx, args) => { | |
| + const { projectDir, deployment } = await ctx.decodeDeploymentSelector( | |
| + args.deploymentSelector, | |
| + ); | |
| + process.chdir(projectDir); | |
| + const deploymentSelection = await getDeploymentSelection( | |
| + ctx, | |
| + ctx.options, | |
| + ); | |
| + const credentials = await loadSelectedDeploymentCredentials( | |
| + ctx, | |
| + deploymentSelection, | |
| + deployment, | |
| + ); | |
| + | |
| + const { allComponents } = await resolveComponent( | |
| + ctx, | |
| + credentials, | |
| + undefined, | |
| + ); | |
| + return { components: allComponents }; | |
| + }, | |
| + }; | |
| diff --git a/src/cli/lib/mcp/tools/data.ts b/src/cli/lib/mcp/tools/data.ts | |
| index 3f727ab..8b2e182 100644 | |
| --- a/src/cli/lib/mcp/tools/data.ts | |
| +++ b/src/cli/lib/mcp/tools/data.ts | |
| @@ -17,6 +17,12 @@ const inputSchema = z.object({ | |
| .max(1000) | |
| .optional() | |
| .describe("The maximum number of results to return, defaults to 100."), | |
| + componentPath: z | |
| + .string() | |
| + .optional() | |
| + .describe( | |
| + "Component path (e.g., 'widget') or component ID. Use the 'components' tool to list available components. Omit for root.", | |
| + ), | |
| }); | |
| const outputSchema = z.object({ | |
| @@ -54,7 +60,7 @@ export const DataTool: ConvexTool<typeof inputSchema, typeof outputSchema> = { | |
| deploymentUrl: credentials.url, | |
| adminKey: credentials.adminKey, | |
| functionName: "_system/cli/tableData", | |
| - componentPath: undefined, | |
| + componentPath: args.componentPath, | |
| args: { | |
| table: args.tableName, | |
| order: args.order, | |
| diff --git a/src/cli/lib/mcp/tools/functionSpec.ts b/src/cli/lib/mcp/tools/functionSpec.ts | |
| index 6ae33d6..9cd6015 100644 | |
| --- a/src/cli/lib/mcp/tools/functionSpec.ts | |
| +++ b/src/cli/lib/mcp/tools/functionSpec.ts | |
| @@ -10,6 +10,12 @@ const inputSchema = z.object({ | |
| .describe( | |
| "Deployment selector (from the status tool) to get function metadata from.", | |
| ), | |
| + componentPath: z | |
| + .string() | |
| + .optional() | |
| + .describe( | |
| + "Component path (e.g., 'widget') or component ID. Use the 'components' tool to list available components. Omit for root.", | |
| + ), | |
| }); | |
| const outputSchema = z | |
| @@ -49,7 +55,7 @@ export const FunctionSpecTool: ConvexTool< | |
| deploymentUrl: credentials.url, | |
| adminKey: credentials.adminKey, | |
| functionName: "_system/cli/modules:apiSpec", | |
| - componentPath: undefined, | |
| + componentPath: args.componentPath, | |
| args: {}, | |
| }); | |
| return functions; | |
| diff --git a/src/cli/lib/mcp/tools/index.ts b/src/cli/lib/mcp/tools/index.ts | |
| index 7f12353..b58ccf8 100644 | |
| --- a/src/cli/lib/mcp/tools/index.ts | |
| +++ b/src/cli/lib/mcp/tools/index.ts | |
| @@ -9,6 +9,7 @@ import { RunTool } from "./run.js"; | |
| import { EnvListTool, EnvGetTool, EnvSetTool, EnvRemoveTool } from "./env.js"; | |
| import { RunOneoffQueryTool } from "./runOneoffQuery.js"; | |
| import { LogsTool } from "./logs.js"; | |
| +import { ComponentsTool } from "./components.js"; | |
| import { Tool } from "@modelcontextprotocol/sdk/types.js"; | |
| export type ConvexTool<Input extends ZodTypeAny, Output extends ZodTypeAny> = { | |
| @@ -34,6 +35,7 @@ export function mcpTool(tool: ConvexTool<ZodTypeAny, ZodTypeAny>): Tool { | |
| export const convexTools: ConvexTool<any, any>[] = [ | |
| StatusTool, | |
| + ComponentsTool, | |
| DataTool, | |
| TablesTool, | |
| FunctionSpecTool, | |
| diff --git a/src/cli/lib/mcp/tools/run.ts b/src/cli/lib/mcp/tools/run.ts | |
| index 2341d6c..9ad6350 100644 | |
| --- a/src/cli/lib/mcp/tools/run.ts | |
| +++ b/src/cli/lib/mcp/tools/run.ts | |
| @@ -23,6 +23,12 @@ const inputSchema = z.object({ | |
| .describe( | |
| "The argument object to pass to the function, JSON-encoded as a string.", | |
| ), | |
| + componentPath: z | |
| + .string() | |
| + .optional() | |
| + .describe( | |
| + "Component path (e.g., 'widget') or component ID. Use the 'components' tool to list available components. Omit for root.", | |
| + ), | |
| }); | |
| const outputSchema = z.object({ | |
| @@ -72,7 +78,11 @@ export const RunTool: ConvexTool<typeof inputSchema, typeof outputSchema> = { | |
| client.setAdminAuth(credentials.adminKey); | |
| let result: Value; | |
| try { | |
| - result = await client.function(parsedFunctionName, undefined, parsedArgs); | |
| + result = await client.function( | |
| + parsedFunctionName, | |
| + args.componentPath, | |
| + parsedArgs, | |
| + ); | |
| } catch (err) { | |
| return await ctx.crash({ | |
| exitCode: 1, | |
| diff --git a/src/cli/lib/mcp/tools/tables.ts b/src/cli/lib/mcp/tools/tables.ts | |
| index c9a5977..ba28c82 100644 | |
| --- a/src/cli/lib/mcp/tools/tables.ts | |
| +++ b/src/cli/lib/mcp/tools/tables.ts | |
| @@ -4,6 +4,7 @@ import { loadSelectedDeploymentCredentials } from "../../api.js"; | |
| import { runSystemQuery } from "../../run.js"; | |
| import { deploymentFetch } from "../../utils/utils.js"; | |
| import { getDeploymentSelection } from "../../deploymentSelection.js"; | |
| +import { resolveComponent, formatComponentError } from "./componentResolver.js"; | |
| const inputSchema = z.object({ | |
| deploymentSelector: z | |
| @@ -11,6 +12,12 @@ const inputSchema = z.object({ | |
| .describe( | |
| "Deployment selector (from the status tool) to read tables from.", | |
| ), | |
| + componentPath: z | |
| + .string() | |
| + .optional() | |
| + .describe( | |
| + "Component path (e.g., 'widget') or component ID. Use the 'components' tool to list available components. Omit for root.", | |
| + ), | |
| }); | |
| const outputSchema = z.object({ | |
| @@ -26,7 +33,7 @@ const outputSchema = z.object({ | |
| export const TablesTool: ConvexTool<typeof inputSchema, typeof outputSchema> = { | |
| name: "tables", | |
| description: | |
| - "List all tables in a particular Convex deployment and their inferred and declared schema.", | |
| + "List all tables in a Convex deployment (or component) and their inferred and declared schema. Use componentPath to view a component's tables.", | |
| inputSchema, | |
| outputSchema, | |
| handler: async (ctx, args) => { | |
| @@ -40,11 +47,29 @@ export const TablesTool: ConvexTool<typeof inputSchema, typeof outputSchema> = { | |
| deploymentSelection, | |
| deployment, | |
| ); | |
| + | |
| + // Resolve componentPath to get both path and ID | |
| + const { component, allComponents } = await resolveComponent( | |
| + ctx, | |
| + credentials, | |
| + args.componentPath, | |
| + ); | |
| + | |
| + // If componentPath was provided but not found, return helpful error | |
| + if (args.componentPath && !component) { | |
| + return await ctx.crash({ | |
| + exitCode: 1, | |
| + errorType: "fatal", | |
| + printedMessage: formatComponentError(args.componentPath, allComponents), | |
| + }); | |
| + } | |
| + | |
| + // Use path for WebSocket calls (backend resolves path to context) | |
| const schemaResponse: any = await runSystemQuery(ctx, { | |
| deploymentUrl: credentials.url, | |
| adminKey: credentials.adminKey, | |
| functionName: "_system/frontend/getSchemas", | |
| - componentPath: undefined, | |
| + componentPath: component?.path, | |
| args: {}, | |
| }); | |
| const schema: Record<string, z.infer<typeof activeSchemaEntry>> = {}; | |
| @@ -54,11 +79,14 @@ export const TablesTool: ConvexTool<typeof inputSchema, typeof outputSchema> = { | |
| schema[table.tableName] = table; | |
| } | |
| } | |
| + | |
| + // Use ID for HTTP calls (shapes2 expects document ID, not path) | |
| const fetch = deploymentFetch(ctx, { | |
| deploymentUrl: credentials.url, | |
| adminKey: credentials.adminKey, | |
| }); | |
| - const response = await fetch("/api/shapes2", {}); | |
| + const componentQuery = component ? `?component=${component.id}` : ""; | |
| + const response = await fetch(`/api/shapes2${componentQuery}`, {}); | |
| const shapesResult: Record<string, any> = await response.json(); | |
| const allTablesSet = new Set([ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment