@kirjapro/mcp-server 0.5.10 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +32 -3
  2. package/README.md +2 -2
  3. package/dist/accounts.d.ts +40 -0
  4. package/dist/accounts.js +45 -0
  5. package/dist/auth.d.ts +17 -0
  6. package/dist/auth.js +245 -0
  7. package/dist/compliance/rates.d.ts +53 -0
  8. package/dist/compliance/rates.js +57 -0
  9. package/dist/database.types.d.ts +6525 -0
  10. package/dist/database.types.js +20 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +58 -52
  13. package/dist/lib/incomeStatementSynthesis.d.ts +47 -0
  14. package/dist/lib/incomeStatementSynthesis.js +140 -0
  15. package/dist/resources/company.d.ts +2 -0
  16. package/dist/resources/company.js +75 -0
  17. package/dist/resources/compliance.d.ts +2 -0
  18. package/dist/resources/compliance.js +338 -0
  19. package/dist/resources/index.d.ts +2 -0
  20. package/dist/resources/index.js +10 -0
  21. package/dist/resources/schema.d.ts +2 -0
  22. package/dist/resources/schema.js +63 -0
  23. package/dist/resources/security.d.ts +2 -0
  24. package/dist/resources/security.js +33 -0
  25. package/dist/schemas.d.ts +12 -0
  26. package/dist/schemas.js +12 -0
  27. package/dist/tools/accounts.d.ts +2 -0
  28. package/dist/tools/accounts.js +48 -0
  29. package/dist/tools/banking.d.ts +2 -0
  30. package/dist/tools/banking.js +196 -0
  31. package/dist/tools/bookings.d.ts +2 -0
  32. package/dist/tools/bookings.js +294 -0
  33. package/dist/tools/customers.d.ts +2 -0
  34. package/dist/tools/customers.js +320 -0
  35. package/dist/tools/financialNotes.d.ts +2 -0
  36. package/dist/tools/financialNotes.js +255 -0
  37. package/dist/tools/fingerprints.d.ts +6 -0
  38. package/dist/tools/fingerprints.js +28 -0
  39. package/dist/tools/fiscal-years.d.ts +2 -0
  40. package/dist/tools/fiscal-years.js +35 -0
  41. package/dist/tools/index.d.ts +9 -0
  42. package/dist/tools/index.js +105 -0
  43. package/dist/tools/invoices.d.ts +2 -0
  44. package/dist/tools/invoices.js +489 -0
  45. package/dist/tools/offers.d.ts +2 -0
  46. package/dist/tools/offers.js +443 -0
  47. package/dist/tools/received-invoices.d.ts +2 -0
  48. package/dist/tools/received-invoices.js +134 -0
  49. package/dist/tools/recurring-invoices.d.ts +2 -0
  50. package/dist/tools/recurring-invoices.js +348 -0
  51. package/dist/tools/reports.d.ts +2 -0
  52. package/dist/tools/reports.js +869 -0
  53. package/dist/tools/templates.d.ts +89 -0
  54. package/dist/tools/templates.js +636 -0
  55. package/dist/tools/transactions.d.ts +2 -0
  56. package/dist/tools/transactions.js +597 -0
  57. package/dist/tools/validation.d.ts +26 -0
  58. package/dist/tools/validation.js +127 -0
  59. package/dist/types.d.ts +388 -0
  60. package/dist/types.js +34 -0
  61. package/dist/utils.d.ts +276 -0
  62. package/dist/utils.js +637 -0
  63. package/dist/version.d.ts +1 -0
  64. package/dist/version.js +3 -0
  65. package/package.json +3 -2
  66. package/server.json +5 -1
  67. package/tool-fingerprints.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,17 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0] - 2026-05-26
