@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.
Files changed (108) hide show
  1. package/README.md +168 -82
  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 +552 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +262 -0
  10. package/dist/claude-candidate-contract.d.ts +482 -0
  11. package/dist/claude-candidate-contract.js +87 -0
  12. package/dist/claude-output-privacy.d.ts +17 -0
  13. package/dist/claude-output-privacy.js +240 -0
  14. package/dist/claude-profile.d.ts +18 -0
  15. package/dist/claude-profile.js +260 -0
  16. package/dist/client-interface.d.ts +47 -33
  17. package/dist/client.d.ts +172 -40
  18. package/dist/client.js +708 -165
  19. package/dist/demo-client.d.ts +41 -32
  20. package/dist/demo-client.js +202 -31
  21. package/dist/demo-fixtures.d.ts +3 -3
  22. package/dist/demo-fixtures.js +7 -7
  23. package/dist/deposit-contract.d.ts +29 -0
  24. package/dist/deposit-contract.js +76 -0
  25. package/dist/fetch-no-redirect.d.ts +3 -0
  26. package/dist/fetch-no-redirect.js +16 -0
  27. package/dist/fiscal-aliases.d.ts +44 -0
  28. package/dist/fiscal-aliases.js +62 -0
  29. package/dist/fiscal-period.d.ts +26 -0
  30. package/dist/fiscal-period.js +31 -0
  31. package/dist/google-profile.d.ts +19 -0
  32. package/dist/google-profile.js +122 -0
  33. package/dist/index.js +68 -59
  34. package/dist/logger.d.ts +2 -2
  35. package/dist/logger.js +140 -8
  36. package/dist/muse-profile.d.ts +19 -0
  37. package/dist/muse-profile.js +122 -0
  38. package/dist/observability.d.ts +18 -29
  39. package/dist/observability.js +194 -106
  40. package/dist/openai-profile.d.ts +26 -22
  41. package/dist/openai-profile.js +1193 -157
  42. package/dist/openai-review-contract.d.ts +15 -6
  43. package/dist/openai-review-contract.js +714 -41
  44. package/dist/openai-review-oauth.d.ts +149 -2
  45. package/dist/openai-review-oauth.js +220 -6
  46. package/dist/prompts/register-all.d.ts +2 -0
  47. package/dist/prompts/register-all.js +34 -78
  48. package/dist/public-capability-contract.d.ts +39 -0
  49. package/dist/public-capability-contract.js +186 -0
  50. package/dist/redaction.d.ts +15 -0
  51. package/dist/redaction.js +83 -0
  52. package/dist/resources/register-all.js +154 -64
  53. package/dist/retry-after.d.ts +19 -0
  54. package/dist/retry-after.js +84 -0
  55. package/dist/server-composition.d.ts +19 -0
  56. package/dist/server-composition.js +74 -0
  57. package/dist/tool-exposure.d.ts +31 -27
  58. package/dist/tool-exposure.js +211 -87
  59. package/dist/tools/accountingClose.d.ts +14 -5
  60. package/dist/tools/accountingClose.js +41 -23
  61. package/dist/tools/audit_gl.js +8 -8
  62. package/dist/tools/backend-availability.d.ts +8 -29
  63. package/dist/tools/backend-availability.js +31 -30
  64. package/dist/tools/bank_rules.js +66 -15
  65. package/dist/tools/banking.js +5 -13
  66. package/dist/tools/clients.js +261 -15
  67. package/dist/tools/crm.js +22 -21
  68. package/dist/tools/deposits.js +114 -85
  69. package/dist/tools/einvoice.d.ts +16 -2
  70. package/dist/tools/einvoice.js +59 -12
  71. package/dist/tools/expenses.d.ts +16 -0
  72. package/dist/tools/expenses.js +47 -27
  73. package/dist/tools/fiscal.d.ts +3 -4
  74. package/dist/tools/fiscal.js +93 -60
  75. package/dist/tools/gestoria.js +15 -7
  76. package/dist/tools/hr.d.ts +2 -3
  77. package/dist/tools/hr.js +49 -20
  78. package/dist/tools/igic.d.ts +9 -8
  79. package/dist/tools/igic.js +34 -64
  80. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  81. package/dist/tools/impuesto_sociedades.js +16 -37
  82. package/dist/tools/intelligence.js +166 -12
  83. package/dist/tools/invoices.js +224 -54
  84. package/dist/tools/kitchen.js +19 -9
  85. package/dist/tools/payroll.d.ts +5 -8
  86. package/dist/tools/payroll.js +19 -32
  87. package/dist/tools/permissions.d.ts +4 -7
  88. package/dist/tools/permissions.js +11 -14
  89. package/dist/tools/products.js +6 -6
  90. package/dist/tools/quotes.js +172 -26
  91. package/dist/tools/recurring.js +26 -25
  92. package/dist/tools/register-all.d.ts +2 -2
  93. package/dist/tools/register-all.js +9 -6
  94. package/dist/tools/search.d.ts +19 -0
  95. package/dist/tools/search.js +84 -0
  96. package/dist/tools/shared.d.ts +1445 -109
  97. package/dist/tools/shared.js +478 -102
  98. package/dist/tools/team.js +24 -28
  99. package/dist/tools/time.js +4 -12
  100. package/dist/tools/vendors.js +249 -15
  101. package/dist/tools/webhooks.js +35 -30
  102. package/dist/types.d.ts +31 -5
  103. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  104. package/dist/workflows/daily-operational-brief.js +881 -0
  105. package/docs/agent-onboarding.json +391 -0
  106. package/package.json +49 -12
  107. package/dist/tools/onboarding.d.ts +0 -19
  108. package/dist/tools/onboarding.js +0 -64
