@open-nav/mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 open-nav contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This repository vendors XML schemas, sample documents and validation message
26
+ catalogues published by the Hungarian Tax and Customs Administration
27
+ (Nemzeti Adó- és Vámhivatal) under the MIT license. Those files live under
28
+ `schemas/` and `conformance/`, retain NAV's copyright, and are covered by
29
+ `schemas/NAV-LICENCE.md`. See `schemas/README.md` for provenance.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @open-nav/mcp
2
+
3
+ An MCP server that gives an AI agent the NAV **Online Számla** invoice
4
+ service: validate invoice data, look up a rejection, render an invoice,
5
+ report to NAV, and read back what was reported.
6
+
7
+ ## Setup
8
+
9
+ ```sh
10
+ claude mcp add open-nav -- npx -y @open-nav/mcp
11
+ ```
12
+
13
+ Or in `.mcp.json`:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "open-nav": {
19
+ "command": "npx",
20
+ "args": ["-y", "@open-nav/mcp"],
21
+ "env": {
22
+ "NAV_ENVIRONMENT": "test",
23
+ "NAV_LOGIN": "...",
24
+ "NAV_PASSWORD": "...",
25
+ "NAV_SIGN_KEY": "...",
26
+ "NAV_EXCHANGE_KEY": "...",
27
+ "NAV_TAX_NUMBER": "12345678",
28
+ "NAV_SOFTWARE_ID": "..."
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Same variables as the CLI, so one configuration serves both.
36
+ `NAV_TAX_NUMBER` is the **8 digit** core number, not the 11 digit form.
37
+
38
+ ## Tools
39
+
40
+ Four work with no credentials at all:
41
+
42
+ | Tool | What it does |
43
+ | ------------------- | ---------------------------------------------------------------------------------------- |
44
+ | `validate_invoice` | Check invoice data against the schema and the business rules, with NAV's own fault codes |
45
+ | `lookup_fault_code` | Explain a NAV fault code in NAV's words, in `hu`, `en` or `de` |
46
+ | `render_invoice` | Render a printable invoice, with the phrases the VAT Act requires |
47
+ | `export_invoices` | Build the data export required by decree 23/2014. (VI. 30.) NGM |
48
+
49
+ Five more appear once credentials are configured:
50
+
51
+ | Tool | What it does |
52
+ | -------------------- | ------------------------------------------------------ |
53
+ | `query_taxpayer` | The authoritative check on a Hungarian tax number |
54
+ | `submit_invoices` | Report invoice data; validates locally first |
55
+ | `transaction_status` | The verdict on a submission, optionally waiting for it |
56
+ | `list_invoices` | Invoices issued by you or to you, in a date range |
57
+ | `get_invoice` | One invoice in full, as reported |
58
+
59
+ ## Decisions that matter for an agent
60
+
61
+ **Tools that cannot work are not offered.** Without credentials the NAV tools
62
+ are not registered at all, so an agent is never handed a tool that is
63
+ guaranteed to fail, and never has to discover that by calling it.
64
+
65
+ **A validation failure is a result, not an error.** The fault list _is_ the
66
+ answer, and returning it as an error would hide it behind an exception. Real
67
+ errors — a document that is not an invoice, a NAV rejection — do come back as
68
+ errors, carrying the fault code so the agent can act on it.
69
+
70
+ **`submit_invoices` validates before sending** and refuses a document with
71
+ errors. A rejection costs a round trip and consumes the request id, and an
72
+ agent that retries blindly would burn both.
73
+
74
+ **Credentials never travel as tool arguments.** A tool argument passes through
75
+ the agent's context and its transcript; an environment variable does not.
76
+
77
+ **Everything is structured content**, not prose to be re-parsed.
78
+
79
+ ## Trying it without credentials
80
+
81
+ ```sh
82
+ npx @open-nav/mock-server # prints fake credentials
83
+ ```
84
+
85
+ Point `NAV_BASE_URL` at the mock and the whole tool set works, including
86
+ submission, against a service that verifies signatures the way NAV does. That
87
+ is how this package's own tests exercise it.
88
+
89
+ ## Licence
90
+
91
+ MIT. Not affiliated with NAV.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createNavMcpServer } from './server.js';
4
+ import { resolveConfig } from './config.js';
5
+ const config = resolveConfig();
6
+ if (config.missing.length > 0) {
7
+ // stderr, never stdout: stdout carries the MCP protocol.
8
+ process.stderr.write(`open-nav MCP: running with the offline tools only.\n` +
9
+ `Set ${config.missing.join(', ')} to enable the tools that talk to NAV.\n`);
10
+ }
11
+ const server = createNavMcpServer({
12
+ ...(config.credentials ? { credentials: config.credentials } : {}),
13
+ ...(config.software ? { software: config.software } : {}),
14
+ environment: config.environment,
15
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
16
+ });
17
+ await server.connect(new StdioServerTransport());
18
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;AAE/B,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IAC9B,yDAAyD;IACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,sDAAsD;QACpD,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,0CAA0C,CAC7E,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,kBAAkB,CAAC;IAChC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACzD,WAAW,EAAE,MAAM,CAAC,WAAW;IAC/B,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;CACvD,CAAC,CAAC;AAEH,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { SoftwareType } from '@open-nav/core';
2
+ import type { NavCredentials } from '@open-nav/client';
3
+ /**
4
+ * Configuration from the environment, using the same variables as the CLI so
5
+ * one `.env` serves both.
6
+ *
7
+ * Credentials are never taken as tool arguments. A tool argument travels
8
+ * through the agent's context and its transcript; an environment variable does
9
+ * not.
10
+ */
11
+ export interface ResolvedConfig {
12
+ credentials?: NavCredentials;
13
+ software?: SoftwareType;
14
+ environment: 'test' | 'production';
15
+ baseUrl?: string;
16
+ /** Which required variables are absent, if any. */
17
+ missing: string[];
18
+ }
19
+ export declare function resolveConfig(env?: Record<string, string | undefined>): ResolvedConfig;
20
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGvD;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,YAAY,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAWD,wBAAgB,aAAa,CAC3B,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,cAAc,CAsChB"}
package/dist/config.js ADDED
@@ -0,0 +1,45 @@
1
+ import { VERSION } from './version.js';
2
+ const REQUIRED = [
3
+ 'NAV_LOGIN',
4
+ 'NAV_PASSWORD',
5
+ 'NAV_SIGN_KEY',
6
+ 'NAV_EXCHANGE_KEY',
7
+ 'NAV_TAX_NUMBER',
8
+ 'NAV_SOFTWARE_ID',
9
+ ];
10
+ export function resolveConfig(env = process.env) {
11
+ const missing = REQUIRED.filter((name) => !env[name]);
12
+ const environment = env['NAV_ENVIRONMENT'] === 'production' ? 'production' : 'test';
13
+ if (missing.length > 0) {
14
+ return {
15
+ environment,
16
+ missing,
17
+ ...(env['NAV_BASE_URL'] ? { baseUrl: env['NAV_BASE_URL'] } : {}),
18
+ };
19
+ }
20
+ const operation = env['NAV_SOFTWARE_OPERATION'] === 'ONLINE_SERVICE' ? 'ONLINE_SERVICE' : 'LOCAL_SOFTWARE';
21
+ return {
22
+ credentials: {
23
+ login: env['NAV_LOGIN'],
24
+ password: env['NAV_PASSWORD'],
25
+ signKey: env['NAV_SIGN_KEY'],
26
+ exchangeKey: env['NAV_EXCHANGE_KEY'],
27
+ taxNumber: env['NAV_TAX_NUMBER'],
28
+ },
29
+ software: {
30
+ softwareId: env['NAV_SOFTWARE_ID'],
31
+ softwareName: env['NAV_SOFTWARE_NAME'] ?? 'open-nav',
32
+ softwareOperation: operation,
33
+ softwareMainVersion: env['NAV_SOFTWARE_VERSION'] ?? VERSION,
34
+ softwareDevName: env['NAV_SOFTWARE_DEV_NAME'] ?? 'open-nav',
35
+ softwareDevContact: env['NAV_SOFTWARE_DEV_CONTACT'] ?? 'unknown',
36
+ ...(env['NAV_SOFTWARE_DEV_TAX_NUMBER']
37
+ ? { softwareDevTaxNumber: env['NAV_SOFTWARE_DEV_TAX_NUMBER'] }
38
+ : {}),
39
+ },
40
+ environment,
41
+ ...(env['NAV_BASE_URL'] ? { baseUrl: env['NAV_BASE_URL'] } : {}),
42
+ missing: [],
43
+ };
44
+ }
45
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAmBvC,MAAM,QAAQ,GAAG;IACf,WAAW;IACX,cAAc;IACd,cAAc;IACd,kBAAkB;IAClB,gBAAgB;IAChB,iBAAiB;CACT,CAAC;AAEX,MAAM,UAAU,aAAa,CAC3B,MAA0C,OAAO,CAAC,GAAG;IAErD,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,MAAM,WAAW,GAAG,GAAG,CAAC,iBAAiB,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC;IAEpF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,WAAW;YACX,OAAO;YACP,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjE,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GACb,GAAG,CAAC,wBAAwB,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAE3F,OAAO;QACL,WAAW,EAAE;YACX,KAAK,EAAE,GAAG,CAAC,WAAW,CAAE;YACxB,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAE;YAC9B,OAAO,EAAE,GAAG,CAAC,cAAc,CAAE;YAC7B,WAAW,EAAE,GAAG,CAAC,kBAAkB,CAAE;YACrC,SAAS,EAAE,GAAG,CAAC,gBAAgB,CAAE;SAClC;QACD,QAAQ,EAAE;YACR,UAAU,EAAE,GAAG,CAAC,iBAAiB,CAAE;YACnC,YAAY,EAAE,GAAG,CAAC,mBAAmB,CAAC,IAAI,UAAU;YACpD,iBAAiB,EAAE,SAAS;YAC5B,mBAAmB,EAAE,GAAG,CAAC,sBAAsB,CAAC,IAAI,OAAO;YAC3D,eAAe,EAAE,GAAG,CAAC,uBAAuB,CAAC,IAAI,UAAU;YAC3D,kBAAkB,EAAE,GAAG,CAAC,0BAA0B,CAAC,IAAI,SAAS;YAChE,GAAG,CAAC,GAAG,CAAC,6BAA6B,CAAC;gBACpC,CAAC,CAAC,EAAE,oBAAoB,EAAE,GAAG,CAAC,6BAA6B,CAAC,EAAE;gBAC9D,CAAC,CAAC,EAAE,CAAC;SACR;QACD,WAAW;QACX,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,OAAO,EAAE,EAAE;KACZ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './server.js';
2
+ export * from './config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './server.js';
2
+ export * from './config.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type SoftwareType } from '@open-nav/core';
3
+ import { NavClient, type NavCredentials } from '@open-nav/client';
4
+ /**
5
+ * MCP server for the Online Számla invoice service.
6
+ *
7
+ * Shaped for an agent rather than for a person. Three things follow from
8
+ * that:
9
+ *
10
+ * - Tools that need no credentials are always registered, so an agent can
11
+ * validate and render invoices before anyone has a technical user.
12
+ * - Tools that reach NAV are registered only when credentials are present,
13
+ * so an agent is never offered a tool that cannot work, and never has to
14
+ * discover that by calling it.
15
+ * - Every result is structured content, not prose, and a validation failure
16
+ * is a normal result rather than an error: the fault list *is* the answer.
17
+ */
18
+ export interface NavMcpOptions {
19
+ /** Credentials. Omit to run with the offline tools only. */
20
+ credentials?: NavCredentials;
21
+ software?: SoftwareType;
22
+ environment?: 'test' | 'production';
23
+ baseUrl?: string;
24
+ /** Injected for tests. */
25
+ createClient?: (options: {
26
+ credentials: NavCredentials;
27
+ }) => NavClient;
28
+ }
29
+ export declare function createNavMcpServer(options?: NavMcpOptions): McpServer;
30
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EASL,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAsB,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAItF;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,WAAW,EAAE,cAAc,CAAA;KAAE,KAAK,SAAS,CAAC;CACxE;AAiDD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,aAAkB,GAAG,SAAS,CAWzE"}
package/dist/server.js ADDED
@@ -0,0 +1,361 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { NavApiError, decodeInvoiceData, faultMessage, parseDocument, parseTaxNumber, serializeDocument, validateInvoice, } from '@open-nav/core';
4
+ import { NavClient, waitForTransaction } from '@open-nav/client';
5
+ import { createDataExport, renderInvoiceHtml } from '@open-nav/invoicing';
6
+ import { VERSION } from './version.js';
7
+ const OPERATION = z
8
+ .enum(['CREATE', 'MODIFY', 'STORNO'])
9
+ .describe('The operation the document will be submitted under');
10
+ const LANGUAGE = z.enum(['en', 'hu', 'de']).describe("Language for NAV's own fault descriptions");
11
+ const DIRECTION = z
12
+ .enum(['OUTBOUND', 'INBOUND'])
13
+ .describe('OUTBOUND for invoices you issued, INBOUND for invoices issued to you');
14
+ /** Wrap a value as an MCP tool result carrying both text and structure. */
15
+ function result(value) {
16
+ return {
17
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
18
+ structuredContent: value,
19
+ };
20
+ }
21
+ /** An error an agent can act on, rather than a stack trace. */
22
+ function failure(error) {
23
+ const detail = error instanceof NavApiError
24
+ ? {
25
+ message: error.message,
26
+ errorCode: error.errorCode,
27
+ funcCode: error.funcCode,
28
+ status: error.status,
29
+ validationMessages: error.validationMessages,
30
+ }
31
+ : { message: error.message ?? String(error) };
32
+ return { content: [{ type: 'text', text: JSON.stringify(detail, null, 2) }], isError: true };
33
+ }
34
+ function parseInvoice(xml) {
35
+ const parsed = parseDocument(xml, { unknownElements: 'ignore' });
36
+ if (parsed.root !== 'InvoiceData') {
37
+ throw new Error(`Expected an InvoiceData document, found ${parsed.root}`);
38
+ }
39
+ return parsed.value;
40
+ }
41
+ export function createNavMcpServer(options = {}) {
42
+ const server = new McpServer({ name: 'open-nav', version: VERSION });
43
+ registerOfflineTools(server);
44
+ if (options.credentials && options.software) {
45
+ registerOnlineTools(server, {
46
+ ...options,
47
+ credentials: options.credentials,
48
+ software: options.software,
49
+ });
50
+ }
51
+ return server;
52
+ }
53
+ /** Tools that contact nothing and need no credentials. */
54
+ function registerOfflineTools(server) {
55
+ server.registerTool('validate_invoice', {
56
+ title: 'Validate invoice data',
57
+ description: 'Check NAV invoice data (InvoiceData XML) against the schema and the business rules ' +
58
+ 'that are decidable from the document alone. Returns every finding with NAV’s own ' +
59
+ 'fault code, so a failure here names what the service would have rejected. Needs no ' +
60
+ 'credentials. Errors block a submission; warnings do not.',
61
+ inputSchema: {
62
+ xml: z.string().describe('The InvoiceData XML document to check'),
63
+ operation: OPERATION.optional(),
64
+ language: LANGUAGE.optional(),
65
+ },
66
+ annotations: { readOnlyHint: true, openWorldHint: false },
67
+ }, async ({ xml, operation, language }) => {
68
+ try {
69
+ const report = validateInvoice(parseInvoice(xml), {
70
+ ...(operation ? { operation } : {}),
71
+ ...(language ? { language } : {}),
72
+ });
73
+ // A failed validation is the answer, not an error.
74
+ return result({
75
+ valid: report.valid,
76
+ errorCount: report.errors.length,
77
+ warningCount: report.warnings.length,
78
+ issues: report.issues,
79
+ });
80
+ }
81
+ catch (error) {
82
+ return failure(error);
83
+ }
84
+ });
85
+ server.registerTool('lookup_fault_code', {
86
+ title: 'Look up a NAV fault code',
87
+ description: 'Explain a NAV validation fault code, such as INCORRECT_SUMMARY_CALCULATION_' +
88
+ "INVOICE_VAT_AMOUNT_SUMMARY, in NAV's own words. Use this to interpret a rejection.",
89
+ inputSchema: {
90
+ code: z.string().describe('The fault code, as NAV reported it'),
91
+ language: LANGUAGE.optional(),
92
+ },
93
+ annotations: { readOnlyHint: true, openWorldHint: false },
94
+ }, async ({ code, language }) => {
95
+ const normalised = code.trim().toUpperCase();
96
+ const message = faultMessage(normalised, language ?? 'en');
97
+ if (!message) {
98
+ return failure(new Error(`NAV defines no fault code ${normalised}`));
99
+ }
100
+ return result({ code: normalised, language: language ?? 'en', message });
101
+ });
102
+ server.registerTool('render_invoice', {
103
+ title: 'Render a printable invoice',
104
+ description: 'Render NAV invoice data as a self-contained, printable HTML invoice, with the phrases ' +
105
+ 'the VAT Act requires derived from the data. Convert to PDF with any browser. Needs no ' +
106
+ 'credentials.',
107
+ inputSchema: {
108
+ xml: z.string().describe('The InvoiceData XML document to render'),
109
+ language: z
110
+ .enum(['hu', 'en'])
111
+ .optional()
112
+ .describe('Document language, Hungarian by default'),
113
+ note: z.string().optional().describe('Extra note printed under the totals'),
114
+ },
115
+ annotations: { readOnlyHint: true, openWorldHint: false },
116
+ }, async ({ xml, language, note }) => {
117
+ try {
118
+ const html = renderInvoiceHtml(parseInvoice(xml), {
119
+ ...(language ? { language } : {}),
120
+ ...(note ? { note } : {}),
121
+ });
122
+ return {
123
+ content: [{ type: 'text', text: html }],
124
+ structuredContent: { html, bytes: html.length },
125
+ };
126
+ }
127
+ catch (error) {
128
+ return failure(error);
129
+ }
130
+ });
131
+ server.registerTool('export_invoices', {
132
+ title: 'Produce the tax authority data export',
133
+ description: 'Build the data export required of invoicing programs by decree 23/2014. (VI. 30.) NGM ' +
134
+ 'from a set of InvoiceData documents, selected by issue date or invoice number range. ' +
135
+ 'Returns the files rather than writing them. Needs no credentials.',
136
+ inputSchema: {
137
+ invoices: z.array(z.string()).describe('InvoiceData XML documents'),
138
+ issueDateFrom: z.string().optional().describe('First issue date, inclusive, as yyyy-mm-dd'),
139
+ issueDateTo: z.string().optional().describe('Last issue date, inclusive'),
140
+ invoiceNumberFrom: z.string().optional(),
141
+ invoiceNumberTo: z.string().optional(),
142
+ },
143
+ annotations: { readOnlyHint: true, openWorldHint: false },
144
+ }, async ({ invoices, ...selection }) => {
145
+ try {
146
+ const parsed = invoices.map((xml) => parseInvoice(xml));
147
+ const exported = createDataExport(parsed, selection);
148
+ return result({
149
+ manifest: exported.manifest,
150
+ files: exported.files,
151
+ });
152
+ }
153
+ catch (error) {
154
+ return failure(error);
155
+ }
156
+ });
157
+ }
158
+ /** Tools that talk to NAV. Registered only when credentials are configured. */
159
+ function registerOnlineTools(server, options) {
160
+ const client = () => options.createClient
161
+ ? options.createClient({ credentials: options.credentials })
162
+ : new NavClient({
163
+ credentials: options.credentials,
164
+ software: options.software,
165
+ environment: options.environment ?? 'test',
166
+ ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
167
+ });
168
+ server.registerTool('query_taxpayer', {
169
+ title: 'Look up a Hungarian taxpayer',
170
+ description: "Check whether a Hungarian tax number is valid and registered, and get the taxpayer's " +
171
+ 'name. This is the authoritative check on a tax number; a check digit is not. Accepts ' +
172
+ 'the 8 digit core or the 11 digit written form.',
173
+ inputSchema: { taxNumber: z.string().describe('8 or 11 digit Hungarian tax number') },
174
+ annotations: { readOnlyHint: true, openWorldHint: true },
175
+ }, async ({ taxNumber }) => {
176
+ try {
177
+ const { taxpayerId } = parseTaxNumber(taxNumber);
178
+ const response = await client().queryTaxpayer({ taxNumber: taxpayerId });
179
+ return result({
180
+ taxNumber: taxpayerId,
181
+ valid: response.taxpayerValidity ?? false,
182
+ taxpayer: response.taxpayerData ?? null,
183
+ });
184
+ }
185
+ catch (error) {
186
+ return failure(error);
187
+ }
188
+ });
189
+ server.registerTool('submit_invoices', {
190
+ title: 'Submit invoice data to NAV',
191
+ description: 'Report invoice data to NAV. Validates locally first and refuses to submit a document ' +
192
+ 'with errors, because a rejection costs a round trip and consumes the request id. ' +
193
+ 'Returns the transaction id, and the per-invoice verdict when waitForVerdict is set. ' +
194
+ 'This reports data to the tax authority and cannot be undone except by a technical ' +
195
+ 'annulment.',
196
+ inputSchema: {
197
+ invoices: z.array(z.string()).describe('InvoiceData XML documents to report'),
198
+ operation: OPERATION.optional(),
199
+ compress: z.boolean().optional().describe('Gzip the payloads'),
200
+ waitForVerdict: z
201
+ .boolean()
202
+ .optional()
203
+ .describe('Poll until NAV decides, and return the per-invoice outcome'),
204
+ },
205
+ annotations: {
206
+ readOnlyHint: false,
207
+ destructiveHint: false,
208
+ idempotentHint: false,
209
+ openWorldHint: true,
210
+ },
211
+ }, async ({ invoices, operation, compress, waitForVerdict }) => {
212
+ try {
213
+ const parsed = invoices.map((xml) => parseInvoice(xml));
214
+ const effective = operation ?? 'CREATE';
215
+ const blocking = parsed
216
+ .map((invoice, index) => ({
217
+ index,
218
+ report: validateInvoice(invoice, { operation: effective }),
219
+ }))
220
+ .filter((entry) => !entry.report.valid);
221
+ if (blocking.length > 0) {
222
+ return result({
223
+ submitted: false,
224
+ reason: 'local validation failed',
225
+ invalid: blocking.map((entry) => ({
226
+ index: entry.index,
227
+ errors: entry.report.errors,
228
+ })),
229
+ });
230
+ }
231
+ const navClient = client();
232
+ const response = await navClient.submitInvoices(parsed.map((invoice) => ({ operation: effective, invoice })), { compress: compress ?? false });
233
+ if (!waitForVerdict) {
234
+ return result({
235
+ submitted: true,
236
+ transactionId: response.transactionId,
237
+ count: parsed.length,
238
+ });
239
+ }
240
+ const outcome = await waitForTransaction(navClient, response.transactionId);
241
+ return result({
242
+ submitted: true,
243
+ transactionId: response.transactionId,
244
+ accepted: outcome.accepted.length,
245
+ rejected: outcome.rejected.length,
246
+ results: outcome.results,
247
+ });
248
+ }
249
+ catch (error) {
250
+ return failure(error);
251
+ }
252
+ });
253
+ server.registerTool('transaction_status', {
254
+ title: 'Check a submission',
255
+ description: 'Get the processing status of a submitted transaction. NAV validates asynchronously, ' +
256
+ 'so a submission has no verdict until this reports one.',
257
+ inputSchema: {
258
+ transactionId: z.string(),
259
+ waitForVerdict: z.boolean().optional().describe('Poll until NAV decides'),
260
+ },
261
+ annotations: { readOnlyHint: true, openWorldHint: true },
262
+ }, async ({ transactionId, waitForVerdict }) => {
263
+ try {
264
+ const navClient = client();
265
+ if (waitForVerdict) {
266
+ const outcome = await waitForTransaction(navClient, transactionId);
267
+ return result({
268
+ transactionId,
269
+ accepted: outcome.accepted.length,
270
+ rejected: outcome.rejected.length,
271
+ results: outcome.results,
272
+ });
273
+ }
274
+ const response = await navClient.queryTransactionStatus({ transactionId });
275
+ return result({
276
+ transactionId,
277
+ results: response.processingResults?.processingResult ?? [],
278
+ });
279
+ }
280
+ catch (error) {
281
+ return failure(error);
282
+ }
283
+ });
284
+ server.registerTool('list_invoices', {
285
+ title: 'List invoices in a date range',
286
+ description: 'List invoices issued by you (OUTBOUND) or issued to you (INBOUND) in a range of issue ' +
287
+ 'dates. Returns a summary per invoice; use get_invoice for the full data. Paged, 100 ' +
288
+ 'per page.',
289
+ inputSchema: {
290
+ dateFrom: z.string().describe('First issue date, inclusive, as yyyy-mm-dd'),
291
+ dateTo: z.string().describe('Last issue date, inclusive, as yyyy-mm-dd'),
292
+ direction: DIRECTION.optional(),
293
+ page: z.number().int().min(1).optional(),
294
+ },
295
+ annotations: { readOnlyHint: true, openWorldHint: true },
296
+ }, async ({ dateFrom, dateTo, direction, page }) => {
297
+ try {
298
+ const response = await client().queryInvoiceDigest({
299
+ page: page ?? 1,
300
+ invoiceDirection: direction ?? 'OUTBOUND',
301
+ invoiceQueryParams: { mandatoryQueryParams: { invoiceIssueDate: { dateFrom, dateTo } } },
302
+ });
303
+ const digest = response.invoiceDigestResult;
304
+ return result({
305
+ page: digest.currentPage,
306
+ availablePages: digest.availablePage,
307
+ invoices: digest.invoiceDigest ?? [],
308
+ });
309
+ }
310
+ catch (error) {
311
+ return failure(error);
312
+ }
313
+ });
314
+ server.registerTool('get_invoice', {
315
+ title: 'Fetch one invoice in full',
316
+ description: 'Fetch the complete data of one invoice by number, as reported to NAV. Returns the ' +
317
+ 'invoice as XML and as structured data.',
318
+ inputSchema: {
319
+ invoiceNumber: z.string(),
320
+ direction: DIRECTION.optional(),
321
+ supplierTaxNumber: z
322
+ .string()
323
+ .optional()
324
+ .describe('Required for an inbound invoice: who issued it'),
325
+ },
326
+ annotations: { readOnlyHint: true, openWorldHint: true },
327
+ }, async ({ invoiceNumber, direction, supplierTaxNumber }) => {
328
+ try {
329
+ const requested = direction ?? 'OUTBOUND';
330
+ if (requested === 'INBOUND' && !supplierTaxNumber) {
331
+ throw new Error('supplierTaxNumber is required for an inbound invoice');
332
+ }
333
+ const response = await client().queryInvoiceData({
334
+ invoiceNumberQuery: {
335
+ invoiceNumber,
336
+ invoiceDirection: requested,
337
+ ...(supplierTaxNumber
338
+ ? { supplierTaxNumber: parseTaxNumber(supplierTaxNumber).taxpayerId }
339
+ : {}),
340
+ },
341
+ });
342
+ const data = response.invoiceDataResult;
343
+ if (!data)
344
+ return result({ found: false, invoiceNumber });
345
+ const invoice = decodeInvoiceData(data.invoiceData, {
346
+ compressed: data.compressedContentIndicator,
347
+ });
348
+ return result({
349
+ found: true,
350
+ invoiceNumber,
351
+ auditData: data.auditData,
352
+ xml: serializeDocument('InvoiceData', invoice, { indent: ' ' }),
353
+ invoice,
354
+ });
355
+ }
356
+ catch (error) {
357
+ return failure(error);
358
+ }
359
+ });
360
+ }
361
+ //# sourceMappingURL=server.js.map