4
+
5
+ Syncs the published package with the 2026-05-22 offers/server-side migrations and fixes two latent bugs surfaced by an audit. Minor-bumped (not patch) because `kirjapro_update_offer_status` narrows its accepted input.
6
+
7
+ ### Changed
8
+
9
+ - **`kirjapro_update_offer_status` (breaking input change)** — now delegates to the `record_offer_status_change` SECURITY DEFINER RPC (atomic status update + audit-event insert, accountant+ role gate, concurrency `not_claimable` guard). The accepted `status` enum is narrowed to `draft` / `accepted` / `rejected` / `expired`; `draft→sent` is no longer here — use the send-offer email path (the recipient gets the public link). **Why it matters:** migration `20260522200300_drop_offer_events_user_insert` dropped the user-side `offer_events` insert policy, so the previous direct `UPDATE + INSERT` path silently failed RLS on the audit row — flipping status with no audit trail. This release ships the fix that was committed but unreleased.
10
+ - **`tool-fingerprints.json`** — regenerated for the `update_offer_status` definition change (rug-pull guard baseline).
11
+ - **`reports.ts`** — income-statement template-line fetch now selects explicit columns instead of `select('*')` (clears the anti-pattern guard; the reference table is never returned to the agent, but the narrow select avoids exposing future schema additions).
12
+
13
+ ### Fixed
14
+
15
+ - **Onboarding prompt (`kirjapro-aloita`) showed every paying customer "Ilmainen".** It resolved the subscription tier with `.eq('company_id', …)`, but `subscriptions.company_id` is nullable and never populated by billing (all subscription writes are keyed by `user_id`). Now resolves via the `subscriptions_select` RLS policy (`user_id = auth.uid()`) with no company filter. Tier gating itself was always correct (enforced server-side in `mcp-auth` by `user_id`); only the cosmetic onboarding label was wrong.
16
+ - **API-key auth exchange had no network timeout.** A hung `mcp-auth` request could hang server startup forever, or permanently stall token refresh (the recursive refresh `tick()` awaited a promise that never rejected, so the backoff never re-armed and the JWT silently expired). The entire exchange — connect, headers, **and the response-body read** — is now bounded with `AbortSignal.timeout(10_000)` (the abort signal aborts the undici body stream too), and transport failures surface an actionable message instead of a raw `DOMException`. Covered by three new `auth.test.ts` cases (timeout, network failure, body-read timeout).
17
+
18
+ ### Added
19
+
20
+ - **`prepublishOnly` release gate** — `npm publish` now runs `check:types-drift → lint → test → check:antipatterns → build` first, so a stale fingerprint, a type error, a failing test, or a `select('*')` regression blocks the publish locally. (This repo runs no GitHub Actions CI by design — the gate lives at the real release moment instead.)
21
+ - **`server.json`** — documents the optional `KIRJAPRO_MCP_SURFACE` env var (`core` default = 26 tools, `full` = 52).
22
+
23
+ ## [0.5.11] - 2026-05-18
24
+
25
+ Documentation-only release: removes named third-party brand references from the public-facing `README.md` and CHANGELOG entries, replacing them with category-level descriptions. No code or behavior changes — `dist/index.js` is byte-identical to v0.5.10 modulo the version string written by the prebuild hook.
26
+
27
+ ### Changed
28
+
29
+ - **`README.md`** — payroll paragraph rewritten to refer to "a dedicated payroll service" with example service names retained only as the redirect destination users actually need.
30
+ - **CHANGELOG entries** below — competitive framing dropped from v0.5.9 / v0.5.10 entries.
31
+
3
32
  ## [0.5.10] - 2026-05-18
4
33
 
5
34
  Documentation-only release. Surfaces the v0.5.9 payroll removal (DT-065) in the npm-visible `README.md` and adds the full CHANGELOG entry for v0.5.9. No code or behavior changes — `dist/index.js` is byte-identical to v0.5.9 modulo the version string written by the prebuild hook.
6
35
 
7
36
  ### Changed
8
37
 
9
- - **`README.md`** — added the "Payroll is intentionally not exposed" paragraph explaining that Kirjapro does not calculate or book salaries, with links to Palkkaus.fi / Palkka.fi as the recommended flow (matches sister-tier competitors: Procountor Solo, NoCFO, Holvi, Talenom Solo). Tool count corrected from 53 → 52 across the three places it appeared.
38
+ - **`README.md`** — added the "Payroll is intentionally not exposed" paragraph explaining that Kirjapro does not calculate or book salaries, with payroll-service redirect references. Tool count corrected from 53 → 52 across the three places it appeared.
10
39
  - **`CHANGELOG.md`** — the v0.5.9 entry detailing the rollback was added to the package's changelog so users browsing the npm page can see the rationale.
11
40
 
12
41
  ## [0.5.9] - 2026-05-18
13
42
 
14
- Removed autonomously-shipped payroll feature (DT-065). The 2026-05-18 audit discovered that `kirjapro_book_salary`, the `salary_payment` template, and supporting parameters had been added across March–May 2026 without explicit user approval — DT-047 (April 2026) had marked palkanlaskenta as `gated_on: klt-advisor-partnership`, but the gate never propagated. The 2026-05-18 Finnish-market scan confirmed every Kirjapro-tier vendor (Procountor Solo, NoCFO, Holvi, Talenom Solo) uses the **Palkkaus.fi/Salaxy partnership pattern** rather than building native payroll — that is the design target when this feature does eventually unblock.
43
+ Removed an autonomously-shipped payroll feature (DT-065). The 2026-05-18 audit discovered that `kirjapro_book_salary`, the `salary_payment` template, and supporting parameters had been added across March–May 2026 without explicit user approval — DT-047 (April 2026) had marked palkanlaskenta as `gated_on: klt-advisor-partnership`, but the gate never propagated. The design target when this feature does eventually unblock is integration with a dedicated payroll-service partner that holds the regulated responsibility for Tulorekisteri / TyEL / TVR reporting and ennakonpidätys submission, rather than a native calculation engine.
15
44
 
