@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.
- package/README.md +92 -31
- 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 +549 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +254 -0
- package/dist/client-interface.d.ts +22 -14
- package/dist/client.d.ts +114 -18
- package/dist/client.js +520 -58
- package/dist/demo-client.d.ts +16 -13
- package/dist/demo-client.js +209 -29
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/index.js +41 -56
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +192 -105
- package/dist/openai-profile.d.ts +27 -15
- package/dist/openai-profile.js +1191 -145
- package/dist/openai-review-contract.d.ts +49 -0
- package/dist/openai-review-contract.js +919 -0
- package/dist/openai-review-oauth.d.ts +98 -0
- package/dist/openai-review-oauth.js +136 -0
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +2 -0
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +10 -0
- package/dist/redaction.js +74 -0
- package/dist/resources/register-all.js +6 -12
- package/dist/server-composition.d.ts +16 -0
- package/dist/server-composition.js +44 -0
- package/dist/tool-exposure.d.ts +25 -22
- package/dist/tool-exposure.js +199 -77
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/backend-availability.d.ts +3 -3
- package/dist/tools/backend-availability.js +3 -3
- package/dist/tools/clients.js +9 -9
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +30 -11
- package/dist/tools/einvoice.d.ts +1 -1
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +8 -9
- package/dist/tools/intelligence.js +95 -4
- package/dist/tools/invoices.js +150 -47
- 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 +109 -24
- package/dist/tools/shared.d.ts +1431 -75
- package/dist/tools/shared.js +386 -76
- package/dist/tools/team.js +18 -14
- package/dist/tools/vendors.js +9 -9
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +28 -4
- package/docs/agent-onboarding.json +381 -0
- package/package.json +32 -7
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical registration path shared by the stdio server, remote Worker, and
|
|
3
|
+
* generated public-surface contract. Transport/auth setup stays with each host;
|
|
4
|
+
* the exposed MCP operations/resources/prompts are composed here exactly once.
|
|
5
|
+
*/
|
|
6
|
+
import { applyPublicCapabilityTruth, buildLocalDiscoveryCapability, CAPABILITY_META_KEY, } from "./capability-truth.js";
|
|
7
|
+
import { applyFiscalAliases } from "./fiscal-aliases.js";
|
|
8
|
+
import { applyOpenAIProfile, applyOpenAIReviewProfiles, } from "./openai-profile.js";
|
|
9
|
+
import { registerAllPrompts } from "./prompts/register-all.js";
|
|
10
|
+
import { registerAllResources } from "./resources/register-all.js";
|
|
11
|
+
import { applyToolExposureProfile } from "./tool-exposure.js";
|
|
12
|
+
import { registerAllTools } from "./tools/register-all.js";
|
|
13
|
+
export function localMcpSurfaceComposition(openaiMode, groupedMode) {
|
|
14
|
+
return { openaiMode, groupedMode, includeDynamicResources: true };
|
|
15
|
+
}
|
|
16
|
+
export function remoteMcpSurfaceComposition(openaiMode, groupedMode) {
|
|
17
|
+
return { openaiMode, groupedMode, includeDynamicResources: false };
|
|
18
|
+
}
|
|
19
|
+
export function registerMcpSurface(server, client, options) {
|
|
20
|
+
if (options.groupedMode) {
|
|
21
|
+
if (options.openaiMode) {
|
|
22
|
+
applyOpenAIReviewProfiles(server);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
applyToolExposureProfile(server, {
|
|
26
|
+
capabilityTruth: {
|
|
27
|
+
metaKey: CAPABILITY_META_KEY,
|
|
28
|
+
localDiscovery: buildLocalDiscoveryCapability,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (options.openaiMode) {
|
|
34
|
+
applyOpenAIProfile(server);
|
|
35
|
+
}
|
|
36
|
+
if (!options.openaiMode) {
|
|
37
|
+
applyPublicCapabilityTruth(server);
|
|
38
|
+
}
|
|
39
|
+
registerAllTools(server, client);
|
|
40
|
+
applyFiscalAliases(server);
|
|
41
|
+
registerAllResources(server, options.includeDynamicResources ? client : undefined);
|
|
42
|
+
registerAllPrompts(server);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=server-composition.js.map
|
package/dist/tool-exposure.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
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
|
|
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
|
*
|
|
@@ -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
|
|
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
|
+
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;
|
package/dist/tool-exposure.js
CHANGED
|
@@ -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
|
-
*
|
|
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
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
|
|
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
|
*
|
|
@@ -53,7 +53,11 @@ 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",
|
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
521
|
+
const out = visibleGroupIds
|
|
385
522
|
.map((id) => ({
|
|
386
523
|
group: id,
|
|
387
|
-
label:
|
|
388
|
-
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
|
|
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
|
-
|
|
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"),
|
|
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
|
|
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
|
-
|
|
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"),
|
|
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
|
|
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";
|