@@ -6,8 +6,9 @@
6
6
  *
7
7
  * Design:
8
8
  * - Fail-open: any Langfuse error logs a warning and lets the tool proceed.
9
- * - PII: tool input content is passed as-is (business data is fine to trace).
10
- * userId / apiKey metadata are hashed with a simple SHA-256 fingerprint.
9
+ * - Data minimization: only bounded operational facts cross the telemetry
10
+ * boundary. Tool input/output, arbitrary error text, and user/workspace
11
+ * identity are never serialized.
11
12
  * - Fire-and-forget: traces are sent via waitUntil (Workers) or unref'd promise
12
13
  * (Node.js) so they never block tool responses.
13
14
  *
@@ -18,7 +19,54 @@
18
19
  *
19
20
  * Docs: https://langfuse.com/docs/api/reference/overview
20
21
  */
21
- import { redactClone, redactText } from "./redaction.js";
22
+ import { log } from "./logger.js";
23
+ import { rejectRedirectResponse } from "./fetch-no-redirect.js";
24
+ const CANONICAL_LANGFUSE_ORIGIN = "https://langfuse.frihet.io";
25
+ /**
26
+ * Langfuse receives a Basic Authorization header, so its authority is exact.
27
+ * The current product contract documents one hosted origin; arbitrary Frihet
28
+ * subdomains and self-hosted overrides are intentionally not trusted here.
29
+ */
30
+ export function normalizeLangfuseBaseUrl(value) {
31
+ if (value !== value.trim()) {
32
+ throw new Error("LANGFUSE_BASE_URL must not contain surrounding whitespace");
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = new URL(value);
37
+ }
38
+ catch {
39
+ throw new Error("LANGFUSE_BASE_URL must be a valid URL");
40
+ }
41
+ if (parsed.protocol !== "https:")
42
+ throw new Error("LANGFUSE_BASE_URL must use HTTPS");
43
+ if (parsed.username || parsed.password) {
44
+ throw new Error("LANGFUSE_BASE_URL must not contain URL credentials");
45
+ }
46
+ if (parsed.port)
47
+ throw new Error("LANGFUSE_BASE_URL must use the default HTTPS port");
48
+ if (parsed.search || parsed.hash) {
49
+ throw new Error("LANGFUSE_BASE_URL must not contain a query or fragment");
50
+ }
51
+ if (parsed.pathname !== "/")
52
+ throw new Error("LANGFUSE_BASE_URL must use the origin root");
53
+ if (parsed.hostname.toLowerCase() !== "langfuse.frihet.io") {
54
+ throw new Error("LANGFUSE_BASE_URL hostname is not trusted");
55
+ }
56
+ return CANONICAL_LANGFUSE_ORIGIN;
57
+ }
58
+ function buildConfig(publicKey, secretKey, baseUrl) {
59
+ if (!publicKey || !secretKey || !baseUrl)
60
+ return null;
61
+ try {
62
+ return { publicKey, secretKey, baseUrl: normalizeLangfuseBaseUrl(baseUrl) };
63
+ }
64
+ catch {
65
+ // Telemetry is optional and fail-open for tools. An invalid authority
66
+ // disables telemetry rather than risking credentials or blocking MCP calls.
67
+ return null;
68
+ }
69
+ }
22
70
  function getConfig() {
23
71
  let publicKey;
24
72
  let secretKey;
@@ -29,44 +77,22 @@ function getConfig() {
29
77
  secretKey = process.env.LANGFUSE_SECRET_KEY;
30
78
  baseUrl = process.env.LANGFUSE_BASE_URL;
31
79
  }
32
- if (!publicKey || !secretKey || !baseUrl)
33
- return null;
34
- return { publicKey, secretKey, baseUrl: baseUrl.replace(/\/$/, "") };
80
+ return buildConfig(publicKey, secretKey, baseUrl);
35
81
  }