16
45
  ### Removed
17
46
 
@@ -22,7 +51,7 @@ Removed autonomously-shipped payroll feature (DT-065). The 2026-05-18 audit disc
22
51
  ### Changed
23
52
 
24
53
  - **`KIRJAPRO_MCP_SURFACE` documentation** — `full` mode now exposes 52 tools instead of 53. Error message for unknown surface values updated.
25
- - **Instructions text** — `full` and `core` mode INSTRUCTIONS both clarify "Payroll (palkanlaskenta) is intentionally NOT exposed" and direct AI assistants to Palkkaus.fi/Palkka.fi for the calculation, then book the resulting payslip via `kirjapro_create_transaction` with explicit debit/credit lines.
54
+ - **Instructions text** — `full` and `core` mode INSTRUCTIONS both clarify "Payroll (palkanlaskenta) is intentionally NOT exposed" and direct AI assistants to a dedicated payroll service for the calculation, then book the resulting payslip via `kirjapro_create_transaction` with explicit debit/credit lines.
26
55
  - **`tool-fingerprints.json`** — regenerated. 3 changed (`kirjapro_create_from_template`, `kirjapro_create_from_template_batch` lost params; `salary_payment` template removed). 1 removed (`kirjapro_book_salary`).
27
56
  - **`server.json`** — caught up to package.json version (was stuck at 0.5.7).
28
57
 
package/README.md CHANGED
@@ -7,7 +7,7 @@ MCP server for Finnish accounting. Exposes Kirjapro bookkeeping, invoicing, and
7
7
  ## Requirements
8
8
 
9
9
  - Node.js 20+
10
- - Kirjapro account with **Entrepreneur**, **Company**, or **Custom** subscription tier
10
+ - Kirjapro account on a paid tier with API access — **Yrittäjä**, **Kasvu**, **Tilitoimisto**, or **Räätälöity**
11
11
  - API key (generate at Settings > API-avaimet)
12
12
 
13
13
  ## Quick Start
@@ -113,7 +113,7 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
113
113
 
114
114
  **Why default to 26 instead of 52:** 2025-2026 SOTA research on tool catalog scaling (Anthropic, OpenAI, MCP authors) shows tool-selection accuracy degrades between 25-50 tools per catalog. A focused surface produces better first-call selections for the daily workflows. As real usage data informs which deferred tools matter most, they'll be promoted into core.
115
115
 
