@frihet/mcp-server 2.0.0 → 2.0.2

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.
@@ -26,6 +26,10 @@ export declare const OPENAI_OAUTH_SECURITY_SCHEMES: readonly [{
26
26
  readonly type: "oauth2";
27
27
  readonly scopes: readonly string[];
28
28
  }];
29
+ export declare const OPENAI_103_READ_ONLY_CANDIDATE_TOOLS: ReadonlySet<string>;
30
+ export interface ApplyOpenAIProfileOptions {
31
+ candidateVersion?: "1.0.2" | "1.0.3";
32
+ }
29
33
  export declare const OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS: ReadonlySet<string>;
30
34
  export declare const OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS: ReadonlySet<string>;
31
35
  /** Strictly necessary row fields for reviewed collection/search responses. */
@@ -62,7 +66,7 @@ export declare const OPENAI_CSP: string;
62
66
  * registerAllResources(server, client);
63
67
  * ```
64
68
  */
65
- export declare function applyOpenAIProfile(server: any): void;
69
+ export declare function applyOpenAIProfile(server: any, options?: ApplyOpenAIProfileOptions): void;
66
70
  /** Number of tools excluded in OpenAI mode (for logging). */
67
71
  export declare const OPENAI_EXCLUDED_COUNT: number;
68
72
  /** Number of tools explicitly allowed in OpenAI mode. */
@@ -429,6 +429,12 @@ function installOpenAIToolsListSecurityProjection(server) {
429
429
  });
430
430
  };
431
431
  }
432
+ export const OPENAI_103_READ_ONLY_CANDIDATE_TOOLS = new Set([
433
+ "get_daily_operational_brief",
434
+ "get_business_period_summary",
435
+ "search_clients",
436
+ "search_vendors",
437
+ ]);
432
438
  export const OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS = new Set([
433
439
  "create_invoice",
434
440
  "create_expense",
@@ -1167,15 +1173,21 @@ export const OPENAI_CSP = "default-src 'self'; " +
1167
1173
  * ```
1168
1174
  */
