@frihet/mcp-server 1.16.6 → 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 +488 -56
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +171 -17
  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 +25 -21
  25. package/dist/openai-profile.js +1166 -156
  26. package/dist/openai-review-contract.d.ts +15 -6
  27. package/dist/openai-review-contract.js +715 -41
  28. package/dist/openai-review-oauth.d.ts +47 -0
  29. package/dist/openai-review-oauth.js +73 -3
  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 +2 -2
  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 +30 -7
@@ -1,9 +1,9 @@
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
- * Default (unset, or FRIHET_TOOL_MODE=full) leaves the server BYTE-IDENTICAL
6
- * to today: all 157 tools registered with their full descriptions/schemas.
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
9
  * Context rot is the 2026 problem: a flat list of 157 tool descriptions,
@@ -23,10 +23,10 @@
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 schema via
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 9-domain map with per-group counts
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
  *
@@ -42,7 +42,7 @@
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. */
@@ -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,24 @@ 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 is the OpenAI-composition path: it pins the grouped catalog (and the
122
- * tools search_tools / describe_tool / list_tool_groups surface) to EXACTLY
123
- * the reviewed allow-list, so progressive disclosure can never reveal or
124
- * describe a tool outside the 53-tool ChatGPT-reviewed surface. Omit (default)
125
- * for the open mcp.frihet.io surface, which catalogs every registered tool.
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
+ capabilityTruth?: {
136
+ metaKey: string;
137
+ localDiscovery: (name: string) => object;
138
+ };
139
+ securitySchemes?: ReadonlyArray<{
140
+ type: "oauth2";
141
+ scopes: readonly string[];
142
+ }>;
140
143
  }): ToolExposureHandle;
141
144
  /** Number of meta-tools added in grouped mode (for logging). */
142
145
  export declare const GROUPED_META_TOOL_COUNT: number;
@@ -1,9 +1,9 @@
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
- * Default (unset, or FRIHET_TOOL_MODE=full) leaves the server BYTE-IDENTICAL
6
- * to today: all 157 tools registered with their full descriptions/schemas.
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
9
  * Context rot is the 2026 problem: a flat list of 157 tool descriptions,
@@ -23,10 +23,10 @@
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 schema via
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 9-domain map with per-group counts
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
  *
@@ -53,7 +53,11 @@ export const GROUPS = {
53
53
  },
