From 4aab5a82e78787eec71305b58cf0c8aadc43e790 Mon Sep 17 00:00:00 2001 From: homen Date: Fri, 11 Sep 2026 01:32:12 -0700 Subject: [PATCH] fix(convex-mcp): resolve the Convex functions directory from convex.json The nightly Convex MCP Eval Gate has failed since PR #590 moved the Convex backend from convex/ to backend/convex/ and left a root convex.json pointing at it: four tools.test.ts cases reported "No convex/ directory found" because schemaTools kept a copy-pasted resolver that only probes convex/ and src/convex, and the single-file architect test hard-coded resolve(PROJECT_DIR, "convex"). Give the package one shared resolver (src/project.ts) that honors the convex.json "functions" path, falls back to convex/, src/convex, backend/convex, and returns null for an invalid config instead of a stale guess; import it from all 19 tool files that carried their own copy; make the test derive the schema path from it; and add the resolver unit test. The real-backend audit test gets the same 30s budget the neighbouring repo-scan tests already use. Carved out of draft PR #621 (which also carries ~1.3k unrelated typing changes). No workflow behaviour changed. Co-Authored-By: Claude Fable 5.1 --- .../src/__tests__/projectResolution.test.ts | 118 ++++++++++++++++++ .../src/__tests__/tools.test.ts | 7 +- packages/convex-mcp-nodebench/src/project.ts | 40 ++++++ .../src/tools/actionAuditTools.ts | 11 +- .../src/tools/architectTools.ts | 30 +---- .../src/tools/authorizationTools.ts | 11 +- .../src/tools/componentTools.ts | 11 +- .../src/tools/cronTools.ts | 11 +- .../src/tools/dataModelingTools.ts | 11 +- .../src/tools/deploymentTools.ts | 15 +-- .../src/tools/devSetupTools.ts | 34 ++--- .../src/tools/functionTools.ts | 8 +- .../src/tools/httpTools.ts | 11 +- .../src/tools/integrationBridgeTools.ts | 11 +- .../src/tools/paginationTools.ts | 11 +- .../src/tools/queryEfficiencyTools.ts | 11 +- .../src/tools/schedulerTools.ts | 11 +- .../src/tools/schemaTools.ts | 24 ++-- .../src/tools/storageAuditTools.ts | 11 +- .../src/tools/transactionSafetyTools.ts | 11 +- .../src/tools/typeSafetyTools.ts | 11 +- .../src/tools/vectorSearchTools.ts | 11 +- 22 files changed, 218 insertions(+), 212 deletions(-) create mode 100644 packages/convex-mcp-nodebench/src/__tests__/projectResolution.test.ts create mode 100644 packages/convex-mcp-nodebench/src/project.ts diff --git a/packages/convex-mcp-nodebench/src/__tests__/projectResolution.test.ts b/packages/convex-mcp-nodebench/src/__tests__/projectResolution.test.ts new file mode 100644 index 000000000..5b7d05215 --- /dev/null +++ b/packages/convex-mcp-nodebench/src/__tests__/projectResolution.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { schemaTools } from "../tools/schemaTools.js"; +import { architectTools } from "../tools/architectTools.js"; +import { devSetupTools } from "../tools/devSetupTools.js"; + +const audit = schemaTools.find(tool => tool.name === "convex_audit_schema")!; +const scan = architectTools.find(tool => tool.name === "convex_scan_capabilities")!; +const setup = devSetupTools.find(tool => tool.name === "convex_audit_dev_setup")!; +function project() { return mkdtempSync(join(homedir(), "project-")); } +function backend(root: string, relative: string, tables = 2) { + const dir = join(root, relative); + mkdirSync(join(dir, "_generated"), { recursive: true }); + writeFileSync(join(dir, "schema.ts"), `import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; +export default defineSchema({ ${Array.from({ length: tables }, (_, i) => `table${i}: defineTable({ title: v.string() })`).join(",")} });`); + writeFileSync(join(dir, "queries.ts"), 'export const list = query({ args: {}, returns: v.array(v.string()), handler: async (ctx) => [] });'); + return dir; +} +function configure(root: string, value: unknown) { + writeFileSync(join(root, "convex.json"), JSON.stringify(value)); +} +async function expectSource(root: string, source: string, tables = 2) { + const result = await audit.handler({ projectDir: root }); + expect(result).not.toHaveProperty("error"); + expect(result).toMatchObject({ summary: { schemaFile: join(source, "schema.ts"), tables } }); + const capabilities = await scan.handler({ projectDir: root }); + expect(capabilities).not.toHaveProperty("error"); + expect(capabilities).toMatchObject({ mode: "directory", convexDir: source, totalFiles: 2 }); +} +async function expectUnavailable(root: string) { + for (const tool of [audit, scan]) { + const result = await tool.handler({ projectDir: root }); + expect(result).toMatchObject({ error: expect.stringMatching(/directory|config/i) }); + expect(result).not.toHaveProperty("summary"); + } +} + +describe("backend resolution for developers and coding agents", () => { + it.each(["convex", "src/convex", "backend/convex"])("audits a developer's conventional %s layout without a config override", async relative => { + const root = project(); const source = backend(root, relative); + await expectSource(root, source); + }); + + it("uses the configured backend even when a stale conventional tree would produce a different audit", async () => { + const root = project(); + backend(root, "convex", 4); backend(root, "src/convex", 5); backend(root, "backend/convex", 6); + const source = backend(root, "services/source code", 3); + configure(root, { functions: "services/source code/" }); + await expectSource(root, source, 3); + expect(await setup.handler({ projectDir: root })).toMatchObject({ checks: expect.arrayContaining([ + expect.objectContaining({ area: "convex_json", status: "pass" }), + expect.objectContaining({ area: "initialization", status: "pass" }), + ]) }); + }); + + it("preserves conventional discovery when a valid config has no functions override", async () => { + const root = project(); const source = backend(root, "backend/convex"); + configure(root, {}); + await expectSource(root, source); + }); + + it("refuses a missing configured directory instead of reassuring the developer about a stale tree", async () => { + const root = project(); backend(root, "convex"); + configure(root, { functions: "backend/missing" }); + await expectUnavailable(root); + const result = await setup.handler({ projectDir: root }); + expect(result).toMatchObject({ checks: expect.arrayContaining([expect.objectContaining({ area: "convex_json", status: "fail" })]) }); + expect(result).not.toMatchObject({ checks: expect.arrayContaining([expect.objectContaining({ area: "initialization", status: "pass" })]) }); + }); + + it("treats a partially written config as an unavailable project, then recovers on the next call", async () => { + const root = project(); backend(root, "convex", 4); + const source = backend(root, "actual/source"); + writeFileSync(join(root, "convex.json"), '{"functions":'); + await expectUnavailable(root); + configure(root, { functions: "actual/source" }); + await expectSource(root, source); + }); + + it.each([null, [], { functions: null }, { functions: 12 }, { functions: "" }, { functions: " " }].map(value => [value]))("rejects an invalid config value %j without falling back", async config => { + const root = project(); backend(root, "convex"); configure(root, config); + await expectUnavailable(root); + }); + + it("reports a configured regular file as unavailable instead of crashing during directory enumeration", async () => { + const root = project(); backend(root, "convex"); + writeFileSync(join(root, "not-a-directory"), "reviewed text"); + configure(root, { functions: "not-a-directory" }); + await expectUnavailable(root); + }); + + it("skips a regular file at a conventional candidate and finds the actual source directory", async () => { + const root = project(); writeFileSync(join(root, "convex"), "not a directory"); + const source = backend(root, "src/convex"); + await expectSource(root, source); + }); + + it("rejects an oversized config before reading an unbounded document or scanning the wrong tree", async () => { + const root = project(); backend(root, "convex"); backend(root, "actual/source"); + configure(root, { functions: "actual/source", description: "x".repeat(70_000) }); + await expectUnavailable(root); + }); + + it("keeps twenty-four simultaneous project audits isolated across twelve successive configuration changes", async () => { + const projects = Array.from({ length: 24 }, () => { + const root = project(); backend(root, "convex", 5); + return { root, a: backend(root, "source/a", 2), b: backend(root, "source/b", 3) }; + }); + for (let round = 0; round < 12; round++) { + const side = round % 2 === 0 ? "a" : "b"; + for (const item of projects) configure(item.root, { functions: `source/${side}` }); + await Promise.all(projects.map(item => expectSource(item.root, item[side], side === "a" ? 2 : 3))); + } + }, 30_000); +}); diff --git a/packages/convex-mcp-nodebench/src/__tests__/tools.test.ts b/packages/convex-mcp-nodebench/src/__tests__/tools.test.ts index bb5394327..f358f531c 100644 --- a/packages/convex-mcp-nodebench/src/__tests__/tools.test.ts +++ b/packages/convex-mcp-nodebench/src/__tests__/tools.test.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { describe, it, expect, beforeAll } from "vitest"; import { resolve } from "node:path"; import { schemaTools } from "../tools/schemaTools.js"; @@ -76,7 +77,7 @@ describe("Function Tools", () => { expect(result.summary).toBeDefined(); expect(typeof result.summary.totalFunctions).toBe("number"); console.log(`Function audit: ${result.summary.totalFunctions} functions, ${result.summary.totalIssues} issues (${result.summary.critical} critical)`); - }); + }, 30_000); // scans the whole backend/convex tree, like the 30s/60s repo scans above it("convex_check_function_refs runs against nodebench-ai", async () => { const tool = functionTools.find((t) => t.name === "convex_check_function_refs")!; @@ -588,7 +589,9 @@ describe("Architect Tools", () => { }); it("convex_scan_capabilities scans a single file", async () => { - const schemaPath = resolve(PROJECT_DIR, "convex", "schema.ts"); + const convexDir = findConvexDir(PROJECT_DIR); + expect(convexDir).not.toBeNull(); + const schemaPath = resolve(convexDir!, "schema.ts"); const tool = architectTools.find((t) => t.name === "convex_scan_capabilities")!; const result = (await tool.handler({ filePath: schemaPath })) as any; expect(result).toBeDefined(); diff --git a/packages/convex-mcp-nodebench/src/project.ts b/packages/convex-mcp-nodebench/src/project.ts new file mode 100644 index 000000000..c450d39c7 --- /dev/null +++ b/packages/convex-mcp-nodebench/src/project.ts @@ -0,0 +1,40 @@ +import { closeSync, openSync, readSync, statSync } from "node:fs"; +import { isAbsolute, join, resolve } from "node:path"; + +const MAX_CONFIG_BYTES = 64 * 1024; + +function directory(path: string): string | null { + try { return statSync(path).isDirectory() ? path : null; } + catch { return null; } +} + +/** Resolve the project being audited; an invalid explicit config never selects a stale tree. */ +export function findConvexDir(projectDir: string): string | null { + let config: unknown; + try { + const fd = openSync(join(projectDir, "convex.json"), "r"); + try { + const buffer = Buffer.alloc(MAX_CONFIG_BYTES + 1); + const bytes = readSync(fd, buffer, 0, buffer.length, 0); + if (bytes > MAX_CONFIG_BYTES) return null; + config = JSON.parse(buffer.toString("utf8", 0, bytes)); + } finally { closeSync(fd); } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") return null; + } + + if (config !== undefined) { + if (typeof config !== "object" || config === null || Array.isArray(config)) return null; + if (Object.hasOwn(config, "functions")) { + const functions = (config as Record).functions; + if (typeof functions !== "string" || !functions.trim() || isAbsolute(functions)) return null; + return directory(resolve(projectDir, functions)); + } + } + + for (const relative of ["convex", "src/convex", "backend/convex"]) { + const found = directory(resolve(projectDir, relative)); + if (found) return found; + } + return null; +} diff --git a/packages/convex-mcp-nodebench/src/tools/actionAuditTools.ts b/packages/convex-mcp-nodebench/src/tools/actionAuditTools.ts index 44c4c1ad6..1458d2245 100644 --- a/packages/convex-mcp-nodebench/src/tools/actionAuditTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/actionAuditTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -191,7 +184,7 @@ export const actionAuditTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditActions(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/architectTools.ts b/packages/convex-mcp-nodebench/src/tools/architectTools.ts index 9ecfa3f6d..9c6913138 100644 --- a/packages/convex-mcp-nodebench/src/tools/architectTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/architectTools.ts @@ -12,6 +12,7 @@ * - convex_generate_plan: Build a plan for missing signatures */ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -129,31 +130,6 @@ function analyzePatterns(content: string, patterns: PatternCategory): Record 0) { - const configured = join(projectDir, config.functions); - if (existsSync(configured)) return configured; - } - } catch { - // fall through to the conventional candidates - } - } - const candidates = [ - join(projectDir, "convex"), - join(projectDir, "src", "convex"), - join(projectDir, "backend", "convex"), - ]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -236,7 +212,7 @@ export const architectTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const files = collectTsFiles(convexDir); @@ -358,7 +334,7 @@ export const architectTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const files = collectTsFiles(convexDir); content = files.map(f => readFileSync(f, "utf-8")).join("\n// ── file boundary ──\n"); diff --git a/packages/convex-mcp-nodebench/src/tools/authorizationTools.ts b/packages/convex-mcp-nodebench/src/tools/authorizationTools.ts index 91c25a5fd..d745686a0 100644 --- a/packages/convex-mcp-nodebench/src/tools/authorizationTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/authorizationTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -211,7 +204,7 @@ export const authorizationTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditAuthorization(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/componentTools.ts b/packages/convex-mcp-nodebench/src/tools/componentTools.ts index 3a17960f8..d5503adba 100644 --- a/packages/convex-mcp-nodebench/src/tools/componentTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/componentTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { getQuickRef } from "./toolRegistry.js"; @@ -5,14 +6,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - interface ComponentInfo { importName: string; packageName: string; @@ -103,7 +96,7 @@ export const componentTools: McpTool[] = [ handler: async (args: { projectDir: string }) => { const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); - if (!convexDir) return { error: "No convex/ directory found" }; + if (!convexDir) return { error: "No configured Convex functions directory found" }; const configPath = join(convexDir, "convex.config.ts"); if (!existsSync(configPath)) { diff --git a/packages/convex-mcp-nodebench/src/tools/cronTools.ts b/packages/convex-mcp-nodebench/src/tools/cronTools.ts index d5b127c5d..e62b96aa6 100644 --- a/packages/convex-mcp-nodebench/src/tools/cronTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/cronTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { getQuickRef } from "./toolRegistry.js"; @@ -5,14 +6,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - interface CronEntry { name: string; type: "interval" | "daily" | "weekly" | "monthly" | "cron"; @@ -122,7 +115,7 @@ export const cronTools: McpTool[] = [ handler: async (args: { projectDir: string }) => { const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); - if (!convexDir) return { error: "No convex/ directory found" }; + if (!convexDir) return { error: "No configured Convex functions directory found" }; const cronsPath = join(convexDir, "crons.ts"); if (!existsSync(cronsPath)) { diff --git a/packages/convex-mcp-nodebench/src/tools/dataModelingTools.ts b/packages/convex-mcp-nodebench/src/tools/dataModelingTools.ts index fb6441a12..ca35d5b95 100644 --- a/packages/convex-mcp-nodebench/src/tools/dataModelingTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/dataModelingTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - // ── Data Modeling Audit ───────────────────────────────────────────── interface ModelingIssue { @@ -229,7 +222,7 @@ export const dataModelingTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditDataModeling(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/deploymentTools.ts b/packages/convex-mcp-nodebench/src/tools/deploymentTools.ts index ad8dad776..a1c416250 100644 --- a/packages/convex-mcp-nodebench/src/tools/deploymentTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/deploymentTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool, DeployGateResult } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - // ── Pre-Deploy Gate ───────────────────────────────────────────────── function runPreDeployChecks(projectDir: string): DeployGateResult { @@ -23,13 +16,13 @@ function runPreDeployChecks(projectDir: string): DeployGateResult { // Check 1: convex/ directory exists const convexDir = findConvexDir(projectDir); if (!convexDir) { - checks.push({ name: "convex_dir_exists", passed: false, message: "No convex/ directory found" }); - blockers.push("No convex/ directory found"); + checks.push({ name: "convex_dir_exists", passed: false, message: "No configured Convex functions directory found" }); + blockers.push("No configured Convex functions directory found"); return { passed: false, checks, blockers: blockers.map((b, i) => ({ priority: i + 1, blocker: b, fixFirst: i === 0 })), - fixOrder: "Fix #1: Create a convex/ directory. Then re-run convex_pre_deploy_gate.", + fixOrder: "Fix #1: Check convex.json functions and create that directory. Then re-run convex_pre_deploy_gate.", }; } checks.push({ name: "convex_dir_exists", passed: true, message: `Found at ${convexDir}` }); diff --git a/packages/convex-mcp-nodebench/src/tools/devSetupTools.ts b/packages/convex-mcp-nodebench/src/tools/devSetupTools.ts index 601dd3843..fa6ab08f5 100644 --- a/packages/convex-mcp-nodebench/src/tools/devSetupTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/devSetupTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -64,24 +65,14 @@ function auditDevSetup(projectDir: string): { } } - // Check 3: convex.json exists and points to valid deployment - const convexJsonPath = join(projectDir, "convex.json"); - if (existsSync(convexJsonPath)) { - try { - const convexJson = JSON.parse(readFileSync(convexJsonPath, "utf-8")); - if (convexJson.project) { - checks.push({ area: "convex_json", status: "pass", detail: `convex.json configured for project: ${convexJson.project}` }); - } else { - checks.push({ area: "convex_json", status: "warn", detail: "convex.json exists but no project configured" }); - } - } catch { - checks.push({ area: "convex_json", status: "fail", detail: "convex.json exists but is invalid JSON" }); - issues.push({ - severity: "critical", - area: "convex_json", - message: "convex.json is invalid JSON. Convex CLI won't work.", - fix: "Fix the JSON syntax in convex.json or delete and run `npx convex dev` to regenerate", - }); + // Check 3: configuration resolves to the backend actually being audited. + const convexDir = findConvexDir(projectDir); + if (existsSync(join(projectDir, "convex.json"))) { + if (convexDir) { + checks.push({ area: "convex_json", status: "pass", detail: "Convex functions directory resolved from project configuration" }); + } else { + checks.push({ area: "convex_json", status: "fail", detail: "Convex configuration does not resolve to a functions directory" }); + issues.push({ severity: "critical", area: "convex_json", message: "Convex configuration or functions directory is unavailable.", fix: "Check convex.json and its functions path; do not audit a stale directory." }); } } @@ -117,11 +108,10 @@ function auditDevSetup(projectDir: string): { } // Check 6: _generated/ directory exists (project initialized) - const convexDir = join(projectDir, "convex"); - const generatedDir = join(convexDir, "_generated"); - if (existsSync(generatedDir)) { + const generatedDir = convexDir ? join(convexDir, "_generated") : null; + if (generatedDir && existsSync(generatedDir)) { checks.push({ area: "initialization", status: "pass", detail: "_generated/ exists — project is initialized" }); - } else if (existsSync(convexDir)) { + } else if (convexDir) { checks.push({ area: "initialization", status: "warn", detail: "_generated/ not found — run `npx convex dev` to initialize" }); issues.push({ severity: "warning", diff --git a/packages/convex-mcp-nodebench/src/tools/functionTools.ts b/packages/convex-mcp-nodebench/src/tools/functionTools.ts index 45810ce8d..0d1497f7f 100644 --- a/packages/convex-mcp-nodebench/src/tools/functionTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/functionTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -21,13 +22,6 @@ function collectTsFiles(dir: string): string[] { return results; } -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} // ── Function Analysis Engine ──────────────────────────────────────── diff --git a/packages/convex-mcp-nodebench/src/tools/httpTools.ts b/packages/convex-mcp-nodebench/src/tools/httpTools.ts index 0f571d3a1..bc293adf1 100644 --- a/packages/convex-mcp-nodebench/src/tools/httpTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/httpTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve, dirname } from "node:path"; import { getQuickRef } from "./toolRegistry.js"; @@ -5,14 +6,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - // ── HTTP Endpoint Analysis ────────────────────────────────────────── interface HttpRoute { @@ -249,7 +242,7 @@ export const httpTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const result = analyzeHttpEndpoints(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/integrationBridgeTools.ts b/packages/convex-mcp-nodebench/src/tools/integrationBridgeTools.ts index f50c8cd0b..4f33b8d26 100644 --- a/packages/convex-mcp-nodebench/src/tools/integrationBridgeTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/integrationBridgeTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -300,7 +293,7 @@ export const integrationBridgeTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const checks: Array<{ area: string; status: "good" | "warning" | "critical"; detail: string }> = []; diff --git a/packages/convex-mcp-nodebench/src/tools/paginationTools.ts b/packages/convex-mcp-nodebench/src/tools/paginationTools.ts index 0b551e8bc..601f7e0fc 100644 --- a/packages/convex-mcp-nodebench/src/tools/paginationTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/paginationTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -139,7 +132,7 @@ export const paginationTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditPagination(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/queryEfficiencyTools.ts b/packages/convex-mcp-nodebench/src/tools/queryEfficiencyTools.ts index 7a9d55ee5..6c5db8711 100644 --- a/packages/convex-mcp-nodebench/src/tools/queryEfficiencyTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/queryEfficiencyTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -193,7 +186,7 @@ export const queryEfficiencyTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditQueryEfficiency(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/schedulerTools.ts b/packages/convex-mcp-nodebench/src/tools/schedulerTools.ts index 44e4ab2a6..2993cafc7 100644 --- a/packages/convex-mcp-nodebench/src/tools/schedulerTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/schedulerTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -207,7 +200,7 @@ export const schedulerTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditSchedulers(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/schemaTools.ts b/packages/convex-mcp-nodebench/src/tools/schemaTools.ts index 12b16afe9..2f7092de4 100644 --- a/packages/convex-mcp-nodebench/src/tools/schemaTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/schemaTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,17 +7,6 @@ import type { McpTool, SchemaIssue } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [ - join(projectDir, "convex"), - join(projectDir, "src", "convex"), - ]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function readSchemaFile(convexDir: string): string | null { const schemaPath = join(convexDir, "schema.ts"); if (!existsSync(schemaPath)) return null; @@ -392,16 +382,16 @@ export const schemaTools: McpTool[] = [ const convexDir = findConvexDir(projectDir); if (!convexDir) { return { - error: "No convex/ directory found", - hint: "Ensure projectDir points to a directory containing a convex/ folder", + error: "No configured Convex functions directory found", + hint: "Check projectDir and the functions path in convex.json", }; } const schemaContent = readSchemaFile(convexDir); if (!schemaContent) { return { - error: "No convex/schema.ts found", - hint: "Create a schema.ts file in your convex/ directory", + error: "No schema.ts found in the resolved functions directory", + hint: "Create schema.ts in the configured functions directory", quickRef: getQuickRef("convex_audit_schema"), }; } @@ -465,7 +455,7 @@ export const schemaTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const suggestions = suggestIndexes(convexDir); @@ -525,7 +515,7 @@ export const schemaTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const result = analyzeValidatorCoverage(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/storageAuditTools.ts b/packages/convex-mcp-nodebench/src/tools/storageAuditTools.ts index 6289e8db6..10499f011 100644 --- a/packages/convex-mcp-nodebench/src/tools/storageAuditTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/storageAuditTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -158,7 +151,7 @@ export const storageAuditTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditStorageUsage(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/transactionSafetyTools.ts b/packages/convex-mcp-nodebench/src/tools/transactionSafetyTools.ts index 63899aa10..f0c0c7dec 100644 --- a/packages/convex-mcp-nodebench/src/tools/transactionSafetyTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/transactionSafetyTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -168,7 +161,7 @@ export const transactionSafetyTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditTransactionSafety(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/typeSafetyTools.ts b/packages/convex-mcp-nodebench/src/tools/typeSafetyTools.ts index 65c71a80f..42536a072 100644 --- a/packages/convex-mcp-nodebench/src/tools/typeSafetyTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/typeSafetyTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -155,7 +148,7 @@ export const typeSafetyTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditTypeSafety(convexDir); diff --git a/packages/convex-mcp-nodebench/src/tools/vectorSearchTools.ts b/packages/convex-mcp-nodebench/src/tools/vectorSearchTools.ts index 46fad9bae..7f33735a2 100644 --- a/packages/convex-mcp-nodebench/src/tools/vectorSearchTools.ts +++ b/packages/convex-mcp-nodebench/src/tools/vectorSearchTools.ts @@ -1,3 +1,4 @@ +import { findConvexDir } from "../project.js"; import { readFileSync, existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { getDb, genId } from "../db.js"; @@ -6,14 +7,6 @@ import type { McpTool } from "../types.js"; // ── Helpers ────────────────────────────────────────────────────────── -function findConvexDir(projectDir: string): string | null { - const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex"), join(projectDir, "backend", "convex")]; - for (const c of candidates) { - if (existsSync(c)) return c; - } - return null; -} - function collectTsFiles(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; @@ -217,7 +210,7 @@ export const vectorSearchTools: McpTool[] = [ const projectDir = resolve(args.projectDir); const convexDir = findConvexDir(projectDir); if (!convexDir) { - return { error: "No convex/ directory found" }; + return { error: "No configured Convex functions directory found" }; } const { issues, stats } = auditVectorSearch(convexDir);