1169
1175
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1170
- export function applyOpenAIProfile(server) {
1176
+ export function applyOpenAIProfile(server, options) {
1171
1177
  installOpenAIToolsListSecurityProjection(server);
1172
1178
  const fieldsToRedact = PROFILE.redactOutputFields;
1179
+ const activeIncludeTools = new Set(PROFILE.includeTools);
1180
+ if (options?.candidateVersion === "1.0.3") {
1181
+ for (const tool of OPENAI_103_READ_ONLY_CANDIDATE_TOOLS) {
1182
+ activeIncludeTools.add(tool);
1183
+ }
1184
+ }
1173
1185
  /* ── Intercept registerTool ─────────────────────────────────────── */
1174
1186
  const originalRegisterTool = server.registerTool.bind(server);
1175
1187
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1176
1188
  server.registerTool = (name, config, handler) => {
1177
1189
  // 0. Keep the public ChatGPT app to the reviewed core tool surface
1178
- if (!PROFILE.includeTools.has(name))
1190
+ if (!activeIncludeTools.has(name))
1179
1191
  return;
1180
1192
  // 1. Skip excluded tools entirely
1181
1193
  if (PROFILE.excludeTools.has(name))
@@ -657,7 +657,7 @@ export function assertOpenAIReviewContract(actual, expected) {
657
657
  const outputRequired = outputSchema?.required;
658
658
  const success = jsonObject(outputProperties?.success);
659
659
  const result = jsonObject(outputProperties?.result);
660
- const branches = result?.anyOf;
660
+ const branches = result?.oneOf;
661
661
  const branchObjects = Array.isArray(branches)
662
662
  ? branches.map((branch) => jsonObject(branch))
663
663
  : [];
@@ -687,6 +687,7 @@ export function assertOpenAIReviewContract(actual, expected) {
687
687
  outputRequired.length !== 3 ||
688
688
  success?.const !== true ||
689
689
  !result ||
690
+ Object.keys(result).length !== 1 ||
690
691
  branchObjects.length !== 2 ||
691
692
  branchObjects.some((branch) => !branch) ||
692
693
  branchByOutcome.size !== 2 ||
@@ -12,6 +12,7 @@ export interface McpSurfaceComposition {
12
12
  readonly claudeMode?: boolean;
13
13
  readonly googleMode?: boolean;
14
14
  readonly museMode?: boolean;
15
+ readonly demoMode?: boolean;
15
16
  }
16
17
  export declare function localMcpSurfaceComposition(openaiMode: boolean, groupedMode: boolean, claudeMode?: boolean, googleMode?: boolean, museMode?: boolean): McpSurfaceComposition;
17
18
  export declare function remoteMcpSurfaceComposition(openaiMode: boolean, groupedMode: boolean, claudeMode?: boolean, googleMode?: boolean, museMode?: boolean): McpSurfaceComposition;
@@ -3,6 +3,8 @@
3
3
  * generated public-surface contract. Transport/auth setup stays with each host;
4
4
  * the exposed MCP operations/resources/prompts are composed here exactly once.
5
5
  */
6
+ import { z } from "zod/v4";
7
+ import { READ_STAMP } from "./demo-fixtures.js";
6
8
  import { applyPublicCapabilityTruth, buildLocalDiscoveryCapability, CAPABILITY_META_KEY, } from "./capability-truth.js";
7
9
  import { applyFiscalAliases } from "./fiscal-aliases.js";
8
10
  import { applyClaudeCandidateProfile, CLAUDE_CANDIDATE_TOOLS, CLAUDE_GROUP_METADATA } from "./claude-profile.js";
@@ -33,6 +35,48 @@ export function registerMcpSurface(server, client, options) {
33
35
  annotations: { ...config.annotations, title: config.title },
34
36
  }, handler);
35
37
  }
38
+ let clientIsDemo = false;
39
+ try {
40
+ clientIsDemo =
41
+ client?.isDemoClient === true ||
42
+ client?.constructor?.name === "DemoFrihetClient";
43
+ }
44
+ catch {
45
+ clientIsDemo = false;
46
+ }
47
+ const isDemoMode = options.demoMode === true || clientIsDemo;
48
+ if (isDemoMode) {
49
+ // Install closest to the SDK so profile projection/redaction runs first.
50
+ // Only trusted server/client configuration enables this boundary; neither
51
+ // input arguments nor response metadata can switch a live server to demo.
52
+ const registerTool = server.registerTool.bind(server);
53
+ server.registerTool = (name, config, handler) => {
54
+ const schema = config.outputSchema;
55
+ if (!(schema instanceof z.ZodObject)
56
+ || !["data", "total", "limit", "offset"].every((key) => key in schema.shape)) {
57
+ return registerTool(name, config, handler);
58
+ }
59
+ return registerTool(name, {
60
+ ...config,
61
+ outputSchema: schema.extend({
62
+ _demo: z.literal(true),
63
+ _demoNotice: z.literal(READ_STAMP._demoNotice),
64
+ }),
65
+ }, (async (args, extra) => {
66
+ const result = await handler(args, extra);
67
+ if (result.isError || !result.structuredContent)
68
+ return result;
69
+ const structuredContent = { ...result.structuredContent, ...READ_STAMP };
70
+ return {
71
+ ...result,
72
+ structuredContent,
73
+ // JSON mirrors structuredContent for hosts that only read text.
74
+ // Keep existing summaries/hints too; no warning is silently dropped.
75
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }, ...result.content],
76
+ };
77
+ }));
78
+ };
79
+ }
36
80
  if (options.groupedMode) {
37
81
  if (options.openaiMode) {
38
82
  applyOpenAIReviewProfiles(server);
@@ -48,6 +92,7 @@ export function registerMcpSurface(server, client, options) {
48
92
  metaKey: CAPABILITY_META_KEY,
49
93
  localDiscovery: buildLocalDiscoveryCapability,
50
94
  },
95
+ demoMode: isDemoMode,
51
96
  });
52
97
  }
53
98
  }
@@ -141,6 +141,7 @@ export declare function applyToolExposureProfile(server: any, options?: {
141
141
  type: "oauth2";
142
142
  scopes: readonly string[];
143
143
  }>;
144
+ demoMode?: boolean;
144
145
  }): ToolExposureHandle;
145
146
  /** Number of meta-tools added in grouped mode (for logging). */
146
147
  export declare const GROUPED_META_TOOL_COUNT: number;
@@ -41,6 +41,7 @@
41
41
  * @see ./openai-profile.ts — the sibling interceptor this mirrors.
42
42
  */
43
43
  import { z } from "zod/v4";
