From 5f79a74a2e7a22337f695fa9d8773e533e2047dc Mon Sep 17 00:00:00 2001 From: Anton <122736546+antonisoaho@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:41:43 +0200 Subject: [PATCH] fix(mcp): allow null optional fields on transaction list tools (#484) * fix(mcp): allow null optional fields on transaction list tools Signed-off-by: antonisoaho * test(mcp): assert null optional fields on list_transactions_without_documents * Update extensions/general/mcp-server/__tests__/list-uncategorized-transactions.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Signed-off-by: antonisoaho Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- ...ist-transactions-without-documents.test.ts | 39 +++++++++++++ .../list-uncategorized-transactions.test.ts | 55 +++++++++++++++++++ extensions/general/mcp-server/server.ts | 16 +++--- 3 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 extensions/general/mcp-server/__tests__/list-uncategorized-transactions.test.ts diff --git a/extensions/general/mcp-server/__tests__/list-transactions-without-documents.test.ts b/extensions/general/mcp-server/__tests__/list-transactions-without-documents.test.ts index 7af2b417..b0d96475 100644 --- a/extensions/general/mcp-server/__tests__/list-transactions-without-documents.test.ts +++ b/extensions/general/mcp-server/__tests__/list-transactions-without-documents.test.ts @@ -55,6 +55,45 @@ describe('gnubok_list_transactions_without_documents', () => { expect(result.transactions[0].journal_entry_id).toBe('je-1') }) + it('returns rows when DB has null merchant_name, reference, is_business, category (MCP structured output)', async () => { + const rows = [ + { + id: 't-no-doc-1', + date: '2026-03-17', + description: 'Nolla skuld', + amount: -2745, + currency: 'SEK', + merchant_name: null, + reference: null, + is_business: null, + category: null, + journal_entry_id: 'je-nolla-1', + }, + ] + const { supabase, enqueue } = createQueuedMockSupabase() + enqueue({ data: null, error: null, count: 1 }) + enqueue({ data: rows, error: null }) + + const result = (await tool.execute( + { limit: 20 }, + 'company-1', + 'user-1', + supabase as never + )) as { + transactions: typeof rows + count: number + total_count: number + has_more: boolean + } + + expect(result.count).toBe(1) + expect(result.transactions[0].journal_entry_id).toBe('je-nolla-1') + expect(result.transactions[0].merchant_name).toBeNull() + expect(result.transactions[0].reference).toBeNull() + expect(result.transactions[0].is_business).toBeNull() + expect(result.transactions[0].category).toBeNull() + }) + it('returns empty result when nothing matches', async () => { const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: null, error: null, count: 0 }) diff --git a/extensions/general/mcp-server/__tests__/list-uncategorized-transactions.test.ts b/extensions/general/mcp-server/__tests__/list-uncategorized-transactions.test.ts new file mode 100644 index 00000000..3850a53a --- /dev/null +++ b/extensions/general/mcp-server/__tests__/list-uncategorized-transactions.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createQueuedMockSupabase } from '@/tests/helpers' +import { tools } from '../server' + +const tool = tools.find((t) => t.name === 'gnubok_list_uncategorized_transactions')! + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('gnubok_list_uncategorized_transactions', () => { + it('is registered as a read-only paginated tool', () => { + expect(tool).toBeDefined() + expect(tool.annotations?.readOnlyHint).toBe(true) + const schema = tool.outputSchema as Record + expect((schema.properties as Record).transactions).toBeDefined() + expect((schema.properties as Record).total_count).toBeDefined() + }) + + it('returns rows when DB has null merchant_name, reference, is_business (MCP structured output)', async () => { + const rows = [ + { + id: 't-uncat-1', + date: '2026-03-09', + description: 'Transfer', + amount: -6000, + currency: 'SEK', + merchant_name: null, + reference: null, + is_business: null, + category: null, + }, + ] + const { supabase, enqueue } = createQueuedMockSupabase() + enqueue({ data: null, error: null, count: 1 }) + enqueue({ data: rows, error: null }) + + const result = (await tool.execute( + { limit: 20 }, + 'company-1', + 'user-1', + supabase as never + )) as { + transactions: typeof rows + count: number + total_count: number + has_more: boolean + } + + expect(result.count).toBe(1) + expect(result.transactions[0].merchant_name).toBeNull() + expect(result.transactions[0].reference).toBeNull() + expect(result.transactions[0].is_business).toBeNull() + }) +}) diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index fb386af9..9ca97a82 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -2231,10 +2231,10 @@ export const tools: McpTool[] = [ description: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, - merchant_name: { type: 'string' }, - reference: { type: 'string' }, - is_business: { type: 'boolean' }, - category: { type: 'string' }, + merchant_name: { type: ['string', 'null'] }, + reference: { type: ['string', 'null'] }, + is_business: { type: ['boolean', 'null'] }, + category: { type: ['string', 'null'] }, }, }), annotations: { @@ -2303,10 +2303,10 @@ export const tools: McpTool[] = [ description: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, - merchant_name: { type: 'string' }, - reference: { type: 'string' }, - is_business: { type: 'boolean' }, - category: { type: 'string' }, + merchant_name: { type: ['string', 'null'] }, + reference: { type: ['string', 'null'] }, + is_business: { type: ['boolean', 'null'] }, + category: { type: ['string', 'null'] }, journal_entry_id: { type: 'string' }, }, }),