36
82
  // ── Worker env injection (for Cloudflare Workers) ───────────────────────────
37
- let workerEnv = null;
83
+ // `undefined` means no Worker-specific decision was made, so Node.js may read
84
+ // process.env. `null` is an explicit Worker-side opt-out and must never fall
85
+ // back to process.env (Workers using nodejs_compat expose that global too).
86
+ let workerEnv;
38
87
  /**
39
88
  * Called once from FrihetMCP.init() in the Worker to inject env vars.
40
89
  * Not needed in Node.js stdio mode (reads from process.env directly).
41
90
  */
42
91
  export function initLangfuse(config) {
43
- if (config.publicKey && config.secretKey && config.baseUrl) {
44
- workerEnv = {
45
- publicKey: config.publicKey,
46
- secretKey: config.secretKey,
47
- baseUrl: config.baseUrl.replace(/\/$/, ""),
48
- };
49
- }
92
+ workerEnv = buildConfig(config.publicKey, config.secretKey, config.baseUrl);
50
93
  }
51
94
  function resolveConfig() {
52
- return workerEnv ?? getConfig();
53
- }
54
- // ── PII helpers ──────────────────────────────────────────────────────────────
55
- /**
56
- * One-way fingerprint for PII values (apiKey, userId, email).
57
- * Uses Web Crypto API (available in both Node.js ≥18 and Workers).
58
- */
59
- async function hashPii(value) {
60
- try {
61
- const encoder = new TextEncoder();
62
- const data = encoder.encode(value);
63
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
64
- const hashArray = Array.from(new Uint8Array(hashBuffer));
65
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
66
- }
67
- catch {
68
- return "[hash-error]";
69
- }
95
+ return workerEnv === undefined ? getConfig() : workerEnv;
70
96
  }
71
97
  // ── ID generation ────────────────────────────────────────────────────────────