44
+ import { READ_STAMP } from "./demo-fixtures.js";
44
45
  /**
45
46
  * Domain metadata. Keep these terse — they are the only group-level prose the
46
47
  * agent loads up front. Depth lives in the per-tool descriptions, fetched via
@@ -112,6 +113,7 @@ export const FILE_TO_GROUP = {
112
113
  deposits: "invoicing",
113
114
  expenses: "expenses",
114
115
  vendors: "expenses",
116
+ purchaseOrders: "expenses",
115
117
  fiscal: "fiscal",
116
118
  igic: "fiscal",
117
119
  impuesto_sociedades: "fiscal",
@@ -176,7 +178,7 @@ export function groupForTool(name) {
176
178
  return "fiscal";
177
179
  if (/(invoice|quote|credit_note|late_fee|recurring|deposit)/.test(n))
178
180
  return "invoicing";
179
- if (/(expense|vendor)/.test(n))
181
+ if (/(expense|vendor|purchase_order)/.test(n))
180
182
  return "expenses";
181
183
  if (/(bank|transaction)/.test(n))
182
184
  return "banking";
@@ -390,7 +392,7 @@ server, options) {
390
392
  };
391
393
  // Register the meta-tools eagerly. Their handlers close over `catalog`/`groups`
392
394
  // which finish populating as the real tools register right after this call.
393
- registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes, options?.describeToolExample);
395
+ registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes, options?.describeToolExample, options?.demoMode === true);
394
396
  return handle;
395
397
  }
396
398
  /* ------------------------------------------------------------------ */
