@frihet/mcp-server 1.16.5 → 1.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.
Files changed (68) hide show
  1. package/README.md +92 -31
  2. package/dist/agent-onboarding.d.ts +98 -0
  3. package/dist/agent-onboarding.js +275 -0
  4. package/dist/api-origin.d.ts +9 -0
  5. package/dist/api-origin.js +41 -0
  6. package/dist/canary/mcp-harness.d.ts +77 -0
  7. package/dist/canary/mcp-harness.js +549 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +254 -0
  10. package/dist/client-interface.d.ts +22 -14
  11. package/dist/client.d.ts +114 -18
  12. package/dist/client.js +520 -58
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +209 -29
  15. package/dist/demo-fixtures.d.ts +3 -3
  16. package/dist/demo-fixtures.js +7 -7
  17. package/dist/fiscal-aliases.d.ts +44 -0
  18. package/dist/fiscal-aliases.js +62 -0
  19. package/dist/index.js +41 -56
  20. package/dist/logger.d.ts +2 -2
  21. package/dist/logger.js +140 -8
  22. package/dist/observability.d.ts +18 -29
  23. package/dist/observability.js +192 -105
  24. package/dist/openai-profile.d.ts +27 -15
  25. package/dist/openai-profile.js +1191 -145
  26. package/dist/openai-review-contract.d.ts +49 -0
  27. package/dist/openai-review-contract.js +919 -0
  28. package/dist/openai-review-oauth.d.ts +98 -0
  29. package/dist/openai-review-oauth.js +136 -0
  30. package/dist/prompts/register-all.d.ts +2 -0
  31. package/dist/prompts/register-all.js +2 -0
  32. package/dist/public-capability-contract.d.ts +39 -0
  33. package/dist/public-capability-contract.js +186 -0
  34. package/dist/redaction.d.ts +10 -0
  35. package/dist/redaction.js +74 -0
  36. package/dist/resources/register-all.js +6 -12
  37. package/dist/server-composition.d.ts +16 -0
  38. package/dist/server-composition.js +44 -0
  39. package/dist/tool-exposure.d.ts +25 -22
  40. package/dist/tool-exposure.js +199 -77
  41. package/dist/tools/accountingClose.d.ts +14 -5
  42. package/dist/tools/accountingClose.js +41 -23
  43. package/dist/tools/backend-availability.d.ts +3 -3
  44. package/dist/tools/backend-availability.js +3 -3
  45. package/dist/tools/clients.js +9 -9
  46. package/dist/tools/crm.js +22 -21
  47. package/dist/tools/deposits.js +30 -11
  48. package/dist/tools/einvoice.d.ts +1 -1
  49. package/dist/tools/expenses.d.ts +16 -0
  50. package/dist/tools/expenses.js +47 -27
  51. package/dist/tools/hr.d.ts +2 -3
  52. package/dist/tools/hr.js +8 -9
  53. package/dist/tools/intelligence.js +95 -4
  54. package/dist/tools/invoices.js +150 -47
  55. package/dist/tools/payroll.d.ts +5 -8
  56. package/dist/tools/payroll.js +19 -32
  57. package/dist/tools/permissions.d.ts +4 -7
  58. package/dist/tools/permissions.js +11 -14
  59. package/dist/tools/products.js +6 -6
  60. package/dist/tools/quotes.js +109 -24
  61. package/dist/tools/shared.d.ts +1431 -75
  62. package/dist/tools/shared.js +386 -76
  63. package/dist/tools/team.js +18 -14
  64. package/dist/tools/vendors.js +9 -9
  65. package/dist/tools/webhooks.js +35 -30
  66. package/dist/types.d.ts +28 -4
  67. package/docs/agent-onboarding.json +381 -0
  68. package/package.json +32 -7
package/README.md CHANGED
@@ -21,8 +21,8 @@
21
21
  <a href="https://smithery.ai/servers/frihet/frihet-mcp"><img src="https://smithery.ai/badge/frihet/frihet-mcp" alt="Smithery installs"></a>
22
22
  <a href="https://registry.modelcontextprotocol.io/?q=io.frihet"><img src="https://img.shields.io/badge/MCP_Registry-io.frihet%2Ferp-4A90D9?style=flat&logo=anthropic&logoColor=white" alt="MCP Registry"></a>
23
23
  <a href="https://github.com/Frihet-io/frihet-mcp/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-18181b?style=flat&labelColor=09090b" alt="license"></a>
24
- <img src="https://img.shields.io/badge/tools-157-18181b?style=flat&labelColor=09090b" alt="157 tools">
25
- <img src="https://img.shields.io/badge/node-%3E%3D18-18181b?style=flat&labelColor=09090b" alt="node >=18">
24
+ <img src="https://img.shields.io/badge/catalogue-157_operations-18181b?style=flat&labelColor=09090b" alt="157 canonical catalogue operations">
25
+ <img src="https://img.shields.io/badge/node-%3E%3D20-18181b?style=flat&labelColor=09090b" alt="node >=20">
26
26
  <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-18181b?style=flat&labelColor=09090b" alt="TypeScript"></a>
27
27
  </p>
28
28
 