54
54
  expenses: {
55
55
  label: "Expenses & vendors / Gastos y proveedores",
56
- blurb: "Expenses (with OCR) and vendor/supplier records.",
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",
@@ -204,6 +208,43 @@ function firstSentence(desc) {
204
208
  // Hard cap so a runaway description can't reintroduce context bloat.
205
209
  return sentence.length > 160 ? sentence.slice(0, 157).trimEnd() + "…" : sentence;
206
210
  }
211
+ function inputSchemaFieldNames(schema) {
212
+ if (schema instanceof z.ZodObject) {
213
+ return Object.keys(schema.shape);
214
+ }
215
+ if (schema !== null && typeof schema === "object" && !Array.isArray(schema)) {
216
+ return Object.keys(schema);
217
+ }
218
+ return [];
219
+ }
220
+ /**
221
+ * Does this tool declare `confirm` as a REQUIRED input?
222
+ *
223
+ * Handles both a raw registration shape and a strict ZodObject. The latter is
224
+ * used by create_expense; looking only at `schema["confirm"]` made its required
225
+ * confirmation disappear from the collapsed tools/list description.
226
+ *
227
+ * ── Why this exists ──────────────────────────────────────────────────
228
+ * `firstSentence` keeps only the opening sentence, so a confirm requirement
229
+ * stated in sentence two vanished from every collapsed listing: all 12
230
+ * confirm-gated tools shipped a description that never mentioned the input the
231
+ * SDK rejects the call for. The agent then reads "Delete an invoice by its ID",
232
+ * calls `{id}`, and gets an input-validation error with no prose basis to
233
+ * recover. Deriving the marker here fixes every present AND future guarded tool
234
+ * at once — the alternative (editing 12 first sentences by hand) is the same
235
+ * enumerate-instead-of-derive mistake GAP-04 was raised for.
236
+ */
237
+ function requiresConfirm(inputSchema) {
238
+ const shape = inputSchema instanceof z.ZodObject
239
+ ? inputSchema.shape
240
+ : inputSchema;
241
+ if (!shape || typeof shape !== "object")
242
+ return false;
243
+ const field = shape["confirm"];
244
+ if (typeof field?.safeParse !== "function")
245
+ return false;
246
+ return field.safeParse(undefined).success === false;
247
+ }
207
248
  /**
208
249
  * Read process.env without referencing the `process` global type — keeps this
209
250
  * module compilable in environments without @types/node (e.g. the Cloudflare
@@ -242,21 +283,11 @@ export function resolveToolMode(env = defaultEnv()) {
242
283
  * @param server The McpServer (typed loosely to match the openai-profile shim).
243
284
  * @param options.allowlist When provided, ONLY tools whose name is in this set
244
285
  * are catalogued + collapsed; any other tool is passed through untouched.
245
- * This is the OpenAI-composition path: it pins the grouped catalog (and the
246
- * tools search_tools / describe_tool / list_tool_groups surface) to EXACTLY
247
- * the reviewed allow-list, so progressive disclosure can never reveal or
248
- * describe a tool outside the 53-tool ChatGPT-reviewed surface. Omit (default)
249
- * for the open mcp.frihet.io surface, which catalogs every registered tool.
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.
286
+ * This supports a deliberately restricted grouped surface without allowing
287
+ * discovery to reveal or describe tools outside that surface. Omit (default)
288
+ * for mcp.frihet.io, which catalogs every registered tool. The reviewed
289
+ * OpenAI host does not use grouped exposure: it publishes the exact 33-tool
290
+ * allow-list with complete descriptions and no discovery meta-tools.
260
291
  * @returns a handle exposing the live catalog (useful for tests/logging).
261
292
  */
262
293
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -267,6 +298,13 @@ server, options) {
267
298
  const groups = new Map();
268
299
  const handle = { catalog, groups };
269
300
  const allowlist = options?.allowlist;
301
+ const visibleGroupIds = allowlist
302
+ ? TOOL_GROUP_IDS.filter((group) => [...allowlist].some((name) => groupForTool(name) === group))
303
+ : [...TOOL_GROUP_IDS];
304
+ const groupMetadata = Object.fromEntries(TOOL_GROUP_IDS.map((id) => [
305
+ id,
306
+ options?.groupMetadata?.[id] ?? GROUPS[id],
307
+ ]));
270
308
  const originalRegisterTool = server.registerTool.bind(server);
271
309
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
272
310
  server.registerTool = (name, config, handler) => {
@@ -286,10 +324,12 @@ server, options) {
286
324
  }
287
325
  const group = groupForTool(name);
288
326
  const fullDescription = typeof config?.description === "string" ? config.description : "";
289
- const inputFields = config?.inputSchema
290
- ? Object.keys(config.inputSchema)
291
- : [];
327
+ const inputFields = inputSchemaFieldNames(config?.inputSchema);
292
328
  const readOnly = config?.annotations?.readOnlyHint === true;
329
+ const annotations = config?.annotations;
330
+ const capability = options?.capabilityTruth
331
+ ? config?._meta?.[options.capabilityTruth.metaKey]
332
+ : undefined;
293
333
  const entry = {
294
334
  name,
295
335
  group,
@@ -297,6 +337,7 @@ server, options) {
297
337
  summary: firstSentence(fullDescription),
298
338
  description: fullDescription,
299
339
  readOnly,
340
+ ...(options?.capabilityTruth ? { annotations, capability } : {}),
300
341
  inputFields,
301
342
  };
302
343
  catalog.set(name, entry);
@@ -313,27 +354,41 @@ server, options) {
313
354
  // a per-tool open-world justification on EVERY reviewed tool). When composed
314
355
  // with the OpenAI profile, that profile runs first (outermost) and has
315
356
  // already set the correct annotations.openWorldHint (e.g. true for
316
- // send_invoice / send_quote / create_webhook / update_webhook). We re-derive
357
+ // send_invoice). We re-derive
317
358
  // the one-line rationale here from the (now-correct) annotation so the terse
318
359
  // collapsed description carries it — instead of letting the OpenAI-injected
319
360
  // rationale be lost when we overwrite config.description. Only appended in
320
361
  // allow-list (composition) mode; the open mcp.frihet.io surface keeps its
321
362
  // pure terse line (no OpenAI review constraint there).
363
+ // A required `confirm` is the single most consequential thing an agent can
364
+ // miss: omit it and the call dies on schema validation with no prose basis
365
+ // to recover. firstSentence() keeps only the first English sentence, so a
366
+ // "Requires confirm=true." written into sentence 2 of a tool description is
367
+ // invisible on the surface the agent actually loads. Derive the marker from
368
+ // the SCHEMA instead of trusting prose — it then covers every confirm-gated
369
+ // tool, including ones added after this line was written.
370
+ //
371
+ // In OpenAI mode this is deliberately load-bearing: the reviewed schema
372
+ // keeps `confirm` visible and required, so the collapsed tools/list line
373
+ // tells the agent to obtain explicit authorization before invoking it.
322
374
  let collapsed = `[${group}] ${entry.summary} ` +
323
- `— full schema via describe_tool('${name}').`;
375
+ (requiresConfirm(config?.inputSchema) ? "Requires confirm=true. " : "") +
376
+ `— full description and input fields via describe_tool('${name}').`;
324
377
  if (allowlist) {
325
378
  const ow = config?.annotations?.openWorldHint;
326
- collapsed +=
327
- ow === true
328
- ? " [openWorldHint: true — contacts an entity outside Frihet (an email recipient or an external webhook URL).]"
329
- : " [openWorldHint: false — operates only against the Frihet API (api.frihet.io); no third-party/external calls.]";
379
+ const explicitRationale = fullDescription.match(/\[openWorldHint:\s*(?:true|false)\s+—[^\]]+\]/u)?.[0];
380
+ collapsed += explicitRationale
381
+ ? ` ${explicitRationale}`
382
+ : ow === true
383
+ ? " [openWorldHint: true — may interact with an external endpoint disclosed in describe_tool.]"
384
+ : " [openWorldHint: false — has no user-directed external effect; it only reads or changes the authenticated Frihet workspace.]";
330
385
  }
