@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
@@ -0,0 +1,20 @@
1
+ export const Constants = {
2
+ public: {
3
+ Enums: {
4
+ account_type: ["asset", "liability", "equity", "revenue", "expense"],
5
+ company_role: ["owner", "admin", "accountant", "viewer"],
6
+ company_size: ["micro", "small", "large"],
7
+ platform_role: ["admin", "superadmin"],
8
+ subscription_tier: ["free", "entrepreneur", "growth", "company", "custom"],
9
+ vat_category: [
10
+ "standard",
11
+ "zero_rate_export",
12
+ "zero_rate_eu",
13
+ "reverse_charge",
14
+ "exempt",
15
+ "margin_scheme",
16
+ ],
17
+ vat_period: ["monthly", "quarterly", "yearly"],
18
+ },
19
+ },
20
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
@@ -30869,18 +30869,27 @@ var companyId = null;
30869
30869
  var refreshTimer = null;
30870
30870
  async function authenticateWithApiKey(apiKey) {
30871
30871
  const url2 = `${SUPABASE_PROJECT_URL}/functions/v1/mcp-auth`;
30872
- const response = await fetch(url2, {
30873
- method: "POST",
30874
- headers: { "Content-Type": "application/json" },
30875
- body: JSON.stringify({ api_key: apiKey })
30876
- });
30877
- if (!response.ok) {
30878
- const body = await response.json().catch(() => ({ error: "Unknown error" }));
30879
- throw new Error(
30880
- `API key authentication failed (${response.status}): ${body.error || "Unknown error"}`
30881
- );
30872
+ try {
30873
+ const response = await fetch(url2, {
30874
+ method: "POST",
30875
+ headers: { "Content-Type": "application/json" },
30876
+ body: JSON.stringify({ api_key: apiKey }),
30877
+ signal: AbortSignal.timeout(1e4)
30878
+ });
30879
+ if (!response.ok) {
30880
+ const body = await response.json().catch(() => ({ error: "Unknown error" }));
30881
+ throw new Error(
30882
+ `API key authentication failed (${response.status}): ${body.error || "Unknown error"}`
30883
+ );
30884
+ }
30885
+ return await response.json();
30886
+ } catch (err) {
30887
+ if (err instanceof Error && err.message.startsWith("API key authentication failed")) {
30888
+ throw err;
30889
+ }
30890
+ const reason = err?.name === "TimeoutError" ? "timed out after 10s" : `failed: ${err?.message ?? "network error"}`;
30891
+ throw new Error(`API key authentication request to mcp-auth ${reason}`);
30882
30892
  }
30883
- return response.json();
30884
30893
  }
30885
30894
  function createClientFromAuth(auth) {
30886
30895
  return createClient(auth.supabase_url, auth.anon_key, {
@@ -30984,10 +30993,10 @@ async function initLegacyAuth() {
30984
30993
  }
30985
30994
  const { data: subscription } = await supabase.from("subscriptions").select("tier").eq("user_id", userData.user.id).single();
30986
30995
  const tier = subscription?.tier || "free";
30987
- const apiTiers = ["entrepreneur", "company", "custom"];
30996
+ const apiTiers = ["entrepreneur", "growth", "company", "custom"];
30988
30997
  if (!apiTiers.includes(tier)) {
30989
30998
  throw new Error(
30990
- `API access requires Entrepreneur, Company, or Custom subscription tier. Current tier: ${tier}. Upgrade at https://kirjapro.fi/settings/billing`
30999
+ `API access requires a Yritt\xE4j\xE4, Kasvu, Tilitoimisto, or R\xE4\xE4t\xE4l\xF6ity subscription. Current tier: ${tier}. Upgrade at https://kirjapro.fi/settings/billing`
30991
31000
  );
30992
31001
  }
30993
31002
  companyId = kirjaproCompanyId;
@@ -32029,8 +32038,8 @@ function registerTransactionTools(server2) {
32029
32038
 
32030
32039
  // src/types.ts
32031
32040
  var VALID_OFFER_TRANSITIONS = {
32032
- draft: ["sent", "expired"],
32033
- sent: ["viewed", "accepted", "rejected", "expired", "draft"],
32041
+ draft: ["expired"],
32042
+ sent: ["accepted", "rejected", "expired", "draft"],
32034
32043
  viewed: ["accepted", "rejected", "expired", "draft"],
32035
32044
  accepted: [],
32036
32045
  rejected: ["draft"],
@@ -33235,7 +33244,7 @@ function registerReportTools(server2) {
33235
33244
  if (error51) {
33236
33245
  return toErrorContent(sanitizeDbError("Failed to generate income statement", error51));
33237
33246
  }
33238
- const { data: lineTypes, error: lineTypesError } = await supabase2.from("income_statement_line_types").select("*").eq("template_id", templateId).order("display_order", { ascending: true });
33247
+ const { data: lineTypes, error: lineTypesError } = await supabase2.from("income_statement_line_types").select("line_code, line_name_fi, line_type, indent_level, is_calculated, sign_convention, display_order").eq("template_id", templateId).order("display_order", { ascending: true });
33239
33248
  if (lineTypesError || !lineTypes) {
33240
33249
  return toErrorContent(
33241
33250
  sanitizeDbError("Failed to load income statement template lines", lineTypesError)
@@ -35144,10 +35153,10 @@ function registerOfferTools(server2) {
35144
35153
  server2.registerTool(
35145
35154
  "kirjapro_update_offer_status",
35146
35155
  {
35147
- description: "Update offer status with state machine validation. Valid transitions: draft\u2192sent/expired, sent\u2192viewed/accepted/rejected/expired/draft, viewed\u2192accepted/rejected/expired/draft, rejected/expired\u2192draft. Accepted is terminal (convert to invoice instead).",
35156
+ description: "Update offer status (sender-side). Valid transitions: draft\u2192expired, sent\u2192accepted/rejected/expired/draft, viewed\u2192accepted/rejected/expired/draft, rejected/expired\u2192draft. draft\u2192sent is NOT here \u2014 use the send-offer-email path (recipient gets the public link). Accepted is terminal (convert to invoice instead).",
35148
35157
  inputSchema: {
35149
35158
  offerId: external_exports.string().uuid().describe("Offer UUID"),
35150
- status: external_exports.enum(["draft", "sent", "viewed", "accepted", "rejected", "expired"]).describe("New status"),
35159
+ status: external_exports.enum(["draft", "accepted", "rejected", "expired"]).describe("New status"),
35151
35160
  idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
35152
35161
  dryRun: DRY_RUN_SCHEMA
35153
35162
  },
@@ -35195,43 +35204,32 @@ function registerOfferTools(server2) {
35195
35204
  params.idempotencyKey,
35196
35205
  params,
35197
35206
  async () => {
35198
- const updateFields = { status: newStatus };
35199
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
35200
- switch (newStatus) {
35201
- case "sent":
35202
- updateFields.sent_at = nowIso;
35203
- break;
35204
- case "viewed":
35205
- updateFields.viewed_at = nowIso;
35206
- break;
35207
- case "accepted":
35208
- updateFields.accepted_at = nowIso;
35209
- break;
35210
- case "rejected":
35211
- updateFields.rejected_at = nowIso;
35212
- break;
35213
- }
35214
- const { data: updated, error: updateError } = await supabase2.from("offers").update(updateFields).eq("id", params.offerId).eq("company_id", companyId2).eq("status", currentStatus).select("id");
35215
- if (updateError) {
35216
- return toErrorContent(sanitizeDbError("Failed to update offer status", updateError));
35217
- }
35218
- if (!updated || updated.length === 0) {
35219
- return toErrorContent(
35220
- `Offer ${offer.offer_number} status changed concurrently. Please retry.`
35221
- );
35207
+ const { data: rpcData, error: rpcError } = await supabase2.rpc(
35208
+ "record_offer_status_change",
35209
+ {
35210
+ p_offer_id: params.offerId,
35211
+ p_company_id: companyId2,
35212
+ p_to_status: newStatus
35213
+ }
35214
+ );
35215
+ if (rpcError) {
35216
+ return toErrorContent(sanitizeDbError("Failed to update offer status", rpcError));
35222
35217
  }
35223
- const { error: eventError } = await supabase2.from("offer_events").insert({
35224
- offer_id: params.offerId,
35225
- event_type: `status_${newStatus}`
35226
- });
35227
- if (eventError) {
35228
- console.error("Failed to log offer event:", eventError);
35218
+ const result = rpcData;
35219
+ if (!result?.success) {
35220
+ if (result?.error === "not_claimable") {
35221
+ return toErrorContent(
35222
+ `Offer ${offer.offer_number} status changed concurrently (current: ${result.current_status ?? "unknown"}). Please re-check and retry.`
35223
+ );
35224
+ }
35225
+ return toErrorContent(result?.error ?? "Failed to update offer status");
35229
35226
  }
35230
35227
  return toResult({
35231
35228
  message: `Offer ${offer.offer_number} status changed from "${currentStatus}" to "${newStatus}"`,
35232
35229
  offer_number: offer.offer_number,
35233
35230
  previous_status: currentStatus,
35234
- new_status: newStatus
35231
+ new_status: newStatus,
35232
+ stamp: result.stamp
35235
35233
  });
35236
35234
  }
35237
35235
  );
@@ -36637,7 +36635,7 @@ function registerResources(server2) {
36637
36635
  }
36638
36636
 
36639
36637
  // src/version.ts
36640
- var VERSION = "0.5.10";
36638
+ var VERSION = "0.6.0";
36641
36639
 
36642
36640
  // src/index.ts
36643
36641
  var INSTRUCTIONS_COMMON = `
@@ -36801,8 +36799,9 @@ var server = new McpServer(
36801
36799
  var TIER_LABELS = {
36802
36800
  free: "Ilmainen",
36803
36801
  entrepreneur: "Yritt\xE4j\xE4 (19 \u20AC/kk)",
36802
+ growth: "Kasvu (39 \u20AC/kk)",
36804
36803
  company: "Tilitoimisto (97 \u20AC/kk)",
36805
- custom: "Custom"
36804
+ custom: "R\xE4\xE4t\xE4l\xF6ity"
36806
36805
  };
36807
36806
  function getRegisteredToolCount(mcpServer) {
36808
36807
  return Object.keys(
@@ -36822,7 +36821,14 @@ function registerPrompts(mcpServer) {
36822
36821
  const [companyResult, fyResult, subResult] = await Promise.all([
36823
36822
  supabase2.from("companies").select("name, business_id, industry, address, postal_code, city").eq("id", companyId2).single(),
36824
36823
  supabase2.from("fiscal_years").select("start_date, end_date, is_closed").eq("company_id", companyId2).order("start_date", { ascending: false }),
36825
- supabase2.from("subscriptions").select("tier").eq("company_id", companyId2).maybeSingle()
36824
+ // Tier is keyed by the subscription OWNER (user_id), not company_id.
36825
+ // `subscriptions.company_id` is nullable and never populated by billing
36826
+ // (stripe-webhook keys every write by user_id), so `.eq('company_id', …)`
36827
+ // matched 0 rows and silently showed every paying customer "Ilmainen".
36828
+ // The `subscriptions_select` RLS policy (user_id = auth.uid()) already
36829
+ // scopes this to the caller's own row — no explicit user filter needed.
36830
+ // Newest row wins if more than one exists.
36831
+ supabase2.from("subscriptions").select("tier").order("created_at", { ascending: false }).limit(1).maybeSingle()
36826
36832
  ]);
36827
36833
  const company = companyResult.data;
36828
36834
  let companyBlock;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Income statement synthesis — calculated subtotals from leaf-line amounts.
3
+ *
4
+ * DUPLICATED BY INTENT from `src/hooks/useIncomeStatementCalculation.ts`
5
+ * (`synthesizeIncomeStatementLines`). The mcp-server is published as a
6
+ * separate npm package and cannot import from `src/`. The contract is:
7
+ *
8
+ * Any change to the web hook's synthesis switch (line_code cases,
9
+ * addends, sign conventions) MUST be mirrored here, and vice versa.
10
+ *
11
+ * Both paths consume the same `get_income_statement` RPC, which returns
12
+ * ONLY leaf rows (range-based line_codes). Calculated subtotals
13
+ * (LIIKEVOITTO, RAHOITUS_TOTAL, TILIKAUDEN_TULOS, ...) are synthesized
14
+ * client-side.
15
+ */
16
+ import type { Database } from "../database.types.js";
17
+ type IncomeStatementLineType = Database["public"]["Tables"]["income_statement_line_types"]["Row"];
18
+ export interface CalculatedIncomeStatementLine {
19
+ lineCode: string;
20
+ lineName: string;
21
+ lineType: string | null;
22
+ indentLevel: number;
23
+ currentAmount: number;
24
+ previousAmount: number;
25
+ variance: number;
26
+ variancePercentage: number | null;
27
+ isCalculated: boolean;
28
+ signConvention: string | null;
29
+ }
30
+ export interface CalculationTotals {
31
+ liikevaihto: number;
32
+ liikevoitto: number;
33
+ voittoEnnenVeroja: number;
34
+ tilikausiTulos: number;
35
+ }
36
+ export interface SynthesisResult {
37
+ lines: CalculatedIncomeStatementLine[];
38
+ totals: CalculationTotals;
39
+ }
40
+ /**
41
+ * Synthesize calculated subtotals (LIIKEVOITTO, MATERIAALIT_TOTAL, ...)
42
+ * from pre-computed leaf-line amounts. Calculated subtotals are processed
43
+ * in lineTypes order (display_order); each subtotal MUST appear after its
44
+ * dependencies in the template.
45
+ */
46
+ export declare function synthesizeIncomeStatementLines(lineTypes: IncomeStatementLineType[], currentByLine: Map<string, number>, previousByLine: Map<string, number>): SynthesisResult;
47
+ export {};
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Synthesize calculated subtotals (LIIKEVOITTO, MATERIAALIT_TOTAL, ...)
3
+ * from pre-computed leaf-line amounts. Calculated subtotals are processed
4
+ * in lineTypes order (display_order); each subtotal MUST appear after its
5
+ * dependencies in the template.
6
+ */
7
+ export function synthesizeIncomeStatementLines(lineTypes, currentByLine, previousByLine) {
8
+ // Defensive copy — we mutate during the calculated-subtotal pass below.
9
+ const lineAmounts = new Map(currentByLine);
10
+ const prevLineAmounts = new Map(previousByLine);
11
+ lineTypes.forEach((lineType) => {
12
+ if (!lineType.is_calculated)
13
+ return;
14
+ let currentAmount = 0;
15
+ let previousAmount = 0;
16
+ const get = (code) => lineAmounts.get(code) || 0;
17
+ const getPrev = (code) => prevLineAmounts.get(code) || 0;
18
+ switch (lineType.line_code) {
19
+ case "MATERIAALIT_TOTAL":
20
+ currentAmount =
21
+ get("OSTOT_TILIKAUDEN") +
22
+ get("VARASTOJEN_MUUTOS_OSTOT") +
23
+ get("ULKOPUOLISET_PALVELUT");
24
+ previousAmount =
25
+ getPrev("OSTOT_TILIKAUDEN") +
26
+ getPrev("VARASTOJEN_MUUTOS_OSTOT") +
27
+ getPrev("ULKOPUOLISET_PALVELUT");
28
+ break;
29
+ case "HENKILOSTO_TOTAL":
30
+ currentAmount =
31
+ get("PALKAT_PALKKIOT") + get("ELAKEKULUT") + get("MUUT_HENKILOSIVUKULUT");
32
+ previousAmount =
33
+ getPrev("PALKAT_PALKKIOT") +
34
+ getPrev("ELAKEKULUT") +
35
+ getPrev("MUUT_HENKILOSIVUKULUT");
36
+ break;
37
+ case "POISTOT_TOTAL":
38
+ currentAmount =
39
+ get("SUUNNITELMAN_POISTOT") +
40
+ get("ARVONALENT_PYSYVAT") +
41
+ get("ARVONALENT_VAIHTUVAT");
42
+ previousAmount =
43
+ getPrev("SUUNNITELMAN_POISTOT") +
44
+ getPrev("ARVONALENT_PYSYVAT") +
45
+ getPrev("ARVONALENT_VAIHTUVAT");
46
+ break;
47
+ case "LIIKEVOITTO":
48
+ currentAmount =
49
+ get("LIIKEVAIHTO") +
50
+ get("VARASTOJEN_MUUTOS") +
51
+ get("VALMISTUS_OMAAN") +
52
+ get("MUUT_TUOTOT") +
53
+ get("MATERIAALIT_TOTAL") +
54
+ get("HENKILOSTO_TOTAL") +
55
+ get("POISTOT_TOTAL") +
56
+ get("MUUT_KULUT");
57
+ previousAmount =
58
+ getPrev("LIIKEVAIHTO") +
59
+ getPrev("VARASTOJEN_MUUTOS") +
60
+ getPrev("VALMISTUS_OMAAN") +
61
+ getPrev("MUUT_TUOTOT") +
62
+ getPrev("MATERIAALIT_TOTAL") +
63
+ getPrev("HENKILOSTO_TOTAL") +
64
+ getPrev("POISTOT_TOTAL") +
65
+ getPrev("MUUT_KULUT");
66
+ break;
67
+ case "RAHOITUS_TOTAL":
68
+ currentAmount =
69
+ get("TUOTOT_KONSERNI") +
70
+ get("TUOTOT_OMISTUSYHTEYS") +
71
+ get("TUOTOT_SIJOITUKSET") +
72
+ get("MUUT_KORKOTUOTOT") +
73
+ get("ARVONALENT_SIJOITUKSET") +
74
+ get("ARVONALENT_RAHOITUS") +
75
+ get("KORKOKULUT");
76
+ previousAmount =
77
+ getPrev("TUOTOT_KONSERNI") +
78
+ getPrev("TUOTOT_OMISTUSYHTEYS") +
79
+ getPrev("TUOTOT_SIJOITUKSET") +
80
+ getPrev("MUUT_KORKOTUOTOT") +
81
+ getPrev("ARVONALENT_SIJOITUKSET") +
82
+ getPrev("ARVONALENT_RAHOITUS") +
83
+ getPrev("KORKOKULUT");
84
+ break;
85
+ case "VOITTO_ENNEN_SIIRTOJA":
86
+ currentAmount = get("LIIKEVOITTO") + get("RAHOITUS_TOTAL");
87
+ previousAmount = getPrev("LIIKEVOITTO") + getPrev("RAHOITUS_TOTAL");
88
+ break;
89
+ case "SIIRROT_TOTAL":
90
+ currentAmount =
91
+ get("POISTOERON_MUUTOS") + get("VARAUSTEN_MUUTOS") + get("KONSERNIAVUSTUS");
92
+ previousAmount =
93
+ getPrev("POISTOERON_MUUTOS") +
94
+ getPrev("VARAUSTEN_MUUTOS") +
95
+ getPrev("KONSERNIAVUSTUS");
96
+ break;
97
+ case "TILIKAUDEN_TULOS":
98
+ currentAmount =
99
+ get("VOITTO_ENNEN_SIIRTOJA") +
100
+ get("SIIRROT_TOTAL") +
101
+ get("TULOVEROT") +
102
+ get("MUUT_VEROT");
103
+ previousAmount =
104
+ getPrev("VOITTO_ENNEN_SIIRTOJA") +
105
+ getPrev("SIIRROT_TOTAL") +
106
+ getPrev("TULOVEROT") +
107
+ getPrev("MUUT_VEROT");
108
+ break;
109
+ }
110
+ lineAmounts.set(lineType.line_code, currentAmount);
111
+ prevLineAmounts.set(lineType.line_code, previousAmount);
112
+ });
113
+ const calculatedLines = lineTypes.map((lineType) => {
114
+ const currentAmount = lineAmounts.get(lineType.line_code) || 0;
115
+ const previousAmount = prevLineAmounts.get(lineType.line_code) || 0;
116
+ const variance = currentAmount - previousAmount;
117
+ const variancePercentage = previousAmount !== 0 ? (variance / Math.abs(previousAmount)) * 100 : null;
118
+ return {
119
+ lineCode: lineType.line_code,
120
+ lineName: lineType.line_name_fi,
121
+ lineType: lineType.line_type,
122
+ indentLevel: lineType.indent_level ?? 0,
123
+ currentAmount,
124
+ previousAmount,
125
+ variance,
126
+ variancePercentage,
127
+ isCalculated: lineType.is_calculated ?? false,
128
+ signConvention: lineType.sign_convention,
129
+ };
130
+ });
131
+ // `??` not `||` so a legitimate break-even 0 is preserved.
132
+ // Skill: fallback-chain-zero-respect.
133
+ const totals = {
134
+ liikevaihto: lineAmounts.get("LIIKEVAIHTO") ?? 0,
135
+ liikevoitto: lineAmounts.get("LIIKEVOITTO") ?? 0,
136
+ voittoEnnenVeroja: lineAmounts.get("VOITTO_ENNEN_SIIRTOJA") ?? 0,
137
+ tilikausiTulos: lineAmounts.get("TILIKAUDEN_TULOS") ?? 0,
138
+ };
139
+ return { lines: calculatedLines, totals };
140
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerCompanyResources(server: McpServer): void;
@@ -0,0 +1,75 @@
1
+ import { getSupabase, getCompanyId } from '../auth.js';
2
+ import { helsinkiDateString, sanitizeDbError } from '../utils.js';
3
+ export function registerCompanyResources(server) {
4
+ server.registerResource('company-info', 'kirjapro://company/info', {
5
+ description: 'Current company details: name, business ID (Y-tunnus), address, and settings.',
6
+ mimeType: 'application/json',
7
+ }, async (uri) => {
8
+ const supabase = getSupabase();
9
+ const companyId = getCompanyId();
10
+ const { data: company, error } = await supabase
11
+ .from('companies')
12
+ .select('id, name, business_id, address, postal_code, city, country, created_at')
13
+ .eq('id', companyId)
14
+ .single();
15
+ // Don't swallow the error. An empty company object is indistinguishable
16
+ // from auth expiry, RLS denial, or PostgREST failure — and the agent
17
+ // would build decisions on a blank Y-tunnus. Surface the sanitized
18
+ // error so the agent can tell the user to re-auth or retry.
19
+ if (error || !company) {
20
+ return {
21
+ contents: [{
22
+ uri: uri.href,
23
+ mimeType: 'application/json',
24
+ text: JSON.stringify({
25
+ error: error
26
+ ? sanitizeDbError('Failed to read company', error)
27
+ : 'Company not found for current context.',
28
+ }),
29
+ }],
30
+ };
31
+ }
32
+ return {
33
+ contents: [{
34
+ uri: uri.href,
35
+ mimeType: 'application/json',
36
+ text: JSON.stringify(company),
37
+ }],
38
+ };
39
+ });
40
+ server.registerResource('fiscal-years', 'kirjapro://fiscal-years', {
41
+ description: 'List of fiscal years (tilikaudet) for the company. Shows which are open/closed and their date ranges.',
42
+ mimeType: 'application/json',
43
+ }, async (uri) => {
44
+ const supabase = getSupabase();
45
+ const companyId = getCompanyId();
46
+ const { data: fiscalYears, error } = await supabase
47
+ .from('fiscal_years')
48
+ .select('id, start_date, end_date, is_closed, created_at')
49
+ .eq('company_id', companyId)
50
+ .order('start_date', { ascending: false });
51
+ if (error) {
52
+ return {
53
+ contents: [{
54
+ uri: uri.href,
55
+ mimeType: 'application/json',
56
+ text: JSON.stringify({
57
+ error: sanitizeDbError('Failed to read fiscal years', error),
58
+ }),
59
+ }],
60
+ };
61
+ }
62
+ const today = helsinkiDateString();
63
+ const enriched = (fiscalYears || []).map((fy) => ({
64
+ ...fy,
65
+ is_current: !fy.is_closed && fy.start_date <= today && fy.end_date >= today,
66
+ }));
67
+ return {
68
+ contents: [{
69
+ uri: uri.href,
70
+ mimeType: 'application/json',
71
+ text: JSON.stringify(enriched),
72
+ }],
73
+ };
74
+ });
75
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerComplianceResources(server: McpServer): void;