feat(mcp): connect_migration: one-click card into the previous-system wizard (#1960)

* feat(mcp): gnubok_connect_migration: one-click connect card into the previous-system wizard

'Jag hade Fortnox' now gets the same feel as Skatteverket: the tool
returns the migration-wizard link for the named provider and renders
the connect-card widget (new migration branch: 'Hämta från Fortnox',
button opens the wizard that logs into the old system and fetches all
fiscal years plus invoices, customers, suppliers and documents). For
visma/bokio (no API export) the instructions order the SIE drop card
first and this card as the complement. Scope companies:read; skill
step 3 points at the tool instead of raw wizard links; ceiling 62.4K
to 63K documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): 5-minute freshness hint on tools/list, widgets and prompts: the catalog changes with every deploy

The stateless-client CacheableResult hint on tools/list, resources/read
(widget HTML) and prompts/list was 1 hour. Claude.ai honors it, so for
up to an hour after a deploy the connector served a pre-deploy catalog:
a freshly shipped tool flapped in and out of the tool list depending on
which fetch hit the client cache, and two E2E runs dead-ended on
'tool does not exist' for a tool that was live server-side. These
payloads are static only within one deploy; 5 minutes bounds the stale
window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-26 19:19:24 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 5715dd3def
commit d8244ecaff
7 changed files with 159 additions and 15 deletions
+73 -1
View File
@@ -3557,6 +3557,72 @@ export const tools: McpTool[] = [
},
},
{
name: 'gnubok_connect_migration',
title: 'Connect Previous System',
description:
'Connect card into the migration wizard for a NAMED previous system. API systems (fortnox/bjornlunden/briox/wint) fetch all fiscal years plus invoices, customers and documents; visma/bokio complement AFTER a SIE import. Same one-click feel as the bank/Skatteverket cards.',
inputSchema: {
type: 'object',
additionalProperties: false,
properties: {
provider: {
type: 'string',
enum: ['fortnox', 'bjornlunden', 'briox', 'wint', 'visma', 'bokio'],
description: 'The previous system the user named',
},
},
required: ['provider'],
},
outputSchema: {
type: 'object',
additionalProperties: false,
properties: {
provider: { type: 'string' },
provider_name: { type: 'string' },
api_connected: { type: 'boolean', description: 'true = the wizard fetches SIE directly from the system; false = a SIE file import comes first' },
connect_url: { type: 'string' },
instructions: { type: 'string' },
},
required: ['provider', 'provider_name', 'api_connected', 'connect_url', 'instructions'],
},
_meta: { ui: { resourceUri: 'ui://connect-card/app.html' } },
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
async execute(args) {
const PROVIDERS: Record<string, { name: string; api: boolean }> = {
fortnox: { name: 'Fortnox', api: true },
bjornlunden: { name: 'Björn Lundén', api: true },
briox: { name: 'Briox', api: true },
wint: { name: 'Wint', api: true },
visma: { name: 'Visma eEkonomi', api: false },
bokio: { name: 'Bokio', api: false },
}
const provider = String(args.provider ?? '')
const info = PROVIDERS[provider]
if (!info) {
throw Object.assign(
new Error(`Unknown provider "${provider}". Supported: ${Object.keys(PROVIDERS).join(', ')}.`),
{ code: 'VALIDATION_ERROR' }
)
}
const connectUrl = `${connectLinkBaseUrl()}/import?mode=migration&provider=${provider}`
return {
provider,
provider_name: info.name,
api_connected: info.api,
connect_url: connectUrl,
instructions: info.api
? `On claude.ai/Claude Desktop a connect card with an open-in-browser button renders with this result; elsewhere give the user the connect_url. The wizard connects to ${info.name} (login there), fetches every fiscal year and imports bookkeeping PLUS invoices, customers, suppliers and documents. The user comes back here when the wizard reports done.`
: `${info.name} has no API export: run the SIE-file import FIRST (gnubok_create_sie_upload drop card). This wizard link then complements with invoices and customers. On claude.ai/Desktop a connect card renders; elsewhere give the user the connect_url.`,
}
},
},
{
name: 'gnubok_get_company_settings',
title: 'Get Company Settings',
@@ -18917,7 +18983,13 @@ const JSONRPC_UNSUPPORTED_PROTOCOL_VERSION = -32022
// change only on deploy; skills live in the DB and can change between
// deploys; data resources are live ledger state and must never be cached.
// Everything is served behind Authorization, so cacheScope stays private.
const CACHE_STATIC = { ttlMs: 3_600_000, cacheScope: 'private' } as const
// "Static" content (tools/list, widget HTML, prompts) is static only within
// one deploy: every deploy can add tools and change widgets. The 1-hour hint
// this used to carry made Claude.ai serve a pre-deploy catalog for up to an
// hour after a release: a freshly shipped tool flapped in and out of the
// connector's tool list depending on which fetch hit the client cache
// (E2E #7/#8, 2026-08-26). 5 minutes bounds that window to one coffee sip.
const CACHE_STATIC = { ttlMs: 300_000, cacheScope: 'private' } as const
const CACHE_SKILLS = { ttlMs: 300_000, cacheScope: 'private' } as const
const CACHE_LIVE = { ttlMs: 0, cacheScope: 'private' } as const