331
386
  config.description = collapsed;
332
387
  return originalRegisterTool(name, config, handler);
333
388
  };
334
389
  // Register the meta-tools eagerly. Their handlers close over `catalog`/`groups`
335
390
  // which finish populating as the real tools register right after this call.
336
- registerMetaTools(originalRegisterTool, handle);
391
+ registerMetaTools(originalRegisterTool, handle, visibleGroupIds, groupMetadata, options?.capabilityTruth, options?.securitySchemes);
337
392
  return handle;
338
393
  }
339
394
  /* ------------------------------------------------------------------ */
@@ -363,29 +418,111 @@ function textResult(payload) {
363
418
  */
364
419
  function registerMetaTools(
365
420
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
366
- originalRegisterTool, handle) {
421
+ originalRegisterTool, handle, visibleGroupIds, groupMetadata, capabilityTruth, securitySchemes) {
367
422
  const { catalog, groups } = handle;
423
+ const oauthConfig = securitySchemes
424
+ ? {
425
+ securitySchemes: securitySchemes.map((scheme) => ({
426
+ type: scheme.type,
427
+ scopes: [...scheme.scopes],
428
+ })),
429
+ }
430
+ : {};
431
+ const metaConfig = (name) => {
432
+ const metadata = {};
433
+ if (securitySchemes) {
434
+ metadata.securitySchemes = securitySchemes.map((scheme) => ({
435
+ type: scheme.type,
436
+ scopes: [...scheme.scopes],
437
+ }));
438
+ }
439
+ if (capabilityTruth) {
440
+ metadata[capabilityTruth.metaKey] = capabilityTruth.localDiscovery(name);
441
+ }
442
+ return Object.keys(metadata).length > 0 ? { _meta: metadata } : {};
443
+ };
444
+ const searchToolsInputShape = {
445
+ query: z
446
+ .string()
447
+ .optional()
448
+ .describe("Free-text query — matches tool name, title, summary and group. " +
449
+ "Omit to browse a whole group. / Texto libre; omitir para listar un grupo entero."),
450
+ group: z
451
+ .enum(visibleGroupIds)
452
+ .optional()
453
+ .describe("Restrict results to one tool domain / Filtrar por dominio."),
454
+ limit: z
455
+ .number()
456
+ .int()
457
+ .positive()
458
+ .optional()
459
+ .describe("Max results (default 25) / Máximo de resultados (por defecto 25)."),
460
+ };
461
+ const describeToolInputShape = {
462
+ name: z
463
+ .string()
464
+ .describe("Exact tool name as returned by search_tools (e.g. 'create_invoice') / " +
465
+ "Nombre exacto de la herramienta devuelto por search_tools."),
466
+ };
467
+ const listGroupsOutput = securitySchemes
468
+ ? z.object({
469
+ groups: z.array(z.object({
470
+ group: z.enum(visibleGroupIds),
471
+ label: z.string(),
472
+ blurb: z.string(),
473
+ toolCount: z.number().int().nonnegative(),
474
+ })),
475
+ totalGroups: z.number().int().nonnegative(),
476
+ totalTools: z.number().int().nonnegative(),
477
+ next: z.string(),
478
+ })
479
+ : z.object({}).passthrough();
480
+ const searchToolsOutput = securitySchemes
481
+ ? z.object({
482
+ query: z.string().nullable(),
483
+ group: z.enum(visibleGroupIds).nullable(),
484
+ count: z.number().int().nonnegative(),
485
+ tools: z.array(z.object({
486
+ name: z.string(),
487
+ group: z.enum(visibleGroupIds),
488
+ title: z.string(),
489
+ summary: z.string(),
490
+ readOnly: z.boolean(),
491
+ inputFields: z.array(z.string()),
492
+ score: z.number().int().nonnegative(),
493
+ })),
494
+ next: z.string(),
495
+ })
496
+ : z.object({}).passthrough();
497
+ const describeToolOutput = securitySchemes
498
+ ? z.object({
499
+ name: z.string(),
500
+ group: z.enum(visibleGroupIds),
501
+ title: z.string(),
502
+ readOnly: z.boolean(),
503
+ inputFields: z.array(z.string()),
504
+ description: z.string(),
505
+ })
506
+ : z.object({}).passthrough();
368
507
  // -- list_tool_groups --
369
508
  originalRegisterTool("list_tool_groups", {
370
509
  title: "List tool groups",
371
- description: "List the Frihet ERP tool domains (invoicing, expenses, fiscal/compliance, banking, CRM, " +
372
- "HR/payroll, stay/PMS, POS, intelligence, products, platform) with a one-line blurb and tool " +
510
+ ...oauthConfig,
511
+ ...metaConfig("list_tool_groups"),
512
+ description: `List the available Frihet tool domains (${visibleGroupIds.join(", ")}) with a one-line blurb and tool ` +
373
513
  "count for each. Start here, then use search_tools(query) to find tools and describe_tool(name) " +
374
- "for a tool's full schema. Frihet serves deep ES/EU fiscal + native compliance on demand. " +
514
+ "for a tool's full description and input fields. Only domains present in this reviewed connector are listed. " +
375
515
  "[openWorldHint: false — reads the in-process tool catalog only.] " +
376
516
  "/ Lista los dominios de herramientas de Frihet ERP con descripción y recuento. Empieza aquí.",
377
517
  annotations: META_ANNOTATIONS,
378
- inputSchema: {},
379
- outputSchema: z
380
- .object({})
381
- .passthrough()
382
- .describe("Tool domains with per-group blurb and tool count / Dominios de herramientas con descripción y recuento"),
518
+ inputSchema: securitySchemes ? z.object({}).strict() : {},
519
+ outputSchema: listGroupsOutput.describe("Tool domains with per-group blurb and tool count / Dominios de herramientas con descripción y recuento"),
383
520
  }, async () => {
384
- const out = Object.keys(GROUPS)
521
+ const out = visibleGroupIds
385
522
  .map((id) => ({
386
523
  group: id,
387
- label: GROUPS[id].label,
388
- blurb: GROUPS[id].blurb,
524
+ label: groupMetadata[id].label,
525
+ blurb: groupMetadata[id].blurb,
389
526
  toolCount: groups.get(id)?.length ?? 0,
390
527
  }))
391
528
  .filter((g) => g.toolCount > 0);
@@ -393,12 +530,14 @@ originalRegisterTool, handle) {
393
530
  groups: out,
394
531
  totalGroups: out.length,
395
532
  totalTools: catalog.size,
396
- next: "search_tools(query) to find tools; describe_tool(name) for full schema.",
533
+ next: "search_tools(query) to find tools; describe_tool(name) for full description and input fields.",
397
534
  });
398
535
  });
