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:
@@ -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
|
||||
}
|
||||
@@ -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}"`)
|
||||
|
||||
Reference in New Issue
Block a user