72
98
  function newId() {
@@ -100,17 +126,11 @@ export function inspectStubMarker(output) {
100
126
  candidates.push(sc);
101
127
  for (const obj of candidates) {
102
128
  if (obj["_stub"] === true || obj["_notImplemented"] === true || obj["_unavailable"] === true) {
103
- return {
104
- plannedEndpoint: typeof obj["_plannedEndpoint"] === "string" ? obj["_plannedEndpoint"] : undefined,
105
- note: typeof obj["_note"] === "string" ? obj["_note"] : undefined,
106
- };
129
+ return { stub: true };
107
130
  }
108
131
  // A bare _plannedEndpoint (without an explicit flag) is also a stub signal.
109
132
  if (typeof obj["_plannedEndpoint"] === "string") {
110
- return {
111
- plannedEndpoint: obj["_plannedEndpoint"],
112
- note: typeof obj["_note"] === "string" ? obj["_note"] : undefined,
113
- };
133
+ return { stub: true };
114
134
  }
115
135
  }
116
136
  return null;
@@ -120,29 +140,34 @@ async function sendBatch(config, batch) {
120
140
  const credentials = btoa(`${config.publicKey}:${config.secretKey}`);
121
141
  const resp = await fetch(`${config.baseUrl}/api/public/ingestion`, {
122
142
  method: "POST",
143
+ redirect: "manual",
123
144
  headers: {
124
145
  "Content-Type": "application/json",
125
146
  "Authorization": `Basic ${credentials}`,
126
147
  },
127
148
  body: JSON.stringify(batch),
128
149
  signal: AbortSignal.timeout(5000),
129
- });
150
+ }).then(rejectRedirectResponse);
130
151
  if (!resp.ok) {
131
- // Log but don't throw — fail-open
132
- const body = await resp.text().catch(() => "");
133
- console.error(JSON.stringify({
134
- service: "frihet-mcp",
152
+ // Never read/log the provider response body. Status is sufficient to
153
+ // correlate an operational delivery failure without importing arbitrary
154
+ // third-party text into telemetry.
155
+ log({
135
156
  level: "warn",
136
- message: `Langfuse ingestion failed: ${resp.status} ${body.slice(0, 200)}`,
157
+ message: "Langfuse ingestion request failed",
137
158
  operation: "langfuse_send",
138
- timestamp: new Date().toISOString(),
139
- }));
159
+ error: {
160
+ message: "MCP operation failed",
161
+ code: "telemetry_http_error",
162
+ statusCode: resp.status,
163
+ },
164
+ });
140
165
  }
141
166
  }
142
167
  // Module-level context set once per session (Workers: per DO init, Node.js: startup)
143
168
  let _sessionContext = {};
144
169
  /**
145
- * Set session-level context (client identity, MCP version).
170
+ * Set session-level protocol context. Client identity is intentionally absent.
146
171
  * Call once from server init; applies to all subsequent traces.
147
172
  */