@@ -420,8 +422,9 @@ function textResult(payload) {
420
422
  */
421
423
  function registerMetaTools(
422
424
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
423
- originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes, describeToolExample = "create_invoice") {
425
+ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes, describeToolExample = "create_invoice", demoMode = false) {
424
426
  const { catalog, groups } = handle;
427
+ const demoStamp = demoMode ? READ_STAMP : {};
425
428
  const oauthConfig = securitySchemes
426
429
  ? {
427
430
  securitySchemes: securitySchemes.map((scheme) => ({
@@ -529,6 +532,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
529
532
  }))
530
533
  .filter((g) => g.toolCount > 0);
531
534
  return textResult({
535
+ ...demoStamp,
532
536
  groups: out,
533
537
  totalGroups: out.length,
534
538
  totalTools: catalog.size,
@@ -591,6 +595,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
591
595
  score,
592
596
  }));
593
597
  return textResult({
598
+ ...demoStamp,
594
599
  query: query || null,
595
600
  group: groupFilter ?? null,
596
601
  count: scored.length,
@@ -626,6 +631,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
626
631
  {
627
632
  type: "text",
628
633
  text: JSON.stringify({
634
+ ...demoStamp,
629
635
  error: name
630
636
  ? `No tool named '${name}'.`
631
637
  : "Provide { name } — the exact tool name from search_tools.",
@@ -638,6 +644,7 @@ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, s
638
644
  };
639
645
  }
640
646
  return textResult({
647
+ ...demoStamp,
641
648
  name: entry.name,
642
649
  group: entry.group,
643
650
  title: entry.title,
@@ -226,6 +226,7 @@ export function registerClientTools(server, client) {
226
226
  "Error: la busqueda textual ('q') no se puede combinar con el cursor 'after' porque el backend pagina las busquedas mediante 'offset'/'limit' e ignora 'after'. Usa 'offset' y 'limit' con 'q', u omite 'q' al paginar con 'after'.",
227
227
  }],
228
228
  isError: true,
229
+ _meta: { "io.frihet/cursorWithQueryUnsupported": true },
229
230
  };
230
231
  }
231
232
  let result = await client.listClients({ limit, offset, after, fields, q, stage });
@@ -278,6 +279,7 @@ export function registerClientTools(server, client) {
278
279
  `Error: la busqueda de clientes alcanzo el limite de escaneo de 500 documentos del ERP (truncated=true; ${partialCount} coincidencia(s) en los primeros 500 registros) y no puede devolverse como resultado exhaustivo. Acota 'q' (o filtra por 'stage') o pagina sin 'q' usando 'after'.`,
279
280
  }],
280
281
  isError: true,
282
+ _meta: { "io.frihet/searchScanTruncated": true },
281
283
  };
282
284
  }
283
285
  const cleanEnvelope = {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Purchase-order read tools for the Frihet MCP server.
3
+ *
4
+ * READ-ONLY surface for the /v1/purchase-orders family (ADR 0027 S2, server
5
+ * authority shipped in Frihet-ERP #2359). The single-object read returns the
6
+ * stored document as it lives in the workspace; list returns the same shape
7
+ * paged by status and vendorId, ordered by createdAt desc.
8
+ *
9
+ * Scope semantics: list_purchase_orders and get_purchase_order both classify
10
+ * as `read` under the family router (functions/src/publicApi/apiKeyScopes.ts
11
+ * → requiredPurchaseOrderApiAccess), so a restricted API key needs at least
12
+ * one of `*`, `read`, `purchase-orders:*` or `purchase-orders:read`.
13
+ *
14
+ * WRITE tools (create draft, send, confirm, cancel, receive, reverse-receipt,
15
+ * bill) are deliberately NOT exposed here: they move money or stock and need
16
+ * human-authority semantics (draft → show → confirm) that this surface does not
17
+ * define. No write route is called from this file or from the client methods.
18
+ *
19
+ * NOT IN SCOPE for this surface (correction to a prior brief):
20
+ * - Physical count truth (#2355): no callable, no API route. Not designed.
21
+ * - Inventory balances (/v1/inventory/balances): ADR 0027 S4, not landed.
22
+ */
23
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ import type { IFrihetClient } from "../client-interface.js";
25
+ /**
26
+ * Read tools for purchase orders. Tool names follow the canonical
27
+ * snake_case convention used across the catalog.
28
+ */
29
+ export declare function registerPurchaseOrderTools(server: McpServer, client: IFrihetClient): void;
30
+ //# sourceMappingURL=purchaseOrders.d.ts.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Purchase-order read tools for the Frihet MCP server.
3
+ *
4
+ * READ-ONLY surface for the /v1/purchase-orders family (ADR 0027 S2, server
5
+ * authority shipped in Frihet-ERP #2359). The single-object read returns the
6
+ * stored document as it lives in the workspace; list returns the same shape
7
+ * paged by status and vendorId, ordered by createdAt desc.
8
+ *
9
+ * Scope semantics: list_purchase_orders and get_purchase_order both classify
10
+ * as `read` under the family router (functions/src/publicApi/apiKeyScopes.ts
11
+ * → requiredPurchaseOrderApiAccess), so a restricted API key needs at least
12
+ * one of `*`, `read`, `purchase-orders:*` or `purchase-orders:read`.
13
+ *
14
+ * WRITE tools (create draft, send, confirm, cancel, receive, reverse-receipt,
15
+ * bill) are deliberately NOT exposed here: they move money or stock and need
16
+ * human-authority semantics (draft → show → confirm) that this surface does not
17
+ * define. No write route is called from this file or from the client methods.
18
+ *
19
+ * NOT IN SCOPE for this surface (correction to a prior brief):
20
+ * - Physical count truth (#2355): no callable, no API route. Not designed.
21
+ * - Inventory balances (/v1/inventory/balances): ADR 0027 S4, not landed.
22
+ */
23
+ import { z } from "zod/v4";
24
+ import { withToolLogging, formatPaginatedResponse, formatRecord, listContent, getContent, READ_ONLY_ANNOTATIONS, paginatedOutput, purchaseOrderItemOutput, } from "./shared.js";
25
+ /**
26
+ * Read tools for purchase orders. Tool names follow the canonical
27
+ * snake_case convention used across the catalog.
28
+ */
29
+ export function registerPurchaseOrderTools(server, client) {
30
+ // -- list_purchase_orders --
31
+ server.registerTool("list_purchase_orders", {
32
+ title: "List Purchase Orders",
33
+ description: "List purchase orders in the workspace with optional filters. " +
34
+ "Returns one entry per PO with vendor, status, line items and totals. " +
35
+ "Statuses reflect the server-owned lifecycle (ADR 0027 S2): " +
36
+ "draft → sent → confirmed → received → billed, with cancel as a terminal branch. " +
37
+ "/ Lista pedidos de compra del workspace con filtros opcionales. " +
38
+ "Devuelve un registro por pedido con proveedor, estado, lineas y totales. " +
39
+ "Los estados reflejan el ciclo de vida del servidor (ADR 0027 S2): " +
40
+ "borrador → enviado → confirmado → recibido → facturado, con cancelado como rama terminal.",
41
+ annotations: READ_ONLY_ANNOTATIONS,
42
+ inputSchema: {
43
+ status: z
44
+ .string()
45
+ .optional()
46
+ .describe("Filter by PO status (draft, sent, confirmed, received, billed, cancelled) / " +
47
+ "Filtro por estado del pedido"),
48
+ vendorId: z
49
+ .string()
50
+ .trim()
51
+ .min(1)
52
+ .max(128)
53
+ .optional()
54
+ .describe("Filter by vendor ID / Filtro por ID de proveedor"),
55
+ limit: z
56
+ .number()
57
+ .int()
58
+ .min(1)
59
+ .max(100)
60
+ .optional()
61
+ .describe("Max results (1-100) / Resultados maximos"),
62
+ offset: z
63
+ .number()
64
+ .int()
65
+ .min(0)
66
+ .optional()
67
+ .describe("Offset / Desplazamiento"),
68
+ },
69
+ outputSchema: paginatedOutput(purchaseOrderItemOutput),
70
+ }, async ({ status, vendorId, limit, offset }) => withToolLogging("list_purchase_orders", async () => {
71
+ const result = await client.listPurchaseOrders({ status, vendorId, limit, offset });
72
+ return {
73
+ content: [listContent(formatPaginatedResponse("purchase_orders", result))],
74
+ structuredContent: result,
75
+ };
76
+ }));
77
+ // -- get_purchase_order --
78
+ server.registerTool("get_purchase_order", {
79
+ title: "Get Purchase Order",
80
+ description: "Get a single purchase order by its ID. Returns the full document: " +
81
+ "vendor, line items, totals, status, issue/expected dates and currency. " +
82
+ "/ Obtiene un pedido de compra por su ID. Devuelve el documento completo: " +
83
+ "proveedor, lineas, totales, estado, fechas de emision y esperada y moneda.",
84
+ annotations: READ_ONLY_ANNOTATIONS,
85
+ inputSchema: {
86
+ id: z
87
+ .string()
88
+ .trim()
89
+ .min(1)
90
+ .max(128)
91
+ .describe("Purchase-order ID / ID del pedido de compra"),
92
+ },
93
+ outputSchema: purchaseOrderItemOutput,
94
+ }, async ({ id }) => withToolLogging("get_purchase_order", async () => {
95
+ const result = await client.getPurchaseOrder(id);
96
+ return {
97
+ content: [getContent(formatRecord("PurchaseOrder", result))],
98
+ structuredContent: result,
99
+ };
100
+ }));
101
+ }
102
+ //# sourceMappingURL=purchaseOrders.js.map
@@ -15,6 +15,7 @@ import { registerClientTools } from "./clients.js";
15
15
  import { registerProductTools } from "./products.js";
16
16
  import { registerQuoteTools } from "./quotes.js";
17
17
  import { registerVendorTools } from "./vendors.js";
18
+ import { registerPurchaseOrderTools } from "./purchaseOrders.js";
18
19
  import { registerWebhookTools } from "./webhooks.js";
19
20
  import { registerIntelligenceTools } from "./intelligence.js";
20
21
  import { registerCrmTools } from "./crm.js";
@@ -89,6 +90,7 @@ export function registerAllTools(server, client) {
89
90
  registerProductTools(server, client);
90
91
  registerQuoteTools(server, client);
91
92
  registerVendorTools(server, client);
93
+ registerPurchaseOrderTools(server, client);
92
94
  registerWebhookTools(server, client);
93
95
  registerDepositTools(server, client);
94
96
  registerEInvoiceTools(server, client);
@@ -247,6 +247,48 @@ export declare const vendorItemOutput: z.ZodObject<{
247
247
  createdAt: z.ZodOptional<z.ZodString>;
248
248
  updatedAt: z.ZodOptional<z.ZodString>;
249
249
  }, z.core.$loose>;
250
+ /**
251
+ * Purchase-order output (ADR 0027 S2, server-owned lifecycle).
252
+ *
253
+ * Fields mirror the live API shape returned by /v1/purchase-orders GETs:
254
+ * - `status` ∈ {draft, sent, confirmed, received, billed, cancelled}
255
+ * - `items[].taxRate` is optional because per-line tax rates may override
256
+ * the document tax rate; the family router accepts it as a string-coerced
257
+ * number in the request, so the response can carry it.
258
+ * - `currency` is an ISO 4217 code (e.g. "EUR"). It is the same code the
259
+ * server stamps on the stored doc.
260
+ * - `createdAt`/`updatedAt` are ISO 8601; `issueDate`/`expectedDate` are
261
+ * YYYY-MM-DD.
262
+ *
263
+ * `.passthrough()` preserves receipts (`receipts[]`) and the linked expense
264
+ * id (`billedExpenseId`) once the order moves past `billed`, plus any
265
+ * additional fields the server adds in later S-stages without breaking
266
+ * downstream consumers.
267
+ */
268
+ export declare const purchaseOrderItemOutput: z.ZodObject<{
269
+ id: z.ZodString;
270
+ poNumber: z.ZodOptional<z.ZodString>;
271
+ vendorId: z.ZodOptional<z.ZodString>;
272
+ vendorName: z.ZodOptional<z.ZodString>;
273
+ status: z.ZodOptional<z.ZodString>;
274
+ items: z.ZodOptional<z.ZodArray<z.ZodObject<{
275
+ description: z.ZodString;
276
+ quantity: z.ZodNumber;
277
+ unitPrice: z.ZodNumber;
278
+ productId: z.ZodOptional<z.ZodString>;
279
+ taxRate: z.ZodOptional<z.ZodNumber>;
280
+ }, z.core.$loose>>>;
281
+ issueDate: z.ZodOptional<z.ZodString>;
282
+ expectedDate: z.ZodOptional<z.ZodString>;
283
+ taxRate: z.ZodOptional<z.ZodNumber>;
284
+ subtotal: z.ZodOptional<z.ZodNumber>;
285
+ tax: z.ZodOptional<z.ZodNumber>;
286
+ total: z.ZodOptional<z.ZodNumber>;
287
+ currency: z.ZodOptional<z.ZodString>;
288
+ notes: z.ZodOptional<z.ZodString>;
289
+ createdAt: z.ZodOptional<z.ZodString>;
290
+ updatedAt: z.ZodOptional<z.ZodString>;
291
+ }, z.core.$loose>;
250
292
  export declare const webhookItemOutput: z.ZodObject<{
251
293
  id: z.ZodString;
252
294
  userId: z.ZodOptional<z.ZodString>;
@@ -537,8 +579,8 @@ export declare const bankTransactionItemOutput: z.ZodObject<{
537
579
  postedAt: z.ZodOptional<z.ZodString>;
538
580
  category: z.ZodOptional<z.ZodString>;
539
581
  status: z.ZodOptional<z.ZodEnum<{
540
- pending: "pending";
541
582
  posted: "posted";
583
+ pending: "pending";
542
584
  excluded: "excluded";
543
585
  }>>;
544
586
  matchedDocId: z.ZodOptional<z.ZodString>;
@@ -555,6 +555,52 @@ export const vendorItemOutput = z.object({
555
555
  createdAt: z.string().optional(),
556
556
  updatedAt: z.string().optional(),
557
557
  }).passthrough();
558
+ /**
559
+ * Purchase-order output (ADR 0027 S2, server-owned lifecycle).
560
+ *
561
+ * Fields mirror the live API shape returned by /v1/purchase-orders GETs:
562
+ * - `status` ∈ {draft, sent, confirmed, received, billed, cancelled}
563
+ * - `items[].taxRate` is optional because per-line tax rates may override
564
+ * the document tax rate; the family router accepts it as a string-coerced
565
+ * number in the request, so the response can carry it.
566
+ * - `currency` is an ISO 4217 code (e.g. "EUR"). It is the same code the
567
+ * server stamps on the stored doc.
568
+ * - `createdAt`/`updatedAt` are ISO 8601; `issueDate`/`expectedDate` are
569
+ * YYYY-MM-DD.
570
+ *
571
+ * `.passthrough()` preserves receipts (`receipts[]`) and the linked expense
572
+ * id (`billedExpenseId`) once the order moves past `billed`, plus any
573
+ * additional fields the server adds in later S-stages without breaking
574
+ * downstream consumers.
575
+ */
576
+ export const purchaseOrderItemOutput = z.object({
577
+ id: z.string(),
578
+ poNumber: z.string().optional(),
579
+ vendorId: z.string().optional(),
580
+ vendorName: z.string().optional(),
581
+ status: z.string().optional(),
582
+ items: z
583
+ .array(z
584
+ .object({
585
+ description: z.string(),
586
+ quantity: z.number(),
587
+ unitPrice: z.number(),
588
+ productId: z.string().optional(),
589
+ taxRate: z.number().optional(),
590
+ })
591
+ .passthrough())
592
+ .optional(),
593
+ issueDate: z.string().optional(),
594
+ expectedDate: z.string().optional(),
595
+ taxRate: z.number().optional(),
596
+ subtotal: z.number().optional(),
597
+ tax: z.number().optional(),
598
+ total: z.number().optional(),
599
+ currency: z.string().optional(),
600
+ notes: z.string().optional(),
601
+ createdAt: z.string().optional(),
602
+ updatedAt: z.string().optional(),
603
+ }).passthrough();
558
604
  export const webhookItemOutput = z.object({
559
605
  id: z.string(),
560
606
  userId: z.string().optional(),
@@ -209,6 +209,7 @@ export function registerVendorTools(server, client) {
209
209
  "Error: la busqueda textual ('q') no se puede combinar con el cursor 'after' porque el backend pagina las busquedas mediante 'offset'/'limit' e ignora 'after'. Usa 'offset' y 'limit' con 'q', u omite 'q' al paginar con 'after'.",
210
210
  }],
211
211
  isError: true,
212
+ _meta: { "io.frihet/cursorWithQueryUnsupported": true },
212
213
  };
213
214
  }
214
215
  let result = await client.listVendors({ q, limit, offset, after, fields });
@@ -253,6 +254,7 @@ export function registerVendorTools(server, client) {
253
254
  `Error: la busqueda de proveedores alcanzo el limite de escaneo de 500 documentos del ERP (truncated=true; ${partialCount} coincidencia(s) en los primeros 500 registros) y no puede devolverse como resultado exhaustivo. Acota 'q' o pagina sin 'q' usando 'after'.`,
254
255
  }],
255
256
  isError: true,
257
+ _meta: { "io.frihet/searchScanTruncated": true },
256
258
  };
257
259
  }
258
260
  const cleanEnvelope = {
@@ -68,8 +68,8 @@
68
68
  "FRIHET_TOOL_MODE": "grouped"
69
69
  },
70
70
  "counts": {
71
- "toolNames": 161,
72
- "readOnly": 81,
71
+ "toolNames": 163,
72
+ "readOnly": 83,
73
73
  "writesFrihet": 80,
74
74
  "externalInteraction": 37,
75
75
  "destructive": 60,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frihet/mcp-server",
3
- "version": "2.0.0",
4
- "description": "AI-native MCP server for Frihet ERP — 156 canonical operations for invoicing, expenses, CRM, banking, POS and ES/EU fiscal workflows, with conservative capability and side-effect metadata.",
3
+ "version": "2.0.2",
4
+ "description": "AI-native MCP server for Frihet ERP — 158 canonical operations for invoicing, expenses, CRM, banking, POS and ES/EU fiscal workflows, with conservative capability and side-effect metadata.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "build": "tsc",
12
- "test": "npm run build && node --test dist/__tests__/resources-eu-catalog.test.js && node --test dist/__tests__/resources-p1-contract.test.js dist/__tests__/openai-profile.test.js dist/__tests__/openai-client-tax-id-contract.test.js dist/__tests__/tool-exposure.test.js dist/__tests__/fiscal-aliases.test.js dist/__tests__/einvoice-tools.test.js dist/__tests__/einvoice-day4-tools.test.js dist/__tests__/expenses-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__/fiscal-period-contract.test.js dist/__tests__/time-tools.test.js dist/__tests__/deposits-tools.test.js dist/__tests__/recurring-tools.test.js dist/__tests__/team-tools.test.js dist/__tests__/team-tools-real-sdk.test.js dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js dist/__tests__/hr-payroll-period-contract.test.js dist/__tests__/webhook-crm-reviewed-contract.test.js dist/__tests__/audit-server-version.test.js dist/__tests__/search-tools.test.js dist/__tests__/openai-full-compose.test.js dist/__tests__/openai-review-descriptor.test.js dist/__tests__/directory-profile-policy.test.js dist/__tests__/claude-profile.test.js dist/__tests__/claude-output-privacy.test.js dist/__tests__/claude-write-route-contract.test.js dist/__tests__/contract.test.js dist/__tests__/observability-redaction.test.js dist/__tests__/security-containment-gate0.test.js dist/__tests__/intelligence-duplicate-invoice.test.js dist/__tests__/get-envelope-unwrap-regression.test.js dist/__tests__/binary-content-types.test.js dist/__tests__/api-error-remediation.test.js dist/__tests__/mutation-unwrap-and-schema-regression.test.js dist/__tests__/schema-envelope-guard.test.js dist/__tests__/paginated-strict-output.test.js dist/__tests__/paginated-metadata-contract.test.js dist/__tests__/demo-mode.test.js dist/__tests__/idempotency-key-contract.test.js dist/__tests__/retry-after-contract.test.js dist/__tests__/source-header-contract.test.js dist/__tests__/no-provisioning-credential-to-public-api.test.js dist/__tests__/truth-in-descriptions.test.js dist/__tests__/mcp-baseline-mutation.test.js dist/__tests__/forms-elicitation.test.js dist/__tests__/agent-onboarding.test.js scripts/__tests__/openai-worker-review-wire.test.mjs scripts/__tests__/sync-openapi-retry.test.mjs scripts/__tests__/conformance-provenance.test.mjs scripts/__tests__/conformance-phase0.test.mjs scripts/__tests__/published-artifact-drift.test.mjs scripts/__tests__/cross-surface-authority.test.mjs scripts/__tests__/release-workflow-contract.test.mjs scripts/__tests__/openai-wrangler-resolution.test.mjs scripts/__tests__/audit-mcp-refs-detector.test.mjs scripts/__tests__/openai-public-surface.test.mjs scripts/__tests__/claude-plugin-package.test.mjs scripts/__tests__/claude-hosted-release.test.mjs scripts/__tests__/claude-hosted-acceptance.test.mjs scripts/__tests__/claude-oauth-provider.test.mjs scripts/__tests__/audit-mcp-refs-published-sot.test.mjs scripts/__tests__/claude-bootstrap-secrets.test.mjs scripts/__tests__/no-public-leak.test.mjs",
12
+ "test": "npm run build && node --test dist/__tests__/resources-eu-catalog.test.js && node --test dist/__tests__/resources-p1-contract.test.js dist/__tests__/openai-profile.test.js dist/__tests__/openai-client-tax-id-contract.test.js dist/__tests__/tool-exposure.test.js dist/__tests__/fiscal-aliases.test.js dist/__tests__/einvoice-tools.test.js dist/__tests__/einvoice-day4-tools.test.js dist/__tests__/expenses-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__/fiscal-period-contract.test.js dist/__tests__/time-tools.test.js dist/__tests__/deposits-tools.test.js dist/__tests__/recurring-tools.test.js dist/__tests__/team-tools.test.js dist/__tests__/team-tools-real-sdk.test.js dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js dist/__tests__/hr-payroll-period-contract.test.js dist/__tests__/webhook-crm-reviewed-contract.test.js dist/__tests__/audit-server-version.test.js dist/__tests__/search-tools.test.js dist/__tests__/openai-full-compose.test.js dist/__tests__/openai-review-descriptor.test.js dist/__tests__/directory-profile-policy.test.js dist/__tests__/claude-profile.test.js dist/__tests__/claude-output-privacy.test.js dist/__tests__/claude-write-route-contract.test.js dist/__tests__/contract.test.js dist/__tests__/observability-redaction.test.js dist/__tests__/security-containment-gate0.test.js dist/__tests__/intelligence-duplicate-invoice.test.js dist/__tests__/get-envelope-unwrap-regression.test.js dist/__tests__/binary-content-types.test.js dist/__tests__/api-error-remediation.test.js dist/__tests__/mutation-unwrap-and-schema-regression.test.js dist/__tests__/schema-envelope-guard.test.js dist/__tests__/paginated-strict-output.test.js dist/__tests__/paginated-metadata-contract.test.js dist/__tests__/purchase-orders-tools.test.js dist/__tests__/purchase-orders-client-contract.test.js dist/__tests__/demo-mode.test.js dist/__tests__/idempotency-key-contract.test.js dist/__tests__/retry-after-contract.test.js dist/__tests__/source-header-contract.test.js dist/__tests__/no-provisioning-credential-to-public-api.test.js dist/__tests__/truth-in-descriptions.test.js dist/__tests__/mcp-baseline-mutation.test.js dist/__tests__/forms-elicitation.test.js dist/__tests__/agent-onboarding.test.js scripts/__tests__/openai-worker-review-wire.test.mjs scripts/__tests__/sync-openapi-retry.test.mjs scripts/__tests__/conformance-provenance.test.mjs scripts/__tests__/conformance-phase0.test.mjs scripts/__tests__/published-artifact-drift.test.mjs scripts/__tests__/cross-surface-authority.test.mjs scripts/__tests__/release-workflow-contract.test.mjs scripts/__tests__/openai-wrangler-resolution.test.mjs scripts/__tests__/audit-mcp-refs-detector.test.mjs scripts/__tests__/openai-public-surface.test.mjs scripts/__tests__/claude-plugin-package.test.mjs scripts/__tests__/claude-hosted-release.test.mjs scripts/__tests__/claude-hosted-acceptance.test.mjs scripts/__tests__/claude-oauth-provider.test.mjs scripts/__tests__/audit-mcp-refs-published-sot.test.mjs scripts/__tests__/claude-bootstrap-secrets.test.mjs scripts/__tests__/no-public-leak.test.mjs",
13
13
  "canary:mcp": "npm run build && node scripts/canary-mcp.mjs",
14
14
  "canary:mcp:check": "npm run build && node scripts/canary-mcp.mjs --check",
15
15
  "canary:mcp:snapshot": "npm run build && node scripts/canary-mcp.mjs --snapshot",
@@ -121,17 +121,17 @@
121
121
  "LICENSE"
122
122
  ],
123
123
  "dependencies": {
124
- "@modelcontextprotocol/sdk": "1.31.0"
124
+ "@modelcontextprotocol/sdk": "1.32.2"
125
125
  },
126
126
  "overrides": {
127
- "hono": ">=4.13.13",
128
- "ip-address": ">=10.7.3",
127
+ "hono": ">=4.13.16 <5",
128
+ "ip-address": ">=10.7.6 <11",
129
129
  "proxy-addr": ">=2.0.8",
130
- "qs": ">=6.15.4",
130
+ "qs": ">=6.17.1 <7",
131
131
  "@ai-sdk/provider-utils": ">=4.0.33 <5"
132
132
  },
133
133
  "peerDependencies": {
134
- "zod": ">=3.25.1"
134
+ "zod": "^4.6.6"
135
135
  },
136
136
  "peerDependenciesMeta": {
137
137
  "zod": {
@@ -142,6 +142,6 @@
142
142
  "@types/node": "^22.0.0",
143
143
  "typescript": "^5.7.0",
144
144
  "ajv": "8.18.0",
145
- "zod": "^3.25.1"
145
+ "zod": "^4.6.7"
146
146
  }
147
147
  }