@frihet/mcp-server 1.16.6 → 2.0.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/README.md +168 -82
- package/dist/agent-onboarding.d.ts +98 -0
- package/dist/agent-onboarding.js +275 -0
- package/dist/api-origin.d.ts +9 -0
- package/dist/api-origin.js +41 -0
- package/dist/canary/mcp-harness.d.ts +77 -0
- package/dist/canary/mcp-harness.js +552 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +262 -0
- package/dist/claude-candidate-contract.d.ts +482 -0
- package/dist/claude-candidate-contract.js +87 -0
- package/dist/claude-output-privacy.d.ts +17 -0
- package/dist/claude-output-privacy.js +240 -0
- package/dist/claude-profile.d.ts +18 -0
- package/dist/claude-profile.js +260 -0
- package/dist/client-interface.d.ts +47 -33
- package/dist/client.d.ts +172 -40
- package/dist/client.js +708 -165
- package/dist/demo-client.d.ts +41 -32
- package/dist/demo-client.js +202 -31
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/deposit-contract.d.ts +29 -0
- package/dist/deposit-contract.js +76 -0
- package/dist/fetch-no-redirect.d.ts +3 -0
- package/dist/fetch-no-redirect.js +16 -0
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/fiscal-period.d.ts +26 -0
- package/dist/fiscal-period.js +31 -0
- package/dist/google-profile.d.ts +19 -0
- package/dist/google-profile.js +122 -0
- package/dist/index.js +68 -59
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +194 -106
- package/dist/openai-profile.d.ts +26 -22
- package/dist/openai-profile.js +1193 -157
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +714 -41
- package/dist/openai-review-oauth.d.ts +149 -2
- package/dist/openai-review-oauth.js +220 -6
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +34 -78
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +15 -0
- package/dist/redaction.js +83 -0
- package/dist/resources/register-all.js +154 -64
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +19 -0
- package/dist/server-composition.js +74 -0
- package/dist/tool-exposure.d.ts +31 -27
- package/dist/tool-exposure.js +211 -87
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/audit_gl.js +8 -8
- package/dist/tools/backend-availability.d.ts +8 -29
- package/dist/tools/backend-availability.js +31 -30
- package/dist/tools/bank_rules.js +66 -15
- package/dist/tools/banking.js +5 -13
- package/dist/tools/clients.js +261 -15
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +114 -85
- package/dist/tools/einvoice.d.ts +16 -2
- package/dist/tools/einvoice.js +59 -12
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/fiscal.d.ts +3 -4
- package/dist/tools/fiscal.js +93 -60
- package/dist/tools/gestoria.js +15 -7
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +49 -20
- package/dist/tools/igic.d.ts +9 -8
- package/dist/tools/igic.js +34 -64
- package/dist/tools/impuesto_sociedades.d.ts +4 -5
- package/dist/tools/impuesto_sociedades.js +16 -37
- package/dist/tools/intelligence.js +166 -12
- package/dist/tools/invoices.js +224 -54
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/payroll.d.ts +5 -8
- package/dist/tools/payroll.js +19 -32
- package/dist/tools/permissions.d.ts +4 -7
- package/dist/tools/permissions.js +11 -14
- package/dist/tools/products.js +6 -6
- package/dist/tools/quotes.js +172 -26
- package/dist/tools/recurring.js +26 -25
- package/dist/tools/register-all.d.ts +2 -2
- package/dist/tools/register-all.js +9 -6
- package/dist/tools/search.d.ts +19 -0
- package/dist/tools/search.js +84 -0
- package/dist/tools/shared.d.ts +1445 -109
- package/dist/tools/shared.js +478 -102
- package/dist/tools/team.js +24 -28
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +249 -15
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +31 -5
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +391 -0
- package/package.json +49 -12
- package/dist/tools/onboarding.d.ts +0 -19
- package/dist/tools/onboarding.js +0 -64
package/dist/tool-exposure.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool-exposure profile for the Frihet MCP server — progressive disclosure.
|
|
3
3
|
*
|
|
4
|
-
* Activated by FRIHET_TOOL_MODE=grouped (env var or Worker binding).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Activated by FRIHET_TOOL_MODE=grouped (env var or Worker binding). Default
|
|
5
|
+
* full mode keeps full descriptions/schemas and does not add discovery names;
|
|
6
|
+
* the outer public composition may still attach capability truth metadata.
|
|
7
7
|
*
|
|
8
8
|
* ── Why ──────────────────────────────────────────────────────────────────
|
|
9
|
-
* Context rot is the 2026 problem: a flat list of
|
|
9
|
+
* Context rot is the 2026 problem: a flat list of 158 tool descriptions,
|
|
10
10
|
* each a multi-paragraph bilingual blob, eats the agent's context window and
|
|
11
11
|
* degrades tool selection before any work begins. Leaders cut flat lists.
|
|
12
12
|
*
|
|
@@ -23,26 +23,26 @@
|
|
|
23
23
|
* the server.
|
|
24
24
|
* 2. Still registers every tool so it stays INVOCABLE (nothing breaks; tool
|
|
25
25
|
* logic, names and behavior are untouched) — but COLLAPSES its registered
|
|
26
|
-
* description to a single terse "[group] summary — full
|
|
27
|
-
* describe_tool('name')" line. That is the context saving.
|
|
26
|
+
* description to a single terse "[group] summary — full description and
|
|
27
|
+
* input fields via describe_tool('name')" line. That is the context saving.
|
|
28
28
|
* 3. Adds three lightweight META-TOOLS as the entry point for discovery:
|
|
29
|
-
* • list_tool_groups() — the
|
|
29
|
+
* • list_tool_groups() — the 11-domain map with per-group counts
|
|
30
30
|
* • search_tools(query) — fuzzy match → matching tool summaries
|
|
31
31
|
* • describe_tool(name) — full original description + input fields
|
|
32
32
|
*
|
|
33
|
-
* The agent loads ~3 meta-tool descriptions +
|
|
34
|
-
*
|
|
33
|
+
* The agent loads ~3 meta-tool descriptions + 158 terse one-liners instead of
|
|
34
|
+
* 158 full bilingual blobs, then pulls full depth only for the handful of
|
|
35
35
|
* tools it actually needs. Progressive disclosure, zero behavior change.
|
|
36
36
|
*
|
|
37
37
|
* IMPORTANT: this is purely an EXPOSURE layer. It does NOT live in
|
|
38
|
-
* src/tools/*.ts, so the audited tool count stays
|
|
38
|
+
* src/tools/*.ts, so the audited tool count stays 158 (+ meta). The meta-tools
|
|
39
39
|
* are added only in grouped mode and are NOT counted as ERP tools.
|
|
40
40
|
*
|
|
41
41
|
* @see ./openai-profile.ts — the sibling interceptor this mirrors.
|
|
42
42
|
*/
|
|
43
43
|
/** Stable domain identifiers used by the group router. */
|
|
44
44
|
export type ToolGroupId = "invoicing" | "expenses" | "fiscal" | "banking" | "crm" | "hr" | "stay" | "pos" | "intelligence" | "catalog" | "platform";
|
|
45
|
-
interface GroupMeta {
|
|
45
|
+
export interface GroupMeta {
|
|
46
46
|
/** Human label, bilingual. */
|
|
47
47
|
label: string;
|
|
48
48
|
/** One-line domain blurb shown in list_tool_groups. */
|
|
@@ -64,7 +64,7 @@ export declare const GROUPS: Record<ToolGroupId, GroupMeta>;
|
|
|
64
64
|
export declare const FILE_TO_GROUP: Record<string, ToolGroupId>;
|
|
65
65
|
/**
|
|
66
66
|
* Assign a group by tool NAME. The name-based mapping reproduces the
|
|
67
|
-
* source-file grouping exactly for
|
|
67
|
+
* source-file grouping exactly for every current tool (verified), with the
|
|
68
68
|
* eight cross-file cases pinned via NAME_OVERRIDES. Driven off the name (not a
|
|
69
69
|
* hand-kept list) so a future tool lands somewhere sensible automatically.
|
|
70
70
|
*
|
|
@@ -81,6 +81,10 @@ interface CatalogEntry {
|
|
|
81
81
|
description: string;
|
|
82
82
|
/** Whether the tool mutates state (from annotations.readOnlyHint). */
|
|
83
83
|
readOnly: boolean;
|
|
84
|
+
/** Full MCP action annotations, exposed only on the full capability profile. */
|
|
85
|
+
annotations?: Record<string, boolean>;
|
|
86
|
+
/** Conservative runtime capability fact, exposed only on the full profile. */
|
|
87
|
+
capability?: Record<string, unknown>;
|
|
84
88
|
/** Input field names, for quick schema shape without dumping zod. */
|
|
85
89
|
inputFields: string[];
|
|
86
90
|
}
|
|
@@ -118,25 +122,25 @@ export declare function resolveToolMode(env?: Record<string, string | undefined>
|
|
|
118
122
|
* @param server The McpServer (typed loosely to match the openai-profile shim).
|
|
119
123
|
* @param options.allowlist When provided, ONLY tools whose name is in this set
|
|
120
124
|
* are catalogued + collapsed; any other tool is passed through untouched.
|
|
121
|
-
* This
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* COMPOSITION ORDERING (openai-mcp): apply this profile FIRST so its
|
|
128
|
-
* originalRegisterTool is the REAL server.registerTool — the three meta-tools
|
|
129
|
-
* are then registered straight onto the real server and BYPASS the OpenAI
|
|
130
|
-
* allow-list gate entirely (so they always materialise without polluting the
|
|
131
|
-
* OpenAI includeTools set / its advertised 53-tool count). Apply
|
|
132
|
-
* applyOpenAIProfile() SECOND (outermost) so a business-tool registration is
|
|
133
|
-
* first redacted + annotated + openWorldHint-justified by OpenAI, and only
|
|
134
|
-
* THEN collapsed here — making the terse line the final description while the
|
|
135
|
-
* OpenAI redaction wrapper around the handler survives intact.
|
|
125
|
+
* This supports a deliberately restricted grouped surface without allowing
|
|
126
|
+
* discovery to reveal or describe tools outside that surface. Omit (default)
|
|
127
|
+
* for mcp.frihet.io, which catalogs every registered tool. The reviewed
|
|
128
|
+
* OpenAI host does not use grouped exposure: it publishes the exact 33-tool
|
|
129
|
+
* allow-list with complete descriptions and no discovery meta-tools.
|
|
136
130
|
* @returns a handle exposing the live catalog (useful for tests/logging).
|
|
137
131
|
*/
|
|
138
132
|
export declare function applyToolExposureProfile(server: any, options?: {
|
|
139
133
|
allowlist?: ReadonlySet<string>;
|
|
134
|
+
groupMetadata?: Partial<Record<ToolGroupId, GroupMeta>>;
|
|
135
|
+
describeToolExample?: string;
|
|
136
|
+
capabilityTruth?: {
|
|
137
|
+
metaKey: string;
|
|
138
|
+
localDiscovery: (name: string) => object;
|
|
139
|
+
};
|
|
140
|
+
securitySchemes?: ReadonlyArray<{
|
|
141
|
+
type: "oauth2";
|
|
142
|
+
scopes: readonly string[];
|
|
143
|
+
}>;
|
|
140
144
|
}): ToolExposureHandle;
|
|
141
145
|
/** Number of meta-tools added in grouped mode (for logging). */
|
|
142
146
|
export declare const GROUPED_META_TOOL_COUNT: number;
|
package/dist/tool-exposure.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool-exposure profile for the Frihet MCP server — progressive disclosure.
|
|
3
3
|
*
|
|
4
|
-
* Activated by FRIHET_TOOL_MODE=grouped (env var or Worker binding).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Activated by FRIHET_TOOL_MODE=grouped (env var or Worker binding). Default
|
|
5
|
+
* full mode keeps full descriptions/schemas and does not add discovery names;
|
|
6
|
+
* the outer public composition may still attach capability truth metadata.
|
|
7
7
|
*
|
|
8
8
|
* ── Why ──────────────────────────────────────────────────────────────────
|
|
9
|
-
* Context rot is the 2026 problem: a flat list of
|
|
9
|
+
* Context rot is the 2026 problem: a flat list of 158 tool descriptions,
|
|
10
10
|
* each a multi-paragraph bilingual blob, eats the agent's context window and
|
|
11
11
|
* degrades tool selection before any work begins. Leaders cut flat lists.
|
|
12
12
|
*
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
* the server.
|
|
24
24
|
* 2. Still registers every tool so it stays INVOCABLE (nothing breaks; tool
|
|
25
25
|
* logic, names and behavior are untouched) — but COLLAPSES its registered
|
|
26
|
-
* description to a single terse "[group] summary — full
|
|
27
|
-
* describe_tool('name')" line. That is the context saving.
|
|
26
|
+
* description to a single terse "[group] summary — full description and
|
|
27
|
+
* input fields via describe_tool('name')" line. That is the context saving.
|
|
28
28
|
* 3. Adds three lightweight META-TOOLS as the entry point for discovery:
|
|
29
|
-
* • list_tool_groups() — the
|
|
29
|
+
* • list_tool_groups() — the 11-domain map with per-group counts
|
|
30
30
|
* • search_tools(query) — fuzzy match → matching tool summaries
|
|
31
31
|
* • describe_tool(name) — full original description + input fields
|
|
32
32
|
*
|
|
33
|
-
* The agent loads ~3 meta-tool descriptions +
|
|
34
|
-
*
|
|
33
|
+
* The agent loads ~3 meta-tool descriptions + 158 terse one-liners instead of
|
|
34
|
+
* 158 full bilingual blobs, then pulls full depth only for the handful of
|
|
35
35
|
* tools it actually needs. Progressive disclosure, zero behavior change.
|
|
36
36
|
*
|
|
37
37
|
* IMPORTANT: this is purely an EXPOSURE layer. It does NOT live in
|
|
38
|
-
* src/tools/*.ts, so the audited tool count stays
|
|
38
|
+
* src/tools/*.ts, so the audited tool count stays 158 (+ meta). The meta-tools
|
|
39
39
|
* are added only in grouped mode and are NOT counted as ERP tools.
|
|
40
40
|
*
|
|
41
41
|
* @see ./openai-profile.ts — the sibling interceptor this mirrors.
|
|
@@ -53,12 +53,17 @@ export const GROUPS = {
|
|
|
53
53
|
},
|
|
54
54
|
expenses: {
|
|
55
55
|
label: "Expenses & vendors / Gastos y proveedores",
|
|
56
|
-
|
|
56
|
+
// C38: this used to read "Expenses (with OCR)". No OCR tool exists — the
|
|
57
|
+
// expenses family is list/get/create/update/delete plus vendor CRUD, and
|
|
58
|
+
// create_expense takes no file, image or URL. Do NOT re-add a capability
|
|
59
|
+
// token here that search_tools cannot resolve to a real tool.
|
|
60
|
+
blurb: "Expenses and vendor/supplier records.",
|
|
57
61
|
},
|
|
58
62
|
fiscal: {
|
|
59
63
|
label: "Fiscal & compliance / Fiscal y cumplimiento",
|
|
60
|
-
blurb: "Spanish/EU fiscal depth served on demand: Modelo 303/130/390/
|
|
61
|
-
"
|
|
64
|
+
blurb: "Spanish/EU fiscal depth served on demand: Modelo 303/130/390/347, VeriFactu, TicketBAI, " +
|
|
65
|
+
"Facturae/FACe e-invoicing, GL audit, period close, VIES " +
|
|
66
|
+
"(Modelo 180/200/202/415/418/425, AIEM and KSeF registered but not deployed).",
|
|
62
67
|
},
|
|
63
68
|
banking: {
|
|
64
69
|
label: "Banking / Banca",
|
|
@@ -129,13 +134,14 @@ export const FILE_TO_GROUP = {
|
|
|
129
134
|
kitchen: "pos",
|
|
130
135
|
intelligence: "intelligence",
|
|
131
136
|
gestoria: "intelligence",
|
|
137
|
+
search: "intelligence",
|
|
132
138
|
products: "catalog",
|
|
133
139
|
webhooks: "platform",
|
|
134
140
|
portal_domain: "platform",
|
|
135
141
|
};
|
|
136
142
|
/**
|
|
137
143
|
* Per-tool overrides where the source FILE places a tool in a different group
|
|
138
|
-
* than a naive name match would (verified against the
|
|
144
|
+
* than a naive name match would (verified against the 158 registration sites).
|
|
139
145
|
* These eight names live in a file whose domain differs from their name prefix
|
|
140
146
|
* (e.g. e-invoicing tools say "invoice" but belong to fiscal/compliance).
|
|
141
147
|
*/
|
|
@@ -155,7 +161,7 @@ const NAME_OVERRIDES = {
|
|
|
155
161
|
};
|
|
156
162
|
/**
|
|
157
163
|
* Assign a group by tool NAME. The name-based mapping reproduces the
|
|
158
|
-
* source-file grouping exactly for
|
|
164
|
+
* source-file grouping exactly for every current tool (verified), with the
|
|
159
165
|
* eight cross-file cases pinned via NAME_OVERRIDES. Driven off the name (not a
|
|
160
166
|
* hand-kept list) so a future tool lands somewhere sensible automatically.
|
|
161
167
|
*
|
|
@@ -184,7 +190,7 @@ export function groupForTool(name) {
|
|
|
184
190
|
return "pos";
|
|
185
191
|
if (/(kitchen|menu_item)/.test(n))
|
|
186
192
|
return "pos";
|
|
187
|
-
if (/(business_context|monthly_summary|gestoria)/.test(n))
|
|
193
|
+
if (/(business_context|monthly_summary|gestoria|global_search)/.test(n))
|
|
188
194
|
return "intelligence";
|
|
189
195
|
if (/(product)/.test(n))
|
|
190
196
|
return "catalog";
|
|
@@ -202,7 +208,44 @@ function firstSentence(desc) {
|
|
|
202
208
|
const match = englishHalf.match(/^(.*?[.!?])(\s|$)/);
|
|
203
209
|
const sentence = (match ? match[1] : englishHalf).trim();
|
|
204
210
|
// Hard cap so a runaway description can't reintroduce context bloat.
|
|
205
|
-
return sentence.length > 160 ? sentence.slice(0,
|
|
211
|
+
return sentence.length > 160 ? sentence.slice(0, 158).trimEnd() + "…" : sentence;
|
|
212
|
+
}
|
|
213
|
+
function inputSchemaFieldNames(schema) {
|
|
214
|
+
if (schema instanceof z.ZodObject) {
|
|
215
|
+
return Object.keys(schema.shape);
|
|
216
|
+
}
|
|
217
|
+
if (schema !== null && typeof schema === "object" && !Array.isArray(schema)) {
|
|
218
|
+
return Object.keys(schema);
|
|
219
|
+
}
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Does this tool declare `confirm` as a REQUIRED input?
|
|
224
|
+
*
|
|
225
|
+
* Handles both a raw registration shape and a strict ZodObject. The latter is
|
|
226
|
+
* used by create_expense; looking only at `schema["confirm"]` made its required
|
|
227
|
+
* confirmation disappear from the collapsed tools/list description.
|
|
228
|
+
*
|
|
229
|
+
* ── Why this exists ──────────────────────────────────────────────────
|
|
230
|
+
* `firstSentence` keeps only the opening sentence, so a confirm requirement
|
|
231
|
+
* stated in sentence two vanished from every collapsed listing: all 12
|
|
232
|
+
* confirm-gated tools shipped a description that never mentioned the input the
|
|
233
|
+
* SDK rejects the call for. The agent then reads "Delete an invoice by its ID",
|
|
234
|
+
* calls `{id}`, and gets an input-validation error with no prose basis to
|
|
235
|
+
* recover. Deriving the marker here fixes every present AND future guarded tool
|
|
236
|
+
* at once — the alternative (editing 12 first sentences by hand) is the same
|
|
237
|
+
* enumerate-instead-of-derive mistake GAP-04 was raised for.
|
|
238
|
+
*/
|
|
239
|
+
function requiresConfirm(inputSchema) {
|
|
240
|
+
const shape = inputSchema instanceof z.ZodObject
|
|
241
|
+
? inputSchema.shape
|
|
242
|
+
: inputSchema;
|
|
243
|
+
if (!shape || typeof shape !== "object")
|
|
244
|
+
return false;
|
|
245
|
+
const field = shape["confirm"];
|
|
246
|
+
if (typeof field?.safeParse !== "function")
|
|
247
|
+
return false;
|
|
248
|
+
return field.safeParse(undefined).success === false;
|
|
206
249
|
}
|
|
207
250
|
/**
|
|
208
251
|
* Read process.env without referencing the `process` global type — keeps this
|
|
@@ -242,21 +285,11 @@ export function resolveToolMode(env = defaultEnv()) {
|
|
|
242
285
|
* @param server The McpServer (typed loosely to match the openai-profile shim).
|
|
243
286
|
* @param options.allowlist When provided, ONLY tools whose name is in this set
|
|
244
287
|
* are catalogued + collapsed; any other tool is passed through untouched.
|
|
245
|
-
* This
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* COMPOSITION ORDERING (openai-mcp): apply this profile FIRST so its
|
|
252
|
-
* originalRegisterTool is the REAL server.registerTool — the three meta-tools
|
|
253
|
-
* are then registered straight onto the real server and BYPASS the OpenAI
|
|
254
|
-
* allow-list gate entirely (so they always materialise without polluting the
|
|
255
|
-
* OpenAI includeTools set / its advertised 53-tool count). Apply
|
|
256
|
-
* applyOpenAIProfile() SECOND (outermost) so a business-tool registration is
|
|
257
|
-
* first redacted + annotated + openWorldHint-justified by OpenAI, and only
|
|
258
|
-
* THEN collapsed here — making the terse line the final description while the
|
|
259
|
-
* OpenAI redaction wrapper around the handler survives intact.
|
|
288
|
+
* This supports a deliberately restricted grouped surface without allowing
|
|
289
|
+
* discovery to reveal or describe tools outside that surface. Omit (default)
|
|
290
|
+
* for mcp.frihet.io, which catalogs every registered tool. The reviewed
|
|
291
|
+
* OpenAI host does not use grouped exposure: it publishes the exact 33-tool
|
|
292
|
+
* allow-list with complete descriptions and no discovery meta-tools.
|
|
260
293
|
* @returns a handle exposing the live catalog (useful for tests/logging).
|
|
261
294
|
*/
|
|
262
295
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -267,6 +300,13 @@ server, options) {
|
|
|
267
300
|
const groups = new Map();
|
|
268
301
|
const handle = { catalog, groups };
|
|
269
302
|
const allowlist = options?.allowlist;
|
|
303
|
+
const visibleGroupIds = allowlist
|
|
304
|
+
? TOOL_GROUP_IDS.filter((group) => [...allowlist].some((name) => groupForTool(name) === group))
|
|
305
|
+
: [...TOOL_GROUP_IDS];
|
|
306
|
+
const groupMetadata = Object.fromEntries(TOOL_GROUP_IDS.map((id) => [
|
|
307
|
+
id,
|
|
308
|
+
options?.groupMetadata?.[id] ?? GROUPS[id],
|
|
309
|
+
]));
|
|
270
310
|
const originalRegisterTool = server.registerTool.bind(server);
|
|
271
311
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
272
312
|
server.registerTool = (name, config, handler) => {
|
|
@@ -286,10 +326,12 @@ server, options) {
|
|
|
286
326
|
}
|
|
287
327
|
const group = groupForTool(name);
|
|
288
328
|
const fullDescription = typeof config?.description === "string" ? config.description : "";
|
|
289
|
-
const inputFields = config?.inputSchema
|
|
290
|
-
? Object.keys(config.inputSchema)
|
|
291
|
-
: [];
|
|
329
|
+
const inputFields = inputSchemaFieldNames(config?.inputSchema);
|
|
292
330
|
const readOnly = config?.annotations?.readOnlyHint === true;
|
|
331
|
+
const annotations = config?.annotations;
|
|
332
|
+
const capability = options?.capabilityTruth
|
|
333
|
+
? config?._meta?.[options.capabilityTruth.metaKey]
|
|
334
|
+
: undefined;
|
|
293
335
|
const entry = {
|
|
294
336
|
name,
|
|
295
337
|
group,
|
|
@@ -297,6 +339,7 @@ server, options) {
|
|
|
297
339
|
summary: firstSentence(fullDescription),
|
|
298
340
|
description: fullDescription,
|
|
299
341
|
readOnly,
|
|
342
|
+
...(options?.capabilityTruth ? { annotations, capability } : {}),
|
|
300
343
|
inputFields,
|
|
301
344
|
};
|
|
302
345
|
catalog.set(name, entry);
|
|
@@ -313,27 +356,41 @@ server, options) {
|
|
|
313
356
|
// a per-tool open-world justification on EVERY reviewed tool). When composed
|
|
314
357
|
// with the OpenAI profile, that profile runs first (outermost) and has
|
|
315
358
|
// already set the correct annotations.openWorldHint (e.g. true for
|
|
316
|
-
// send_invoice
|
|
359
|
+
// send_invoice). We re-derive
|
|
317
360
|
// the one-line rationale here from the (now-correct) annotation so the terse
|
|
318
361
|
// collapsed description carries it — instead of letting the OpenAI-injected
|
|
319
362
|
// rationale be lost when we overwrite config.description. Only appended in
|
|
320
363
|
// allow-list (composition) mode; the open mcp.frihet.io surface keeps its
|
|
321
364
|
// pure terse line (no OpenAI review constraint there).
|
|
365
|
+
// A required `confirm` is the single most consequential thing an agent can
|
|
366
|
+
// miss: omit it and the call dies on schema validation with no prose basis
|
|
367
|
+
// to recover. firstSentence() keeps only the first English sentence, so a
|
|
368
|
+
// "Requires confirm=true." written into sentence 2 of a tool description is
|
|
369
|
+
// invisible on the surface the agent actually loads. Derive the marker from
|
|
370
|
+
// the SCHEMA instead of trusting prose — it then covers every confirm-gated
|
|
371
|
+
// tool, including ones added after this line was written.
|
|
372
|
+
//
|
|
373
|
+
// In OpenAI mode this is deliberately load-bearing: the reviewed schema
|
|
374
|
+
// keeps `confirm` visible and required, so the collapsed tools/list line
|
|
375
|
+
// tells the agent to obtain explicit authorization before invoking it.
|
|
322
376
|
let collapsed = `[${group}] ${entry.summary} ` +
|
|
323
|
-
|
|
377
|
+
(requiresConfirm(config?.inputSchema) ? "Requires confirm=true. " : "") +
|
|
378
|
+
`— full description and input fields via describe_tool('${name}').`;
|
|
324
379
|
if (allowlist) {
|
|
325
380
|
const ow = config?.annotations?.openWorldHint;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
381
|
+
const explicitRationale = fullDescription.match(/\[openWorldHint:\s*(?:true|false)\s+—[^\]]+\]/u)?.[0];
|
|
382
|
+
collapsed += explicitRationale
|
|
383
|
+
? ` ${explicitRationale}`
|
|
384
|
+
: ow === true
|
|
385
|
+
? " [openWorldHint: true — may interact with an external endpoint disclosed in describe_tool.]"
|
|
386
|
+
: " [openWorldHint: false — has no user-directed external effect; it only reads or changes the authenticated Frihet workspace.]";
|
|
330
387
|
}
|
|
331
388
|
config.description = collapsed;
|
|
332
389
|
return originalRegisterTool(name, config, handler);
|
|
333
390
|
};
|
|
334
391
|
// Register the meta-tools eagerly. Their handlers close over `catalog`/`groups`
|
|
335
392
|
// which finish populating as the real tools register right after this call.
|
|
336
|
-
registerMetaTools(originalRegisterTool, handle);
|
|
393
|
+
registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes, options?.describeToolExample);
|
|
337
394
|
return handle;
|
|
338
395
|
}
|
|
339
396
|
/* ------------------------------------------------------------------ */
|
|
@@ -363,29 +420,111 @@ function textResult(payload) {
|
|
|
363
420
|
*/
|
|
364
421
|
function registerMetaTools(
|
|
365
422
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
366
|
-
originalRegisterTool, handle) {
|
|
423
|
+
originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes, describeToolExample = "create_invoice") {
|
|
367
424
|
const { catalog, groups } = handle;
|
|
425
|
+
const oauthConfig = securitySchemes
|
|
426
|
+
? {
|
|
427
|
+
securitySchemes: securitySchemes.map((scheme) => ({
|
|
428
|
+
type: scheme.type,
|
|
429
|
+
scopes: [...scheme.scopes],
|
|
430
|
+
})),
|
|
431
|
+
}
|
|
432
|
+
: {};
|
|
433
|
+
const metaConfig = (name) => {
|
|
434
|
+
const metadata = {};
|
|
435
|
+
if (securitySchemes) {
|
|
436
|
+
metadata.securitySchemes = securitySchemes.map((scheme) => ({
|
|
437
|
+
type: scheme.type,
|
|
438
|
+
scopes: [...scheme.scopes],
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
if (capabilityTruth) {
|
|
442
|
+
metadata[capabilityTruth.metaKey] = capabilityTruth.localDiscovery(name);
|
|
443
|
+
}
|
|
444
|
+
return Object.keys(metadata).length > 0 ? { _meta: metadata } : {};
|
|
445
|
+
};
|
|
446
|
+
const searchToolsInputShape = {
|
|
447
|
+
query: z
|
|
448
|
+
.string()
|
|
449
|
+
.optional()
|
|
450
|
+
.describe("Free-text query — matches tool name, title, summary and group. " +
|
|
451
|
+
"Omit to browse a whole group. / Texto libre; omitir para listar un grupo entero."),
|
|
452
|
+
group: z
|
|
453
|
+
.enum(visibleGroupIds)
|
|
454
|
+
.optional()
|
|
455
|
+
.describe("Restrict results to one tool domain / Filtrar por dominio."),
|
|
456
|
+
limit: z
|
|
457
|
+
.number()
|
|
458
|
+
.int()
|
|
459
|
+
.positive()
|
|
460
|
+
.optional()
|
|
461
|
+
.describe("Max results (default 25) / Máximo de resultados (por defecto 25)."),
|
|
462
|
+
};
|
|
463
|
+
const describeToolInputShape = {
|
|
464
|
+
name: z
|
|
465
|
+
.string()
|
|
466
|
+
.describe(`Exact tool name as returned by search_tools (e.g. '${describeToolExample}') / ` +
|
|
467
|
+
"Nombre exacto de la herramienta devuelto por search_tools."),
|
|
468
|
+
};
|
|
469
|
+
const listGroupsOutput = securitySchemes
|
|
470
|
+
? z.object({
|
|
471
|
+
groups: z.array(z.object({
|
|
472
|
+
group: z.enum(visibleGroupIds),
|
|
473
|
+
label: z.string(),
|
|
474
|
+
blurb: z.string(),
|
|
475
|
+
toolCount: z.number().int().nonnegative(),
|
|
476
|
+
})),
|
|
477
|
+
totalGroups: z.number().int().nonnegative(),
|
|
478
|
+
totalTools: z.number().int().nonnegative(),
|
|
479
|
+
next: z.string(),
|
|
480
|
+
})
|
|
481
|
+
: z.object({}).passthrough();
|
|
482
|
+
const searchToolsOutput = securitySchemes
|
|
483
|
+
? z.object({
|
|
484
|
+
query: z.string().nullable(),
|
|
485
|
+
group: z.enum(visibleGroupIds).nullable(),
|
|
486
|
+
count: z.number().int().nonnegative(),
|
|
487
|
+
tools: z.array(z.object({
|
|
488
|
+
name: z.string(),
|
|
489
|
+
group: z.enum(visibleGroupIds),
|
|
490
|
+
title: z.string(),
|
|
491
|
+
summary: z.string(),
|
|
492
|
+
readOnly: z.boolean(),
|
|
493
|
+
inputFields: z.array(z.string()),
|
|
494
|
+
score: z.number().int().nonnegative(),
|
|
495
|
+
})),
|
|
496
|
+
next: z.string(),
|
|
497
|
+
})
|
|
498
|
+
: z.object({}).passthrough();
|
|
499
|
+
const describeToolOutput = securitySchemes
|
|
500
|
+
? z.object({
|
|
501
|
+
name: z.string(),
|
|
502
|
+
group: z.enum(visibleGroupIds),
|
|
503
|
+
title: z.string(),
|
|
504
|
+
readOnly: z.boolean(),
|
|
505
|
+
inputFields: z.array(z.string()),
|
|
506
|
+
description: z.string(),
|
|
507
|
+
})
|
|
508
|
+
: z.object({}).passthrough();
|
|
368
509
|
// -- list_tool_groups --
|
|
369
510
|
originalRegisterTool("list_tool_groups", {
|
|
370
511
|
title: "List tool groups",
|
|
371
|
-
|
|
372
|
-
|
|
512
|
+
...oauthConfig,
|
|
513
|
+
...metaConfig("list_tool_groups"),
|
|
514
|
+
description: `List the available Frihet tool domains (${visibleGroupIds.join(", ")}) with a one-line blurb and tool ` +
|
|
373
515
|
"count for each. Start here, then use search_tools(query) to find tools and describe_tool(name) " +
|
|
374
|
-
"for a tool's full
|
|
516
|
+
"for a tool's full description and input fields. Only domains present in this reviewed connector are listed. " +
|
|
375
517
|
"[openWorldHint: false — reads the in-process tool catalog only.] " +
|
|
376
518
|
"/ Lista los dominios de herramientas de Frihet ERP con descripción y recuento. Empieza aquí.",
|
|
377
519
|
annotations: META_ANNOTATIONS,
|
|
378
|
-
inputSchema: {},
|
|
379
|
-
outputSchema:
|
|
380
|
-
.object({})
|
|
381
|
-
.passthrough()
|
|
382
|
-
.describe("Tool domains with per-group blurb and tool count / Dominios de herramientas con descripción y recuento"),
|
|
520
|
+
inputSchema: securitySchemes ? z.object({}).strict() : {},
|
|
521
|
+
outputSchema: listGroupsOutput.describe("Tool domains with per-group blurb and tool count / Dominios de herramientas con descripción y recuento"),
|
|
383
522
|
}, async () => {
|
|
384
|
-
const out =
|
|
523
|
+
const out = visibleGroupIds
|
|
385
524
|
.map((id) => ({
|
|
386
525
|
group: id,
|
|
387
|
-
label:
|
|
388
|
-
blurb:
|
|
526
|
+
label: groupMetadata[id].label,
|
|
527
|
+
blurb: groupMetadata[id].blurb,
|
|
389
528
|
toolCount: groups.get(id)?.length ?? 0,
|
|
390
529
|
}))
|
|
391
530
|
.filter((g) => g.toolCount > 0);
|
|
@@ -393,12 +532,14 @@ originalRegisterTool, handle) {
|
|
|
393
532
|
groups: out,
|
|
394
533
|
totalGroups: out.length,
|
|
395
534
|
totalTools: catalog.size,
|
|
396
|
-
next: "search_tools(query) to find tools; describe_tool(name) for full
|
|
535
|
+
next: "search_tools(query) to find tools; describe_tool(name) for full description and input fields.",
|
|
397
536
|
});
|
|
398
537
|
});
|
|
399
538
|
// -- search_tools --
|
|
400
539
|
originalRegisterTool("search_tools", {
|
|
401
540
|
title: "Search tools",
|
|
541
|
+
...oauthConfig,
|
|
542
|
+
...metaConfig("search_tools"),
|
|
402
543
|
description: "Find Frihet ERP tools by free-text query (matches tool name, title, summary and group). " +
|
|
403
544
|
"Returns matching tools with their group, one-line summary, read-only flag and input field names — " +
|
|
404
545
|
"progressive disclosure so you load only what you need. Optionally filter by group. " +
|
|
@@ -406,27 +547,10 @@ originalRegisterTool, handle) {
|
|
|
406
547
|
"[openWorldHint: false — reads the in-process tool catalog only.] " +
|
|
407
548
|
"/ Busca herramientas por texto libre. Devuelve coincidencias con grupo, resumen y campos.",
|
|
408
549
|
annotations: META_ANNOTATIONS,
|
|
409
|
-
inputSchema:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
.describe("Free-text query — matches tool name, title, summary and group. " +
|
|
414
|
-
"Omit to browse a whole group. / Texto libre; omitir para listar un grupo entero."),
|
|
415
|
-
group: z
|
|
416
|
-
.enum(TOOL_GROUP_IDS)
|
|
417
|
-
.optional()
|
|
418
|
-
.describe("Restrict results to one tool domain / Filtrar por dominio."),
|
|
419
|
-
limit: z
|
|
420
|
-
.number()
|
|
421
|
-
.int()
|
|
422
|
-
.positive()
|
|
423
|
-
.optional()
|
|
424
|
-
.describe("Max results (default 25) / Máximo de resultados (por defecto 25)."),
|
|
425
|
-
},
|
|
426
|
-
outputSchema: z
|
|
427
|
-
.object({})
|
|
428
|
-
.passthrough()
|
|
429
|
-
.describe("Matching tools with group, summary, read-only flag and input fields / Herramientas coincidentes con grupo, resumen y campos"),
|
|
550
|
+
inputSchema: securitySchemes
|
|
551
|
+
? z.object(searchToolsInputShape).strict()
|
|
552
|
+
: searchToolsInputShape,
|
|
553
|
+
outputSchema: searchToolsOutput.describe("Matching tools with group, summary, read-only flag and input fields / Herramientas coincidentes con grupo, resumen y campos"),
|
|
430
554
|
},
|
|
431
555
|
// The SDK passes parsed args; we read defensively to stay schema-light.
|
|
432
556
|
async (args = {}) => {
|
|
@@ -461,6 +585,8 @@ originalRegisterTool, handle) {
|
|
|
461
585
|
title: e.title,
|
|
462
586
|
summary: e.summary,
|
|
463
587
|
readOnly: e.readOnly,
|
|
588
|
+
...(e.annotations ? { annotations: e.annotations } : {}),
|
|
589
|
+
...(e.capability ? { capability: e.capability } : {}),
|
|
464
590
|
inputFields: e.inputFields,
|
|
465
591
|
score,
|
|
466
592
|
}));
|
|
@@ -469,28 +595,24 @@ originalRegisterTool, handle) {
|
|
|
469
595
|
group: groupFilter ?? null,
|
|
470
596
|
count: scored.length,
|
|
471
597
|
tools: scored,
|
|
472
|
-
next: "describe_tool(name) for full
|
|
598
|
+
next: "describe_tool(name) for full description and input fields, then call the tool by its real name.",
|
|
473
599
|
});
|
|
474
600
|
});
|
|
475
601
|
// -- describe_tool --
|
|
476
602
|
originalRegisterTool("describe_tool", {
|
|
477
603
|
title: "Describe tool",
|
|
604
|
+
...oauthConfig,
|
|
605
|
+
...metaConfig("describe_tool"),
|
|
478
606
|
description: "Return the full original description, group, read-only flag and input field names for a specific " +
|
|
479
607
|
"Frihet ERP tool by its exact name (as returned by search_tools). Use this to load a tool's full " +
|
|
480
608
|
"depth on demand before calling it. " +
|
|
481
609
|
"[openWorldHint: false — reads the in-process tool catalog only.] " +
|
|
482
610
|
"/ Devuelve la descripción completa de una herramienta por su nombre exacto.",
|
|
483
611
|
annotations: META_ANNOTATIONS,
|
|
484
|
-
inputSchema:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
"Nombre exacto de la herramienta devuelto por search_tools."),
|
|
489
|
-
},
|
|
490
|
-
outputSchema: z
|
|
491
|
-
.object({})
|
|
492
|
-
.passthrough()
|
|
493
|
-
.describe("A tool's full description, group, read-only flag and input field names / Descripción completa de una herramienta"),
|
|
612
|
+
inputSchema: securitySchemes
|
|
613
|
+
? z.object(describeToolInputShape).strict()
|
|
614
|
+
: describeToolInputShape,
|
|
615
|
+
outputSchema: describeToolOutput.describe("A tool's full description, group, read-only flag and input field names / Descripción completa de una herramienta"),
|
|
494
616
|
}, async (args = {}) => {
|
|
495
617
|
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
496
618
|
const entry = catalog.get(name);
|
|
@@ -520,6 +642,8 @@ originalRegisterTool, handle) {
|
|
|
520
642
|
group: entry.group,
|
|
521
643
|
title: entry.title,
|
|
522
644
|
readOnly: entry.readOnly,
|
|
645
|
+
...(entry.annotations ? { annotations: entry.annotations } : {}),
|
|
646
|
+
...(entry.capability ? { capability: entry.capability } : {}),
|
|
523
647
|
inputFields: entry.inputFields,
|
|
524
648
|
description: entry.description,
|
|
525
649
|
});
|
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
* Period close tools for the Frihet MCP server — D4-B megasprint (3 tools).
|
|
3
3
|
*
|
|
4
4
|
* Tools:
|
|
5
|
-
* 1. period_close_status — get the current
|
|
5
|
+
* 1. period_close_status — get the current or selected fiscal-year state
|
|
6
6
|
* 2. period_close — close a monthly or quarterly period (TRUST AREA)
|
|
7
7
|
* 3. period_reopen — reopen a closed period with required reason (TRUST AREA)
|
|
8
8
|
*
|
|
9
9
|
* REST surface: /v1/periods/current, /v1/periods/{id}, /v1/periods/close, /v1/periods/{id}/reopen
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* STATE OF THE WORLD (verified against erp-main functions/src/publicApi/families/
|
|
12
|
+
* periods.ts): only the READ route is implemented. Both WRITE routes answer
|
|
13
|
+
* HTTP 501 NOT_IMPLEMENTED — "deferred to the upcoming fiscal-write wave (Trust,
|
|
14
|
+
* serialized). Use year-end closing in the app for now." Nothing is frozen, and
|
|
15
|
+
* there is no idempotent no-op to rely on, because there is no handler at all.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* The descriptions below therefore state the 501 in the present tense. Do not
|
|
18
|
+
* restore aspirational prose about freezing or idempotency until a handler exists
|
|
19
|
+
* — GAP-13 was exactly that: an agent planning a year-end close read a mechanism
|
|
20
|
+
* the backend never had.
|
|
21
|
+
*
|
|
22
|
+
* `withBackendGuard` converts 404 only (backend-availability.ts isBackendNotFound),
|
|
23
|
+
* so a 501 surfaces through the generic error path as isError with the backend's
|
|
24
|
+
* own NOT_IMPLEMENTED message — an honest failure, not a fake success.
|
|
16
25
|
*/
|
|
17
26
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
27
|
import type { IFrihetClient } from "../client-interface.js";
|