399
536
  // -- search_tools --
400
537
  originalRegisterTool("search_tools", {
401
538
  title: "Search tools",
539
+ ...oauthConfig,
540
+ ...metaConfig("search_tools"),
402
541
  description: "Find Frihet ERP tools by free-text query (matches tool name, title, summary and group). " +
403
542
  "Returns matching tools with their group, one-line summary, read-only flag and input field names — " +
404
543
  "progressive disclosure so you load only what you need. Optionally filter by group. " +
@@ -406,27 +545,10 @@ originalRegisterTool, handle) {
406
545
  "[openWorldHint: false — reads the in-process tool catalog only.] " +
407
546
  "/ Busca herramientas por texto libre. Devuelve coincidencias con grupo, resumen y campos.",
408
547
  annotations: META_ANNOTATIONS,
409
- inputSchema: {
410
- query: z
411
- .string()
412
- .optional()
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"),
548
+ inputSchema: securitySchemes
549
+ ? z.object(searchToolsInputShape).strict()
550
+ : searchToolsInputShape,
551
+ outputSchema: searchToolsOutput.describe("Matching tools with group, summary, read-only flag and input fields / Herramientas coincidentes con grupo, resumen y campos"),
430
552
  },
431
553
  // The SDK passes parsed args; we read defensively to stay schema-light.
432
554
  async (args = {}) => {
@@ -461,6 +583,8 @@ originalRegisterTool, handle) {
461
583
  title: e.title,
462
584
  summary: e.summary,
463
585
  readOnly: e.readOnly,
586
+ ...(e.annotations ? { annotations: e.annotations } : {}),
587
+ ...(e.capability ? { capability: e.capability } : {}),
464
588
  inputFields: e.inputFields,
465
589
  score,
466
590
  }));
