@kirjapro/mcp-server 0.6.3 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0] - 2026-07-16
4
+
5
+ Adds invoice delivery as a first-class MCP workflow.
6
+
7
+ ### Added
8
+
9
+ - **`kirjapro_send_invoice`** sends a draft invoice through an explicit `email` or `einvoice` channel. The tool calls the authenticated `dispatch-invoice` Edge Function, keeps provider credentials server-side, and returns the delivery identifier.
10
+ - Invoice create → send guidance now uses the delivery tool instead of redirecting users to the web UI.
11
+
12
+ ### Changed
13
+
14
+ - The default `core` surface now contains 27 tools; the complete catalog remains 52 tools.
15
+ - Package, server manifest, and MCPB versions are aligned at 0.7.0.
16
+ - Client setup now documents native local `stdio` use with Mistral Vibe and ChatGPT access through OpenAI Secure MCP Tunnel. Mistral Le Chat is explicitly marked as requiring a separately hosted remote endpoint.
17
+
18
+ ### Fixed
19
+
20
+ - Server instructions no longer list e-invoice sending as web-only now that `kirjapro_send_invoice` supports the explicit `einvoice` channel.
21
+
3
22
  ## [0.6.1] - 2026-05-29
4
23
 
5
24
  Hardening release from a full server audit (12 dimensions, adversarially verified). No new tools; tightens input validation, closes two security gaps, and corrects compliance guidance + tool descriptions. `tool-fingerprints.json` regenerated for the description/input-schema changes.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kirjapro/mcp-server
2
2
 
3
- MCP server for Finnish accounting. Exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude Code, Claude Desktop, and any MCP client.
3
+ MCP server for Finnish accounting. Exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude Code, Claude Desktop, Mistral Vibe, ChatGPT through Secure MCP Tunnel, and other MCP clients.
4
4
 
