Fix/percistent mcp connection (#392)

* feat(oauth): add support for refresh tokens in OAuth flow and update database schema

* feat(prompts): add MCP prompts and corresponding functionality for prompt retrieval

* feat(auth): enhance error handling for refresh token operations and validation
This commit is contained in:
Mattsson
2026-05-05 13:48:53 +02:00
committed by GitHub
parent fa7d4075cf
commit c03582b5c7
10 changed files with 574 additions and 22 deletions
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest'
import { prompts, findPrompt } from '../prompts'
describe('mcp prompt registry', () => {
it('exposes the five single-action prompts', () => {
expect(prompts).toHaveLength(5)
const names = prompts.map((p) => p.name).sort()
expect(names).toEqual([
'cash_today',
'last_month_result',
'uncategorized_count',
'vat_due',
'whats_overdue',
])
})
it('every prompt has description and non-trivial text', () => {
for (const p of prompts) {
expect(p.description).toBeTruthy()
expect(p.text.length).toBeGreaterThan(40)
}
})
it('prompt names are snake_case and unique', () => {
const seen = new Set<string>()
for (const p of prompts) {
expect(p.name).toMatch(/^[a-z][a-z0-9_]*$/)
expect(seen.has(p.name)).toBe(false)
seen.add(p.name)
}
})
it('findPrompt returns the matching prompt', () => {
expect(findPrompt('vat_due')?.name).toBe('vat_due')
})
it('findPrompt returns null for an unknown name', () => {
expect(findPrompt('does_not_exist')).toBeNull()
})
})
@@ -0,0 +1,55 @@
import type { McpPrompt } from './types'
/**
* Single-action prompts. Each one is a Swedish slash-shortcut that directs
* the model to call exactly one gnubok tool and report a short answer.
*/
export const prompts: McpPrompt[] = [
{
name: 'whats_overdue',
description: 'Visa förfallna kundfakturor',
text:
'Lista mina förfallna kundfakturor. Anropa gnubok_list_invoices med status="overdue" ' +
'och svara på svenska med en kort lista: kundnamn, belopp, antal dagar förfallen. ' +
'Inga rekommendationer — bara fakta.',
},
{
name: 'cash_today',
description: 'Visa banksaldo just nu',
text:
'Hur mycket pengar har jag på företagskontot just nu? Anropa gnubok_get_balance_sheet ' +
'för dagens datum och rapportera saldot på konto 1930. Visa även de senaste 5 transaktionerna ' +
'via gnubok_list_uncategorized_transactions (limit=5, sortera nyast först — men inkludera även ' +
'kategoriserade om verktyget tillåter). Svara kort på svenska.',
},
{
name: 'last_month_result',
description: 'Resultat förra månaden',
text:
'Visa resultaträkningen för föregående kalendermånad. Anropa gnubok_get_income_statement ' +
'med rätt datumintervall och svara på svenska med tre siffror: intäkter, kostnader, resultat. ' +
'Ingen analys.',
},
{
name: 'vat_due',
description: 'Moms att betala / återfå',
text:
'Vad är min momsskuld eller momsfordran för innevarande momsperiod? Anropa gnubok_get_vat_report ' +
'och rapportera enbart ruta 49 (att betala / att få tillbaka) samt deadline för deklarationen. ' +
'Ingen analys.',
},
{
name: 'uncategorized_count',
description: 'Okontrerade transaktioner',
text:
'Hur många banktransaktioner är okontrerade? Anropa gnubok_list_uncategorized_transactions ' +
'och svara på svenska med tre uppgifter: antal, datum för äldsta transaktion, totalbelopp. ' +
'Inga åtgärdsförslag.',
},
]
export function findPrompt(name: string): McpPrompt | null {
return prompts.find((p) => p.name === name) ?? null
}
export type { McpPrompt }
@@ -0,0 +1,14 @@
/**
* MCP prompt — a server-defined chat template the user picks from a slash menu
* in their MCP client. Selecting a prompt sends the message text to the model,
* which then calls the relevant gnubok tools to satisfy the request.
*
* These prompts are intentionally argument-less and single-action: each one
* maps to one read tool, returning one short answer in Swedish.
*/
export interface McpPrompt {
name: string
description: string
/** The user-role message text the client sends to the model. */
text: string
}
+33
View File
@@ -26,6 +26,7 @@ import { generateARLedger } from '@/lib/reports/ar-ledger'
import { generateMonthlyBreakdown } from '@/lib/reports/monthly-breakdown'
import { RECEIPT_MATCHER_HTML } from './widget-html'
import { dataResources, findResource, parseResourceQuery } from './resources'
import { prompts, findPrompt } from './prompts'
import { getRiskLevel } from '@/lib/pending-operations/risk-tiers'
import { shouldAutoCommit } from '@/lib/pending-operations/should-auto-commit'
import { commitPendingOperation } from '@/lib/pending-operations/commit'
@@ -3560,6 +3561,7 @@ export async function handleMcpRequest(request: Request): Promise<Response> {
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false },
prompts: { listChanged: false },
},
serverInfo: SERVER_INFO,
instructions: 'gnubok — Swedish bookkeeping via conversation. Categorize transactions, manage invoices (create, send, mark paid), view suppliers, match payments, get reports (trial balance, income statement, balance sheet, VAT, KPI, general ledger, AR/AP ledgers), and explore chart of accounts.',
@@ -3712,6 +3714,37 @@ export async function handleMcpRequest(request: Request): Promise<Response> {
)
}
case 'prompts/list':
return NextResponse.json(
jsonRpc(id ?? null, {
prompts: prompts.map((p) => ({
name: p.name,
description: p.description,
})),
})
)
case 'prompts/get': {
const promptName = (params as Record<string, unknown>)?.name as string
const prompt = findPrompt(promptName)
if (!prompt) {
return NextResponse.json(
jsonRpcError(id ?? null, -32602, `Unknown prompt: "${promptName}"`)
)
}
return NextResponse.json(
jsonRpc(id ?? null, {
description: prompt.description,
messages: [
{
role: 'user',
content: { type: 'text', text: prompt.text },
},
],
})
)
}
default:
return NextResponse.json(
jsonRpcError(id ?? null, -32601, `Method not found: "${method}"`)