148
173
  export function setTraceContext(ctx) {
@@ -151,71 +176,69 @@ export function setTraceContext(ctx) {
151
176
  /**
152
177
  * Builds the Langfuse trace+span ingestion batch for a single tool call.
153
178
  *
154
- * CRITICAL (Trust): `input` and `output` are passed through {@link redactClone}
155
- * before they enter the payload, so government IDs (taxId/NIF/CIF), banking
156
- * identifiers (IBAN), webhook signing secrets, and auth tokens NEVER reach the
157
- * external Langfuse service — in ANY profile mode. The tool-call output handed
158
- * to the user is redacted later (OpenAI mode only); this redacts the trace copy
159
- * unconditionally. Exported so a test can assert no sensitive value survives.
179
+ * CRITICAL (Trust): this builder is an allowlist. It has no input/output or
180
+ * identity fields and ignores any extra runtime properties a caller supplies.
181
+ * Exported so tests can assert the exact operational shape.
160
182
  */
161
183
  export function buildTracePayload(p) {
162
- const { toolName, input, output, isError, errorMessage, startTime, endTime, traceId, spanId, userIdHashed, clientName, mcpVersion, stub, } = p;
184
+ const { toolName, isError, errorClass, errorCode, statusCode, startTime, endTime, traceId, spanId, mcpVersion, stub, } = p;
163
185
  const stubbed = stub !== null;
164
186
  // success: false whenever the call threw OR returned a fabricated stub body.
165
187
  const success = !isError && !stubbed;
166
- const stubNote = stub?.note ??
167
- (stub?.plannedEndpoint
168
- ? `Backend endpoint not yet available: ${stub.plannedEndpoint}`
169
- : undefined);
170
- // ── Redact BEFORE the payload is built (never mutates the live response) ──
171
- const safeArgs = redactClone(input);
172
- const safeError = errorMessage ? redactText(errorMessage) : errorMessage;
173
- const safeOutput = isError ? { error: safeError } : redactClone(output);
188
+ const safeToolName = toolNameToken(toolName) ?? "unknown_tool";
189
+ const safeMcpVersion = mcpVersion === "mcp/1.0" ? mcpVersion : undefined;
190
+ const safeErrorClass = TELEMETRY_ERROR_CLASSES.has(errorClass ?? "")
191
+ ? errorClass
192
+ : undefined;
193
+ const safeErrorCode = TELEMETRY_ERROR_CODES.has(errorCode ?? "")
194
+ ? errorCode
195
+ : undefined;
196
+ const safeStatusCode = typeof statusCode === "number"
197
+ && Number.isInteger(statusCode)
198
+ && statusCode >= 0
199
+ && statusCode <= 599
200
+ ? statusCode
201
+ : undefined;
202
+ const errorFacts = {
203
+ ...(safeErrorClass ? { errorClass: safeErrorClass } : {}),
204
+ ...(safeErrorCode ? { errorCode: safeErrorCode } : {}),
205
+ ...(safeStatusCode !== undefined ? { statusCode: safeStatusCode } : {}),
206
+ };
174
207
  const traceBody = {
175
208
  id: traceId,
176
209
  name: "mcp_request",
177
210
  timestamp: startTime.toISOString(),
178
- input: { tool: toolName, args: safeArgs },
179
- output: safeOutput,
180
211
  metadata: {
181
- tool: toolName,
182
- clientName,
183
- mcpVersion,
212
+ tool: safeToolName,
213
+ ...(safeMcpVersion ? { mcpVersion: safeMcpVersion } : {}),
184
214
  // FALSE on a thrown error AND on a fabricated-stub fallback.
185
215
  success,
186
- ...(stubbed
187
- ? {
188
- stub: true,
189
- ...(stub?.plannedEndpoint ? { plannedEndpoint: stub.plannedEndpoint } : {}),
190
- ...(stubNote ? { note: stubNote } : {}),
191
- }
192
- : {}),
216
+ ...(stubbed ? { stub: true } : {}),
217
+ ...errorFacts,
193
218
  },
194
- tags: [`mcp.tool.${toolName}`, ...(stubbed ? ["mcp.stub"] : [])],
195
- ...(userIdHashed ? { userId: userIdHashed } : {}),
219
+ tags: [`mcp.tool.${safeToolName}`, ...(stubbed ? ["mcp.stub"] : [])],
196
220
  };
197
221
  const spanBody = {
198
222
  id: spanId,
199
223
  traceId,
200
- name: `tool.${toolName}`,
224
+ name: `tool.${safeToolName}`,
201
225
  startTime: startTime.toISOString(),
202
226
  endTime: endTime.toISOString(),
203
- input: safeArgs,
204
- output: safeOutput,
205
227
  metadata: {
206
228
  durationMs: endTime.getTime() - startTime.getTime(),
207
- clientName,
208
- mcpVersion,
229
+ ...(safeMcpVersion ? { mcpVersion: safeMcpVersion } : {}),
230
+ success,
209
231
  ...(stubbed ? { stub: true } : {}),
232
+ ...errorFacts,
210
233
  },
211
234
  // ERROR on a thrown error; WARNING on a fabricated-stub fallback (the call
212
235
  // "succeeded" mechanically but returned no real backend data); DEFAULT only
213
236
  // on a genuine success.
214
237
  level: isError ? "ERROR" : stubbed ? "WARNING" : "DEFAULT",
215
- ...(isError && safeError
216
- ? { statusMessage: safeError }
217
- : stubbed && stubNote
218
- ? { statusMessage: stubNote }
238
+ ...(isError
239
+ ? { statusMessage: safeErrorCode ?? safeErrorClass ?? "operation_failed" }
240
+ : stubbed
241
+ ? { statusMessage: "stub_response" }
219
242
  : {}),
220
243
  };
221
244
  return {
@@ -225,6 +248,72 @@ export function buildTracePayload(p) {
225
248
  ],
226
249
  };
227
250
  }
251
+ const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/u;
252
+ const TELEMETRY_ERROR_CLASSES = new Set([
253
+ "handled_mcp_error",
254
+ "request_timeout",
255
+ "abort_error",
256
+ "api_error",
257
+ "telemetry_error",
258
+ "unknown_error",
259
+ ]);
260
+ const TELEMETRY_ERROR_CODES = new Set([
261
+ "tool_error",
262
+ "request_timeout",
263
+ "unauthorized",
264
+ "forbidden",
265
+ "not_found",
266
+ "rate_limited",
267
+ "client_error",
268
+ "backend_error",
269
+ "telemetry_http_error",
270
+ "telemetry_error",
271
+ "unknown_error",
272
+ ]);
273
+ function toolNameToken(value) {
274
+ return typeof value === "string" && TOOL_NAME.test(value) ? value : undefined;
275
+ }
276
+ function statusErrorCode(statusCode) {
277
+ if (typeof statusCode !== "number" || !Number.isInteger(statusCode))
278
+ return undefined;
279
+ if (statusCode === 401)
280
+ return "unauthorized";
281
+ if (statusCode === 403)
282
+ return "forbidden";
283
+ if (statusCode === 404)
284
+ return "not_found";
285
+ if (statusCode === 408)
286
+ return "request_timeout";
287
+ if (statusCode === 429)
288
+ return "rate_limited";
289
+ if (statusCode >= 400 && statusCode < 500)
290
+ return "client_error";
291
+ if (statusCode >= 500 && statusCode <= 599)
292
+ return "backend_error";
293
+ return undefined;
294
+ }
295
+ function operationalErrorFacts(error) {
296
+ if (!error || typeof error !== "object")
297
+ return { errorClass: "unknown_error" };
298
+ const record = error;
299
+ const statusCode = typeof record.statusCode === "number" ? record.statusCode : undefined;
300
+ const name = record.name;
301
+ const errorClass = name === "TimeoutError"
302
+ ? "request_timeout"
303
+ : name === "AbortError"
304
+ ? "abort_error"
305
+ : statusErrorCode(statusCode)
306
+ ? "api_error"
307
+ : "unknown_error";
308
+ return {
309
+ errorClass,
310
+ errorCode: statusErrorCode(statusCode) ?? (errorClass === "request_timeout" ? "request_timeout" : "unknown_error"),
311
+ statusCode,
312
+ };
313
+ }
314
+ function isHandledMcpError(output) {
315
+ return !!output && typeof output === "object" && output.isError === true;
316
+ }
228
317
  /**
229
318
  * Wraps a tool handler fn and sends a Langfuse trace+span for the call.
230
319
  *
@@ -236,7 +325,7 @@ export function buildTracePayload(p) {
236
325
  * @param fn Async tool handler to wrap
237
326
  * @returns Result of fn
238
327
  */
239
- export async function traceMCPTool(toolName, input, fn) {
328
+ export async function traceMCPTool(toolName, _input, fn) {
240
329
  const config = resolveConfig();
241
330
  // No config — pass through silently
242
331
  if (!config) {
@@ -247,16 +336,20 @@ export async function traceMCPTool(toolName, input, fn) {
247
336
  const startTime = new Date();
248
337
  let result;
249
338
  let isError = false;
250
- let errorMessage;
339
+ let errorFacts = {};
251
340
  let output;
252
341
  try {
253
342
  result = await fn();
254
343
  output = result;
344
+ if (isHandledMcpError(result)) {
345
+ isError = true;
346
+ errorFacts = { errorClass: "handled_mcp_error", errorCode: "tool_error" };
347
+ }
255
348
  return result;
256
349
  }
257
350
  catch (err) {
258
351
  isError = true;
259
- errorMessage = err instanceof Error ? err.message : String(err);
352
+ errorFacts = operationalErrorFacts(err);
260
353
  throw err;
261
354
  }
262
355
  finally {
@@ -274,37 +367,32 @@ export async function traceMCPTool(toolName, input, fn) {
274
367
  // Fire-and-forget — build and send async, never awaited
275
368
  void (async () => {
276
369
  try {
277
- // Hash PII fields
278
- const userIdRaw = _sessionContext.userId;
279
- const userIdHashed = userIdRaw ? await hashPii(userIdRaw) : undefined;
280
- // buildTracePayload redacts input/output (taxId/secret/IBAN/tokens) so
281
- // the external Langfuse service never stores cleartext PII or credentials.
282
370
  const batch = buildTracePayload({
283
371
  toolName,
284
- input,
285
- output,
286
372
  isError,
287
- errorMessage,
373
+ ...errorFacts,
288
374
  startTime,
289
375
  endTime,
290
376
  traceId,
291
377
  spanId,
292
- userIdHashed,
293
- clientName: _sessionContext.clientName,
294
378
  mcpVersion: _sessionContext.mcpVersion,
295
379
  stub,
296
380
  });
297
381
  await sendBatch(config, batch);
298
382
  }
299
383
  catch (langfuseErr) {
300
- // Fail-open: log warn only
301
- console.error(JSON.stringify({
302
- service: "frihet-mcp",
384
+ // Fail-open, but never serialize the provider/runtime exception text.
385
+ const facts = operationalErrorFacts(langfuseErr);
386
+ log({
303
387
  level: "warn",
304
- message: `Langfuse trace failed (non-blocking): ${langfuseErr instanceof Error ? langfuseErr.message : String(langfuseErr)}`,
388
+ message: "Langfuse trace failed (non-blocking)",
305
389
  operation: "langfuse_trace",
306
- timestamp: new Date().toISOString(),
307
- }));
390
+ error: {
391
+ message: "MCP operation failed",
392
+ code: "telemetry_error",
393
+ statusCode: facts.statusCode,
394
+ },
395
+ });
308
396
  }
309
397
  })();
310
398
  }
@@ -12,14 +12,34 @@
12
12
  * 4. Redacts sensitive fields from all tool outputs
13
13
  * 5. Updates descriptions to reflect modified behavior + openWorldHint justifications
14
14
  *
15
- * The full MCP server (157 business tools + MCP extras) remains available for Claude, Cursor,
15
+ * The full MCP server (158 business tools + MCP extras) remains available for Claude, Cursor,
16
16
  * Windsurf, Cline, Codex, and all other MCP clients.
17
17
  *
18
- * OpenAI-safe mode: 53 reviewed business tools, 0 prompts, 0 resources, 0 government IDs in I/O.
18
+ * OpenAI-safe mode: 33 reviewed business tools, 0 prompts, 0 resources, and no
19
+ * dedicated government-ID fields in I/O.
19
20
  * The full MCP surface remains available outside FRIHET_OPENAI_MODE.
20
21
  *
21
22
  * @see https://developers.openai.com/apps-sdk/app-submission-guidelines
22
23
  */
24
+ /** OAuth requirement advertised on every reviewed business tool. */
25
+ export declare const OPENAI_OAUTH_SECURITY_SCHEMES: readonly [{
26
+ readonly type: "oauth2";
27
+ readonly scopes: readonly string[];
28
+ }];
29
+ export declare const OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS: ReadonlySet<string>;
30
+ export declare const OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS: ReadonlySet<string>;
31
+ /** Strictly necessary row fields for reviewed collection/search responses. */
32
+ export declare const OPENAI_REVIEW_LIST_OUTPUT_FIELDS: Readonly<Record<string, readonly string[]>>;
33
+ export declare const OPENAI_REVIEW_LIST_LIMIT_MAX = 50;
34
+ export declare const OPENAI_REVIEW_OFFSET_MAX = 10000;
35
+ export declare const OPENAI_REVIEW_PAGINATION_DEFAULT = 20;
36
+ export declare const OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX = 100;
37
+ export declare const OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX = 5;
38
+ export declare const OPENAI_REVIEW_FREE_TEXT_WARNING = "Do not include passwords, credentials, payment-card data, health data, or official/government identifiers in this free-text field / No incluyas contrasenas, credenciales, datos de tarjetas, datos de salud ni identificadores oficiales en este campo de texto libre";
39
+ /** Dot paths whose persisted or query text needs an explicit model-facing warning. */
40
+ export declare const OPENAI_REVIEW_FREE_TEXT_WARNING_PATHS: Readonly<Record<string, readonly string[]>>;
41
+ export declare const OPENAI_REVIEW_PAGINATION_LIMITS: Readonly<Record<string, number>>;
42
+ export declare const OPENAI_REVIEW_TEXT_INPUT_LIMITS: Readonly<Record<string, Readonly<Record<string, number>>>>;
23
43
  /**
24
44
  * Content-Security-Policy for the OpenAI-safe MCP endpoint.
25
45
  * OpenAI requires CSP specifying the exact domains the app fetches from.
@@ -47,28 +67,12 @@ export declare function applyOpenAIProfile(server: any): void;
47
67
  export declare const OPENAI_EXCLUDED_COUNT: number;
48
68
  /** Number of tools explicitly allowed in OpenAI mode. */
49
69
  export declare const OPENAI_ALLOWED_TOOL_COUNT: number;
50
- /**
51
- * The reviewed OpenAI tool allow-list (the 53 business tools), exposed read-only
52
- * so the grouped tool-exposure profile can pin its catalog to EXACTLY this set
53
- * when the two profiles are composed on openai-mcp.frihet.io. The progressive-
54
- * disclosure meta-tools (search_tools / describe_tool / list_tool_groups) must
55
- * never surface a tool outside this allow-list; passing it as the grouped
56
- * `allowlist` enforces that statically. This is the SAME object used to gate
57
- * registerTool, so the two can never drift apart.
58
- *
59
- * NOTE: this is the 53 BUSINESS tools only. It deliberately does NOT contain the
60
- * 3 grouped meta-tools — those are registered directly on the real server
61
- * (bypassing the OpenAI gate) by applyToolExposureProfile when it runs first, so
62
- * OPENAI_ALLOWED_TOOL_COUNT (and every advertised "53 reviewed tools" doc) stays
63
- * correct while the live tools/list still materialises 53 + 3 = 56 tools.
64
- */
70
+ /** The exact 33-tool allow-list reviewed for the OpenAI connector. */
65
71
  export declare const OPENAI_REVIEWED_TOOL_ALLOWLIST: ReadonlySet<string>;
66
72
  /**
67
- * Apply the exact profile composition used by the ChatGPT review Worker.
68
- *
69
- * Keep this helper as the single source for the security-sensitive ordering:
70
- * grouped exposure first (with the frozen allow-list), OpenAI profile second.
71
- * Tool registration happens afterwards through registerAllTools().
73
+ * Backwards-compatible helper for tests and gates that capture the exact
74
+ * ChatGPT review profile. The reviewed host deliberately uses full tool
75
+ * descriptions and exposes no grouped discovery meta-tools.
72
76
  */
73
77
  export declare function applyOpenAIReviewProfiles(server: any): void;
74
78
  /** Number of resources excluded in OpenAI mode (for logging). */