5
5
  **Full documentation:** [kirjapro.fi/docs/mcp](https://kirjapro.fi/docs/mcp/)
6
6
 
@@ -72,9 +72,30 @@ Add to `claude_desktop_config.json`:
72
72
 
73
73
  Restart Claude Desktop after saving.
74
74
 
75
+ #### Mistral Vibe
76
+
77
+ Add to `~/.vibe/config.toml`:
78
+
79
+ ```toml
80
+ [[mcp_servers]]
81
+ name = "kirjapro"
82
+ transport = "stdio"
83
+ command = "npx"
84
+ args = ["-y", "@kirjapro/mcp-server"]
85
+ env = { KIRJAPRO_API_KEY = "kp_your_key_here" }
86
+ ```
87
+
88
+ Restart Vibe and verify the connection from `/mcp`.
89
+
90
+ #### ChatGPT
91
+
92
+ ChatGPT requires a remote MCP connection and cannot launch this local `stdio` package directly. Use [OpenAI Secure MCP Tunnel](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels) to bridge ChatGPT to `npx -y @kirjapro/mcp-server`. Full write/modify support currently requires ChatGPT Business or Enterprise/Edu; see the [full setup guide](https://kirjapro.fi/docs/mcp/) for the tunnel commands and plan limitations.
93
+
94
+ Mistral Le Chat likewise requires a remote MCP server URL. The local npm package works directly with Mistral Vibe; Le Chat support requires a separately hosted remote endpoint.
95
+
75
96
  #### Other clients
76
97
 
77
- See [full setup guide](https://kirjapro.fi/docs/mcp/) for Cursor, VS Code / Copilot, Windsurf, and generic stdio.
98
+ See [full setup guide](https://kirjapro.fi/docs/mcp/) for ChatGPT, Mistral Vibe, Cursor, VS Code / Copilot, Windsurf, and generic stdio.
78
99
 
79
100
  ### 3. Verify
80
101
 
@@ -86,6 +107,7 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
86
107
 
87
108
  - *"Show my dashboard"* — financial snapshot of the current fiscal year
88
109
  - *"Create an invoice for Acme Oy, 5000 € for consulting in March"* — full lifecycle from customer lookup to draft invoice
110
+ - *"Send invoice #42 by email"* — server-rendered PDF delivery with an explicit channel
89
111
  - *"Draft a contract for Acme Oy and email it to john@acme.fi, 3-month retainer at 5000 € / month"* — `tarjous`/`sopimus` create + customer-visible email link
90
112
  - *"Record that invoice #42 was paid yesterday"* — payment recording + auto status transition
91
113
  - *"What's my VAT for this quarter?"* — `kirjapro_get_alv_summary` with OmaVero field codes
@@ -94,7 +116,7 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
94
116
 
95
117
  ## What's included
96
118
 
97
- **26 tools** in the default `core` surface, focused on the daily-use workflows: read your books, invoice flow, transactions + banking, contract create / send / convert, Finnish validators. The full 52-tool catalog (booking shortcuts, recurring invoices, templates, batch tools, etc.) is available via opt-in:
119
+ **27 tools** in the default `core` surface, focused on the daily-use workflows: read your books, invoice flow, transactions + banking, contract create / send / convert, Finnish validators. The full 52-tool catalog (booking shortcuts, recurring invoices, templates, batch tools, etc.) is available via opt-in:
98
120
 
99
121
  ```jsonc
100
122
  {
@@ -104,14 +126,14 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
104
126
  "args": ["-y", "@kirjapro/mcp-server"],
105
127
  "env": {
106
128
  "KIRJAPRO_API_KEY": "kp_your_key_here",
107
- "KIRJAPRO_MCP_SURFACE": "full" // 52 tools instead of 26
129
+ "KIRJAPRO_MCP_SURFACE": "full" // 52 tools instead of 27
108
130
  }
109
131
  }
110
132
  }
111
133
  }
112
134
  ```
113
135
 
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.
136
+ **Why default to 27 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
137
 
116
138
  **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
139
 
@@ -119,7 +141,7 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
119
141
  |---|---|
120
142
  | Read your books | `list_fiscal_years`, `list_accounts`, `get_dashboard`, `get_income_statement`, `get_balance_sheet`, `get_alv_summary`, `get_ar_aging` |
121
143
  | Customers | `list_customers`, `get_customer`, `create_customer`, `lookup_business` (PRH) |
122
- | Invoice lifecycle | `list_invoices`, `get_invoice`, `create_invoice`, `record_payment`, `update_invoice_status` |
144
+ | Invoice lifecycle | `list_invoices`, `get_invoice`, `create_invoice`, `send_invoice`, `record_payment`, `update_invoice_status` |
123
145
  | Transactions + banking | `list_transactions`, `create_transaction`, `list_bank_transactions`, `match_bank_transaction` |
124
146
  | Contracts | `create_offer` (`tarjous` / `sopimus`), `send_offer` (email link), `convert_offer_to_invoice` |
125
147
  | Validators (free) | `validate_business_id`, `validate_iban`, `generate_reference` |
package/dist/index.js CHANGED
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
10
14
  };
11
15
  var __export = (target, all) => {
12
16
  for (var name in all)
@@ -3105,6 +3109,9 @@ var require_utils = __commonJS({
3105
3109
  "use strict";
3106
3110
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3107
3111
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3112
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3113
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3114
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3108
3115
  function stringArrayToHexStripped(input) {
3109
3116
  let acc = "";
3110
3117
  let code = 0;
@@ -3297,27 +3304,77 @@ var require_utils = __commonJS({
3297
3304
  }
3298
3305
  return output.join("");
3299
3306
  }
3300
- function normalizeComponentEncoding(component, esc2) {
3301
- const func = esc2 !== true ? escape : unescape;
3302
- if (component.scheme !== void 0) {
3303
- component.scheme = func(component.scheme);
3304
- }
3305
- if (component.userinfo !== void 0) {
3306
- component.userinfo = func(component.userinfo);
3307
- }
3308
- if (component.host !== void 0) {
3309
- component.host = func(component.host);
3307
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3308
+ var HOST_DELIM_RE = /[@/?#:]/g;
3309
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3310
+ function reescapeHostDelimiters(host, isIP) {
3311
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3312
+ re.lastIndex = 0;
3313
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
3314
+ }
3315
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
3316
+ if (input.indexOf("%") === -1) {
3317
+ return input;
3310
3318
  }
3311
- if (component.path !== void 0) {
3312
- component.path = func(component.path);
3319
+ let output = "";
3320
+ for (let i = 0; i < input.length; i++) {
3321
+ if (input[i] === "%" && i + 2 < input.length) {
3322
+ const hex3 = input.slice(i + 1, i + 3);
3323
+ if (isHexPair(hex3)) {
3324
+ const normalizedHex = hex3.toUpperCase();
3325
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3326
+ if (decodeUnreserved && isUnreserved(decoded)) {
3327
+ output += decoded;
3328
+ } else {
3329
+ output += "%" + normalizedHex;
3330
+ }
3331
+ i += 2;
3332
+ continue;
3333
+ }
3334
+ }
3335
+ output += input[i];
3313
3336
  }
3314
- if (component.query !== void 0) {
3315
- component.query = func(component.query);
3337
+ return output;
3338
+ }
3339
+ function normalizePathEncoding(input) {
3340
+ let output = "";
3341
+ for (let i = 0; i < input.length; i++) {
3342
+ if (input[i] === "%" && i + 2 < input.length) {
3343
+ const hex3 = input.slice(i + 1, i + 3);
3344
+ if (isHexPair(hex3)) {
3345
+ const normalizedHex = hex3.toUpperCase();
3346
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3347
+ if (decoded !== "." && isUnreserved(decoded)) {
3348
+ output += decoded;
3349
+ } else {
3350
+ output += "%" + normalizedHex;
3351
+ }
3352
+ i += 2;
3353
+ continue;
3354
+ }
3355
+ }
3356
+ if (isPathCharacter(input[i])) {
3357
+ output += input[i];
3358
+ } else {
3359
+ output += escape(input[i]);
3360
+ }
3316
3361
  }
3317
- if (component.fragment !== void 0) {
3318
- component.fragment = func(component.fragment);
3362
+ return output;
3363
+ }
3364
+ function escapePreservingEscapes(input) {
3365
+ let output = "";
3366
+ for (let i = 0; i < input.length; i++) {
3367
+ if (input[i] === "%" && i + 2 < input.length) {
3368
+ const hex3 = input.slice(i + 1, i + 3);
3369
+ if (isHexPair(hex3)) {
3370
+ output += "%" + hex3.toUpperCase();
3371
+ i += 2;
3372
+ continue;
3373
+ }
3374
+ }
3375
+ output += escape(input[i]);
3319
3376
  }
3320
- return component;
3377
+ return output;
3321
3378
  }
3322
3379
  function recomposeAuthority(component) {
3323
3380
  const uriTokens = [];
@@ -3332,7 +3389,7 @@ var require_utils = __commonJS({
3332
3389
  if (ipV6res.isIPV6 === true) {
3333
3390
  host = `[${ipV6res.escapedHost}]`;
3334
3391
  } else {
3335
- host = component.host;
3392
+ host = reescapeHostDelimiters(host, false);
3336
3393
  }
3337
3394
  }
3338
3395
  uriTokens.push(host);
@@ -3346,7 +3403,10 @@ var require_utils = __commonJS({
3346
3403
  module.exports = {
3347
3404
  nonSimpleDomain,
3348
3405
  recomposeAuthority,
3349
- normalizeComponentEncoding,
3406
+ reescapeHostDelimiters,
3407
+ normalizePercentEncoding,
3408
+ normalizePathEncoding,
3409
+ escapePreservingEscapes,
3350
3410
  removeDotSegments,
3351
3411
  isIPv4,
3352
3412
  isUUID,
@@ -3570,12 +3630,12 @@ var require_schemes = __commonJS({
3570
3630
  var require_fast_uri = __commonJS({
3571
3631
  "node_modules/fast-uri/index.js"(exports, module) {
3572
3632
  "use strict";
3573
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3633
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3574
3634
  var { SCHEMES, getSchemeHandler } = require_schemes();
3575
3635
  function normalize(uri, options) {
3576
3636
  if (typeof uri === "string") {
3577
3637
  uri = /** @type {T} */
3578
- serialize(parse3(uri, options), options);
3638
+ normalizeString(uri, options);
3579
3639
  } else if (typeof uri === "object") {
3580
3640
  uri = /** @type {T} */
3581
3641
  parse3(serialize(uri, options), options);
@@ -3642,19 +3702,9 @@ var require_fast_uri = __commonJS({
3642
3702
  return target;
3643
3703
  }
3644
3704
  function equal(uriA, uriB, options) {
3645
- if (typeof uriA === "string") {
3646
- uriA = unescape(uriA);
3647
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
3648
- } else if (typeof uriA === "object") {
3649
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
3650
- }
3651
- if (typeof uriB === "string") {
3652
- uriB = unescape(uriB);
3653
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
3654
- } else if (typeof uriB === "object") {
3655
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
3656
- }
3657
- return uriA.toLowerCase() === uriB.toLowerCase();
3705
+ const normalizedA = normalizeComparableURI(uriA, options);
3706
+ const normalizedB = normalizeComparableURI(uriB, options);
3707
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3658
3708
  }
3659
3709
  function serialize(cmpts, opts) {
3660
3710
  const component = {
@@ -3679,12 +3729,12 @@ var require_fast_uri = __commonJS({
3679
3729
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3680
3730
  if (component.path !== void 0) {
3681
3731
  if (!options.skipEscape) {
3682
- component.path = escape(component.path);
3732
+ component.path = escapePreservingEscapes(component.path);
3683
3733
  if (component.scheme !== void 0) {
3684
3734
  component.path = component.path.split("%3A").join(":");
3685
3735
  }
3686
3736
  } else {
3687
- component.path = unescape(component.path);
3737
+ component.path = normalizePercentEncoding(component.path);
3688
3738
  }
3689
3739
  }
3690
3740
  if (options.reference !== "suffix" && component.scheme) {
@@ -3719,7 +3769,17 @@ var require_fast_uri = __commonJS({
3719
3769
  return uriTokens.join("");
3720
3770
  }
3721
3771
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3722
- function parse3(uri, opts) {
3772
+ var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3773
+ function getParseError(parsed, matches) {
3774
+ if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3775
+ return 'URI path must start with "/" when authority is present.';
3776
+ }
3777
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
3778
+ return "URI port is malformed.";
3779
+ }
3780
+ return void 0;
3781
+ }
3782
+ function parseWithStatus(uri, opts) {
3723
3783
  const options = Object.assign({}, opts);
3724
3784
  const parsed = {
3725
3785
  scheme: void 0,
@@ -3730,6 +3790,7 @@ var require_fast_uri = __commonJS({
3730
3790
  query: void 0,
3731
3791
  fragment: void 0
3732
3792
  };
3793
+ let malformedAuthorityOrPort = false;
3733
3794
  let isIP = false;
3734
3795
  if (options.reference === "suffix") {
3735
3796
  if (options.scheme) {
@@ -3738,6 +3799,11 @@ var require_fast_uri = __commonJS({
3738
3799
  uri = "//" + uri;
3739
3800
  }
3740
3801
  }
3802
+ const authorityMatch = uri.match(AUTHORITY_PREFIX);
3803
+ if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
3804
+ parsed.error = "URI authority must not contain a literal backslash.";
3805
+ malformedAuthorityOrPort = true;
3806
+ }
3741
3807
  const matches = uri.match(URI_PARSE);
3742
3808
  if (matches) {
3743
3809
  parsed.scheme = matches[1];
@@ -3750,6 +3816,11 @@ var require_fast_uri = __commonJS({
3750
3816
  if (isNaN(parsed.port)) {
3751
3817
  parsed.port = matches[5];
3752
3818
  }
3819
+ const parseError = getParseError(parsed, matches);
3820
+ if (parseError !== void 0) {
3821
+ parsed.error = parsed.error || parseError;
3822
+ malformedAuthorityOrPort = true;
3823
+ }
3753
3824
  if (parsed.host) {
3754
3825
  const ipv4result = isIPv4(parsed.host);
3755
3826
  if (ipv4result === false) {
@@ -3776,7 +3847,7 @@ var require_fast_uri = __commonJS({
3776
3847
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
3777
3848
  if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
3778
3849
  try {
3779
- parsed.host = URL.domainToASCII(parsed.host.toLowerCase());
3850
+ parsed.host = new URL("http://" + parsed.host).hostname;
3780
3851
  } catch (e) {
3781
3852
  parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
3782
3853
  }
@@ -3788,14 +3859,18 @@ var require_fast_uri = __commonJS({
3788
3859
  parsed.scheme = unescape(parsed.scheme);
3789
3860
  }
3790
3861
  if (parsed.host !== void 0) {
3791
- parsed.host = unescape(parsed.host);
3862
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3792
3863
  }
3793
3864
  }
3794
3865
  if (parsed.path) {
3795
- parsed.path = escape(unescape(parsed.path));
3866
+ parsed.path = normalizePathEncoding(parsed.path);
3796
3867
  }
3797
3868
  if (parsed.fragment) {
3798
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3869
+ try {
3870
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3871
+ } catch {
3872
+ parsed.error = parsed.error || "URI malformed";
3873
+ }
3799
3874
  }
3800
3875
  }
3801
3876
  if (schemeHandler && schemeHandler.parse) {
@@ -3804,7 +3879,29 @@ var require_fast_uri = __commonJS({
3804
3879
  } else {
3805
3880
  parsed.error = parsed.error || "URI can not be parsed.";
3806
3881
  }
3807
- return parsed;
3882
+ return { parsed, malformedAuthorityOrPort };
3883
+ }
3884
+ function parse3(uri, opts) {
3885
+ return parseWithStatus(uri, opts).parsed;
3886
+ }
3887
+ function normalizeString(uri, opts) {
3888
+ return normalizeStringWithStatus(uri, opts).normalized;
3889
+ }
3890
+ function normalizeStringWithStatus(uri, opts) {
3891
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3892
+ return {
3893
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3894
+ malformedAuthorityOrPort
3895
+ };
3896
+ }
3897
+ function normalizeComparableURI(uri, opts) {
3898
+ if (typeof uri === "string") {
3899
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3900
+ return malformedAuthorityOrPort ? void 0 : normalized;
3901
+ }
3902
+ if (typeof uri === "object") {
3903
+ return serialize(uri, opts);
3904
+ }
3808
3905
  }
3809
3906
  var fastUri = {
3810
3907
  SCHEMES,
@@ -32324,6 +32421,57 @@ function registerInvoiceTools(server2) {
32324
32421
  );
32325
32422
  }
32326
32423
  );
32424
+ server2.registerTool(
32425
+ "kirjapro_send_invoice",
32426
+ {
32427
+ description: "Deliver a draft sales invoice through email or Finnish e-invoice. The server generates the branded PDF/Finvoice payload, performs idempotency checks, finalizes the invoice as sent, and books the sales journal entry. Use email when the customer has an email address; use einvoice only when Maventa and the customer's e-invoice address are configured.",
32428
+ inputSchema: {
32429
+ invoiceId: external_exports.string().uuid().describe("Draft invoice UUID"),
32430
+ channel: external_exports.enum(["email", "einvoice"]).describe("Delivery channel: email or einvoice"),
32431
+ idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
32432
+ },
32433
+ annotations: WRITE_ANNOTATIONS
32434
+ },
32435
+ async (params) => {
32436
+ const supabase2 = getSupabase();
32437
+ const companyId2 = getCompanyId();
32438
+ return withIdempotency(
32439
+ supabase2,
32440
+ companyId2,
32441
+ "kirjapro_send_invoice",
32442
+ params.idempotencyKey,
32443
+ params,
32444
+ async () => {
32445
+ const { data: invoice, error: invoiceError } = await supabase2.from("invoices").select("id, invoice_number, status").eq("id", params.invoiceId).eq("company_id", companyId2).maybeSingle();
32446
+ if (invoiceError || !invoice) {
32447
+ return toErrorContent(`Invoice ${params.invoiceId} not found.`);
32448
+ }
32449
+ if (invoice.status !== "draft") {
32450
+ return toErrorContent(
32451
+ `Invoice ${invoice.invoice_number} cannot be delivered from status "${invoice.status}". Only draft invoices can be sent.`
32452
+ );
32453
+ }
32454
+ const { data, error: error51 } = await supabase2.functions.invoke("dispatch-invoice", {
32455
+ body: { invoiceId: invoice.id, channel: params.channel }
32456
+ });
32457
+ if (error51 || !data?.success) {
32458
+ return toErrorContent(
32459
+ data?.error || error51?.message || `Failed to deliver invoice ${invoice.invoice_number}.`
32460
+ );
32461
+ }
32462
+ return toResult({
32463
+ message: `Invoice ${invoice.invoice_number} delivered via ${params.channel}`,
32464
+ invoice_id: invoice.id,
32465
+ invoice_number: invoice.invoice_number,
32466
+ channel: data.channel,
32467
+ transmission_id: data.transmissionId ?? null,
32468
+ message_id: data.messageId ?? null,
32469
+ status: "sent"
32470
+ });
32471
+ }
32472
+ );
32473
+ }
32474
+ );
32327
32475
  server2.registerTool(
32328
32476
  "kirjapro_record_payment",
32329
32477
  {
@@ -36139,6 +36287,7 @@ var CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
36139
36287
  "kirjapro_list_invoices",
36140
36288
  "kirjapro_get_invoice",
36141
36289
  "kirjapro_create_invoice",
36290
+ "kirjapro_send_invoice",
36142
36291
  "kirjapro_record_payment",
36143
36292
  "kirjapro_update_invoice_status",
36144
36293
  // Transactions + banking
@@ -36162,7 +36311,7 @@ function resolveSurface(explicit) {
36162
36311
  if (env === "core") return "core";
36163
36312
  if (env && env !== "core") {
36164
36313
  console.error(
36165
- `[kirjapro-mcp] Unknown KIRJAPRO_MCP_SURFACE="${env}" \u2014 falling back to "core". Valid values: "core" (default, 26 tools), "full" (52 tools).`
36314
+ `[kirjapro-mcp] Unknown KIRJAPRO_MCP_SURFACE="${env}" \u2014 falling back to "core". Valid values: "core" (default, 27 tools), "full" (52 tools).`
36166
36315
  );
36167
36316
  }
36168
36317
  return "core";
@@ -36720,7 +36869,7 @@ function registerResources(server2) {
36720
36869
  }
36721
36870
 
36722
36871
  // src/version.ts
36723
- var VERSION = "0.6.3";
36872
+ var VERSION = "0.7.0";
36724
36873
 
36725
36874
  // src/index.ts
36726
36875
  var INSTRUCTIONS_COMMON = `
@@ -36750,7 +36899,6 @@ These tasks have no MCP equivalent in any surface mode. If a user asks for them,
36750
36899
 
36751
36900
  - **Document attachments / receipt OCR** \u2014 file uploads are web-only
36752
36901
  - **Bank account configuration** \u2014 Enable Banking connect, CAMT.053 import
36753
- - **E-invoice sending** \u2014 Finvoice via Maventa is web-driven
36754
36902
  - **Multi-company switching** \u2014 one API key is bound to one company
36755
36903
  - **Filling out year-end notes (liitetiedot)** \u2014 the form is web-only
36756
36904
 
@@ -36759,7 +36907,7 @@ Do not invent a "closest match" tool for these tasks. A confidently wrong action
36759
36907
  ## Tool Ordering & Workflows
36760
36908
 
36761
36909
  ### Invoice Lifecycle
36762
- \`kirjapro_create_customer\` (if new) \u2192 \`kirjapro_create_invoice\` (draft) \u2192 \`kirjapro_update_invoice_status\` (draft\u2192sent) \u2192 \`kirjapro_record_payment\` or \`kirjapro_match_bank_transaction\` \u2192 status becomes paid.
36910
+ \`kirjapro_create_customer\` (if new) \u2192 \`kirjapro_create_invoice\` (draft) \u2192 \`kirjapro_send_invoice\` (email/e-invoice) \u2192 \`kirjapro_record_payment\` or \`kirjapro_match_bank_transaction\` \u2192 status becomes paid.
36763
36911
 
36764
36912
  ### Offer / Contract \u2192 Invoice
36765
36913
  \`kirjapro_create_offer\` (type: \`tarjous\` or \`sopimus\`) \u2192 \`kirjapro_send_offer\` (emails the customer a public-token link). The customer's accept/reject click on the email link sets the offer status server-side \u2014 the agent does NOT need to manually update status. Once the user tells you the offer is accepted, call \`kirjapro_convert_offer_to_invoice\` to create a draft invoice, then follow the invoice lifecycle.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kirjapro/mcp-server",
3
- "version": "0.6.3",
4
- "description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude Code, Claude Desktop, and any MCP client.",
3
+ "version": "0.7.0",
4
+ "description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude, ChatGPT, Mistral, and other MCP clients.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "kirjapro-mcp": "dist/index.js"
@@ -30,7 +30,10 @@
30
30
  "finnish",
31
31
  "kirjanpito",
32
32
  "bookkeeping",
33
- "claude"
33
+ "claude",
34
+ "chatgpt",
35
+ "openai",
36
+ "mistral"
34
37
  ],
35
38
  "license": "MIT",
36
39
  "repository": {
@@ -74,5 +77,8 @@
74
77
  "server.json",
75
78
  "CHANGELOG.md",
76
79
  "tool-fingerprints.json"
77
- ]
80
+ ],
81
+ "allowScripts": {
82
+ "esbuild@0.28.1": true
83
+ }
78
84
  }
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@kirjapro/mcp-server",
3
3
  "displayName": "Kirjapro - Finnish Accounting",
4
4
  "description": "Finnish double-entry bookkeeping, invoicing, and financial reporting for growing businesses. Create transactions, generate invoices with VAT calculation, produce income statements and balance sheets, and validate Finnish business identifiers.",
5
- "version": "0.6.1",
5
+ "version": "0.7.0",
6
6
  "homepage": "https://kirjapro.fi",
7
7
  "repository": "https://github.com/Jaollo/kirja-pro",
8
8
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "required": true
17
17
  },
18
18
  "KIRJAPRO_MCP_SURFACE": {
19
- "description": "Tool surface: \"core\" (default, 26 daily-use tools) or \"full\" (all 52 tools).",
19
+ "description": "Tool surface: \"core\" (default, 27 daily-use tools) or \"full\" (all 52 tools).",
20
20
  "required": false
21
21
  }
22
22
  }
@@ -176,6 +176,10 @@
176
176
  "name": "kirjapro_search_transactions",
177
177
  "hash": "2c6afe63e538e0e8"
178
178
  },
179
+ {
180
+ "name": "kirjapro_send_invoice",
181
+ "hash": "caa9854eb9c88a4e"
182
+ },
179
183
  {
180
184
  "name": "kirjapro_send_offer",
181
185
  "hash": "363ba364cb4cba81"