🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@frihet/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@frihet/mcp-server - npm Package Compare versions

Comparing version
1.16.4
to
1.16.5
+1
-1
dist/client-interface.d.ts

@@ -107,3 +107,3 @@ /**

issueDate?: string;
}): Promise<Record<string, unknown>>;
}, idempotencyKey?: string): Promise<Record<string, unknown>>;
applyLateFee(invoiceId: string, data?: {

@@ -110,0 +110,0 @@ amount?: number;

@@ -156,2 +156,14 @@ /**

getInvoiceEInvoice(invoiceId: string): Promise<any>;
/**
* `POST /v1/invoices/:id/credit-note` — creates a rectificativa DRAFT.
*
* The backend REQUIRES an `Idempotency-Key` header (`400
* IDEMPOTENCY_KEY_REQUIRED` without it). `request` mints one for every
* mutation, so passing `idempotencyKey` is optional: supply it to make a
* caller-driven retry replay the stored 201 instead of creating a second
* draft. The backend marks that replay with `X-Idempotent-Replayed: true`,
* but this client reads no response headers, so the replayed 201 and the
* original are indistinguishable to the caller — both are the same draft,
* which is the property that matters here.
*/
createCreditNote(invoiceId: string, data: {

@@ -162,3 +174,3 @@ reason: string;

issueDate?: string;
}): Promise<Record<string, unknown>>;
}, idempotencyKey?: string): Promise<Record<string, unknown>>;
applyLateFee(invoiceId: string, data?: {

@@ -165,0 +177,0 @@ amount?: number;

@@ -19,2 +19,44 @@ /**

const REQUEST_TIMEOUT_MS = 30000;
/**
* HTTP methods for which the backend treats the request as a mutation and
* therefore accepts (and for some endpoints REQUIRES) an `Idempotency-Key`
* header. `POST /v1/invoices/:id/credit-note` rejects a keyless request with
* `400 IDEMPOTENCY_KEY_REQUIRED`, so a client that never sends one fails 100%
* of the time — see src/__tests__/idempotency-key-contract.test.ts.
*/
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
/**
* Fresh idempotency key, always a syntactically valid UUID v4.
*
* `crypto.randomUUID` is a global on the Cloudflare Workers runtime and on
* Node >= 19. On Node 18 — our declared `engines` floor — `globalThis.crypto`
* is behind `--experimental-global-webcrypto`, so the fallback is a REAL code
* path there, not a theoretical one. It therefore has to produce a UUID and
* not an ad-hoc string: the backend documents "UUID v4 recommended", and
* src/__tests__/idempotency-key-contract.test.ts asserts the shape.
*/
function newIdempotencyKey() {
const c = globalThis.crypto;
if (typeof c?.randomUUID === "function") {
return c.randomUUID();
}
// RFC 4122 v4 layout from Math.random. Weaker entropy than the CSPRNG, but
// the key only has to be unique per caller, never unguessable.
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
const r = (Math.random() * 16) | 0;
const v = ch === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* A caller-supplied key counts only if it carries a value. An empty or
* whitespace-only string is what an LLM client emits for "I have nothing to
* put here" on an optional string param — treating it as PRESENT would leave
* the request keyless and reproduce the very `400 IDEMPOTENCY_KEY_REQUIRED`
* this client exists to prevent.
*/
function normalizeIdempotencyKey(key) {
const trimmed = key?.trim();
return trimmed ? trimmed : undefined;
}
export class FrihetApiError extends Error {

@@ -44,4 +86,11 @@ statusCode;

// ------------------------------------------------------------------
async request(method, path, body, query, retryCount = 0) {
async request(method, path, body, query, retryCount = 0, idempotencyKey) {
const url = new URL(`${this.baseUrl}${path}`);
// Resolved once, at the top of the call chain: a caller-supplied key wins,
// otherwise mutations get a freshly minted one. The resolved value is then
// threaded through the 429 recursion below so a retry replays the SAME key
// — retrying with a new key is exactly the duplicate the key exists to
// prevent (a retried credit-note would create a second draft).
const resolvedIdempotencyKey = normalizeIdempotencyKey(idempotencyKey) ??
(MUTATING_METHODS.has(method) ? newIdempotencyKey() : undefined);
if (query) {

@@ -59,2 +108,5 @@ for (const [key, value] of Object.entries(query)) {

};
if (resolvedIdempotencyKey) {
headers["Idempotency-Key"] = resolvedIdempotencyKey;
}
const controller = new AbortController();

@@ -97,3 +149,3 @@ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);

await this.sleep(delayMs);
return this.request(method, path, body, query, retryCount + 1);
return this.request(method, path, body, query, retryCount + 1, resolvedIdempotencyKey);
}

@@ -157,4 +209,4 @@ // Error responses

*/
async requestUnwrapped(method, path, body, query) {
const raw = await this.request(method, path, body, query);
async requestUnwrapped(method, path, body, query, idempotencyKey) {
const raw = await this.request(method, path, body, query, 0, idempotencyKey);
if (raw !== null &&

@@ -366,4 +418,16 @@ typeof raw === "object" &&

}
async createCreditNote(invoiceId, data) {
return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/credit-note`, data);
/**
* `POST /v1/invoices/:id/credit-note` — creates a rectificativa DRAFT.
*
* The backend REQUIRES an `Idempotency-Key` header (`400
* IDEMPOTENCY_KEY_REQUIRED` without it). `request` mints one for every
* mutation, so passing `idempotencyKey` is optional: supply it to make a
* caller-driven retry replay the stored 201 instead of creating a second
* draft. The backend marks that replay with `X-Idempotent-Replayed: true`,
* but this client reads no response headers, so the replayed 201 and the
* original are indistinguishable to the caller — both are the same draft,
* which is the property that matters here.
*/
async createCreditNote(invoiceId, data, idempotencyKey) {
return this.requestUnwrapped("POST", `/invoices/${encodeURIComponent(invoiceId)}/credit-note`, data, undefined, idempotencyKey);
}

@@ -370,0 +434,0 @@ async applyLateFee(invoiceId, data) {

@@ -85,3 +85,3 @@ /**

issueDate?: string;
}): Promise<Rec>;
}, _idempotencyKey?: string): Promise<Rec>;
applyLateFee(invoiceId: string, data?: {

@@ -88,0 +88,0 @@ amount?: number;

@@ -152,3 +152,20 @@ /**

}
async createCreditNote(invoiceId, data) {
// Mirrors the live 201 body: a DRAFT rectificativa, always by differences
// (`rectificationMethod: "I"`), R-type derived from `reason` server-side
// (`error` → R1, everything else → R4 — R2/R3/R5 are unreachable via the API).
async createCreditNote(invoiceId, data, _idempotencyKey) {
// Shape rules taken from the live 201 (functions/src/publicApi.ts) rather
// than invented:
// - documentNumber is `CN-<original document number>` and NEVER encodes
// the R-type (creditNoteService.ts: `CN-${originalDocNumber}`). An
// earlier fixture emitted `R1-DEMO-001`, teaching an agent to parse a
// field that carries no R-type live.
// - fullCredit is hardcoded `true` in the live body: `false` never
// reaches a 201, it is refused with 400 PARTIAL_CREDIT_NOT_IMPLEMENTED.
// Echoing the input would contradict this tool's own description.
// Documented divergence: demo mode never throws (the seam's contract), so
// `fullCredit: false` returns the same simulated draft instead of the live
// 400. Pinned in src/__tests__/demo-mode.test.ts.
const original = findOrStub(demoInvoices, invoiceId);
const originalNumber = typeof original.documentNumber === "string" ? original.documentNumber : "DEMO-001";
return {

@@ -158,6 +175,9 @@ success: true,

id: demoId("demo_cn"),
documentNumber: "R4-DEMO-001",
documentNumber: `CN-${originalNumber}`,
originalInvoiceId: invoiceId,
reason: data.reason,
fullCredit: data.fullCredit ?? true,
fullCredit: true,
status: "draft",
rectificationMethod: "I",
totalCredited: 1210.0,
},

@@ -164,0 +184,0 @@ ...FISCAL_STAMP,

@@ -389,9 +389,10 @@ /**

title: "Create Credit Note",
description: "Create a credit note (factura rectificativa) for an existing invoice. " +
"This reverses all or part of an invoice for compliance. " +
"Spanish market: generates VeriFactu-compliant R1-R5 rectificativa. " +
"Other markets: standard credit note with negative amounts. " +
"/ Crea una factura rectificativa para una factura existente. " +
"Mercado espanol: genera rectificativa R1-R5 conforme a VeriFactu. " +
"Otros mercados: nota de credito estandar con importes negativos.",
description: "Create a full credit note (factura rectificativa) for an existing invoice, as a DRAFT. " +
"It does NOT issue: the draft carries no fiscal number, no hash and is not sent to VeriFactu — " +
"issue it from the app when you are ready. Always rectifies by differences (TipoRectificativa = I). " +
"Spanish market: R-type is derived from `reason` (error -> R1, anything else -> R4). " +
"Partial credits are not supported. Requires the `pro` plan. " +
"/ Crea una factura rectificativa completa como BORRADOR. No emite: sin numero fiscal, sin hash, " +
"sin envio a VeriFactu. Siempre rectifica por diferencias (tipo I). El tipo R se deriva de `reason` " +
"(error -> R1, resto -> R4). No admite abonos parciales. Requiere plan `pro`.",
annotations: CREATE_ANNOTATIONS,

@@ -413,4 +414,7 @@ inputSchema: {

.optional()
.describe("true = full credit (tipo S, sustitucion), false = partial (tipo I, diferencias). Default: true " +
"/ true = abono total (tipo S), false = parcial (tipo I). Por defecto: true"),
.describe("Must be true (the default). `false` is rejected with 400 PARTIAL_CREDIT_NOT_IMPLEMENTED — " +
"the API has no line-level or partial credit. It does NOT select the rectification method: " +
"that is always I (por diferencias). " +
"/ Debe ser true (por defecto). `false` devuelve 400 PARTIAL_CREDIT_NOT_IMPLEMENTED. " +
"No elige el metodo de rectificacion: siempre es I (por diferencias)."),
issueDate: z

@@ -420,5 +424,14 @@ .string()

.describe("ISO date for the credit note (YYYY-MM-DD). Defaults to today. / Fecha de emision (YYYY-MM-DD). Por defecto hoy."),
idempotencyKey: z
.string()
.max(64)
.optional()
.describe("Optional idempotency key, max 64 chars. One is generated per call when omitted or blank; " +
"pass your own to make a retry replay the stored result instead of creating a second draft. " +
"Reusing a key with a different body returns 409 IDEMPOTENCY_KEY_REUSED — reconcile, do not " +
"retry with a new key. " +
"/ Clave de idempotencia opcional (max 64). Se genera una por llamada si se omite o va vacia."),
},
outputSchema: creditNoteResultOutput,
}, async ({ invoiceId, reason, reasonDescription, fullCredit, issueDate }) => withToolLogging("create_credit_note", async () => {
}, async ({ invoiceId, reason, reasonDescription, fullCredit, issueDate, idempotencyKey }) => withToolLogging("create_credit_note", async () => {
const result = await client.createCreditNote(invoiceId, {

@@ -429,3 +442,3 @@ reason,

issueDate,
});
}, idempotencyKey);
const hints = enrichResponse("invoices", "create", result);

@@ -432,0 +445,0 @@ return {

{
"name": "@frihet/mcp-server",
"version": "1.16.4",
"version": "1.16.5",
"description": "AI-native MCP server for Frihet ERP — 157 tools: invoicing, expenses, CRM, banking, POS + ES/EU fiscal compliance (VeriFactu, TicketBAI, Facturae). Zero-install at mcp.frihet.io. Works with Claude, ChatGPT, Cursor, Windsurf, Cline & any MCP client.",

@@ -12,3 +12,3 @@ "type": "module",

"build": "tsc",
"test": "npm run build && node --test dist/__tests__/openai-profile.test.js dist/__tests__/tool-exposure.test.js dist/__tests__/einvoice-tools.test.js dist/__tests__/einvoice-day4-tools.test.js dist/__tests__/stay-tools.test.js dist/__tests__/pos-tools.test.js dist/__tests__/kitchen-tools.test.js dist/__tests__/banking-tools.test.js dist/__tests__/banking-client-contract.test.js dist/__tests__/pagination-cursor-param.test.js dist/__tests__/fiscal-tools.test.js dist/__tests__/time-tools.test.js dist/__tests__/recurring-tools.test.js dist/__tests__/team-tools.test.js dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js dist/__tests__/audit-server-version.test.js dist/__tests__/openai-grouped-compose.test.js dist/__tests__/contract.test.js dist/__tests__/observability-redaction.test.js dist/__tests__/intelligence-duplicate-invoice.test.js dist/__tests__/get-envelope-unwrap-regression.test.js dist/__tests__/mutation-unwrap-and-schema-regression.test.js dist/__tests__/schema-envelope-guard.test.js dist/__tests__/demo-mode.test.js",
"test": "npm run build && node --test dist/__tests__/openai-profile.test.js dist/__tests__/tool-exposure.test.js dist/__tests__/einvoice-tools.test.js dist/__tests__/einvoice-day4-tools.test.js dist/__tests__/stay-tools.test.js dist/__tests__/pos-tools.test.js dist/__tests__/kitchen-tools.test.js dist/__tests__/banking-tools.test.js dist/__tests__/banking-client-contract.test.js dist/__tests__/pagination-cursor-param.test.js dist/__tests__/fiscal-tools.test.js dist/__tests__/time-tools.test.js dist/__tests__/recurring-tools.test.js dist/__tests__/team-tools.test.js dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js dist/__tests__/audit-server-version.test.js dist/__tests__/openai-grouped-compose.test.js dist/__tests__/contract.test.js dist/__tests__/observability-redaction.test.js dist/__tests__/intelligence-duplicate-invoice.test.js dist/__tests__/get-envelope-unwrap-regression.test.js dist/__tests__/mutation-unwrap-and-schema-regression.test.js dist/__tests__/schema-envelope-guard.test.js dist/__tests__/demo-mode.test.js dist/__tests__/idempotency-key-contract.test.js",
"start": "node dist/index.js",

@@ -20,3 +20,6 @@ "postinstall": "node scripts/postinstall.js || true",

"gate:no-leak": "bash scripts/no-public-leak.sh",
"gate:no-legacy-region": "bash scripts/no-legacy-region.sh"
"gate:no-legacy-region": "bash scripts/no-legacy-region.sh",
"sync:openapi": "node scripts/sync-openapi.mjs",
"gate:openapi-fresh": "node scripts/sync-openapi.mjs --check",
"gate:openapi-fresh:live": "node scripts/sync-openapi.mjs --check --live"
},

@@ -23,0 +26,0 @@ "keywords": [

@@ -50,3 +50,3 @@ <p align="center">

> **Tool count:** npm `latest` (1.16.4) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).
> **Tool count:** the package (1.16.5) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).

@@ -53,0 +53,0 @@ ---