116
- **Payroll is intentionally not exposed.** Kirjapro does not calculate or book salaries. Direct users to [Palkkaus.fi](https://www.palkkaus.fi) or [Palkka.fi](https://www.palkka.fi) for the calculation (these hold legal responsibility for Tulorekisteri-ilmoitus, TyEL/TVR raportointi, ennakonpidätys-tilitys), then book the resulting payslip manually via `kirjapro_create_transaction`. Sister-tier competitors (Procountor Solo, NoCFO, Holvi, Talenom Solo) all follow the same partnership pattern.
116
+ **Payroll is intentionally not exposed.** Kirjapro does not calculate or book salaries. Direct users to a dedicated payroll service (such as [Palkkaus.fi](https://www.palkkaus.fi) or [Palkka.fi](https://www.palkka.fi)) for the calculation — these services hold the regulated responsibility for Tulorekisteri-ilmoitus, TyEL/TVR raportointi, and ennakonpidätys-tilitys. Once the payslip is calculated externally, book it manually via `kirjapro_create_transaction`.
117
117
 
118
118
  | Domain (core) | Tools |
119
119
  |---|---|
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Standard Finnish Chart of Accounts (YSE 2017) — commonly used account constants.
3
+ *
4
+ * MIRROR of src/lib/accounts.ts — kept in sync via mcp-server/src/accounts.test.ts.
5
+ * Do not modify without updating the source file.
6
+ */
7
+ export declare const ACCOUNTS: {
8
+ readonly MYYNTISAAMISET: "2210";
9
+ readonly ALV_SAAMISET: "2320";
10
+ readonly KASSA: "2910";
11
+ readonly PANKKITILI: "2920";
12
+ readonly OSTOVELAT: "4130";
13
+ readonly ALV_VELKA: "4210";
14
+ readonly ENNAKONPIDATYSVELKA: "4220";
15
+ readonly TYEL_VELKA: "4240";
16
+ readonly SOTU_VELKA: "4230";
17
+ readonly TTV_VELKA: "4250";
18
+ readonly PALKKAVELKA: "4420";
19
+ readonly MYYNTI_25_5: "5010";
20
+ readonly MYYNTI_13_5: "5020";
21
+ readonly MYYNTI_10: "5030";
22
+ readonly MYYNTI_0: "5040";
23
+ readonly EU_TAVARAT: "5050";
24
+ readonly EU_PALVELUT: "5060";
25
+ readonly VIENTI: "5070";
26
+ readonly OSTOT: "6010";
27
+ readonly OSTOT_PALVELUT: "6030";
28
+ readonly PALKAT: "7010";
29
+ readonly ELAKEKULUT: "7100";
30
+ readonly SOSIAALITURVAMAKSUT: "7210";
31
+ readonly TOIMITILAVUOKRAT: "7420";
32
+ readonly SAHKO: "7510";
33
+ readonly POLTTOAINEET: "7610";
34
+ readonly EDUSTUSKULUT: "7810";
35
+ readonly PUHELIN: "7951";
36
+ readonly TOIMISTOTARVIKKEET: "7952";
37
+ readonly OHJELMISTOT: "7954";
38
+ readonly PANKKIKULUT: "7957";
39
+ readonly VAKUUTUKSET: "7970";
40
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Standard Finnish Chart of Accounts (YSE 2017) — commonly used account constants.
3
+ *
4
+ * MIRROR of src/lib/accounts.ts — kept in sync via mcp-server/src/accounts.test.ts.
5
+ * Do not modify without updating the source file.
6
+ */
7
+ export const ACCOUNTS = {
8
+ // ── Assets (2xxx) ──
9
+ MYYNTISAAMISET: '2210',
10
+ ALV_SAAMISET: '2320',
11
+ KASSA: '2910',
12
+ PANKKITILI: '2920',
13
+ // ── Liabilities (4xxx) ──
14
+ OSTOVELAT: '4130',
15
+ ALV_VELKA: '4210',
16
+ ENNAKONPIDATYSVELKA: '4220',
17
+ TYEL_VELKA: '4240',
18
+ SOTU_VELKA: '4230',
19
+ TTV_VELKA: '4250',
20
+ PALKKAVELKA: '4420',
21
+ // ── Revenue (5xxx) ──
22
+ MYYNTI_25_5: '5010',
23
+ MYYNTI_13_5: '5020',
24
+ MYYNTI_10: '5030',
25
+ MYYNTI_0: '5040',
26
+ EU_TAVARAT: '5050',
27
+ EU_PALVELUT: '5060',
28
+ VIENTI: '5070',
29
+ // ── Materials & Services (6xxx) ──
30
+ OSTOT: '6010',
31
+ OSTOT_PALVELUT: '6030',
32
+ // ── Expenses (7xxx) ──
33
+ PALKAT: '7010',
34
+ ELAKEKULUT: '7100',
35
+ SOSIAALITURVAMAKSUT: '7210',
36
+ TOIMITILAVUOKRAT: '7420',
37
+ SAHKO: '7510',
38
+ POLTTOAINEET: '7610',
39
+ EDUSTUSKULUT: '7810',
40
+ PUHELIN: '7951',
41
+ TOIMISTOTARVIKKEET: '7952',
42
+ OHJELMISTOT: '7954',
43
+ PANKKIKULUT: '7957',
44
+ VAKUUTUKSET: '7970',
45
+ };
package/dist/auth.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { Database } from './database.types.js';
3
+ /**
4
+ * Cancel any pending token refresh and release the event loop.
5
+ * Call from `transport.onclose` so the MCP process exits cleanly.
6
+ */
7
+ export declare function stopTokenRefresh(): void;
8
+ /**
9
+ * Initialize authenticated Supabase client.
10
+ *
11
+ * Supports two auth modes:
12
+ * 1. API key (recommended): Set KIRJAPRO_API_KEY
13
+ * 2. Legacy (development): Set SUPABASE_URL + SUPABASE_ACCESS_TOKEN + KIRJAPRO_COMPANY_ID
14
+ */
15
+ export declare function initAuth(): Promise<void>;
16
+ export declare function getSupabase(): SupabaseClient<Database>;
17
+ export declare function getCompanyId(): string;
package/dist/auth.js ADDED
@@ -0,0 +1,245 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ const SUPABASE_PROJECT_URL = 'https://tifewzsimmyoiepblhld.supabase.co';
3
+ let supabase = null;
4
+ let companyId = null;
5
+ let refreshTimer = null;
6
+ /**
7
+ * Authenticate via API key by calling the mcp-auth edge function.
8
+ * Returns the auth response containing a short-lived JWT.
9
+ */
10
+ async function authenticateWithApiKey(apiKey) {
11
+ const url = `${SUPABASE_PROJECT_URL}/functions/v1/mcp-auth`;
12
+ const response = await fetch(url, {
13
+ method: 'POST',
14
+ headers: { 'Content-Type': 'application/json' },
15
+ body: JSON.stringify({ api_key: apiKey }),
16
+ });
17
+ if (!response.ok) {
18
+ const body = await response.json().catch(() => ({ error: 'Unknown error' }));
19
+ throw new Error(`API key authentication failed (${response.status}): ${body.error || 'Unknown error'}`);
20
+ }
21
+ return response.json();
22
+ }
23
+ /**
24
+ * Create a Supabase client from an mcp-auth response.
25
+ */
26
+ function createClientFromAuth(auth) {
27
+ return createClient(auth.supabase_url, auth.anon_key, {
28
+ auth: {
29
+ persistSession: false,
30
+ autoRefreshToken: false,
31
+ },
32
+ global: {
33
+ headers: {
34
+ Authorization: `Bearer ${auth.access_token}`,
35
+ },
36
+ },
37
+ });
38
+ }
39
+ /**
40
+ * Schedule automatic token refresh before the JWT expires.
41
+ *
42
+ * Hardening (see audit M2):
43
+ * - Jitter 75–90% of TTL: multiple concurrent agents sharing one API key
44
+ * don't hit `mcp-auth` in lockstep (10 req/min rate limit).
45
+ * - Recursive `setTimeout`: re-arms with the *new* `expires_in` after every
46
+ * successful refresh (can't do that with `setInterval`).
47
+ * - Exponential backoff (capped at 60s, jittered) on refresh failure.
48
+ * - `.unref()`: doesn't keep the event loop alive if stdio transport closes.
49
+ */
50
+ function scheduleTokenRefresh(apiKey, expiresIn) {
51
+ if (refreshTimer) {
52
+ clearTimeout(refreshTimer);
53
+ }
54
+ const ttlMs = expiresIn * 1000;
55
+ // Refresh at 75–90% of TTL, clamped to [60s, ttl-60s]
56
+ const target = ttlMs * (0.75 + Math.random() * 0.15);
57
+ const refreshMs = Math.max(60_000, Math.min(target, ttlMs - 60_000));
58
+ const tick = async (attempt) => {
59
+ try {
60
+ console.error('[kirjapro-mcp] Refreshing API key token...');
61
+ const auth = await authenticateWithApiKey(apiKey);
62
+ supabase = createClientFromAuth(auth);
63
+ companyId = auth.company_id;
64
+ console.error('[kirjapro-mcp] Token refreshed successfully');
65
+ scheduleTokenRefresh(apiKey, auth.expires_in);
66
+ }
67
+ catch (error) {
68
+ console.error('[kirjapro-mcp] Token refresh failed:', error.message);
69
+ // Exponential backoff with jitter: 1s, 2s, 4s, …, capped at 60s.
70
+ const backoff = Math.min(60_000, 2 ** attempt * 1000) + Math.random() * 1000;
71
+ refreshTimer = setTimeout(() => {
72
+ void tick(attempt + 1);
73
+ }, backoff);
74
+ refreshTimer.unref?.();
75
+ }
76
+ };
77
+ refreshTimer = setTimeout(() => {
78
+ void tick(0);
79
+ }, refreshMs);
80
+ refreshTimer.unref?.();
81
+ }
82
+ /**
83
+ * Cancel any pending token refresh and release the event loop.
84
+ * Call from `transport.onclose` so the MCP process exits cleanly.
85
+ */
86
+ export function stopTokenRefresh() {
87
+ if (refreshTimer) {
88
+ clearTimeout(refreshTimer);
89
+ refreshTimer = null;
90
+ }
91
+ }
92
+ /**
93
+ * Initialize using KIRJAPRO_API_KEY (recommended).
94
+ * Calls the mcp-auth edge function to exchange the API key for a JWT.
95
+ */
96
+ async function initApiKeyAuth(apiKey) {
97
+ const auth = await authenticateWithApiKey(apiKey);
98
+ supabase = createClientFromAuth(auth);
99
+ companyId = auth.company_id;
100
+ // Validate the connection by fetching company info
101
+ const { data: company, error: companyError } = await supabase
102
+ .from('companies')
103
+ .select('id, name')
104
+ .eq('id', companyId)
105
+ .single();
106
+ if (companyError || !company) {
107
+ throw new Error(`Cannot access company ${companyId}. ${companyError?.message || ''}`);
108
+ }
109
+ console.error(`[kirjapro-mcp] Authenticated via API key | Company: ${company.name}`);
110
+ // Schedule automatic token refresh
111
+ scheduleTokenRefresh(apiKey, auth.expires_in);
112
+ }
113
+ /**
114
+ * Initialize using legacy env vars (for development).
115
+ * Requires SUPABASE_URL, SUPABASE_ACCESS_TOKEN, KIRJAPRO_COMPANY_ID.
116
+ */
117
+ async function initLegacyAuth() {
118
+ const supabaseUrl = process.env.SUPABASE_URL;
119
+ const supabaseAccessToken = process.env.SUPABASE_ACCESS_TOKEN;
120
+ const kirjaproCompanyId = process.env.KIRJAPRO_COMPANY_ID;
121
+ if (!supabaseUrl) {
122
+ throw new Error('SUPABASE_URL environment variable is required.');
123
+ }
124
+ if (!supabaseAccessToken) {
125
+ throw new Error('SUPABASE_ACCESS_TOKEN environment variable is required.');
126
+ }
127
+ if (!kirjaproCompanyId) {
128
+ throw new Error('KIRJAPRO_COMPANY_ID environment variable is required.');
129
+ }
130
+ // Use anon key if available, otherwise fall back to access token (works but semantically wrong)
131
+ const anonKey = process.env.SUPABASE_ANON_KEY || supabaseAccessToken;
132
+ supabase = createClient(supabaseUrl, anonKey, {
133
+ auth: {
134
+ persistSession: false,
135
+ autoRefreshToken: false,
136
+ },
137
+ global: {
138
+ headers: {
139
+ Authorization: `Bearer ${supabaseAccessToken}`,
140
+ },
141
+ },
142
+ });
143
+ // Validate the token
144
+ const { data: userData, error: authError } = await supabase.auth.getUser();
145
+ if (authError || !userData.user) {
146
+ throw new Error('Failed to authenticate with Supabase. ' +
147
+ 'Token may be expired. Refresh your access token and restart the MCP server. ' +
148
+ (authError?.message || ''));
149
+ }
150
+ // Verify company access
151
+ const { data: company, error: companyError } = await supabase
152
+ .from('companies')
153
+ .select('id, name')
154
+ .eq('id', kirjaproCompanyId)
155
+ .single();
156
+ if (companyError || !company) {
157
+ throw new Error(`Cannot access company ${kirjaproCompanyId}. ` +
158
+ 'Verify the KIRJAPRO_COMPANY_ID is correct and your user has access. ' +
159
+ (companyError?.message || ''));
160
+ }
161
+ // Check subscription tier for API access (subscriptions are user-keyed)
162
+ const { data: subscription } = await supabase
163
+ .from('subscriptions')
164
+ .select('tier')
165
+ .eq('user_id', userData.user.id)
166
+ .single();
167
+ const tier = subscription?.tier || 'free';
168
+ // Keep in sync with feature-gates.ts (hasApiAccess) + supabase/functions/mcp-auth.
169
+ // 'growth' (Kasvu) is a paid tier with API access — DT-073.
170
+ const apiTiers = ['entrepreneur', 'growth', 'company', 'custom'];
171
+ if (!apiTiers.includes(tier)) {
172
+ throw new Error(`API access requires a Yrittäjä, Kasvu, Tilitoimisto, or Räätälöity subscription. ` +
173
+ `Current tier: ${tier}. Upgrade at https://kirjapro.fi/settings/billing`);
174
+ }
175
+ companyId = kirjaproCompanyId;
176
+ console.error(`[kirjapro-mcp] Authenticated | Company: ${company.name} | Tier: ${tier}`);
177
+ // Parse JWT exp and schedule an expiry warning — legacy auth has no
178
+ // refresh path, so warn the user before the session silently dies.
179
+ try {
180
+ const payload = JSON.parse(Buffer.from(supabaseAccessToken.split('.')[1], 'base64url').toString());
181
+ if (typeof payload.exp === 'number') {
182
+ const expiresInMs = payload.exp * 1000 - Date.now();
183
+ if (expiresInMs > 0 && expiresInMs < 24 * 60 * 60 * 1000) {
184
+ const warnMs = Math.max(0, expiresInMs - 5 * 60 * 1000);
185
+ const timer = setTimeout(() => {
186
+ console.error('[kirjapro-mcp] WARNING: Legacy auth token expires in ~5 minutes. ' +
187
+ 'Restart with a fresh token or switch to KIRJAPRO_API_KEY for automatic refresh.');
188
+ }, warnMs);
189
+ timer.unref?.();
190
+ console.error(`[kirjapro-mcp] Token expires in ${Math.round(expiresInMs / 60_000)} minutes. ` +
191
+ 'Consider switching to KIRJAPRO_API_KEY for automatic refresh.');
192
+ }
193
+ }
194
+ }
195
+ catch {
196
+ // Not a JWT or unparseable — service_role keys don't expire
197
+ }
198
+ }
199
+ /**
200
+ * Initialize authenticated Supabase client.
201
+ *
202
+ * Supports two auth modes:
203
+ * 1. API key (recommended): Set KIRJAPRO_API_KEY
204
+ * 2. Legacy (development): Set SUPABASE_URL + SUPABASE_ACCESS_TOKEN + KIRJAPRO_COMPANY_ID
205
+ */
206
+ export async function initAuth() {
207
+ const apiKey = process.env.KIRJAPRO_API_KEY;
208
+ if (apiKey) {
209
+ if (!apiKey.startsWith('kp_')) {
210
+ throw new Error('Invalid KIRJAPRO_API_KEY format. Keys start with "kp_". ' +
211
+ 'Generate one at https://kirjapro.fi/settings/api-keys');
212
+ }
213
+ await initApiKeyAuth(apiKey);
214
+ return;
215
+ }
216
+ // Legacy env vars present?
217
+ if (process.env.SUPABASE_URL && process.env.SUPABASE_ACCESS_TOKEN && process.env.KIRJAPRO_COMPANY_ID) {
218
+ if (process.env.ALLOW_LEGACY_AUTH !== 'true') {
219
+ throw new Error('Legacy auth (SUPABASE_URL + SUPABASE_ACCESS_TOKEN + KIRJAPRO_COMPANY_ID) is disabled.\n\n' +
220
+ 'Legacy auth bypasses the mcp-auth exchange — no audit log, no IP trail, no key rotation signal.\n' +
221
+ 'Switch to KIRJAPRO_API_KEY: generate a key at https://kirjapro.fi/settings/api-keys.\n\n' +
222
+ 'If you must use legacy auth temporarily (e.g. local development against a dev project),\n' +
223
+ 'set ALLOW_LEGACY_AUTH=true to opt in. This path will be removed in a future release.');
224
+ }
225
+ console.error('[kirjapro-mcp] WARNING: Using legacy auth. This bypasses audit logging and will be removed ' +
226
+ 'in a future release. Migrate to KIRJAPRO_API_KEY: https://kirjapro.fi/settings/api-keys');
227
+ await initLegacyAuth();
228
+ return;
229
+ }
230
+ throw new Error('Missing authentication configuration.\n\n' +
231
+ 'Set KIRJAPRO_API_KEY (recommended):\n' +
232
+ ' Generate an API key at https://kirjapro.fi/settings/api-keys');
233
+ }
234
+ export function getSupabase() {
235
+ if (!supabase) {
236
+ throw new Error('Supabase client not initialized. Call initAuth() first.');
237
+ }
238
+ return supabase;
239
+ }
240
+ export function getCompanyId() {
241
+ if (!companyId) {
242
+ throw new Error('Company ID not set. Call initAuth() first.');
243
+ }
244
+ return companyId;
245
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Single source of truth for Finnish tax + social insurance rates.
3
+ *
4
+ * Every tool that needs a rate and every compliance resource that publishes
5
+ * a rate MUST import from this module — do NOT re-declare rates elsewhere.
6
+ * When rates change for a new year, update this file and bump `sourcesVerifiedAt`.
7
+ *
8
+ * Sources:
9
+ * - VAT: AVL 1501/1993 §§ 84–85a, vero.fi/syventavat
10
+ * - TyEL: https://www.etk.fi/ajankohtaista/tyoelakemaksut-vuonna-2026/
11
+ * - Elo 2026: https://www.elo.fi/fi-fi/tyonantaja/tyel-vakuuttaminen/tyel-maksu/sosiaalivakuutusmaksut-2026
12
+ * - Sotu/TTV: tyel-vakuutus.fi/tyonantajan-sivukulut/
13
+ */
14
+ export declare const FINNISH_RATES_2026: {
15
+ readonly year: 2026;
16
+ readonly validFrom: "2026-01-01";
17
+ readonly validUntil: "2026-12-31";
18
+ readonly sourcesVerifiedAt: "2026-03-15";
19
+ readonly source: "AVL 1501/1993, ETK 2026, Elo 2026, vero.fi 2026";
20
+ readonly vat: {
21
+ readonly standard: 25.5;
22
+ readonly reduced: 13.5;
23
+ readonly secondReduced: 10;
24
+ readonly zero: 0;
25
+ };
26
+ readonly social: {
27
+ readonly tyelEmployee: 7.3;
28
+ readonly tyelEmployer: 17.55;
29
+ readonly tyelEmployerAverage: 17.1;
30
+ readonly tyelKeskimaarainen: 24.4;
31
+ readonly tyelPerusmaksu: 24.85;
32
+ readonly sotu: 1.91;
33
+ readonly ttvEmployer: 0.31;
34
+ readonly ttvEmployerLarge: 1.23;
35
+ readonly ttvEmployerLargeThreshold: 2509500;
36
+ readonly ttvEmployee: 0.89;
37
+ readonly tapaturmaAverage: 0.51;
38
+ readonly ryhmahenki: 0.06;
39
+ };
40
+ };
41
+ export type FinnishRates = typeof FINNISH_RATES_2026;
42
+ /**
43
+ * Compute freshness metadata for a dated resource. `today` is injectable for
44
+ * tests; production callers pass nothing and get the current date.
45
+ */
46
+ export declare function freshnessMeta(rates?: FinnishRates, today?: Date): {
47
+ published_at: string;
48
+ valid_from: string;
49
+ valid_through: string;
50
+ sources_verified_at: string;
51
+ stale: boolean;
52
+ days_until_expiry: number;
53
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Single source of truth for Finnish tax + social insurance rates.
3
+ *
4
+ * Every tool that needs a rate and every compliance resource that publishes
5
+ * a rate MUST import from this module — do NOT re-declare rates elsewhere.
6
+ * When rates change for a new year, update this file and bump `sourcesVerifiedAt`.
7
+ *
8
+ * Sources:
9
+ * - VAT: AVL 1501/1993 §§ 84–85a, vero.fi/syventavat
10
+ * - TyEL: https://www.etk.fi/ajankohtaista/tyoelakemaksut-vuonna-2026/
11
+ * - Elo 2026: https://www.elo.fi/fi-fi/tyonantaja/tyel-vakuuttaminen/tyel-maksu/sosiaalivakuutusmaksut-2026
12
+ * - Sotu/TTV: tyel-vakuutus.fi/tyonantajan-sivukulut/
13
+ */
14
+ export const FINNISH_RATES_2026 = {
15
+ year: 2026,
16
+ validFrom: '2026-01-01',
17
+ validUntil: '2026-12-31',
18
+ sourcesVerifiedAt: '2026-03-15',
19
+ source: 'AVL 1501/1993, ETK 2026, Elo 2026, vero.fi 2026',
20
+ vat: {
21
+ standard: 25.5,
22
+ reduced: 13.5,
23
+ secondReduced: 10,
24
+ zero: 0,
25
+ },
26
+ social: {
27
+ tyelEmployee: 7.30,
28
+ tyelEmployer: 17.55,
29
+ tyelEmployerAverage: 17.10,
30
+ tyelKeskimaarainen: 24.40,
31
+ tyelPerusmaksu: 24.85,
32
+ sotu: 1.91,
33
+ ttvEmployer: 0.31,
34
+ ttvEmployerLarge: 1.23,
35
+ ttvEmployerLargeThreshold: 2_509_500,
36
+ ttvEmployee: 0.89,
37
+ tapaturmaAverage: 0.51,
38
+ ryhmahenki: 0.06,
39
+ },
40
+ };
41
+ /**
42
+ * Compute freshness metadata for a dated resource. `today` is injectable for
43
+ * tests; production callers pass nothing and get the current date.
44
+ */
45
+ export function freshnessMeta(rates = FINNISH_RATES_2026, today = new Date()) {
46
+ const validUntil = new Date(`${rates.validUntil}T23:59:59Z`);
47
+ const msPerDay = 86_400_000;
48
+ const daysUntilExpiry = Math.ceil((validUntil.getTime() - today.getTime()) / msPerDay);
49
+ return {
50
+ published_at: rates.sourcesVerifiedAt,
51
+ valid_from: rates.validFrom,
52
+ valid_through: rates.validUntil,
53
+ sources_verified_at: rates.sourcesVerifiedAt,
54
+ stale: daysUntilExpiry < 0,
55
+ days_until_expiry: daysUntilExpiry,
56
+ };
57
+ }