@@ -469,28 +593,24 @@ originalRegisterTool, handle) {
469
593
  group: groupFilter ?? null,
470
594
  count: scored.length,
471
595
  tools: scored,
472
- next: "describe_tool(name) for full schema, then call the tool by its real name.",
596
+ next: "describe_tool(name) for full description and input fields, then call the tool by its real name.",
473
597
  });
474
598
  });
475
599
  // -- describe_tool --
476
600
  originalRegisterTool("describe_tool", {
477
601
  title: "Describe tool",
602
+ ...oauthConfig,
603
+ ...metaConfig("describe_tool"),
478
604
  description: "Return the full original description, group, read-only flag and input field names for a specific " +
479
605
  "Frihet ERP tool by its exact name (as returned by search_tools). Use this to load a tool's full " +
480
606
  "depth on demand before calling it. " +
481
607
  "[openWorldHint: false — reads the in-process tool catalog only.] " +
482
608
  "/ Devuelve la descripción completa de una herramienta por su nombre exacto.",
483
609
  annotations: META_ANNOTATIONS,
484
- inputSchema: {
485
- name: z
486
- .string()
487
- .describe("Exact tool name as returned by search_tools (e.g. 'create_invoice') / " +
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"),
610
+ inputSchema: securitySchemes
611
+ ? z.object(describeToolInputShape).strict()
612
+ : describeToolInputShape,
613
+ outputSchema: describeToolOutput.describe("A tool's full description, group, read-only flag and input field names / Descripción completa de una herramienta"),
494
614
  }, async (args = {}) => {
495
615
  const name = typeof args.name === "string" ? args.name.trim() : "";
496
616
  const entry = catalog.get(name);
@@ -520,6 +640,8 @@ originalRegisterTool, handle) {
520
640
  group: entry.group,
521
641
  title: entry.title,
522
642
  readOnly: entry.readOnly,
643
+ ...(entry.annotations ? { annotations: entry.annotations } : {}),
644
+ ...(entry.capability ? { capability: entry.capability } : {}),
523
645
  inputFields: entry.inputFields,
524
646
  description: entry.description,
525
647
  });
@@ -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 accounting period state (open/closing/closed)
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
- * Closing a period freezes invoices/expenses/journal entries. Reopening requires a
12
- * compliance reason logged for audit. These are TRUST AREA operations.
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
- * NOTE: ERP backend endpoints land in parallel D4-A wave. 404s propagate as isError
15
- * until backend ships. TODO: confirm callable boundary vs REST shell for closePeriod.
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";