@kirjapro/mcp-server 0.6.1 → 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 +19 -0
- package/README.md +29 -7
- package/dist/index.js +212 -102
- package/package.json +10 -4
- package/server.json +2 -2
- package/tool-fingerprints.json +4 -4
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,13 +1,13 @@
|
|
|
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
|
|
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
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
9
|
- Node.js 20+
|
|
10
|
-
- Kirjapro account on a paid tier with API access — **Yrittäj
|
|
10
|
+
- Kirjapro account on a paid tier with API access — **Yrittäjä** or **Kasvu**
|
|
11
11
|
- API key (generate at Settings > API-avaimet)
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
@@ -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
|
-
**
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
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
|
-
|
|
3312
|
-
|
|
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
|
-
|
|
3315
|
-
|
|
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
|
-
|
|
3318
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3866
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3796
3867
|
}
|
|
3797
3868
|
if (parsed.fragment) {
|
|
3798
|
-
|
|
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,
|
|
@@ -30996,8 +31093,9 @@ async function initLegacyAuth() {
|
|
|
30996
31093
|
`Cannot access company ${kirjaproCompanyId}. Verify the KIRJAPRO_COMPANY_ID is correct and your user has access. ` + (companyError?.message || "")
|
|
30997
31094
|
);
|
|
30998
31095
|
}
|
|
30999
|
-
const { data: subscription } = await supabase.from("subscriptions").select("tier").eq("user_id", userData.user.id).single();
|
|
31000
|
-
const
|
|
31096
|
+
const { data: subscription } = await supabase.from("subscriptions").select("tier, status").eq("user_id", userData.user.id).single();
|
|
31097
|
+
const hasActiveSub = subscription?.status === "active" || subscription?.status === "trialing";
|
|
31098
|
+
const tier = hasActiveSub ? subscription?.tier ?? "free" : "free";
|
|
31001
31099
|
const apiTiers = ["entrepreneur", "growth", "company", "custom"];
|
|
31002
31100
|
if (!apiTiers.includes(tier)) {
|
|
31003
31101
|
throw new Error(
|
|
@@ -32323,6 +32421,57 @@ function registerInvoiceTools(server2) {
|
|
|
32323
32421
|
);
|
|
32324
32422
|
}
|
|
32325
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
|
+
);
|
|
32326
32475
|
server2.registerTool(
|
|
32327
32476
|
"kirjapro_record_payment",
|
|
32328
32477
|
{
|
|
@@ -33772,7 +33921,15 @@ function registerBankingTools(server2) {
|
|
|
33772
33921
|
p_match_type: "invoice",
|
|
33773
33922
|
p_matched_invoice_id: params.invoiceId,
|
|
33774
33923
|
p_confidence: 100,
|
|
33775
|
-
|
|
33924
|
+
// Must be a value in the reconciliation_matches/bank_transactions
|
|
33925
|
+
// match_method CHECK constraint (see migration
|
|
33926
|
+
// 20260408044634_align_check_constraints_with_ts_unions.sql — 12
|
|
33927
|
+
// legal values). 'mcp_manual' is NOT in the set, so the RPC's
|
|
33928
|
+
// INSERT raised 23514 on every call (no tsc guard: p_match_method
|
|
33929
|
+
// is typed `string`; mocked tests never hit the real CHECK). The
|
|
33930
|
+
// agent-origin is already captured by the mcp-auth JWT + API-key
|
|
33931
|
+
// audit trail — don't smuggle it into a constraint-gated column.
|
|
33932
|
+
p_match_method: "manual"
|
|
33776
33933
|
}
|
|
33777
33934
|
);
|
|
33778
33935
|
if (matchError || !matchResult) {
|
|
@@ -34072,7 +34229,8 @@ var ACCOUNTS = {
|
|
|
34072
34229
|
VIENTI: "5070",
|
|
34073
34230
|
// ── Materials & Services (6xxx) ──
|
|
34074
34231
|
OSTOT: "6010",
|
|
34075
|
-
OSTOT_PALVELUT: "
|
|
34232
|
+
OSTOT_PALVELUT: "6200",
|
|
34233
|
+
// Ulkopuoliset palvelut (was 6030 = "Ostot 13,5 %", a materials acct)
|
|
34076
34234
|
// ── Expenses (7xxx) ──
|
|
34077
34235
|
PALKAT: "7010",
|
|
34078
34236
|
ELAKEKULUT: "7100",
|
|
@@ -34192,7 +34350,7 @@ var TEMPLATES = [
|
|
|
34192
34350
|
expand: saleCashTemplate(ACCOUNTS.MYYNTI_25_5, 25.5)
|
|
34193
34351
|
},
|
|
34194
34352
|
{
|
|
34195
|
-
id: "
|
|
34353
|
+
id: "cash_sale_13_5",
|
|
34196
34354
|
description: "K\xE4teismyynti ALV 13,5%",
|
|
34197
34355
|
vatRate: 13.5,
|
|
34198
34356
|
expand: saleCashTemplate(ACCOUNTS.MYYNTI_13_5, 13.5)
|
|
@@ -34217,7 +34375,7 @@ var TEMPLATES = [
|
|
|
34217
34375
|
expand: saleBankTemplate(ACCOUNTS.MYYNTI_25_5, 25.5)
|
|
34218
34376
|
},
|
|
34219
34377
|
{
|
|
34220
|
-
id: "
|
|
34378
|
+
id: "bank_sale_13_5",
|
|
34221
34379
|
description: "Pankkimyynti ALV 13,5%",
|
|
34222
34380
|
vatRate: 13.5,
|
|
34223
34381
|
expand: saleBankTemplate(ACCOUNTS.MYYNTI_13_5, 13.5)
|
|
@@ -35802,54 +35960,6 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
35802
35960
|
);
|
|
35803
35961
|
}
|
|
35804
35962
|
);
|
|
35805
|
-
server2.registerTool(
|
|
35806
|
-
"kirjapro_trigger_recurring_invoice",
|
|
35807
|
-
{
|
|
35808
|
-
description: "Manually trigger generation of a recurring invoice now, regardless of its next scheduled date. Returns the generated invoice.",
|
|
35809
|
-
inputSchema: {
|
|
35810
|
-
recurringInvoiceId: external_exports.string().uuid().describe("Recurring invoice UUID"),
|
|
35811
|
-
idempotencyKey: IDEMPOTENCY_KEY_SCHEMA
|
|
35812
|
-
},
|
|
35813
|
-
annotations: WRITE_ANNOTATIONS
|
|
35814
|
-
},
|
|
35815
|
-
async (params) => {
|
|
35816
|
-
const supabase2 = getSupabase();
|
|
35817
|
-
const companyId2 = getCompanyId();
|
|
35818
|
-
return withIdempotency(
|
|
35819
|
-
supabase2,
|
|
35820
|
-
companyId2,
|
|
35821
|
-
"kirjapro_trigger_recurring_invoice",
|
|
35822
|
-
params.idempotencyKey,
|
|
35823
|
-
params,
|
|
35824
|
-
async () => {
|
|
35825
|
-
const { data: existing, error: fetchError } = await supabase2.from("recurring_invoices").select("id, name, is_active").eq("id", params.recurringInvoiceId).eq("company_id", companyId2).single();
|
|
35826
|
-
if (fetchError || !existing) {
|
|
35827
|
-
return toErrorContent(
|
|
35828
|
-
`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`
|
|
35829
|
-
);
|
|
35830
|
-
}
|
|
35831
|
-
if (!existing.is_active) {
|
|
35832
|
-
return toErrorContent(
|
|
35833
|
-
`Recurring invoice "${existing.name}" is paused. Activate it first with kirjapro_update_recurring_invoice.`
|
|
35834
|
-
);
|
|
35835
|
-
}
|
|
35836
|
-
const result = await invokeEdgeFunction(
|
|
35837
|
-
supabase2,
|
|
35838
|
-
"generate-recurring-invoices",
|
|
35839
|
-
{ recurring_invoice_id: params.recurringInvoiceId, company_id: companyId2 }
|
|
35840
|
-
);
|
|
35841
|
-
if (result.error !== null) {
|
|
35842
|
-
return toErrorContent(result.error);
|
|
35843
|
-
}
|
|
35844
|
-
return toResult({
|
|
35845
|
-
message: `Invoice generated from "${existing.name}"`,
|
|
35846
|
-
invoice_id: result.data.invoice_id,
|
|
35847
|
-
invoice_number: result.data.invoice_number
|
|
35848
|
-
});
|
|
35849
|
-
}
|
|
35850
|
-
);
|
|
35851
|
-
}
|
|
35852
|
-
);
|
|
35853
35963
|
}
|
|
35854
35964
|
|
|
35855
35965
|
// src/tools/received-invoices.ts
|
|
@@ -36177,6 +36287,7 @@ var CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
36177
36287
|
"kirjapro_list_invoices",
|
|
36178
36288
|
"kirjapro_get_invoice",
|
|
36179
36289
|
"kirjapro_create_invoice",
|
|
36290
|
+
"kirjapro_send_invoice",
|
|
36180
36291
|
"kirjapro_record_payment",
|
|
36181
36292
|
"kirjapro_update_invoice_status",
|
|
36182
36293
|
// Transactions + banking
|
|
@@ -36200,7 +36311,7 @@ function resolveSurface(explicit) {
|
|
|
36200
36311
|
if (env === "core") return "core";
|
|
36201
36312
|
if (env && env !== "core") {
|
|
36202
36313
|
console.error(
|
|
36203
|
-
`[kirjapro-mcp] Unknown KIRJAPRO_MCP_SURFACE="${env}" \u2014 falling back to "core". Valid values: "core" (default,
|
|
36314
|
+
`[kirjapro-mcp] Unknown KIRJAPRO_MCP_SURFACE="${env}" \u2014 falling back to "core". Valid values: "core" (default, 27 tools), "full" (52 tools).`
|
|
36204
36315
|
);
|
|
36205
36316
|
}
|
|
36206
36317
|
return "core";
|
|
@@ -36758,7 +36869,7 @@ function registerResources(server2) {
|
|
|
36758
36869
|
}
|
|
36759
36870
|
|
|
36760
36871
|
// src/version.ts
|
|
36761
|
-
var VERSION = "0.
|
|
36872
|
+
var VERSION = "0.7.0";
|
|
36762
36873
|
|
|
36763
36874
|
// src/index.ts
|
|
36764
36875
|
var INSTRUCTIONS_COMMON = `
|
|
@@ -36788,7 +36899,6 @@ These tasks have no MCP equivalent in any surface mode. If a user asks for them,
|
|
|
36788
36899
|
|
|
36789
36900
|
- **Document attachments / receipt OCR** \u2014 file uploads are web-only
|
|
36790
36901
|
- **Bank account configuration** \u2014 Enable Banking connect, CAMT.053 import
|
|
36791
|
-
- **E-invoice sending** \u2014 Finvoice via Maventa is web-driven
|
|
36792
36902
|
- **Multi-company switching** \u2014 one API key is bound to one company
|
|
36793
36903
|
- **Filling out year-end notes (liitetiedot)** \u2014 the form is web-only
|
|
36794
36904
|
|
|
@@ -36797,7 +36907,7 @@ Do not invent a "closest match" tool for these tasks. A confidently wrong action
|
|
|
36797
36907
|
## Tool Ordering & Workflows
|
|
36798
36908
|
|
|
36799
36909
|
### Invoice Lifecycle
|
|
36800
|
-
\`kirjapro_create_customer\` (if new) \u2192 \`kirjapro_create_invoice\` (draft) \u2192 \`
|
|
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.
|
|
36801
36911
|
|
|
36802
36912
|
### Offer / Contract \u2192 Invoice
|
|
36803
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.
|
|
@@ -36899,7 +37009,7 @@ Faster than composing journal entries via \`kirjapro_create_transaction\`:
|
|
|
36899
37009
|
- \`kirjapro_get_transaction\` \u2014 single transaction with all journal entries
|
|
36900
37010
|
|
|
36901
37011
|
### Recurring + Received Invoices
|
|
36902
|
-
- \`kirjapro_list_recurring_invoices\`, \`kirjapro_create_recurring_invoice
|
|
37012
|
+
- \`kirjapro_list_recurring_invoices\`, \`kirjapro_create_recurring_invoice\` \u2014 manage templates
|
|
36903
37013
|
- \`kirjapro_list_received_invoices\`, \`kirjapro_get_received_invoice\` \u2014 read-only view of purchase invoices
|
|
36904
37014
|
- \`kirjapro_get_financial_notes\` \u2014 liitetiedot (KPL 3:1 / PMA 3:2 disclosures, year-end)
|
|
36905
37015
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Finnish accounting — exposes Kirjapro bookkeeping, invoicing, and reporting tools to Claude
|
|
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.
|
|
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,
|
|
19
|
+
"description": "Tool surface: \"core\" (default, 27 daily-use tools) or \"full\" (all 52 tools).",
|
|
20
20
|
"required": false
|
|
21
21
|
}
|
|
22
22
|
}
|
package/tool-fingerprints.json
CHANGED
|
@@ -177,12 +177,12 @@
|
|
|
177
177
|
"hash": "2c6afe63e538e0e8"
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
|
-
"name": "
|
|
181
|
-
"hash": "
|
|
180
|
+
"name": "kirjapro_send_invoice",
|
|
181
|
+
"hash": "caa9854eb9c88a4e"
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
|
-
"name": "
|
|
185
|
-
"hash": "
|
|
184
|
+
"name": "kirjapro_send_offer",
|
|
185
|
+
"hash": "363ba364cb4cba81"
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
"name": "kirjapro_update_customer",
|