@@ -47,7 +47,7 @@
47
47
  | **ChatGPT Apps** | Coming soon | [chatgpt.com](https://chatgpt.com) |
48
48
  | **Anthropic Claude Directory** | Coming soon | [claude.ai/settings/connectors](https://claude.ai/settings/connectors) |
49
49
 
50
- > **Tool count:** the package (1.16.5) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).
50
+ > **Surface truth:** the catalogue contains 157 canonical operations. The local full profile serves those plus 5 fiscal alias names (162 names). The hosted grouped profile also adds 3 local discovery names (165 names). Catalogue membership is not a promise that a backing API is enabled for every workspace.
51
51
 
52
52
  ---
53
53
 
@@ -60,7 +60,7 @@ You: "Create an invoice for TechStart SL, 40 hours of consulting at 75 EUR/h
60
60
  Claude: Done. Invoice INV-2026-089 created. Total: 3,000.00 EUR + 21% IVA = 3,630.00 EUR.
61
61
  ```
62
62
 
63
- 157 tools. 11 resources. 10 prompts. Structured output on every tool. Zero boilerplate.
63
+ 157 canonical operations. Five fiscal aliases. Ten prompts. The local package serves 11 resources; the hosted Worker deliberately serves the 7 static resources, while API-backed workspace resources remain local-profile only.
64
64
 
65
65
  <!-- v1.12.0-beta.1 — D4-B megasprint: HR (9), payroll (2), onboarding (2), permissions (2), period close (3), webhook test (1) = +19 = 157 tools total -->
66
66
 
@@ -80,6 +80,26 @@ When you're ready for your real data, drop the flag and add your key ([app.frihe
80
80
 
81
81
  ---
82
82
 
83
+ ## For AI agents
84
+
85
+ If you are an agent reading this repository rather than a person reading a page, everything you need is machine-readable and generated from the running server — you do not have to parse this README.
86
+
87
+ | What | Where |
88
+ |------|-------|
89
+ | Onboarding contract: quickstart per client, auth, safe workflow, human-authority tool lists, error recovery | [`docs/agent-onboarding.json`](./docs/agent-onboarding.json) — also shipped inside the npm package |
90
+ | Per-tool capability truth: `callability`, `writesFrihet`, `externalInteraction`, `externalSideEffects` | `_meta["io.frihet/capability"]` on every entry of `tools/list` |
91
+ | How to behave once connected | the `instructions` string returned by `initialize` — your client hands it to you automatically |
92
+
93
+ Three rules the contract encodes, in short:
94
+
95
+ 1. **Orient before acting.** `get_business_context` and the `frihet://tax/rates` resource decide the correct fiscal treatment. Do not recall a Spanish tax rate from memory.
96
+ 2. **Draft, show, stop.** `create_invoice`, `create_quote` and `create_credit_note` all default to `status=draft` — no fiscal number, no hash, nothing sent to a tax authority. Present the draft and hand back.
97
+ 3. **Human authority is not yours to assume.** Any tool with a non-empty `externalSideEffects` reaches a client's inbox, a webhook, money, or AEAT / VeriFactu / TicketBAI / FACe. Several also take `confirm=true`; that flag records a human decision — never set it to satisfy your own plan.
98
+
99
+ `docs/agent-onboarding.json` is regenerated from the live surface by `npm run generate:agent-onboarding` and gated in CI by `npm run gate:agent-onboarding`, so its tool lists and counts cannot drift from the server.
100
+
101
+ ---
102
+
83
103
  ## Install
84
104
 
85
105
  ### One-line (Claude Code, Cursor, Copilot, Codex, Windsurf, Gemini CLI, and more)
@@ -106,7 +126,36 @@ Once available in the community marketplace:
106
126
 
107
127
  Skill invocation: `/frihet-erp:frihet-mcp`. The bundled `.mcp.json` launches `@frihet/mcp-server` via `npx` — set `FRIHET_API_KEY` in your environment (get one at [app.frihet.io](https://app.frihet.io) → Settings → API keys).
108
128
 
109
- ### Claude Code / Claude Desktop
129
+ ### Claude Code — one command
130
+
131
+ ```bash
132
+ claude mcp add frihet -s user -e FRIHET_API_KEY=fri_your_key_here -- npx -y @frihet/mcp-server
133
+ claude mcp list # verify: frihet ✓ Connected
134
+ ```
135
+
136
+ The CLI owns the config file, so there is nothing to hand-edit and no path to get wrong. (User scope writes `~/.claude.json`, not `~/.claude/mcp.json`.)
137
+
138
+ ### Codex CLI — one command
139
+
140
+ ```bash
141
+ codex mcp add frihet --env FRIHET_API_KEY=fri_your_key_here -- npx -y @frihet/mcp-server
142
+ codex mcp list # verify
143
+ ```
144
+
145
+ Codex config is **TOML**, not JSON. `codex mcp add` writes:
146
+
147
+ ```toml
148
+ [mcp_servers.frihet]
149
+ command = "npx"
150
+ args = ["-y", "@frihet/mcp-server"]
151
+
152
+ [mcp_servers.frihet.env]
153
+ FRIHET_API_KEY = "fri_your_key_here"
154
+ ```
155
+
156
+ > Pasting a JSON `mcpServers` block into `~/.codex/config.toml` is a TOML parse error that takes down your **whole** Codex config, not just this server. Use the command above.
157
+
158
+ ### Claude Desktop, Cursor, Windsurf, Cline — JSON config
110
159
 
111
160
  ```json
112
161
  {
@@ -124,14 +173,12 @@ Skill invocation: `/frihet-erp:frihet-mcp`. The bundled `.mcp.json` launches `@f
124
173
 
125
174
  | Tool | Config file |
126
175
  |------|------------|
127
- | Claude Code | `~/.claude/mcp.json` |
128
176
  | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` |
129
177
  | Cursor | `.cursor/mcp.json` or `~/.cursor/mcp.json` |
130
178
  | Windsurf | `~/.windsurf/mcp.json` |
131
179
  | Cline | VS Code settings or `.cline/mcp.json` |
132
- | Codex CLI | `~/.codex/config.toml` (MCP section) |
133
180
 
134
- The JSON config is identical for all tools. Only the file path changes.
181
+ The JSON above is identical for these four clients; only the file path changes. Claude Code and Codex are **not** in this table — they manage their own config through the CLI commands shown above.
135
182
 
136
183
  ### Remote (no install)
137
184
 
@@ -222,7 +269,7 @@ Talk to your ERP. These are real prompts, not marketing copy.
222
269
 
223
270
  ## What to expect
224
271
 
225
- This MCP is a **structured data interface** -- you describe what you want in natural language, and the AI creates, queries, or modifies business records in Frihet. Most of the 157 tools are CRUD operations over the REST API; the rest are read-only summaries and fiscal/e-invoice actions.
272
+ This MCP is a **structured data interface** -- you describe what you want in natural language, and the AI creates, queries, or modifies business records in Frihet. Most of the 157 canonical operations are CRUD operations over the REST API; the rest are read-only summaries and fiscal/e-invoice actions. Alias and discovery names are counted separately.
226
273
 
227
274
  **Works great:**
228
275
 
@@ -249,7 +296,7 @@ If you need to digitize paper invoices or receipts, extract the data first (e.g.
249
296
 
250
297
  ---
251
298
 
252
- ## Tools (157)
299
+ ## Catalogue operations (157)
253
300
 
254
301
  ### Invoices (12)
255
302
 
@@ -259,12 +306,12 @@ If you need to digitize paper invoices or receipts, extract the data first (e.g.
259
306
  | `get_invoice` | Get full invoice details by ID |
260
307
  | `create_invoice` | Create a new invoice with line items |
261
308
  | `update_invoice` | Update any invoice field |
262
- | `delete_invoice` | Permanently delete an invoice |
309
+ | `delete_invoice` | Delete a draft invoice; a sent/paid one is cancelled, not destroyed (`confirm=true` required) |
263
310
  | `search_invoices` | Find invoices by client name, date, or status |
264
- | `send_invoice` | Email invoice to client (PDF attachment) |
311
+ | `send_invoice` | Email invoice to client (PDF attachment) — reaches a third party, `confirm=true` required |
265
312
  | `mark_invoice_paid` | Mark an invoice as paid with optional payment date |
266
- | `get_invoice_pdf` | Get a download URL for the invoice PDF |
267
- | `get_invoice_einvoice` | Get the e-invoice XML for a given invoice ID |
313
+ | `get_invoice_pdf` | Get bounded invoice PDF bytes as base64 |
314
+ | `get_invoice_einvoice` | Get bounded XML or Factur-X PDF bytes for an invoice |
268
315
  | `create_credit_note` | Create a credit note linked to an existing invoice |
269
316
  | `apply_late_fee` | Apply a late payment fee to an overdue invoice |
270
317
 
@@ -329,7 +376,7 @@ If you need to digitize paper invoices or receipts, extract the data first (e.g.
329
376
  | `get_quote` | Get quote details |
330
377
  | `create_quote` | Draft a new quote |
331
378
  | `update_quote` | Modify a quote |
332
- | `delete_quote` | Delete a quote |
379
+ | `delete_quote` | Delete only a clean draft with no delivery, response, attachment, or conversion evidence; refuse protected drafts; cancel non-drafts (`confirm=true` required) |
333
380
  | `send_quote` | Email quote to client for acceptance |
334
381
 
335
382
  ### Webhooks (6)
@@ -395,9 +442,9 @@ If you need to digitize paper invoices or receipts, extract the data first (e.g.
395
442
 
396
443
  | Tool | What it does |
397
444
  |------|-------------|
398
- | `list_team_members` | List all workspace members with role and invite status |
399
- | `invite_team_member` | Invite a new member by email with role (admin/member/viewer) |
400
- | `update_team_member_role` | Change an existing member's role |
445
+ | `list_team_members` | List active members + pending invites (owner excluded) |
446
+ | `invite_team_member` | Invite a new member by email with role (admin/editor/accountant/viewer) |
447
+ | `update_team_member_role` | Change an existing member's role (admin/editor/accountant/viewer) |
401
448
  | `remove_team_member` | Remove a member from the workspace (confirm=true required) |
402
449
 
403
450
  ### Gestoria — Accountants (5)
@@ -542,15 +589,15 @@ If you need to digitize paper invoices or receipts, extract the data first (e.g.
542
589
  | `leave_list` | List leave requests with filters (user, status, date range) |
543
590
  | `attendance_clock_in` | Record clock-in for an employee |
544
591
  | `attendance_clock_out` | Record clock-out for an employee |
545
- | `overtime_report` | Generate overtime report for a period |
592
+ | `overtime_report` | Read daily/weekly overtime, aggregate minutes/hours, and compliance alerts computed over the selected YYYY or YYYY-MM records |
546
593
  | `anomaly_list` | List attendance anomalies (missing punches, excessive overtime) |
547
594
 
548
595
  ### Payroll (2)
549
596
 
550
597
  | Tool | What it does |
551
598
  |------|-------------|
552
- | `payroll_export` | Export payroll data for a period (CSV/PDF for gestoría) |
553
- | `payroll_checklist` | Generate pre-payroll checklist: pending leaves, anomalies, overtime |
599
+ | `payroll_export` | Read normalized payroll-ready employee data; the format value is an echoed destination label, not a generated file |
600
+ | `payroll_checklist` | List payable employees with payroll-profile readiness, missing fields, and monthly review state |
554
601
 
555
602
  ### Onboarding (2)
556
603
 
@@ -563,32 +610,45 @@ If you need to digitize paper invoices or receipts, extract the data first (e.g.
563
610
 
564
611
  | Tool | What it does |
565
612
  |------|-------------|
566
- | `permissions_matrix` | Get the full permissions matrix for all roles in the workspace |
567
- | `permissions_me` | Get the current API key's effective permissions |
613
+ | `permissions_matrix` | Get the documented RBAC-model snapshot (not a runtime authorization guarantee) |
614
+ | `permissions_me` | Compare RBAC-model fields with actual API-key scopes and known scope denials (non-exhaustive) |
568
615
 
569
616
  ### Period Close (3)
570
617
 
571
618
  | Tool | What it does |
572
619
  |------|-------------|
573
- | `period_close_status` | Get the close status for an accounting period |
620
+ | `period_close_status` | Get the current or selected YYYY fiscal-year range, open/closed state, and nullable closing details |
574
621
  | `period_close` | Close an accounting period (gestor/admin only — TRUST AREA) |
575
622
  | `period_reopen` | Reopen a closed period with a mandatory reason (TRUST AREA) |
576
623
 
577
- All 157 tools return **structured output** via `outputSchema` -- typed JSON, not raw text. List tools return paginated results (`{ data, total, limit, offset }`).
624
+ All canonical operations (and their aliases) return **structured output** via `outputSchema` -- typed JSON, not raw text. List response shapes follow their API family; not every list endpoint is paginated.
625
+
626
+ ### Capability and side-effect truth
627
+
628
+ On the full MCP surfaces, every `tools/list` entry includes `_meta["io.frihet/capability"]`:
629
+
630
+ - `registered` means the name and handler exist in this server build;
631
+ - `callability` is `api_dependent` (the handler calls the API; deployment, workspace enablement, and authorization still decide), `runtime_checked` (the handler explicitly distinguishes an absent backend from empty data), `deferred`, `unavailable`, or `local`—never an unconditional “available” claim;
632
+ - `writesFrihet`, `externalInteraction`, and `externalSideEffects` distinguish state changes and calls to external entities/providers;
633
+ - MCP action annotations remain the standard source for read-only, destructive, idempotent, and open-world hints.
634
+
635
+ The ChatGPT/OpenAI host is a separately reviewed surface: exactly 33 business operations with complete descriptions, 0 discovery meta-tools, 0 prompts, and 0 resources. Its 17 reads and 16 writes are deliberately narrow, and all writes require literal `confirm=true`. Ten writes may deliver full business events to active endpoints previously configured by the workspace owner; webhook administration itself remains excluded. Direct email delivery, the legacy monthly summary, raw invoice PDFs, invoice lifecycle transitions, updating an existing quote, regulated filing, deleting client parent records, deleting expenses with their linked files, deleting products, and deleting vendors are also excluded. This surface must not be inferred from the full catalogue.
578
636
 
579
637
  ---
580
638
 
581
- ## Resources (11)
639
+ ## Resources
582
640
 
583
641
  Context the AI can read to make smarter decisions.
584
642
 
643
+ The local package serves 11 resources: 7 static references plus 4 API-backed workspace resources. The hosted Worker serves the 7 static resources. The OpenAI-reviewed host serves 0 resources.
644
+
585
645
  **Static** (reference data, no API calls):
586
646
 
587
647
  | Resource | URI | What it provides |
588
648
  |----------|-----|-----------------|
589
649
  | API Schema | `frihet://api/schema` | OpenAPI summary: endpoints, auth, rate limits, pagination, error codes |
590
650
  | Tax Rates | `frihet://tax/rates` | Tax rates by Spanish fiscal zone: IVA, IGIC, IPSI, EU reverse charge, IRPF |
591
- | Tax Calendar | `frihet://tax/calendar` | Quarterly filing deadlines: Modelo 303, 130, 390, 420, VeriFactu timeline |
651
+ | Tax Calendar | `frihet://tax/calendar` | Quarterly and annual filing deadlines for the listed Spanish tax models |
592
652
  | Expense Categories | `frihet://config/expense-categories` | 8 categories with deductibility rules, IVA treatment, amortization |
593
653
  | Invoice Statuses | `frihet://config/invoice-statuses` | Status flow (draft > sent > paid/overdue > cancelled), transition rules, webhook events |
594
654
  | Currencies | `frihet://config/currencies` | 40 supported currencies with ISO codes, symbols, decimal places, locale formatting |
@@ -670,10 +730,10 @@ Frihet's differentiator is **depth** — full ES/EU fiscal coverage plus native
670
730
 
671
731
  | Mode | Behavior |
672
732
  |------|----------|
673
- | `full` (default) | All tools are exposed with their full descriptions and schemas. **Unchanged from previous releases** — existing setups are unaffected. |
733
+ | `full` (default) | Canonical tools and fiscal aliases are exposed with full descriptions and schemas. Public descriptors add conservative callability and side-effect truth; operation names, schemas and handlers are unchanged. |
674
734
  | `grouped` | **Progressive disclosure.** Each tool's description collapses to a one-line `[group] summary — full schema via describe_tool('name')`, and three lightweight discovery tools are added. The agent loads depth only for the tools it actually needs. |
675
735
 
676
- In `grouped` mode the tools are unchanged — same names, same input schemas, same behavior. Only the *up-front context cost* changes. Discovery flows through three meta-tools:
736
+ In `grouped` mode operation names, input schemas and handlers are unchanged. Descriptors also expose the same conservative capability and action truth as the full profile. Discovery flows through three meta-tools:
677
737
 
678
738
  - **`list_tool_groups()`** — the domain map (invoicing, expenses, fiscal/compliance, banking, CRM, HR/payroll, stay/PMS, POS, intelligence, products, platform) with a one-line blurb and tool count for each.
679
739
  - **`search_tools(query)`** — free-text search across tool name, title, summary and group; returns matching tools with their group, summary, read-only flag and input fields. Optional `group` filter and `limit`.
@@ -695,7 +755,7 @@ In `grouped` mode the tools are unchanged — same names, same input schemas, sa
695
755
  }
696
756
  ```
697
757
 
698
- > This is an opt-in exposure layer only — it never changes a tool's logic, name or behavior, and `full` mode is byte-identical to before. The same interceptor pattern powers the OpenAI-safe profile (`FRIHET_OPENAI_MODE`).
758
+ > Grouped exposure changes description density, not operation behavior. The versioned OpenAI-reviewed profile is composed separately and remains independently gated.
699
759
 
700
760
  ---
701
761
 
@@ -797,7 +857,7 @@ npm run build # must pass before submitting
797
857
 
798
858
  | Package | What it is |
799
859
  |---------|-----------|
800
- | [`@frihet/mcp-server`](https://www.npmjs.com/package/@frihet/mcp-server) | This MCP server (157 tools, 11 resources, 10 prompts) |
860
+ | [`@frihet/mcp-server`](https://www.npmjs.com/package/@frihet/mcp-server) | This MCP server (157 canonical operations + 5 alias names; 11 local resources; 10 prompts) |
801
861
  | [`@frihet/sdk`](https://github.com/Frihet-io/frihet-sdk) | TypeScript SDK (`frihet.invoices.create()`) |
802
862
  | [`frihet`](https://www.npmjs.com/package/frihet) | CLI (`frihet invoices list --status overdue`) |
803
863
  | [`n8n-nodes-frihet`](https://www.npmjs.com/package/n8n-nodes-frihet) | n8n community node for workflow automation |
@@ -816,6 +876,7 @@ npm run build # must pass before submitting
816
876
  - [MCP Registry](https://registry.modelcontextprotocol.io/?q=io.frihet) -- Anthropic official registry
817
877
  - [Remote endpoint](https://mcp.frihet.io) -- Hosted MCP server (Cloudflare Workers)
818
878
  - [OpenAPI spec](https://api.frihet.io/openapi.json) -- Machine-readable API definition
879
+ - [Security policy](./SECURITY.md) -- Private vulnerability reporting guidance
819
880
 
820
881
  ---
821
882
 
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Agent-native onboarding contract.
3
+ *
4
+ * Two artefacts, one source:
5
+ *
6
+ * 1. `AGENT_SERVER_INSTRUCTIONS` — the MCP `instructions` string returned by
7
+ * `initialize`. Every MCP client hands this to the model before the first
8
+ * tool call, so it is the only onboarding channel that costs the human
9
+ * zero configuration. It deliberately contains NO counts: a number in a
10
+ * per-session prompt is a number that drifts. It teaches the *mechanism*
11
+ * for finding the current numbers instead.
12
+ *
13
+ * 2. `captureAgentOnboardingDescriptor()` — the machine-readable descriptor
14
+ * written to `docs/agent-onboarding.json` by
15
+ * `scripts/generate-agent-onboarding.mjs`. It is captured through the real
16
+ * MCP SDK against the real registration path, so the tool lists in it
17
+ * cannot drift from the server: `npm run gate:agent-onboarding` re-captures
18
+ * and byte-compares.
19
+ *
20
+ * Nothing here registers a tool, a resource or a prompt. The surface counts the
21
+ * conformance baseline and the public capability contract already pin are
22
+ * unchanged by this module, on purpose.
23
+ */
24
+ export declare const AGENT_ONBOARDING_CONTRACT_VERSION = 1;
25
+ /** Where an API key actually comes from — a deep link, not a navigation
26
+ * instruction, so the human has one click instead of a hunt.
27
+ *
28
+ * Verified against the ERP: `settingsSectionToPath` renders `/settings/<id>`
29
+ * and `api` is in `ALL_SETTINGS_SECTIONS`, so this URL resolves to the
30
+ * ApiKeysSettings panel. The old index.ts wording ("Settings > Developers >
31
+ * API Keys") pointed at a section that does not exist. */
32
+ export declare const API_KEY_LOCATION = "https://app.frihet.io/settings/api";
33
+ export declare const AGENT_SERVER_INSTRUCTIONS = "Frihet ERP \u2014 real business and fiscal records. Read freely, propose before you mutate.\n\nAUTH\n- Local (stdio): FRIHET_API_KEY=fri_... \u2014 create one at https://app.frihet.io/settings/api.\n- No key yet: FRIHET_DEMO=1 serves example fixtures. No network call, nothing persisted, demo_-prefixed IDs, fiscal actions simulated.\n- Hosted: https://mcp.frihet.io/mcp \u2014 OAuth 2.1 + PKCE, or Authorization: Bearer fri_...\n\nORIENT BEFORE ACTING (read-only)\n- get_business_context \u2014 fiscal zone, currency, IRPF/IVA defaults, plan limits. Call this first; the correct tax treatment depends on it.\n- Resources frihet://tax/rates and frihet://tax/calendar carry the Spanish rate table (IVA / IGIC / IPSI) and the filing deadlines. Read them instead of recalling a rate.\n\nCAPABILITY DISCOVERY\n- Every tool in tools/list carries _meta[\"io.frihet/capability\"] = {callability, writesFrihet, externalInteraction, externalSideEffects}. Read it before you call.\n- callability is a conservative fact, not a promise: api_dependent | runtime_checked | deferred | unavailable | local. Registration does NOT mean the backing API is enabled for this workspace; deferred and unavailable tools will refuse.\n- Large surface: set FRIHET_TOOL_MODE=grouped for progressive disclosure (list_tool_groups, search_tools, describe_tool) instead of loading every schema.\n\nTHE SAFE WORKFLOW \u2014 draft, show, stop\n- create_invoice defaults to status=draft: no fiscal number, no hash, nothing submitted to a tax authority. create_quote and create_credit_note are drafts too.\n- Build the draft, read it back, present it to the human, and stop there. Issuing is a separate, human-authorised step.\n\nHUMAN AUTHORITY \u2014 do not call unprompted\n- Any tool whose capability shows a non-empty externalSideEffects reaches outside Frihet \u2014 email to a client, webhook delivery, money movement, or a submission to AEAT / VeriFactu / TicketBAI / FACe. You cannot undo those.\n- Several also take confirm=true. That flag records the human's decision. Never set it to satisfy your own plan; ask, then pass what you were told.\n- delete_invoice does not always delete: an issued invoice is CANCELLED (status=cancelled) because VeriFactu forbids breaking the hash chain. Same for quotes.\n\nWHEN A CALL FAILS\n- Errors carry sanitized remediation text \u2014 read it, do not guess. 401 = key; 403 = scope or plan; 429 = wait Retry-After seconds.\n- After 409 IDEMPOTENCY_REQUEST_IN_PROGRESS, do NOT retry with a fresh Idempotency-Key. Read the resource back and decide from its actual state; a new key would create a second fiscal document.\n\nMachine-readable version of this contract: https://github.com/Frihet-io/frihet-mcp/blob/main/docs/agent-onboarding.json";
34
+ export interface AgentOnboardingDescriptor {
35
+ onboardingContractVersion: number;
36
+ server: {
37
+ mcpName: string;
38
+ npmPackage: string;
39
+ remoteEndpoint: string;
40
+ transport: readonly string[];
41
+ };
42
+ auth: {
43
+ envVar: string;
44
+ keyFormat: string;
45
+ obtainAt: string;
46
+ remoteMethods: readonly string[];
47
+ noAuthTrial: {
48
+ env: Record<string, string>;
49
+ persists: false;
50
+ network: false;
51
+ };
52
+ };
53
+ /** One verified command per client. Every `command` here was executed against
54
+ * the real binary in an isolated HOME before being written down. */
55
+ quickstart: Record<string, {
56
+ steps: readonly string[];
57
+ configFormat: string;
58
+ verify: string;
59
+ }>;
60
+ capabilityDiscovery: {
61
+ metaKey: string;
62
+ callabilityValues: readonly string[];
63
+ groupedModeEnv: Record<string, string>;
64
+ counts: {
65
+ toolNames: number;
66
+ readOnly: number;
67
+ writesFrihet: number;
68
+ externalInteraction: number;
69
+ destructive: number;
70
+ resources: number;
71
+ prompts: number;
72
+ };
73
+ };
74
+ safeWorkflow: {
75
+ description: string;
76
+ steps: readonly {
77
+ call: string;
78
+ kind: string;
79
+ note: string;
80
+ }[];
81
+ };
82
+ humanAuthority: {
83
+ rule: string;
84
+ confirmRequired: readonly string[];
85
+ externalSideEffects: readonly {
86
+ tool: string;
87
+ effects: readonly string[];
88
+ }[];
89
+ };
90
+ errors: readonly {
91
+ condition: string;
92
+ meaning: string;
93
+ agentAction: string;
94
+ }[];
95
+ }
96
+ export declare function captureAgentOnboardingDescriptor(): Promise<AgentOnboardingDescriptor>;
97
+ export declare function serializeAgentOnboardingDescriptor(descriptor: AgentOnboardingDescriptor): string;
98
+ //# sourceMappingURL=agent-onboarding.d.ts.map
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Agent-native onboarding contract.
3
+ *
4
+ * Two artefacts, one source:
5
+ *
6
+ * 1. `AGENT_SERVER_INSTRUCTIONS` — the MCP `instructions` string returned by
7
+ * `initialize`. Every MCP client hands this to the model before the first
8
+ * tool call, so it is the only onboarding channel that costs the human
9
+ * zero configuration. It deliberately contains NO counts: a number in a
10
+ * per-session prompt is a number that drifts. It teaches the *mechanism*
11
+ * for finding the current numbers instead.
12
+ *
13
+ * 2. `captureAgentOnboardingDescriptor()` — the machine-readable descriptor
14
+ * written to `docs/agent-onboarding.json` by
15
+ * `scripts/generate-agent-onboarding.mjs`. It is captured through the real
16
+ * MCP SDK against the real registration path, so the tool lists in it
17
+ * cannot drift from the server: `npm run gate:agent-onboarding` re-captures
18
+ * and byte-compares.
19
+ *
20
+ * Nothing here registers a tool, a resource or a prompt. The surface counts the
21
+ * conformance baseline and the public capability contract already pin are
22
+ * unchanged by this module, on purpose.
23
+ */
24
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
25
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
26
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
+ import { CAPABILITY_META_KEY } from "./capability-truth.js";
28
+ import { localMcpSurfaceComposition, registerMcpSurface, } from "./server-composition.js";
29
+ export const AGENT_ONBOARDING_CONTRACT_VERSION = 1;
30
+ /** Where an API key actually comes from — a deep link, not a navigation
31
+ * instruction, so the human has one click instead of a hunt.
32
+ *
33
+ * Verified against the ERP: `settingsSectionToPath` renders `/settings/<id>`
34
+ * and `api` is in `ALL_SETTINGS_SECTIONS`, so this URL resolves to the
35
+ * ApiKeysSettings panel. The old index.ts wording ("Settings > Developers >
36
+ * API Keys") pointed at a section that does not exist. */
37
+ export const API_KEY_LOCATION = "https://app.frihet.io/settings/api";
38
+ /* ------------------------------------------------------------------ */
39
+ /* 1. initialize.instructions */
40
+ /* ------------------------------------------------------------------ */
41
+ export const AGENT_SERVER_INSTRUCTIONS = `Frihet ERP — real business and fiscal records. Read freely, propose before you mutate.
42
+
43
+ AUTH
44
+ - Local (stdio): FRIHET_API_KEY=fri_... — create one at ${API_KEY_LOCATION}.
45
+ - No key yet: FRIHET_DEMO=1 serves example fixtures. No network call, nothing persisted, demo_-prefixed IDs, fiscal actions simulated.
46
+ - Hosted: https://mcp.frihet.io/mcp — OAuth 2.1 + PKCE, or Authorization: Bearer fri_...
47
+
48
+ ORIENT BEFORE ACTING (read-only)
49
+ - get_business_context — fiscal zone, currency, IRPF/IVA defaults, plan limits. Call this first; the correct tax treatment depends on it.
50
+ - Resources frihet://tax/rates and frihet://tax/calendar carry the Spanish rate table (IVA / IGIC / IPSI) and the filing deadlines. Read them instead of recalling a rate.
51
+
52
+ CAPABILITY DISCOVERY
53
+ - Every tool in tools/list carries _meta["${CAPABILITY_META_KEY}"] = {callability, writesFrihet, externalInteraction, externalSideEffects}. Read it before you call.
54
+ - callability is a conservative fact, not a promise: api_dependent | runtime_checked | deferred | unavailable | local. Registration does NOT mean the backing API is enabled for this workspace; deferred and unavailable tools will refuse.
55
+ - Large surface: set FRIHET_TOOL_MODE=grouped for progressive disclosure (list_tool_groups, search_tools, describe_tool) instead of loading every schema.
56
+
57
+ THE SAFE WORKFLOW — draft, show, stop
58
+ - create_invoice defaults to status=draft: no fiscal number, no hash, nothing submitted to a tax authority. create_quote and create_credit_note are drafts too.
59
+ - Build the draft, read it back, present it to the human, and stop there. Issuing is a separate, human-authorised step.
60
+
61
+ HUMAN AUTHORITY — do not call unprompted
62
+ - Any tool whose capability shows a non-empty externalSideEffects reaches outside Frihet — email to a client, webhook delivery, money movement, or a submission to AEAT / VeriFactu / TicketBAI / FACe. You cannot undo those.
63
+ - Several also take confirm=true. That flag records the human's decision. Never set it to satisfy your own plan; ask, then pass what you were told.
64
+ - delete_invoice does not always delete: an issued invoice is CANCELLED (status=cancelled) because VeriFactu forbids breaking the hash chain. Same for quotes.
65
+
66
+ WHEN A CALL FAILS
67
+ - Errors carry sanitized remediation text — read it, do not guess. 401 = key; 403 = scope or plan; 429 = wait Retry-After seconds.
68
+ - After 409 IDEMPOTENCY_REQUEST_IN_PROGRESS, do NOT retry with a fresh Idempotency-Key. Read the resource back and decide from its actual state; a new key would create a second fiscal document.
69
+
70
+ Machine-readable version of this contract: https://github.com/Frihet-io/frihet-mcp/blob/main/docs/agent-onboarding.json`;
71
+ function makeRegistrationClient() {
72
+ return new Proxy({}, { get: () => async () => ({ data: [], total: 0, limit: 0, offset: 0 }) });
73
+ }
74
+ /** True when the tool's JSON Schema declares a top-level `confirm` property.
75
+ * Derived, never hand-listed — a new confirm guard shows up in the descriptor
76
+ * on the next generate, and the gate fails until it is committed. */
77
+ function declaresConfirm(inputSchema) {
78
+ if (typeof inputSchema !== "object" || inputSchema === null)
79
+ return false;
80
+ const properties = inputSchema.properties;
81
+ if (typeof properties !== "object" || properties === null)
82
+ return false;
83
+ return Object.hasOwn(properties, "confirm");
84
+ }
85
+ async function captureLocalFullSurface() {
86
+ const server = new McpServer({
87
+ name: "agent-onboarding-capture",
88
+ version: "1.0.0",
89
+ });
90
+ registerMcpSurface(server, makeRegistrationClient(), localMcpSurfaceComposition(false, false));
91
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
92
+ const client = new Client({ name: "agent-onboarding-capture-client", version: "1.0.0" }, { capabilities: {} });
93
+ try {
94
+ await Promise.all([
95
+ server.connect(serverTransport),
96
+ client.connect(clientTransport),
97
+ ]);
98
+ const listed = await client.listTools();
99
+ const tools = listed.tools
100
+ .map((tool) => {
101
+ const meta = tool._meta;
102
+ const capability = meta?.[CAPABILITY_META_KEY];
103
+ return {
104
+ name: tool.name,
105
+ annotations: (tool.annotations ?? {}),
106
+ ...(capability ? { capability } : {}),
107
+ hasConfirm: declaresConfirm(tool.inputSchema),
108
+ };
109
+ })
110
+ .sort((left, right) => left.name.localeCompare(right.name));
111
+ const [resources, prompts] = await Promise.all([
112
+ client.listResources(),
113
+ client.listPrompts(),
114
+ ]);
115
+ return {
116
+ tools,
117
+ resources: resources.resources.length,
118
+ prompts: prompts.prompts.length,
119
+ };
120
+ }
121
+ finally {
122
+ await Promise.allSettled([client.close(), server.close()]);
123
+ }
124
+ }
125
+ export async function captureAgentOnboardingDescriptor() {
126
+ const { tools, resources, prompts } = await captureLocalFullSurface();
127
+ const confirmRequired = tools
128
+ .filter((tool) => tool.hasConfirm)
129
+ .map((tool) => tool.name);
130
+ const externalSideEffects = tools
131
+ .filter((tool) => (tool.capability?.externalSideEffects.length ?? 0) > 0)
132
+ .map((tool) => ({
133
+ tool: tool.name,
134
+ effects: [...(tool.capability?.externalSideEffects ?? [])],
135
+ }));
136
+ return {
137
+ onboardingContractVersion: AGENT_ONBOARDING_CONTRACT_VERSION,
138
+ server: {
139
+ mcpName: "io.frihet/erp",
140
+ npmPackage: "@frihet/mcp-server",
141
+ remoteEndpoint: "https://mcp.frihet.io/mcp",
142
+ transport: ["stdio", "streamable-http"],
143
+ },
144
+ auth: {
145
+ envVar: "FRIHET_API_KEY",
146
+ keyFormat: "fri_<key>",
147
+ obtainAt: API_KEY_LOCATION,
148
+ remoteMethods: ["oauth2.1-pkce", "bearer"],
149
+ noAuthTrial: {
150
+ env: { FRIHET_DEMO: "1" },
151
+ persists: false,
152
+ network: false,
153
+ },
154
+ },
155
+ quickstart: {
156
+ "claude-code": {
157
+ steps: [
158
+ "claude mcp add frihet -s user -e FRIHET_API_KEY=fri_... -- npx -y @frihet/mcp-server",
159
+ ],
160
+ configFormat: "managed by the CLI (user scope writes ~/.claude.json)",
161
+ verify: "claude mcp list",
162
+ },
163
+ codex: {
164
+ steps: [
165
+ "codex mcp add frihet --env FRIHET_API_KEY=fri_... -- npx -y @frihet/mcp-server",
166
+ ],
167
+ configFormat: "toml (~/.codex/config.toml — JSON in this file is a parse error)",
168
+ verify: "codex mcp list",
169
+ },
170
+ "generic-mcp-client-stdio": {
171
+ steps: [
172
+ 'mcpServers.frihet = { "command": "npx", "args": ["-y", "@frihet/mcp-server"], "env": { "FRIHET_API_KEY": "fri_..." } }',
173
+ ],
174
+ configFormat: "json",
175
+ verify: "initialize → tools/list returns a non-empty catalogue",
176
+ },
177
+ "generic-mcp-client-http": {
178
+ steps: [
179
+ 'mcpServers.frihet = { "type": "streamable-http", "url": "https://mcp.frihet.io/mcp", "headers": { "Authorization": "Bearer fri_..." } }',
180
+ ],
181
+ configFormat: "json",
182
+ verify: "initialize → tools/list returns a non-empty catalogue",
183
+ },
184
+ },
185
+ capabilityDiscovery: {
186
+ metaKey: CAPABILITY_META_KEY,
187
+ callabilityValues: [
188
+ "api_dependent",
189
+ "runtime_checked",
190
+ "deferred",
191
+ "unavailable",
192
+ "local",
193
+ ],
194
+ groupedModeEnv: { FRIHET_TOOL_MODE: "grouped" },
195
+ counts: {
196
+ toolNames: tools.length,
197
+ readOnly: tools.filter((tool) => tool.annotations.readOnlyHint === true)
198
+ .length,
199
+ writesFrihet: tools.filter((tool) => tool.capability?.writesFrihet === true).length,
200
+ externalInteraction: externalSideEffects.length,
201
+ destructive: tools.filter((tool) => tool.annotations.destructiveHint === true).length,
202
+ resources,
203
+ prompts,
204
+ },
205
+ },
206
+ safeWorkflow: {
207
+ description: "Discover → orient → retrieve → propose → hand back. No step mutates data or reaches a third party.",
208
+ steps: [
209
+ {
210
+ call: "get_business_context",
211
+ kind: "read",
212
+ note: "Fiscal zone, currency and defaults. Everything downstream depends on it.",
213
+ },
214
+ {
215
+ call: "resources/read frihet://tax/rates",
216
+ kind: "read",
217
+ note: "Authoritative IVA / IGIC / IPSI table. Do not recall a rate from memory.",
218
+ },
219
+ {
220
+ call: "list_clients",
221
+ kind: "read",
222
+ note: "Resolve the client by name before referencing an id.",
223
+ },
224
+ {
225
+ call: "STOP",
226
+ kind: "handoff",
227
+ note: "Present the proposed draft inputs and all disclosed effects. Creating, issuing, sending, or submitting is a separate human decision.",
228
+ },
229
+ ],
230
+ },
231
+ humanAuthority: {
232
+ rule: "Do not call a tool with non-empty externalSideEffects, and do not set confirm=true, without an explicit human instruction for that specific action.",
233
+ confirmRequired,
234
+ externalSideEffects,
235
+ },
236
+ errors: [
237
+ {
238
+ condition: "startup exits 1 with code FRIHET_API_KEY_MISSING",
239
+ meaning: "No credential in the environment.",
240
+ agentAction: "Ask the human for a key from " +
241
+ API_KEY_LOCATION +
242
+ ", or re-launch with FRIHET_DEMO=1 to explore without one.",
243
+ },
244
+ {
245
+ condition: "HTTP 401",
246
+ meaning: "Key missing, malformed or revoked.",
247
+ agentAction: "Stop and ask for a new key. Do not retry.",
248
+ },
249
+ {
250
+ condition: "HTTP 403",
251
+ meaning: "The key is valid but lacks the scope, or the plan excludes it.",
252
+ agentAction: "Report which operation was refused. Do not try a different tool to route around it.",
253
+ },
254
+ {
255
+ condition: "HTTP 429",
256
+ meaning: "Rate limited.",
257
+ agentAction: "Wait the Retry-After seconds, then retry once.",
258
+ },
259
+ {
260
+ condition: "HTTP 409 IDEMPOTENCY_REQUEST_IN_PROGRESS",
261
+ meaning: "The first attempt's outcome could not be recorded; the server refuses to execute twice.",
262
+ agentAction: "Read the resource back and reconcile from its actual state. Never retry with a fresh Idempotency-Key.",
263
+ },
264
+ {
265
+ condition: "capability.callability is deferred or unavailable",
266
+ meaning: "The tool is registered but the backing API is not serving it.",
267
+ agentAction: "Do not call it. Tell the human the capability is not live for this workspace.",
268
+ },
269
+ ],
270
+ };
271
+ }
272
+ export function serializeAgentOnboardingDescriptor(descriptor) {
273
+ return JSON.stringify(descriptor, null, 2) + "\n";
274
+ }
275
+ //# sourceMappingURL=agent-onboarding.js.map