From 81fcea340d309370a1c7a3d48e140a077beae271 Mon Sep 17 00:00:00 2001 From: Nick Bianchi Date: Mon, 16 Mar 2026 12:32:03 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20MCP=20tool=20parity=20=E2=80=94=206?= =?UTF-8?q?=20new=20tools=20+=20fix=20get=5Fschema=5Frefs=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add query_legal_deadlines, query_documents, get_payment_plan, query_revenue_sources, get_sync_status, trigger_sync to close coverage gaps between REST API routes and MCP tool surface. Fix get_schema_refs to list all 29 endpoint groups and 16 db tables instead of the previous incomplete 6/12 hardcoded lists. Total MCP tools: 32 → 38. All tests pass (15/15). Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 7 +- src/routes/mcp.ts | 172 +++++++++++++++++++++++++++++++++++++++++++++- tests/mcp.test.ts | 4 +- 3 files changed, 176 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4fc66e4..4a1cd56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,10 +125,10 @@ Example client-side MCP configuration (conceptual): } ``` -The server exposes 32 tools across 9 domains: +The server exposes 38 tools across 9 domains: **Core meta** — `get_canon_info`, `get_registry_status`, `get_schema_refs`, `whoami`, `get_context_summary` -**Financial** — `query_obligations`, `query_accounts`, `query_disputes`, `get_recommendations`, `get_cash_position`, `get_cashflow_projections` +**Financial** — `query_obligations`, `query_accounts`, `query_disputes`, `get_recommendations`, `get_cash_position`, `get_cashflow_projections`, `query_revenue_sources`, `get_payment_plan` **Ledger** — `ledger_stats`, `ledger_get_evidence`, `ledger_record_custody`, `ledger_facts`, `ledger_contradictions`, `ledger_create_case_for_dispute`, `ledger_link_case_for_dispute` **Connect** — `connect_discover` **ChittyChat** — `chittychat_list_projects`, `chittychat_list_tasks`, `chittychat_get_task` @@ -136,5 +136,8 @@ The server exposes 32 tools across 9 domains: **Cert** — `cert_verify` **Register** — `register_requirements` **Tasks** — `query_tasks`, `get_task`, `update_task_status`, `verify_task` +**Legal** — `query_legal_deadlines` +**Documents** — `query_documents` +**Sync** — `get_sync_status`, `trigger_sync` Tools return structured JSON using MCP `content: [{ type: "json", json: ... }]` where applicable, enabling Claude Code to consume results without text parsing. diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 5ecd360..229e9f9 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -8,7 +8,7 @@ import type { NeonQueryFunction } from '@neondatabase/serverless'; * MCP (Model Context Protocol) server for ChittyCommand. * * Implements JSON-RPC 2.0 over HTTP (Streamable HTTP transport). - * Provides 32 tools across 9 domains for Claude Code sessions. + * Provides 38 tools across 9 domains for Claude Code sessions. */ export const mcpRoutes = new Hono<{ Bindings: Env; Variables: AuthVariables }>(); @@ -187,6 +187,64 @@ const TOOLS = [ description: 'Fetch ChittyRegister compliance requirements schema.', inputSchema: { type: 'object' as const, properties: {}, required: [] as string[] }, }, + { + name: 'query_legal_deadlines', + description: 'List legal deadlines. Filter by status or dispute. Shows urgency scores and deadline types.', + inputSchema: { + type: 'object' as const, + properties: { + status: { type: 'string', description: 'Filter: upcoming, overdue, met, waived', enum: ['upcoming', 'overdue', 'met', 'waived'] }, + dispute_id: { type: 'string', description: 'Filter by linked dispute ID' }, + limit: { type: 'number', description: 'Max results (default 20)' }, + }, + required: [], + }, + }, + { + name: 'query_documents', + description: 'List stored documents. Filter by type or processing status. Shows gaps in document coverage.', + inputSchema: { + type: 'object' as const, + properties: { + doc_type: { type: 'string', description: 'Filter: bill, statement, contract, legal, receipt, correspondence, tax, insurance' }, + processing_status: { type: 'string', description: 'Filter: pending, processed, failed' }, + limit: { type: 'number', description: 'Max results (default 20)' }, + }, + required: [], + }, + }, + { + name: 'get_payment_plan', + description: 'Get current payment plan with schedule, warnings, and balance projections.', + inputSchema: { type: 'object' as const, properties: {}, required: [] as string[] }, + }, + { + name: 'query_revenue_sources', + description: 'List revenue sources with monthly amounts, confidence, and next expected dates.', + inputSchema: { + type: 'object' as const, + properties: { + status: { type: 'string', description: 'Filter: active, paused, ended', enum: ['active', 'paused', 'ended'] }, + }, + required: [], + }, + }, + { + name: 'get_sync_status', + description: 'Get sync status for all data sources — shows last sync time, records synced, and errors.', + inputSchema: { type: 'object' as const, properties: {}, required: [] as string[] }, + }, + { + name: 'trigger_sync', + description: 'Trigger a manual sync for a data source (plaid, finance, ledger, scrape).', + inputSchema: { + type: 'object' as const, + properties: { + source: { type: 'string', description: 'Source to sync: plaid, finance, ledger, scrape, mercury' }, + }, + required: ['source'], + }, + }, { name: 'query_tasks', description: 'List tasks from the backend task system. Filter by status, type, source, or priority.', @@ -350,8 +408,24 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN case 'get_schema_refs': { return { schemaVersion: '0.1.0', - endpoints: ['/api/dashboard', '/api/accounts', '/api/obligations', '/api/disputes', '/api/recommendations', '/api/cashflow'], - db_tables: ['cc_accounts','cc_obligations','cc_transactions','cc_recommendations','cc_cashflow_projections','cc_disputes','cc_dispute_correspondence','cc_legal_deadlines','cc_documents','cc_actions_log','cc_sync_log','cc_properties'], + endpoints: [ + '/api/dashboard', '/api/accounts', '/api/obligations', '/api/disputes', + '/api/recommendations', '/api/cashflow', '/api/legal', '/api/documents', + '/api/sync', '/api/queue', '/api/payment-plan', '/api/revenue', + '/api/email-connections', '/api/chat', '/api/tasks', + '/api/bridge/plaid', '/api/bridge/finance', '/api/bridge/ledger', + '/api/bridge/scrape', '/api/bridge/disputes', '/api/bridge/mercury', + '/api/bridge/books', '/api/bridge/assets', '/api/bridge/status', + '/api/v1/tokens', '/api/v1/context', '/api/v1/connect', + '/auth', '/mcp', + ], + db_tables: [ + 'cc_accounts', 'cc_obligations', 'cc_transactions', 'cc_properties', + 'cc_legal_deadlines', 'cc_disputes', 'cc_dispute_correspondence', + 'cc_documents', 'cc_recommendations', 'cc_actions_log', + 'cc_cashflow_projections', 'cc_decision_feedback', 'cc_revenue_sources', + 'cc_payment_plans', 'cc_sync_log', 'cc_tasks', + ], }; } @@ -744,6 +818,98 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN } catch (err) { return { error: String(err) }; } } + case 'query_legal_deadlines': { + const status = args.status || null; + const disputeId = args.dispute_id || null; + const limit = Math.min(Number(args.limit) || 20, 50); + const rows = await sql` + SELECT d.id, d.case_ref, d.title, d.deadline_date, d.deadline_type, d.status, d.urgency_score, + d.dispute_id, disp.title AS dispute_title + FROM cc_legal_deadlines d + LEFT JOIN cc_disputes disp ON d.dispute_id = disp.id + WHERE (${status}::text IS NULL OR d.status = ${status}) + AND (${disputeId}::text IS NULL OR d.dispute_id::text = ${disputeId}) + ORDER BY d.deadline_date ASC NULLS LAST, d.urgency_score DESC NULLS LAST + LIMIT ${limit} + `; + return { count: rows.length, deadlines: rows }; + } + + case 'query_documents': { + const docType = args.doc_type || null; + const processingStatus = args.processing_status || null; + const limit = Math.min(Number(args.limit) || 20, 50); + const rows = await sql` + SELECT id, doc_type, filename, processing_status, linked_dispute_id, created_at + FROM cc_documents + WHERE (${docType}::text IS NULL OR doc_type = ${docType}) + AND (${processingStatus}::text IS NULL OR processing_status = ${processingStatus}) + ORDER BY created_at DESC + LIMIT ${limit} + `; + return { count: rows.length, documents: rows }; + } + + case 'get_payment_plan': { + const rows = await sql` + SELECT id, plan_type, horizon_days, starting_balance, ending_balance, + lowest_balance, lowest_balance_date, total_inflows, total_outflows, + total_late_fees_avoided, total_late_fees_risked, schedule, warnings, + revenue_summary, status, created_at + FROM cc_payment_plans + WHERE status IN ('active', 'draft') + ORDER BY CASE WHEN status = 'active' THEN 0 ELSE 1 END, created_at DESC + LIMIT 1 + `; + if (rows.length === 0) return { plan: null, message: 'No active or draft payment plan. Generate one via POST /api/payment-plan/generate.' }; + const plan = rows[0] as Record; + for (const field of ['schedule', 'warnings', 'revenue_summary']) { + if (typeof plan[field] === 'string') { + try { plan[field] = JSON.parse(plan[field] as string); } catch {} + } + } + return { plan }; + } + + case 'query_revenue_sources': { + const status = args.status || null; + const rows = await sql` + SELECT r.id, r.source, r.description, r.amount, r.recurrence, + r.recurrence_day, r.next_expected_date, r.confidence, r.status, + a.account_name, a.institution + FROM cc_revenue_sources r + LEFT JOIN cc_accounts a ON r.account_id = a.id + WHERE (${status}::text IS NULL OR r.status = ${status}) + ORDER BY r.amount::numeric DESC + `; + const total = rows.reduce((s, r) => s + parseFloat((r as Record).amount as string || '0'), 0); + return { count: rows.length, total_monthly: Math.round(total * 100) / 100, sources: rows }; + } + + case 'get_sync_status': { + const rows = await sql` + SELECT source, status, records_synced, started_at, completed_at, error_message + FROM cc_sync_log + WHERE id IN ( + SELECT MAX(id) FROM cc_sync_log GROUP BY source + ) + ORDER BY started_at DESC + `; + return { sources: rows }; + } + + case 'trigger_sync': { + const source = String(args.source || '').trim(); + if (!source) throw new Error('Missing argument: source'); + const validSources = ['plaid', 'finance', 'ledger', 'scrape', 'mercury', 'books', 'assets']; + if (!validSources.includes(source)) throw new Error(`Invalid source: ${source}. Valid: ${validSources.join(', ')}`); + await sql` + INSERT INTO cc_sync_log (source, status, records_synced, started_at) + VALUES (${source}, 'triggered', 0, NOW()) + `; + return { ok: true, source, message: `Sync triggered for ${source}. Check status with get_sync_status.` }; + } + case 'query_tasks': { const status = args.status || null; const taskType = args.task_type || null; diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 834821c..afe58ae 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -141,13 +141,13 @@ describe('MCP — tools/list', () => { expect(tools.length).toBeGreaterThanOrEqual(1); }); - it('exposes at least 28 tools', async () => { + it('exposes exactly 38 tools', async () => { const { post } = buildApp(); const res = await post({ jsonrpc: '2.0', id: 1, method: 'tools/list' }); const json = await res.json() as Record; const result = json.result as Record; const tools = result.tools as unknown[]; - expect(tools.length).toBeGreaterThanOrEqual(28); + expect(tools.length).toBe(38); }); it('each tool has a name and inputSchema', async () => { From addd6aa9ba3f2e5ebf9afcae4b214d438409d688 Mon Sep 17 00:00:00 2001 From: Nick Bianchi Date: Mon, 16 Mar 2026 12:39:24 +0000 Subject: [PATCH 2/2] fix: correct schema drift in new MCP tools - query_legal_deadlines: use metadata->>'dispute_id' (no column exists) - get_payment_plan: remove non-existent revenue_summary column - trigger_sync: include required sync_type column in INSERT Addresses CodeRabbit review findings on PR #36. Co-Authored-By: Claude Opus 4.6 --- src/routes/mcp.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/routes/mcp.ts b/src/routes/mcp.ts index 229e9f9..ee3cb1c 100644 --- a/src/routes/mcp.ts +++ b/src/routes/mcp.ts @@ -824,11 +824,11 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN const limit = Math.min(Number(args.limit) || 20, 50); const rows = await sql` SELECT d.id, d.case_ref, d.title, d.deadline_date, d.deadline_type, d.status, d.urgency_score, - d.dispute_id, disp.title AS dispute_title + (d.metadata->>'dispute_id') AS dispute_id, disp.title AS dispute_title FROM cc_legal_deadlines d - LEFT JOIN cc_disputes disp ON d.dispute_id = disp.id + LEFT JOIN cc_disputes disp ON (d.metadata->>'dispute_id')::uuid = disp.id WHERE (${status}::text IS NULL OR d.status = ${status}) - AND (${disputeId}::text IS NULL OR d.dispute_id::text = ${disputeId}) + AND (${disputeId}::text IS NULL OR d.metadata->>'dispute_id' = ${disputeId}) ORDER BY d.deadline_date ASC NULLS LAST, d.urgency_score DESC NULLS LAST LIMIT ${limit} `; @@ -855,7 +855,7 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN SELECT id, plan_type, horizon_days, starting_balance, ending_balance, lowest_balance, lowest_balance_date, total_inflows, total_outflows, total_late_fees_avoided, total_late_fees_risked, schedule, warnings, - revenue_summary, status, created_at + status, created_at FROM cc_payment_plans WHERE status IN ('active', 'draft') ORDER BY CASE WHEN status = 'active' THEN 0 ELSE 1 END, created_at DESC @@ -863,7 +863,7 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN `; if (rows.length === 0) return { plan: null, message: 'No active or draft payment plan. Generate one via POST /api/payment-plan/generate.' }; const plan = rows[0] as Record; - for (const field of ['schedule', 'warnings', 'revenue_summary']) { + for (const field of ['schedule', 'warnings']) { if (typeof plan[field] === 'string') { try { plan[field] = JSON.parse(plan[field] as string); } catch {} } @@ -904,8 +904,8 @@ async function executeTool(env: Env, sql: NeonQueryFunction, toolN const validSources = ['plaid', 'finance', 'ledger', 'scrape', 'mercury', 'books', 'assets']; if (!validSources.includes(source)) throw new Error(`Invalid source: ${source}. Valid: ${validSources.join(', ')}`); await sql` - INSERT INTO cc_sync_log (source, status, records_synced, started_at) - VALUES (${source}, 'triggered', 0, NOW()) + INSERT INTO cc_sync_log (source, sync_type, status, records_synced, started_at) + VALUES (${source}, 'manual', 'triggered', 0, NOW()) `; return { ok: true, source, message: `Sync triggered for ${source}. Check status with get_sync_status.` }; }