@kirjapro/mcp-server 0.16.1 → 0.17.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 +27 -0
- package/README.md +3 -3
- package/dist/index.js +342 -113
- package/package.json +2 -2
- package/server.json +2 -2
- package/tool-fingerprints.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.0] - Unreleased
|
|
4
|
+
|
|
5
|
+
Needs database migration `20261013030000` before publication: until it is deployed the new
|
|
6
|
+
tool answers a permission error and its telemetry is refused. Every other tool keeps its
|
|
7
|
+
fingerprint; 0.16.x keeps working unchanged against the migrated database.
|
|
8
|
+
|
|
9
|
+
- New read tool `kirjapro_get_report_entries` (full surface): the ledger entries behind the
|
|
10
|
+
income statement's expense or revenue lines for one fiscal year, largest first, optionally
|
|
11
|
+
for one account and a date range inside the year, 1-20 per page with the whole-selection
|
|
12
|
+
`total` and `total_count` and `next_offset`. It reads the database function that AI
|
|
13
|
+
Chat's `getReportEntries` uses, so the same selection sums to the income statement; the
|
|
14
|
+
closing voucher of a closed year is excluded. Account names, voucher text and voucher
|
|
15
|
+
numbers are wrapped in `<untrusted-external>`.
|
|
16
|
+
- `kirjapro_get_dashboard` counts every invoice of the fiscal year. The invoice statistics
|
|
17
|
+
read the year's invoices without a limit, so a year above the Data API row cap was
|
|
18
|
+
silently undercounted; they are now read in pages with an exact count, and a page that
|
|
19
|
+
fails or changes mid-read fails the tool instead of returning partial statistics.
|
|
20
|
+
- `kirjapro_create_offer` checks each line's VAT treatment with the same check as
|
|
21
|
+
`kirjapro_create_invoice` and AI Chat, before an offer number is allocated, and answers
|
|
22
|
+
its `CODE: viesti` (for example `VAT_SELLER_NOT_REGISTERED` or `VAT_RATE_NOT_IN_FORCE`)
|
|
23
|
+
instead of the earlier English sentence.
|
|
24
|
+
- `kirjapro_create_recurring_invoice` and `kirjapro_update_recurring_invoice` (with new
|
|
25
|
+
`lines`) refuse a template whose invoices the database would refuse: each line's
|
|
26
|
+
generated treatment (the standard treatment of its rate, `exempt_other` for 0 %) goes
|
|
27
|
+
through the same check for the seller, the customer and the next generation date. A
|
|
28
|
+
seller outside the VAT register gets `VAT_SELLER_NOT_REGISTERED`.
|
|
29
|
+
|
|
3
30
|
## [0.16.1] - 2026-10-11
|
|
4
31
|
|
|
5
32
|
No database migration, and no tool definition changes: every fingerprint is unchanged and
|
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
|
|
|
135
135
|
|
|
136
136
|
## What's included
|
|
137
137
|
|
|
138
|
-
**29 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
|
|
138
|
+
**29 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 56-tool catalog (booking shortcuts, recurring invoices, templates, batch tools, etc.) is available via opt-in:
|
|
139
139
|
|
|
140
140
|
```jsonc
|
|
141
141
|
{
|
|
@@ -145,14 +145,14 @@ A few prompts to feel out the daily workflows the agent handles cleanly:
|
|
|
145
145
|
"args": ["-y", "@kirjapro/mcp-server"],
|
|
146
146
|
"env": {
|
|
147
147
|
"KIRJAPRO_API_KEY": "kp_your_key_here",
|
|
148
|
-
"KIRJAPRO_MCP_SURFACE": "full" //
|
|
148
|
+
"KIRJAPRO_MCP_SURFACE": "full" // 56 tools instead of 29
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
**Why default to 29 instead of
|
|
155
|
+
**Why default to 29 instead of 56:** large catalogs with overlapping tools cost
|
|
156
156
|
more context and can make selection harder, but there is no universal tool-count
|
|
157
157
|
cliff that applies to every model and task. Twenty-seven is a conservative
|
|
158
158
|
launch default for Kirjapro's daily workflows. Its own usage and error data,
|
package/dist/index.js
CHANGED
|
@@ -33089,6 +33089,9 @@ var Protocol = class {
|
|
|
33089
33089
|
this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
|
|
33090
33090
|
const handleTaskResult = async () => {
|
|
33091
33091
|
const taskId = request.params.taskId;
|
|
33092
|
+
if (!await this._taskStore.getTask(taskId, extra.sessionId)) {
|
|
33093
|
+
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
|
|
33094
|
+
}
|
|
33092
33095
|
if (this._taskMessageQueue) {
|
|
33093
33096
|
let queuedMessage;
|
|
33094
33097
|
while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) {
|
|
@@ -33119,12 +33122,12 @@ var Protocol = class {
|
|
|
33119
33122
|
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
|
|
33120
33123
|
}
|
|
33121
33124
|
if (!isTerminal(task.status)) {
|
|
33122
|
-
await this._waitForTaskUpdate(taskId, extra.signal);
|
|
33125
|
+
await this._waitForTaskUpdate(taskId, extra.signal, extra.sessionId);
|
|
33123
33126
|
return await handleTaskResult();
|
|
33124
33127
|
}
|
|
33125
33128
|
if (isTerminal(task.status)) {
|
|
33126
33129
|
const result = await this._taskStore.getTaskResult(taskId, extra.sessionId);
|
|
33127
|
-
this._clearTaskQueue(taskId);
|
|
33130
|
+
this._clearTaskQueue(taskId, extra.sessionId);
|
|
33128
33131
|
return {
|
|
33129
33132
|
...result,
|
|
33130
33133
|
_meta: {
|
|
@@ -33161,7 +33164,7 @@ var Protocol = class {
|
|
|
33161
33164
|
throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
|
|
33162
33165
|
}
|
|
33163
33166
|
await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId);
|
|
33164
|
-
this._clearTaskQueue(request.params.taskId);
|
|
33167
|
+
this._clearTaskQueue(request.params.taskId, extra.sessionId);
|
|
33165
33168
|
const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
|
|
33166
33169
|
if (!cancelledTask) {
|
|
33167
33170
|
throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
|
|
@@ -33289,6 +33292,19 @@ var Protocol = class {
|
|
|
33289
33292
|
const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
|
|
33290
33293
|
const capturedTransport = this._transport;
|
|
33291
33294
|
const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
|
|
33295
|
+
const sessionId = capturedTransport?.sessionId;
|
|
33296
|
+
const store = this._taskStore;
|
|
33297
|
+
let relatedTaskFound = true;
|
|
33298
|
+
let relatedTaskLookup;
|
|
33299
|
+
if (relatedTaskId && store && this._taskMessageQueue && sessionId !== void 0) {
|
|
33300
|
+
relatedTaskFound = false;
|
|
33301
|
+
relatedTaskLookup = (async () => {
|
|
33302
|
+
if (!await store.getTask(relatedTaskId, sessionId)) {
|
|
33303
|
+
throw new McpError(ErrorCode.InvalidParams, `Task not found: ${relatedTaskId}`);
|
|
33304
|
+
}
|
|
33305
|
+
relatedTaskFound = true;
|
|
33306
|
+
})();
|
|
33307
|
+
}
|
|
33292
33308
|
if (handler === void 0) {
|
|
33293
33309
|
const errorResponse = {
|
|
33294
33310
|
jsonrpc: "2.0",
|
|
@@ -33298,7 +33314,10 @@ var Protocol = class {
|
|
|
33298
33314
|
message: "Method not found"
|
|
33299
33315
|
}
|
|
33300
33316
|
};
|
|
33301
|
-
if (relatedTaskId &&
|
|
33317
|
+
if (relatedTaskId && relatedTaskLookup) {
|
|
33318
|
+
const queuedError = { type: "error", message: errorResponse, timestamp: Date.now() };
|
|
33319
|
+
relatedTaskLookup.then(() => this._enqueueTaskMessage(relatedTaskId, queuedError, sessionId), () => capturedTransport?.send(errorResponse)).catch((error61) => this._onerror(new Error(`Failed to send an error response: ${error61}`)));
|
|
33320
|
+
} else if (relatedTaskId && this._taskMessageQueue) {
|
|
33302
33321
|
this._enqueueTaskMessage(relatedTaskId, {
|
|
33303
33322
|
type: "error",
|
|
33304
33323
|
message: errorResponse,
|
|
@@ -33349,7 +33368,10 @@ var Protocol = class {
|
|
|
33349
33368
|
closeSSEStream: extra?.closeSSEStream,
|
|
33350
33369
|
closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
|
|
33351
33370
|
};
|
|
33352
|
-
Promise.resolve().then(() => {
|
|
33371
|
+
(relatedTaskLookup ?? Promise.resolve()).then(() => {
|
|
33372
|
+
if (relatedTaskLookup && abortController.signal.aborted) {
|
|
33373
|
+
throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
|
|
33374
|
+
}
|
|
33353
33375
|
if (taskCreationParams) {
|
|
33354
33376
|
this.assertTaskHandlerCapability(request.method);
|
|
33355
33377
|
}
|
|
@@ -33384,7 +33406,7 @@ var Protocol = class {
|
|
|
33384
33406
|
...error61["data"] !== void 0 && { data: error61["data"] }
|
|
33385
33407
|
}
|
|
33386
33408
|
};
|
|
33387
|
-
if (relatedTaskId && this._taskMessageQueue) {
|
|
33409
|
+
if (relatedTaskId && this._taskMessageQueue && relatedTaskFound) {
|
|
33388
33410
|
await this._enqueueTaskMessage(relatedTaskId, {
|
|
33389
33411
|
type: "error",
|
|
33390
33412
|
message: errorResponse,
|
|
@@ -33864,7 +33886,7 @@ var Protocol = class {
|
|
|
33864
33886
|
throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
|
|
33865
33887
|
}
|
|
33866
33888
|
const maxQueueSize = this._options?.maxTaskQueueSize;
|
|
33867
|
-
await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
|
|
33889
|
+
await this._taskMessageQueue.enqueue(taskId, message, sessionId ?? this._transport?.sessionId, maxQueueSize);
|
|
33868
33890
|
}
|
|
33869
33891
|
/**
|
|
33870
33892
|
* Clears the message queue for a task and rejects any pending request resolvers.
|
|
@@ -33893,12 +33915,13 @@ var Protocol = class {
|
|
|
33893
33915
|
* Uses polling to check for updates at the task's configured poll interval.
|
|
33894
33916
|
* @param taskId The task ID to wait for
|
|
33895
33917
|
* @param signal Abort signal to cancel the wait
|
|
33918
|
+
* @param sessionId Session of the request that waits, passed to the task store
|
|
33896
33919
|
* @returns Promise that resolves when an update occurs or rejects if aborted
|
|
33897
33920
|
*/
|
|
33898
|
-
async _waitForTaskUpdate(taskId, signal) {
|
|
33921
|
+
async _waitForTaskUpdate(taskId, signal, sessionId) {
|
|
33899
33922
|
let interval = this._options?.defaultTaskPollInterval ?? 1e3;
|
|
33900
33923
|
try {
|
|
33901
|
-
const task = await this._taskStore?.getTask(taskId);
|
|
33924
|
+
const task = await this._taskStore?.getTask(taskId, sessionId);
|
|
33902
33925
|
if (task?.pollInterval) {
|
|
33903
33926
|
interval = task.pollInterval;
|
|
33904
33927
|
}
|
|
@@ -34777,6 +34800,42 @@ var ExperimentalMcpServerTasks = class {
|
|
|
34777
34800
|
};
|
|
34778
34801
|
|
|
34779
34802
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
|
|
34803
|
+
function toolInputElementCount(value, max) {
|
|
34804
|
+
let count = 0;
|
|
34805
|
+
const stack = [value];
|
|
34806
|
+
while (stack.length > 0) {
|
|
34807
|
+
const node2 = stack.pop();
|
|
34808
|
+
if (node2 === null || typeof node2 !== "object")
|
|
34809
|
+
continue;
|
|
34810
|
+
if (Array.isArray(node2)) {
|
|
34811
|
+
for (const child of node2) {
|
|
34812
|
+
if (++count > max)
|
|
34813
|
+
return count;
|
|
34814
|
+
if (child !== null && typeof child === "object")
|
|
34815
|
+
stack.push(child);
|
|
34816
|
+
}
|
|
34817
|
+
} else {
|
|
34818
|
+
for (const key in node2) {
|
|
34819
|
+
if (!Object.prototype.hasOwnProperty.call(node2, key))
|
|
34820
|
+
continue;
|
|
34821
|
+
if (++count > max)
|
|
34822
|
+
return count;
|
|
34823
|
+
const child = node2[key];
|
|
34824
|
+
if (child !== null && typeof child === "object")
|
|
34825
|
+
stack.push(child);
|
|
34826
|
+
}
|
|
34827
|
+
}
|
|
34828
|
+
}
|
|
34829
|
+
return count;
|
|
34830
|
+
}
|
|
34831
|
+
function resolveMaxToolInputElements(value) {
|
|
34832
|
+
if (value === void 0 || value === Infinity)
|
|
34833
|
+
return void 0;
|
|
34834
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 1) {
|
|
34835
|
+
throw new RangeError(`maxToolInputElements must be a number of at least 1, or Infinity, got ${String(value)}`);
|
|
34836
|
+
}
|
|
34837
|
+
return value;
|
|
34838
|
+
}
|
|
34780
34839
|
var McpServer = class {
|
|
34781
34840
|
constructor(serverInfo, options) {
|
|
34782
34841
|
this._registeredResources = {};
|
|
@@ -34788,6 +34847,7 @@ var McpServer = class {
|
|
|
34788
34847
|
this._resourceHandlersInitialized = false;
|
|
34789
34848
|
this._promptHandlersInitialized = false;
|
|
34790
34849
|
this.server = new Server(serverInfo, options);
|
|
34850
|
+
this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
|
|
34791
34851
|
}
|
|
34792
34852
|
/**
|
|
34793
34853
|
* Access experimental features.
|
|
@@ -34918,12 +34978,15 @@ var McpServer = class {
|
|
|
34918
34978
|
* Validates tool input arguments against the tool's input schema.
|
|
34919
34979
|
*/
|
|
34920
34980
|
async validateToolInput(tool2, args, toolName) {
|
|
34981
|
+
if (this._maxToolInputElements !== void 0 && toolInputElementCount(args, this._maxToolInputElements) > this._maxToolInputElements) {
|
|
34982
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${toolName}: arguments contain more than the maximum of ${this._maxToolInputElements} elements`);
|
|
34983
|
+
}
|
|
34921
34984
|
if (!tool2.inputSchema) {
|
|
34922
34985
|
return void 0;
|
|
34923
34986
|
}
|
|
34924
34987
|
const inputObj = normalizeObjectSchema(tool2.inputSchema);
|
|
34925
34988
|
const schemaToParse = inputObj ?? tool2.inputSchema;
|
|
34926
|
-
const parseResult = await safeParseAsync2(schemaToParse, args);
|
|
34989
|
+
const parseResult = await safeParseAsync2(schemaToParse, args ?? {});
|
|
34927
34990
|
if (!parseResult.success) {
|
|
34928
34991
|
const error61 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
34929
34992
|
const errorMessage = getParseErrorMessage(error61);
|
|
@@ -35161,7 +35224,7 @@ var McpServer = class {
|
|
|
35161
35224
|
}
|
|
35162
35225
|
if (prompt.argsSchema) {
|
|
35163
35226
|
const argsObj = normalizeObjectSchema(prompt.argsSchema);
|
|
35164
|
-
const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
|
|
35227
|
+
const parseResult = await safeParseAsync2(argsObj, request.params.arguments ?? {});
|
|
35165
35228
|
if (!parseResult.success) {
|
|
35166
35229
|
const error61 = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
35167
35230
|
const errorMessage = getParseErrorMessage(error61);
|
|
@@ -35679,7 +35742,7 @@ var StdioServerTransport = class {
|
|
|
35679
35742
|
import { createClient } from "@supabase/supabase-js";
|
|
35680
35743
|
|
|
35681
35744
|
// src/version.ts
|
|
35682
|
-
var VERSION = "0.
|
|
35745
|
+
var VERSION = "0.17.0";
|
|
35683
35746
|
|
|
35684
35747
|
// src/auth.ts
|
|
35685
35748
|
var SUPABASE_PROJECT_URL = "https://tifewzsimmyoiepblhld.supabase.co";
|
|
@@ -36175,6 +36238,31 @@ function parseDashboardStats(data) {
|
|
|
36175
36238
|
return null;
|
|
36176
36239
|
return { revenue, expenses, transaction_count: transactionCount };
|
|
36177
36240
|
}
|
|
36241
|
+
var DASHBOARD_INVOICE_PAGE_SIZE = 1e3;
|
|
36242
|
+
var DASHBOARD_INVOICE_MAX_PAGES = 50;
|
|
36243
|
+
function inconsistentPage(state, page) {
|
|
36244
|
+
const { data, count } = page;
|
|
36245
|
+
return !data || count === null || !Number.isInteger(count) || count < 0 || state.expectedCount !== void 0 && count !== state.expectedCount || state.offset + data.length > count || data.length === 0 && state.offset < count;
|
|
36246
|
+
}
|
|
36247
|
+
function addDashboardInvoicePage(state, page) {
|
|
36248
|
+
if (inconsistentPage(state, page) || !page.data || page.count === null) {
|
|
36249
|
+
return {
|
|
36250
|
+
kind: "error",
|
|
36251
|
+
message: "The invoice statistics could not be read completely. Try again."
|
|
36252
|
+
};
|
|
36253
|
+
}
|
|
36254
|
+
const rows = [...state.rows, ...page.data];
|
|
36255
|
+
const offset = state.offset + page.data.length;
|
|
36256
|
+
const pages = (state.pages ?? 0) + 1;
|
|
36257
|
+
if (offset === page.count) return { kind: "done", rows };
|
|
36258
|
+
if (pages >= DASHBOARD_INVOICE_MAX_PAGES || page.count > DASHBOARD_INVOICE_MAX_PAGES * DASHBOARD_INVOICE_PAGE_SIZE) {
|
|
36259
|
+
return {
|
|
36260
|
+
kind: "error",
|
|
36261
|
+
message: `The fiscal year has too many invoices (${page.count}) for the dashboard statistics. Use kirjapro_list_invoices with a status filter.`
|
|
36262
|
+
};
|
|
36263
|
+
}
|
|
36264
|
+
return { kind: "next", state: { offset, rows, expectedCount: page.count, pages } };
|
|
36265
|
+
}
|
|
36178
36266
|
function toDashboardReport(stats, invoices, bankAccounts, today) {
|
|
36179
36267
|
const { revenue, expenses, transaction_count } = stats;
|
|
36180
36268
|
const invoiceStats = {
|
|
@@ -36243,7 +36331,7 @@ function tagPayments(payments) {
|
|
|
36243
36331
|
var CREDIT_PAGE_SIZE = 500;
|
|
36244
36332
|
function addCreditPage(state, page) {
|
|
36245
36333
|
const { data, count } = page;
|
|
36246
|
-
if (
|
|
36334
|
+
if (inconsistentPage(state, page) || !data || count === null) {
|
|
36247
36335
|
return {
|
|
36248
36336
|
kind: "error",
|
|
36249
36337
|
message: "The complete finalized credit balance could not be read. Refresh the invoice and try again."
|
|
@@ -39222,6 +39310,17 @@ async function resolveDefaultIncomeStatementTemplate(supabase2) {
|
|
|
39222
39310
|
cachedDefaultTemplateId = data.id;
|
|
39223
39311
|
return data.id;
|
|
39224
39312
|
}
|
|
39313
|
+
async function readDashboardInvoices(supabase2, companyId2, from, to) {
|
|
39314
|
+
let state = { offset: 0, rows: [] };
|
|
39315
|
+
for (; ; ) {
|
|
39316
|
+
const { data, count, error: error61 } = await supabase2.from("invoices").select(DASHBOARD_INVOICE_COLUMNS, { count: "exact" }).eq("company_id", companyId2).gte("invoice_date", from).lte("invoice_date", to).order("id", { ascending: true }).range(state.offset, state.offset + DASHBOARD_INVOICE_PAGE_SIZE - 1);
|
|
39317
|
+
if (error61) return { rows: null, error: sanitizeDbError("Failed to read dashboard", error61) };
|
|
39318
|
+
const step = addDashboardInvoicePage(state, { data, count });
|
|
39319
|
+
if (step.kind === "error") return { rows: null, error: step.message };
|
|
39320
|
+
if (step.kind === "done") return { rows: step.rows, error: null };
|
|
39321
|
+
state = step.state;
|
|
39322
|
+
}
|
|
39323
|
+
}
|
|
39225
39324
|
var INVOICE_STAT_BUCKET = external_exports.object({
|
|
39226
39325
|
count: external_exports.number(),
|
|
39227
39326
|
total: external_exports.number()
|
|
@@ -39405,6 +39504,59 @@ var GENERAL_LEDGER_OUTPUT_SHAPE = {
|
|
|
39405
39504
|
snapshot_at: external_exports.string()
|
|
39406
39505
|
})
|
|
39407
39506
|
};
|
|
39507
|
+
var REPORT_ENTRY_SHAPE = external_exports.object({
|
|
39508
|
+
transaction_id: external_exports.string(),
|
|
39509
|
+
date: external_exports.string(),
|
|
39510
|
+
document_number: external_exports.string().nullable(),
|
|
39511
|
+
description: external_exports.string().nullable(),
|
|
39512
|
+
account_number: external_exports.string(),
|
|
39513
|
+
account_name: external_exports.string(),
|
|
39514
|
+
amount: external_exports.number()
|
|
39515
|
+
});
|
|
39516
|
+
var REPORT_ENTRIES_OUTPUT_SHAPE = {
|
|
39517
|
+
kind: external_exports.enum(["expense", "revenue"]),
|
|
39518
|
+
fiscal_year_id: external_exports.string(),
|
|
39519
|
+
account: external_exports.object({ account_number: external_exports.string(), account_name: external_exports.string() }).nullable(),
|
|
39520
|
+
period: external_exports.object({ from: external_exports.string(), to: external_exports.string() }),
|
|
39521
|
+
total: external_exports.number(),
|
|
39522
|
+
total_formatted: external_exports.string(),
|
|
39523
|
+
total_count: external_exports.number(),
|
|
39524
|
+
shown_total: external_exports.number(),
|
|
39525
|
+
shown_count: external_exports.number(),
|
|
39526
|
+
offset: external_exports.number(),
|
|
39527
|
+
limit: external_exports.number(),
|
|
39528
|
+
truncated: external_exports.boolean(),
|
|
39529
|
+
next_offset: external_exports.number().nullable(),
|
|
39530
|
+
items: external_exports.array(REPORT_ENTRY_SHAPE)
|
|
39531
|
+
};
|
|
39532
|
+
var REPORT_ENTRIES_RPC = external_exports.object({
|
|
39533
|
+
status: external_exports.literal("ok"),
|
|
39534
|
+
companyId: external_exports.string(),
|
|
39535
|
+
fiscalYearId: external_exports.string(),
|
|
39536
|
+
kind: external_exports.enum(["expense", "revenue"]),
|
|
39537
|
+
account: external_exports.object({ accountNumber: external_exports.string(), accountName: external_exports.string() }).nullable(),
|
|
39538
|
+
period: external_exports.object({ from: external_exports.string(), to: external_exports.string() }),
|
|
39539
|
+
offset: external_exports.number().int(),
|
|
39540
|
+
limit: external_exports.number().int(),
|
|
39541
|
+
total: external_exports.number(),
|
|
39542
|
+
totalCount: external_exports.number().int(),
|
|
39543
|
+
shownTotal: external_exports.number(),
|
|
39544
|
+
shownCount: external_exports.number().int(),
|
|
39545
|
+
truncated: external_exports.boolean(),
|
|
39546
|
+
nextOffset: external_exports.number().int().nullable(),
|
|
39547
|
+
items: external_exports.array(
|
|
39548
|
+
external_exports.object({
|
|
39549
|
+
transactionId: external_exports.string(),
|
|
39550
|
+
date: external_exports.string(),
|
|
39551
|
+
documentNumber: external_exports.string().nullable(),
|
|
39552
|
+
description: external_exports.string().nullable(),
|
|
39553
|
+
accountNumber: external_exports.string(),
|
|
39554
|
+
accountName: external_exports.string(),
|
|
39555
|
+
amount: external_exports.number()
|
|
39556
|
+
})
|
|
39557
|
+
)
|
|
39558
|
+
});
|
|
39559
|
+
var REPORT_ENTRIES_DESCRIPTION = "Get the ledger entries behind the expense or revenue lines of the income statement for one fiscal year, largest amount first: optionally only one expense or revenue account (accountNumber) and a date range inside the year (dateFrom and dateTo together). total and total_count cover the whole selection; items is one page of 1-20 entries whose sum and count are shown_total and shown_count. For the next page pass next_offset as offset, and tell the user if total changed between pages. Amounts are the expense or revenue account amounts (deductible VAT is on its own account), so the same selection sums to the income statement. The closing voucher of a closed year is excluded, unlike in kirjapro_get_general_ledger. Each item names its voucher (transaction_id) for kirjapro_get_transaction. If fiscalYearId is omitted, the single open fiscal year is used.";
|
|
39408
39560
|
function registerReportTools(server2) {
|
|
39409
39561
|
server2.registerTool(
|
|
39410
39562
|
DASHBOARD_TOOL.name,
|
|
@@ -39438,18 +39590,19 @@ function registerReportTools(server2) {
|
|
|
39438
39590
|
p_company_id: companyId2,
|
|
39439
39591
|
p_fiscal_year_id: fiscalYearId
|
|
39440
39592
|
}),
|
|
39441
|
-
// Invoice stats scoped to fiscal year date range
|
|
39442
|
-
supabase2
|
|
39593
|
+
// Invoice stats scoped to the fiscal year date range, every page (U-64)
|
|
39594
|
+
readDashboardInvoices(supabase2, companyId2, fiscalYear.start_date, fiscalYear.end_date),
|
|
39443
39595
|
// Bank balances
|
|
39444
39596
|
supabase2.from("bank_accounts").select(BANK_BALANCE_COLUMNS).eq("company_id", companyId2).eq("is_active", true)
|
|
39445
39597
|
]);
|
|
39446
39598
|
const stats = parseDashboardStats(statsResult.data);
|
|
39447
|
-
const readError = statsResult.error ||
|
|
39599
|
+
const readError = statsResult.error || bankResult.error;
|
|
39448
39600
|
if (readError || !stats) {
|
|
39449
39601
|
return toErrorContent(readError ? sanitizeDbError("Failed to read dashboard", readError) : "Invalid dashboard response");
|
|
39450
39602
|
}
|
|
39603
|
+
if (invoiceResult.error !== null) return toErrorContent(invoiceResult.error);
|
|
39451
39604
|
return toResult(
|
|
39452
|
-
toDashboardReport(stats, invoiceResult.
|
|
39605
|
+
toDashboardReport(stats, invoiceResult.rows, bankResult.data || [], helsinkiDateString())
|
|
39453
39606
|
);
|
|
39454
39607
|
}
|
|
39455
39608
|
);
|
|
@@ -39672,6 +39825,97 @@ function registerReportTools(server2) {
|
|
|
39672
39825
|
);
|
|
39673
39826
|
}
|
|
39674
39827
|
);
|
|
39828
|
+
server2.registerTool(
|
|
39829
|
+
"kirjapro_get_report_entries",
|
|
39830
|
+
{
|
|
39831
|
+
description: REPORT_ENTRIES_DESCRIPTION,
|
|
39832
|
+
inputSchema: {
|
|
39833
|
+
kind: external_exports.enum(["expense", "revenue"]).default("expense").describe("expense = the expense lines (kulut), revenue = the revenue lines (tuotot)"),
|
|
39834
|
+
accountNumber: external_exports.string().regex(/^\d{3,8}$/).optional().describe('Only this expense or revenue account (e.g. "4000"); all of the kind when omitted'),
|
|
39835
|
+
fiscalYearId: external_exports.string().uuid().optional().describe("Fiscal year UUID (auto-resolved to the open fiscal year if omitted)"),
|
|
39836
|
+
dateFrom: ISO_DATE_SCHEMA.optional().describe("Start date inside the fiscal year (YYYY-MM-DD); give dateTo too"),
|
|
39837
|
+
dateTo: ISO_DATE_SCHEMA.optional().describe("End date inside the fiscal year (YYYY-MM-DD); give dateFrom too"),
|
|
39838
|
+
limit: external_exports.number().int().min(1).max(20).default(10).describe("Entries per page (default 10, max 20)"),
|
|
39839
|
+
offset: external_exports.number().int().min(0).max(1e6).default(0).describe("Entries to skip; next_offset of the previous page")
|
|
39840
|
+
},
|
|
39841
|
+
outputSchema: REPORT_ENTRIES_OUTPUT_SHAPE,
|
|
39842
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
39843
|
+
},
|
|
39844
|
+
async (params) => {
|
|
39845
|
+
const supabase2 = getSupabase();
|
|
39846
|
+
const companyId2 = getCompanyId();
|
|
39847
|
+
if (params.dateFrom === void 0 !== (params.dateTo === void 0)) {
|
|
39848
|
+
return toErrorContent("Give both dateFrom and dateTo, or neither (the whole fiscal year).");
|
|
39849
|
+
}
|
|
39850
|
+
if (params.dateFrom && params.dateTo) {
|
|
39851
|
+
const rangeError = assertDateRange(params.dateFrom, params.dateTo);
|
|
39852
|
+
if (rangeError) return rangeError;
|
|
39853
|
+
}
|
|
39854
|
+
let fiscalYearId;
|
|
39855
|
+
if (params.fiscalYearId) {
|
|
39856
|
+
fiscalYearId = params.fiscalYearId;
|
|
39857
|
+
} else {
|
|
39858
|
+
const resolved = await resolveOpenFiscalYear(supabase2, companyId2);
|
|
39859
|
+
if ("isError" in resolved) return resolved;
|
|
39860
|
+
fiscalYearId = resolved.id;
|
|
39861
|
+
}
|
|
39862
|
+
const { data, error: error61 } = await supabase2.rpc("get_report_entries", {
|
|
39863
|
+
p_company_id: companyId2,
|
|
39864
|
+
p_fiscal_year_id: fiscalYearId,
|
|
39865
|
+
p_kind: params.kind,
|
|
39866
|
+
p_account_number: params.accountNumber,
|
|
39867
|
+
p_date_from: params.dateFrom,
|
|
39868
|
+
p_date_to: params.dateTo,
|
|
39869
|
+
p_limit: params.limit,
|
|
39870
|
+
p_offset: params.offset
|
|
39871
|
+
});
|
|
39872
|
+
if (error61) {
|
|
39873
|
+
if (error61.code === "P0002" && params.accountNumber) {
|
|
39874
|
+
return toErrorContent(
|
|
39875
|
+
`Account "${params.accountNumber}" is not ${params.kind === "revenue" ? "a revenue" : "an expense"} account of this company. Use kirjapro_list_accounts to see the accounts and their types.`
|
|
39876
|
+
);
|
|
39877
|
+
}
|
|
39878
|
+
if (error61.code === "22023") {
|
|
39879
|
+
return toErrorContent(
|
|
39880
|
+
`Failed to get report entries: ${error61.message}. Read kirjapro://fiscal-years for the fiscal years and their dates.`
|
|
39881
|
+
);
|
|
39882
|
+
}
|
|
39883
|
+
return toErrorContent(sanitizeDbError("Failed to get report entries", error61));
|
|
39884
|
+
}
|
|
39885
|
+
const parsed = REPORT_ENTRIES_RPC.safeParse(data);
|
|
39886
|
+
if (!parsed.success || parsed.data.companyId !== companyId2 || parsed.data.fiscalYearId !== fiscalYearId) {
|
|
39887
|
+
return toErrorContent("Failed to get report entries: unexpected response. Please try again.");
|
|
39888
|
+
}
|
|
39889
|
+
const report = parsed.data;
|
|
39890
|
+
return toResult({
|
|
39891
|
+
kind: report.kind,
|
|
39892
|
+
fiscal_year_id: report.fiscalYearId,
|
|
39893
|
+
account: report.account ? {
|
|
39894
|
+
account_number: report.account.accountNumber,
|
|
39895
|
+
account_name: tagUntrusted(report.account.accountName)
|
|
39896
|
+
} : null,
|
|
39897
|
+
period: { from: report.period.from, to: report.period.to },
|
|
39898
|
+
total: report.total,
|
|
39899
|
+
total_formatted: formatCurrency(report.total),
|
|
39900
|
+
total_count: report.totalCount,
|
|
39901
|
+
shown_total: report.shownTotal,
|
|
39902
|
+
shown_count: report.shownCount,
|
|
39903
|
+
offset: report.offset,
|
|
39904
|
+
limit: report.limit,
|
|
39905
|
+
truncated: report.truncated,
|
|
39906
|
+
next_offset: report.nextOffset,
|
|
39907
|
+
items: report.items.map((item) => ({
|
|
39908
|
+
transaction_id: item.transactionId,
|
|
39909
|
+
date: item.date,
|
|
39910
|
+
document_number: tagUntrusted(item.documentNumber),
|
|
39911
|
+
description: tagUntrustedText(item.description),
|
|
39912
|
+
account_number: item.accountNumber,
|
|
39913
|
+
account_name: tagUntrusted(item.accountName),
|
|
39914
|
+
amount: item.amount
|
|
39915
|
+
}))
|
|
39916
|
+
});
|
|
39917
|
+
}
|
|
39918
|
+
);
|
|
39675
39919
|
}
|
|
39676
39920
|
|
|
39677
39921
|
// src/tools/banking.ts
|
|
@@ -41953,82 +42197,8 @@ function registerBookingTools(server2) {
|
|
|
41953
42197
|
);
|
|
41954
42198
|
}
|
|
41955
42199
|
|
|
41956
|
-
// src/vat.ts
|
|
41957
|
-
var EU_COUNTRY_CODES2 = /* @__PURE__ */ new Set([
|
|
41958
|
-
"AT",
|
|
41959
|
-
"BE",
|
|
41960
|
-
"BG",
|
|
41961
|
-
"HR",
|
|
41962
|
-
"CY",
|
|
41963
|
-
"CZ",
|
|
41964
|
-
"DE",
|
|
41965
|
-
"DK",
|
|
41966
|
-
"EE",
|
|
41967
|
-
"ES",
|
|
41968
|
-
"FI",
|
|
41969
|
-
"FR",
|
|
41970
|
-
"GR",
|
|
41971
|
-
"HU",
|
|
41972
|
-
"IE",
|
|
41973
|
-
"IT",
|
|
41974
|
-
"LT",
|
|
41975
|
-
"LU",
|
|
41976
|
-
"LV",
|
|
41977
|
-
"MT",
|
|
41978
|
-
"NL",
|
|
41979
|
-
"PL",
|
|
41980
|
-
"PT",
|
|
41981
|
-
"RO",
|
|
41982
|
-
"SE",
|
|
41983
|
-
"SI",
|
|
41984
|
-
"SK"
|
|
41985
|
-
]);
|
|
41986
|
-
function normalizedVatNumber(value) {
|
|
41987
|
-
return (value ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
41988
|
-
}
|
|
41989
|
-
function validateVatTreatmentEligibility({
|
|
41990
|
-
code,
|
|
41991
|
-
sellerAlvRegistered,
|
|
41992
|
-
customer,
|
|
41993
|
-
documentDate
|
|
41994
|
-
}) {
|
|
41995
|
-
if (!sellerAlvRegistered && code !== "not_vat_registered") {
|
|
41996
|
-
return "A seller outside the VAT register can only use not_vat_registered.";
|
|
41997
|
-
}
|
|
41998
|
-
if (sellerAlvRegistered && code === "not_vat_registered") {
|
|
41999
|
-
return "A VAT-registered seller cannot use not_vat_registered.";
|
|
42000
|
-
}
|
|
42001
|
-
if (code === "standard_25_5" && documentDate < "2024-09-01") {
|
|
42002
|
-
return "standard_25_5 is not valid before 2024-09-01.";
|
|
42003
|
-
}
|
|
42004
|
-
if (code === "standard_13_5" && documentDate < "2026-01-01") {
|
|
42005
|
-
return "standard_13_5 is not valid before 2026-01-01.";
|
|
42006
|
-
}
|
|
42007
|
-
const requiresBusiness = code.startsWith("reverse_") || code === "intra_eu_goods" || code === "service_outside_eu";
|
|
42008
|
-
if (requiresBusiness && customer?.customer_kind !== "business") {
|
|
42009
|
-
return "The selected VAT treatment requires a business customer.";
|
|
42010
|
-
}
|
|
42011
|
-
if ((code === "reverse_construction" || code === "reverse_scrap") && !isValidFinnishBusinessId(customer?.business_id ?? "")) {
|
|
42012
|
-
return "Domestic reverse charge requires a Finnish buyer business ID in the form 1234567-8.";
|
|
42013
|
-
}
|
|
42014
|
-
if (code === "reverse_eu_service" || code === "intra_eu_goods") {
|
|
42015
|
-
const country = (customer?.country ?? "").trim().toUpperCase();
|
|
42016
|
-
const vatNumber = normalizedVatNumber(customer?.vat_number);
|
|
42017
|
-
const expectedPrefix = country === "GR" ? "EL" : country;
|
|
42018
|
-
if (!EU_COUNTRY_CODES2.has(country) || country === "FI" || !/^[A-Z]{2}[A-Z0-9]{2,12}$/.test(vatNumber) || !vatNumber.startsWith(expectedPrefix)) {
|
|
42019
|
-
return "EU sale requires a non-Finnish EU country and a matching buyer VAT number.";
|
|
42020
|
-
}
|
|
42021
|
-
}
|
|
42022
|
-
if (code === "export_outside_eu" || code === "service_outside_eu") {
|
|
42023
|
-
const country = (customer?.country ?? "").trim().toUpperCase();
|
|
42024
|
-
if (!country || EU_COUNTRY_CODES2.has(country)) {
|
|
42025
|
-
return "Export outside the EU requires a buyer country outside the EU.";
|
|
42026
|
-
}
|
|
42027
|
-
}
|
|
42028
|
-
return null;
|
|
42029
|
-
}
|
|
42030
|
-
|
|
42031
42200
|
// src/tools/offers.ts
|
|
42201
|
+
var NO_CUSTOMER = { customer_kind: "none", business_id: null, vat_number: null, country: null };
|
|
42032
42202
|
var OFFER_DETAIL_OUTPUT_SHAPE = {
|
|
42033
42203
|
id: external_exports.string(),
|
|
42034
42204
|
offer_number: external_exports.string(),
|
|
@@ -42239,16 +42409,6 @@ function registerOfferTools(server2) {
|
|
|
42239
42409
|
`validUntil (${params.validUntil}) must not be earlier than validFrom (${params.validFrom}).`
|
|
42240
42410
|
);
|
|
42241
42411
|
}
|
|
42242
|
-
const { data: offerNumberRaw, error: numberError } = await supabase2.rpc(
|
|
42243
|
-
"get_next_offer_number",
|
|
42244
|
-
{ p_company_id: companyId2, p_type: params.type }
|
|
42245
|
-
);
|
|
42246
|
-
if (numberError || !offerNumberRaw) {
|
|
42247
|
-
return toErrorContent(
|
|
42248
|
-
numberError ? sanitizeDbError("Failed to generate offer number", numberError) : "Failed to generate offer number"
|
|
42249
|
-
);
|
|
42250
|
-
}
|
|
42251
|
-
const offerNumber = offerNumberRaw;
|
|
42252
42412
|
const resolveTreatment = (line) => {
|
|
42253
42413
|
const inputRate = parseFloat(line.vatRate);
|
|
42254
42414
|
return line.vatTreatmentCode ?? (inputRate === 25.5 ? "standard_25_5" : inputRate === 13.5 ? "standard_13_5" : inputRate === 10 ? "standard_10" : null);
|
|
@@ -42259,14 +42419,24 @@ function registerOfferTools(server2) {
|
|
|
42259
42419
|
for (const line of params.lines) {
|
|
42260
42420
|
const code = resolveTreatment(line);
|
|
42261
42421
|
if (!code) continue;
|
|
42262
|
-
const
|
|
42422
|
+
const ineligible = invoiceVatTreatmentError(
|
|
42263
42423
|
code,
|
|
42264
|
-
|
|
42265
|
-
customer,
|
|
42266
|
-
|
|
42267
|
-
|
|
42268
|
-
if (
|
|
42424
|
+
company.alv_registered,
|
|
42425
|
+
customer ?? NO_CUSTOMER,
|
|
42426
|
+
params.validFrom ?? helsinkiDateString()
|
|
42427
|
+
);
|
|
42428
|
+
if (ineligible) return toErrorContent(`${ineligible.code}: ${ineligible.message}`);
|
|
42429
|
+
}
|
|
42430
|
+
const { data: offerNumberRaw, error: numberError } = await supabase2.rpc(
|
|
42431
|
+
"get_next_offer_number",
|
|
42432
|
+
{ p_company_id: companyId2, p_type: params.type }
|
|
42433
|
+
);
|
|
42434
|
+
if (numberError || !offerNumberRaw) {
|
|
42435
|
+
return toErrorContent(
|
|
42436
|
+
numberError ? sanitizeDbError("Failed to generate offer number", numberError) : "Failed to generate offer number"
|
|
42437
|
+
);
|
|
42269
42438
|
}
|
|
42439
|
+
const offerNumber = offerNumberRaw;
|
|
42270
42440
|
const calculatedLines = params.lines.map((line, i) => {
|
|
42271
42441
|
const vatTreatmentCode = resolveTreatment(line) ?? "exempt_other";
|
|
42272
42442
|
const vatRate = vatTreatmentCode === "standard_25_5" ? 25.5 : vatTreatmentCode === "standard_13_5" ? 13.5 : vatTreatmentCode === "standard_10" ? 10 : 0;
|
|
@@ -42600,6 +42770,36 @@ function registerOfferTools(server2) {
|
|
|
42600
42770
|
}
|
|
42601
42771
|
|
|
42602
42772
|
// src/tools/recurring-invoices.ts
|
|
42773
|
+
function generatedTreatment(line, supplyDate) {
|
|
42774
|
+
const stored = typeof line.vat_rate === "number" ? line.vat_rate : Number.NaN;
|
|
42775
|
+
const rate = stored === 24 ? supplyDate <= "2024-08-31" ? 24 : 25.5 : stored === 14 ? supplyDate <= "2025-12-31" ? 14 : 13.5 : [25.5, 13.5, 10, 0].includes(stored) ? stored : 25.5;
|
|
42776
|
+
const code = typeof line.vat_treatment_code === "string" ? line.vat_treatment_code : void 0;
|
|
42777
|
+
if (code === "legacy_standard_14" && rate === 13.5) return "standard_13_5";
|
|
42778
|
+
if (code === "legacy_standard_24" && rate === 25.5) return "standard_25_5";
|
|
42779
|
+
if (code) return code;
|
|
42780
|
+
if (rate === 25.5) return "standard_25_5";
|
|
42781
|
+
if (rate === 13.5) return "standard_13_5";
|
|
42782
|
+
if (rate === 10) return "standard_10";
|
|
42783
|
+
return "exempt_other";
|
|
42784
|
+
}
|
|
42785
|
+
var toTemplateLines = (lines) => lines.map((line) => ({ vat_rate: parseFloat(line.vatRate) }));
|
|
42786
|
+
async function recurringVatError(supabase2, companyId2, customer, lines, supplyDate) {
|
|
42787
|
+
const { data: company, error: error61 } = await supabase2.from("companies").select("alv_registered").eq("id", companyId2).single();
|
|
42788
|
+
if (error61 || !company) return "Failed to verify the seller VAT registration status.";
|
|
42789
|
+
for (const line of lines) {
|
|
42790
|
+
const ineligible = invoiceVatTreatmentError(
|
|
42791
|
+
generatedTreatment(line, supplyDate),
|
|
42792
|
+
company.alv_registered,
|
|
42793
|
+
customer,
|
|
42794
|
+
supplyDate
|
|
42795
|
+
);
|
|
42796
|
+
if (ineligible?.code === "VAT_SELLER_NOT_REGISTERED") {
|
|
42797
|
+
return `${ineligible.code}: ${ineligible.message} Toistuvan laskun pohja ALV-rekisteriin kuulumattomalle myyj\xE4lle tehd\xE4\xE4n Kirjapron verkkosovelluksessa (Laskut \u2192 Toistuvat laskut).`;
|
|
42798
|
+
}
|
|
42799
|
+
if (ineligible) return `${ineligible.code}: ${ineligible.message}`;
|
|
42800
|
+
}
|
|
42801
|
+
return null;
|
|
42802
|
+
}
|
|
42603
42803
|
var RECURRING_INVOICE_DETAIL_OUTPUT_SHAPE = {
|
|
42604
42804
|
id: external_exports.string(),
|
|
42605
42805
|
name: external_exports.string(),
|
|
@@ -42727,7 +42927,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
42727
42927
|
params.idempotencyKey,
|
|
42728
42928
|
params,
|
|
42729
42929
|
async () => {
|
|
42730
|
-
const { data: customer, error: custError } = await supabase2.from("customers").select("id, name").eq("id", params.customerId).eq("company_id", companyId2).maybeSingle();
|
|
42930
|
+
const { data: customer, error: custError } = await supabase2.from("customers").select("id, name, customer_kind, business_id, vat_number, country").eq("id", params.customerId).eq("company_id", companyId2).maybeSingle();
|
|
42731
42931
|
if (custError || !customer) {
|
|
42732
42932
|
return toErrorContent(
|
|
42733
42933
|
`Customer ${params.customerId} not found. Use kirjapro_list_customers to find customer IDs.`
|
|
@@ -42738,6 +42938,14 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
42738
42938
|
`endDate (${params.endDate}) must not be earlier than nextDate (${params.nextDate}). A template with endDate before nextDate would never generate an invoice.`
|
|
42739
42939
|
);
|
|
42740
42940
|
}
|
|
42941
|
+
const vatError = await recurringVatError(
|
|
42942
|
+
supabase2,
|
|
42943
|
+
companyId2,
|
|
42944
|
+
customer,
|
|
42945
|
+
toTemplateLines(params.lines),
|
|
42946
|
+
params.nextDate
|
|
42947
|
+
);
|
|
42948
|
+
if (vatError) return toErrorContent(vatError);
|
|
42741
42949
|
const templateData = {
|
|
42742
42950
|
lines: params.lines.map((l, i) => ({
|
|
42743
42951
|
line_number: i + 1,
|
|
@@ -42800,7 +43008,7 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
42800
43008
|
async (params) => {
|
|
42801
43009
|
const supabase2 = getSupabase();
|
|
42802
43010
|
const companyId2 = getCompanyId();
|
|
42803
|
-
const { data: existing, error: fetchError } = await supabase2.from("recurring_invoices").select("id, name, template_data, next_date, end_date").eq("id", params.recurringInvoiceId).eq("company_id", companyId2).single();
|
|
43011
|
+
const { data: existing, error: fetchError } = await supabase2.from("recurring_invoices").select("id, name, customer_id, template_data, next_date, end_date").eq("id", params.recurringInvoiceId).eq("company_id", companyId2).single();
|
|
42804
43012
|
if (fetchError || !existing) {
|
|
42805
43013
|
return toErrorContent(
|
|
42806
43014
|
`Recurring invoice ${params.recurringInvoiceId} not found. Use kirjapro_list_recurring_invoices to find IDs.`
|
|
@@ -42813,6 +43021,24 @@ function registerRecurringInvoiceTools(server2) {
|
|
|
42813
43021
|
`endDate (${effectiveEnd}) must not be earlier than nextDate (${effectiveNext}). Either clear endDate (set to null) or advance nextDate first.`
|
|
42814
43022
|
);
|
|
42815
43023
|
}
|
|
43024
|
+
const storedLines = existing.template_data?.lines;
|
|
43025
|
+
const checkedLines = params.lines !== void 0 ? toTemplateLines(params.lines) : params.nextDate !== void 0 && Array.isArray(storedLines) ? storedLines : null;
|
|
43026
|
+
if (checkedLines && checkedLines.length > 0) {
|
|
43027
|
+
const { data: customer, error: custError } = await supabase2.from("customers").select("customer_kind, business_id, vat_number, country").eq("id", existing.customer_id).eq("company_id", companyId2).maybeSingle();
|
|
43028
|
+
if (custError || !customer) {
|
|
43029
|
+
return toErrorContent(
|
|
43030
|
+
"The customer of this recurring invoice was not found. Use kirjapro_get_recurring_invoice to check it."
|
|
43031
|
+
);
|
|
43032
|
+
}
|
|
43033
|
+
const vatError = await recurringVatError(
|
|
43034
|
+
supabase2,
|
|
43035
|
+
companyId2,
|
|
43036
|
+
customer,
|
|
43037
|
+
checkedLines,
|
|
43038
|
+
effectiveNext ?? helsinkiDateString()
|
|
43039
|
+
);
|
|
43040
|
+
if (vatError) return toErrorContent(vatError);
|
|
43041
|
+
}
|
|
42816
43042
|
const updates = {};
|
|
42817
43043
|
if (params.name !== void 0) updates.name = params.name;
|
|
42818
43044
|
if (params.frequency !== void 0) updates.frequency = params.frequency;
|
|
@@ -43330,7 +43556,8 @@ var TOOL_REGISTRATION_ORDER = [
|
|
|
43330
43556
|
"kirjapro_get_received_invoice",
|
|
43331
43557
|
"kirjapro_get_financial_notes",
|
|
43332
43558
|
"kirjapro_get_general_ledger",
|
|
43333
|
-
"kirjapro_unreconcile_bank_transaction"
|
|
43559
|
+
"kirjapro_unreconcile_bank_transaction",
|
|
43560
|
+
"kirjapro_get_report_entries"
|
|
43334
43561
|
];
|
|
43335
43562
|
var CORE_TOOL_NAMES = new Set(
|
|
43336
43563
|
TOOL_REGISTRATION_ORDER.slice(0, 29)
|
|
@@ -43342,7 +43569,7 @@ function resolveSurface(explicit) {
|
|
|
43342
43569
|
if (env === "core") return "core";
|
|
43343
43570
|
if (env && env !== "core") {
|
|
43344
43571
|
console.error(
|
|
43345
|
-
`[kirjapro-mcp] Unknown KIRJAPRO_MCP_SURFACE="${env}" \u2014 falling back to "core". Valid values: "core" (default, 29 tools), "full" (
|
|
43572
|
+
`[kirjapro-mcp] Unknown KIRJAPRO_MCP_SURFACE="${env}" \u2014 falling back to "core". Valid values: "core" (default, 29 tools), "full" (56 tools).`
|
|
43346
43573
|
);
|
|
43347
43574
|
}
|
|
43348
43575
|
return "core";
|
|
@@ -43411,7 +43638,7 @@ function registerTools(server2, options = {}) {
|
|
|
43411
43638
|
);
|
|
43412
43639
|
} else {
|
|
43413
43640
|
console.error(
|
|
43414
|
-
`[kirjapro-mcp] surface=full \u2014 registered all
|
|
43641
|
+
`[kirjapro-mcp] surface=full \u2014 registered all 56 tools (CORE_TOOL_NAMES bypass active).`
|
|
43415
43642
|
);
|
|
43416
43643
|
}
|
|
43417
43644
|
}
|
|
@@ -43959,6 +44186,7 @@ var DATA_HANDLING = {
|
|
|
43959
44186
|
{ tool: "kirjapro_list_transactions / kirjapro_search_transactions", fields: ["description", "document_number", "entries[].account (the account name part)"], source: "Typed or imported by users; a bank reconciliation match persists the counterparty name into the description" },
|
|
43960
44187
|
{ tool: "kirjapro_get_transaction", fields: ["description", "document_number", "journal_entries[].description", "journal_entries[].accounts.name"], source: "As above" },
|
|
43961
44188
|
{ tool: "kirjapro_get_general_ledger", fields: ["account.name", "entries[].description", "entries[].document_number"], source: "As above" },
|
|
44189
|
+
{ tool: "kirjapro_get_report_entries", fields: ["account.account_name", "items[].account_name", "items[].description", "items[].document_number"], source: "As above" },
|
|
43962
44190
|
{ tool: "kirjapro_list_bank_accounts", fields: ["account_name", "bank_name", "bic"], source: "Typed by users or provided by the bank (the IBAN is validated by the database and not wrapped)" },
|
|
43963
44191
|
{ tool: "kirjapro_list_bank_transactions", fields: ["counterparty_name", "counterparty_iban", "reference", "message", "currency"], source: "Paying party via CAMT.053 bank feed (the currency has no database constraint, as on received invoices)" },
|
|
43964
44192
|
{ tool: "kirjapro_list_bank_link_candidates", fields: ["label", "document_number"], source: "Purchase-invoice sender name and number, invoice number or voucher description and number of the link candidate" },
|
|
@@ -44244,6 +44472,7 @@ Faster than composing journal entries via \`kirjapro_create_transaction\`:
|
|
|
44244
44472
|
|
|
44245
44473
|
### Extended Reports
|
|
44246
44474
|
- \`kirjapro_get_general_ledger\` (p\xE4\xE4kirja) \u2014 exact full-period summary plus a creation-time-pinned seek-cursor page (max 100 entries); pass \`next_cursor\` unchanged while \`has_more\`
|
|
44475
|
+
- \`kirjapro_get_report_entries\` \u2014 the entries behind the income statement's expense or revenue lines (largest first, 1-20 per page, optionally one account or a date range); pass \`next_offset\` as \`offset\`
|
|
44247
44476
|
- \`kirjapro_search_transactions\` \u2014 text + date + amount range search
|
|
44248
44477
|
- \`kirjapro_get_transaction\` \u2014 single transaction with all journal entries
|
|
44249
44478
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirjapro/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
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": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"node": ">=20"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
56
56
|
"@supabase/supabase-js": "^2.105.4",
|
|
57
57
|
"zod": "^4.4.3"
|
|
58
58
|
},
|
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.17.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, 29 daily-use tools) or \"full\" (all
|
|
19
|
+
"description": "Tool surface: \"core\" (default, 29 daily-use tools) or \"full\" (all 56 tools).",
|
|
20
20
|
"required": false
|
|
21
21
|
},
|
|
22
22
|
"KIRJAPRO_TELEMETRY": {
|
package/tool-fingerprints.json
CHANGED
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
"name": "kirjapro_get_recurring_invoice",
|
|
113
113
|
"hash": "0e6c6a29c1f06fc6"
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
"name": "kirjapro_get_report_entries",
|
|
117
|
+
"hash": "bd721bb442565288"
|
|
118
|
+
},
|
|
115
119
|
{
|
|
116
120
|
"name": "kirjapro_get_transaction",
|
|
117
121
|
"hash": "676af0ec73d52cba"
|