Skip to content

Instantly share code, notes, and snippets.

@lassiter
Created February 5, 2026 16:37
Show Gist options
  • Select an option

  • Save lassiter/8f67bea27d8b588506f532c6f37ba325 to your computer and use it in GitHub Desktop.

Select an option

Save lassiter/8f67bea27d8b588506f532c6f37ba325 to your computer and use it in GitHub Desktop.
Add component support to Convex MCP tools
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