@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.
- 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 +488 -56
- package/dist/demo-client.d.ts +16 -13
- package/dist/demo-client.js +171 -17
- 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 +25 -21
- package/dist/openai-profile.js +1166 -156
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +715 -41
- package/dist/openai-review-oauth.d.ts +47 -0
- package/dist/openai-review-oauth.js +73 -3
- 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 +2 -2
- 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 +30 -7
package/dist/observability.js
CHANGED
|
@@ -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
|
-
* -
|
|
10
|
-
*
|
|
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,53 @@
|
|
|
18
19
|
*
|
|
19
20
|
* Docs: https://langfuse.com/docs/api/reference/overview
|
|
20
21
|
*/
|
|
21
|
-
import {
|
|
22
|
+
import { log } from "./logger.js";
|
|
23
|
+
const CANONICAL_LANGFUSE_ORIGIN = "https://langfuse.frihet.io";
|
|
24
|
+
/**
|
|
25
|
+
* Langfuse receives a Basic Authorization header, so its authority is exact.
|
|
26
|
+
* The current product contract documents one hosted origin; arbitrary Frihet
|
|
27
|
+
* subdomains and self-hosted overrides are intentionally not trusted here.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeLangfuseBaseUrl(value) {
|
|
30
|
+
if (value !== value.trim()) {
|
|
31
|
+
throw new Error("LANGFUSE_BASE_URL must not contain surrounding whitespace");
|
|
32
|
+
}
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = new URL(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error("LANGFUSE_BASE_URL must be a valid URL");
|
|
39
|
+
}
|
|
40
|
+
if (parsed.protocol !== "https:")
|
|
41
|
+
throw new Error("LANGFUSE_BASE_URL must use HTTPS");
|
|
42
|
+
if (parsed.username || parsed.password) {
|
|
43
|
+
throw new Error("LANGFUSE_BASE_URL must not contain URL credentials");
|
|
44
|
+
}
|
|
45
|
+
if (parsed.port)
|
|
46
|
+
throw new Error("LANGFUSE_BASE_URL must use the default HTTPS port");
|
|
47
|
+
if (parsed.search || parsed.hash) {
|
|
48
|
+
throw new Error("LANGFUSE_BASE_URL must not contain a query or fragment");
|
|
49
|
+
}
|
|
50
|
+
if (parsed.pathname !== "/")
|
|
51
|
+
throw new Error("LANGFUSE_BASE_URL must use the origin root");
|
|
52
|
+
if (parsed.hostname.toLowerCase() !== "langfuse.frihet.io") {
|
|
53
|
+
throw new Error("LANGFUSE_BASE_URL hostname is not trusted");
|
|
54
|
+
}
|
|
55
|
+
return CANONICAL_LANGFUSE_ORIGIN;
|
|
56
|
+
}
|
|
57
|
+
function buildConfig(publicKey, secretKey, baseUrl) {
|
|
58
|
+
if (!publicKey || !secretKey || !baseUrl)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
return { publicKey, secretKey, baseUrl: normalizeLangfuseBaseUrl(baseUrl) };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Telemetry is optional and fail-open for tools. An invalid authority
|
|
65
|
+
// disables telemetry rather than risking credentials or blocking MCP calls.
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
22
69
|
function getConfig() {
|
|
23
70
|
let publicKey;
|
|
24
71
|
let secretKey;
|
|
@@ -29,44 +76,22 @@ function getConfig() {
|
|
|
29
76
|
secretKey = process.env.LANGFUSE_SECRET_KEY;
|
|
30
77
|
baseUrl = process.env.LANGFUSE_BASE_URL;
|
|
31
78
|
}
|
|
32
|
-
|
|
33
|
-
return null;
|
|
34
|
-
return { publicKey, secretKey, baseUrl: baseUrl.replace(/\/$/, "") };
|
|
79
|
+
return buildConfig(publicKey, secretKey, baseUrl);
|
|
35
80
|
}
|
|
36
81
|
// ── Worker env injection (for Cloudflare Workers) ───────────────────────────
|
|
37
|
-
|
|
82
|
+
// `undefined` means no Worker-specific decision was made, so Node.js may read
|
|
83
|
+
// process.env. `null` is an explicit Worker-side opt-out and must never fall
|
|
84
|
+
// back to process.env (Workers using nodejs_compat expose that global too).
|
|
85
|
+
let workerEnv;
|
|
38
86
|
/**
|
|
39
87
|
* Called once from FrihetMCP.init() in the Worker to inject env vars.
|
|
40
88
|
* Not needed in Node.js stdio mode (reads from process.env directly).
|
|
41
89
|
*/
|
|
42
90
|
export function initLangfuse(config) {
|
|
43
|
-
|
|
44
|
-
workerEnv = {
|
|
45
|
-
publicKey: config.publicKey,
|
|
46
|
-
secretKey: config.secretKey,
|
|
47
|
-
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
91
|
+
workerEnv = buildConfig(config.publicKey, config.secretKey, config.baseUrl);
|
|
50
92
|
}
|
|
51
93
|
function resolveConfig() {
|
|
52
|
-
return workerEnv
|
|
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
|
-
}
|
|
94
|
+
return workerEnv === undefined ? getConfig() : workerEnv;
|
|
70
95
|
}
|
|
71
96
|
// ── ID generation ────────────────────────────────────────────────────────────
|
|
72
97
|
function newId() {
|
|
@@ -100,17 +125,11 @@ export function inspectStubMarker(output) {
|
|
|
100
125
|
candidates.push(sc);
|
|
101
126
|
for (const obj of candidates) {
|
|
102
127
|
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
|
-
};
|
|
128
|
+
return { stub: true };
|
|
107
129
|
}
|
|
108
130
|
// A bare _plannedEndpoint (without an explicit flag) is also a stub signal.
|
|
109
131
|
if (typeof obj["_plannedEndpoint"] === "string") {
|
|
110
|
-
return {
|
|
111
|
-
plannedEndpoint: obj["_plannedEndpoint"],
|
|
112
|
-
note: typeof obj["_note"] === "string" ? obj["_note"] : undefined,
|
|
113
|
-
};
|
|
132
|
+
return { stub: true };
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
return null;
|
|
@@ -120,6 +139,7 @@ async function sendBatch(config, batch) {
|
|
|
120
139
|
const credentials = btoa(`${config.publicKey}:${config.secretKey}`);
|
|
121
140
|
const resp = await fetch(`${config.baseUrl}/api/public/ingestion`, {
|
|
122
141
|
method: "POST",
|
|
142
|
+
redirect: "error",
|
|
123
143
|
headers: {
|
|
124
144
|
"Content-Type": "application/json",
|
|
125
145
|
"Authorization": `Basic ${credentials}`,
|
|
@@ -128,21 +148,25 @@ async function sendBatch(config, batch) {
|
|
|
128
148
|
signal: AbortSignal.timeout(5000),
|
|
129
149
|
});
|
|
130
150
|
if (!resp.ok) {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
// Never read/log the provider response body. Status is sufficient to
|
|
152
|
+
// correlate an operational delivery failure without importing arbitrary
|
|
153
|
+
// third-party text into telemetry.
|
|
154
|
+
log({
|
|
135
155
|
level: "warn",
|
|
136
|
-
message:
|
|
156
|
+
message: "Langfuse ingestion request failed",
|
|
137
157
|
operation: "langfuse_send",
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
error: {
|
|
159
|
+
message: "MCP operation failed",
|
|
160
|
+
code: "telemetry_http_error",
|
|
161
|
+
statusCode: resp.status,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
140
164
|
}
|
|
141
165
|
}
|
|
142
166
|
// Module-level context set once per session (Workers: per DO init, Node.js: startup)
|
|
143
167
|
let _sessionContext = {};
|
|
144
168
|
/**
|
|
145
|
-
* Set session-level context
|
|
169
|
+
* Set session-level protocol context. Client identity is intentionally absent.
|
|
146
170
|
* Call once from server init; applies to all subsequent traces.
|
|
147
171
|
*/
|
|
148
172
|
export function setTraceContext(ctx) {
|
|
@@ -151,71 +175,69 @@ export function setTraceContext(ctx) {
|
|
|
151
175
|
/**
|
|
152
176
|
* Builds the Langfuse trace+span ingestion batch for a single tool call.
|
|
153
177
|
*
|
|
154
|
-
* CRITICAL (Trust):
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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.
|
|
178
|
+
* CRITICAL (Trust): this builder is an allowlist. It has no input/output or
|
|
179
|
+
* identity fields and ignores any extra runtime properties a caller supplies.
|
|
180
|
+
* Exported so tests can assert the exact operational shape.
|
|
160
181
|
*/
|
|
161
182
|
export function buildTracePayload(p) {
|
|
162
|
-
const { toolName,
|
|
183
|
+
const { toolName, isError, errorClass, errorCode, statusCode, startTime, endTime, traceId, spanId, mcpVersion, stub, } = p;
|
|
163
184
|
const stubbed = stub !== null;
|
|
164
185
|
// success: false whenever the call threw OR returned a fabricated stub body.
|
|
165
186
|
const success = !isError && !stubbed;
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
187
|
+
const safeToolName = toolNameToken(toolName) ?? "unknown_tool";
|
|
188
|
+
const safeMcpVersion = mcpVersion === "mcp/1.0" ? mcpVersion : undefined;
|
|
189
|
+
const safeErrorClass = TELEMETRY_ERROR_CLASSES.has(errorClass ?? "")
|
|
190
|
+
? errorClass
|
|
191
|
+
: undefined;
|
|
192
|
+
const safeErrorCode = TELEMETRY_ERROR_CODES.has(errorCode ?? "")
|
|
193
|
+
? errorCode
|
|
194
|
+
: undefined;
|
|
195
|
+
const safeStatusCode = typeof statusCode === "number"
|
|
196
|
+
&& Number.isInteger(statusCode)
|
|
197
|
+
&& statusCode >= 0
|
|
198
|
+
&& statusCode <= 599
|
|
199
|
+
? statusCode
|
|
200
|
+
: undefined;
|
|
201
|
+
const errorFacts = {
|
|
202
|
+
...(safeErrorClass ? { errorClass: safeErrorClass } : {}),
|
|
203
|
+
...(safeErrorCode ? { errorCode: safeErrorCode } : {}),
|
|
204
|
+
...(safeStatusCode !== undefined ? { statusCode: safeStatusCode } : {}),
|
|
205
|
+
};
|
|
174
206
|
const traceBody = {
|
|
175
207
|
id: traceId,
|
|
176
208
|
name: "mcp_request",
|
|
177
209
|
timestamp: startTime.toISOString(),
|
|
178
|
-
input: { tool: toolName, args: safeArgs },
|
|
179
|
-
output: safeOutput,
|
|
180
210
|
metadata: {
|
|
181
|
-
tool:
|
|
182
|
-
|
|
183
|
-
mcpVersion,
|
|
211
|
+
tool: safeToolName,
|
|
212
|
+
...(safeMcpVersion ? { mcpVersion: safeMcpVersion } : {}),
|
|
184
213
|
// FALSE on a thrown error AND on a fabricated-stub fallback.
|
|
185
214
|
success,
|
|
186
|
-
...(stubbed
|
|
187
|
-
|
|
188
|
-
stub: true,
|
|
189
|
-
...(stub?.plannedEndpoint ? { plannedEndpoint: stub.plannedEndpoint } : {}),
|
|
190
|
-
...(stubNote ? { note: stubNote } : {}),
|
|
191
|
-
}
|
|
192
|
-
: {}),
|
|
215
|
+
...(stubbed ? { stub: true } : {}),
|
|
216
|
+
...errorFacts,
|
|
193
217
|
},
|
|
194
|
-
tags: [`mcp.tool.${
|
|
195
|
-
...(userIdHashed ? { userId: userIdHashed } : {}),
|
|
218
|
+
tags: [`mcp.tool.${safeToolName}`, ...(stubbed ? ["mcp.stub"] : [])],
|
|
196
219
|
};
|
|
197
220
|
const spanBody = {
|
|
198
221
|
id: spanId,
|
|
199
222
|
traceId,
|
|
200
|
-
name: `tool.${
|
|
223
|
+
name: `tool.${safeToolName}`,
|
|
201
224
|
startTime: startTime.toISOString(),
|
|
202
225
|
endTime: endTime.toISOString(),
|
|
203
|
-
input: safeArgs,
|
|
204
|
-
output: safeOutput,
|
|
205
226
|
metadata: {
|
|
206
227
|
durationMs: endTime.getTime() - startTime.getTime(),
|
|
207
|
-
|
|
208
|
-
|
|
228
|
+
...(safeMcpVersion ? { mcpVersion: safeMcpVersion } : {}),
|
|
229
|
+
success,
|
|
209
230
|
...(stubbed ? { stub: true } : {}),
|
|
231
|
+
...errorFacts,
|
|
210
232
|
},
|
|
211
233
|
// ERROR on a thrown error; WARNING on a fabricated-stub fallback (the call
|
|
212
234
|
// "succeeded" mechanically but returned no real backend data); DEFAULT only
|
|
213
235
|
// on a genuine success.
|
|
214
236
|
level: isError ? "ERROR" : stubbed ? "WARNING" : "DEFAULT",
|
|
215
|
-
...(isError
|
|
216
|
-
? { statusMessage:
|
|
217
|
-
: stubbed
|
|
218
|
-
? { statusMessage:
|
|
237
|
+
...(isError
|
|
238
|
+
? { statusMessage: safeErrorCode ?? safeErrorClass ?? "operation_failed" }
|
|
239
|
+
: stubbed
|
|
240
|
+
? { statusMessage: "stub_response" }
|
|
219
241
|
: {}),
|
|
220
242
|
};
|
|
221
243
|
return {
|
|
@@ -225,6 +247,72 @@ export function buildTracePayload(p) {
|
|
|
225
247
|
],
|
|
226
248
|
};
|
|
227
249
|
}
|
|
250
|
+
const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
251
|
+
const TELEMETRY_ERROR_CLASSES = new Set([
|
|
252
|
+
"handled_mcp_error",
|
|
253
|
+
"request_timeout",
|
|
254
|
+
"abort_error",
|
|
255
|
+
"api_error",
|
|
256
|
+
"telemetry_error",
|
|
257
|
+
"unknown_error",
|
|
258
|
+
]);
|
|
259
|
+
const TELEMETRY_ERROR_CODES = new Set([
|
|
260
|
+
"tool_error",
|
|
261
|
+
"request_timeout",
|
|
262
|
+
"unauthorized",
|
|
263
|
+
"forbidden",
|
|
264
|
+
"not_found",
|
|
265
|
+
"rate_limited",
|
|
266
|
+
"client_error",
|
|
267
|
+
"backend_error",
|
|
268
|
+
"telemetry_http_error",
|
|
269
|
+
"telemetry_error",
|
|
270
|
+
"unknown_error",
|
|
271
|
+
]);
|
|
272
|
+
function toolNameToken(value) {
|
|
273
|
+
return typeof value === "string" && TOOL_NAME.test(value) ? value : undefined;
|
|
274
|
+
}
|
|
275
|
+
function statusErrorCode(statusCode) {
|
|
276
|
+
if (typeof statusCode !== "number" || !Number.isInteger(statusCode))
|
|
277
|
+
return undefined;
|
|
278
|
+
if (statusCode === 401)
|
|
279
|
+
return "unauthorized";
|
|
280
|
+
if (statusCode === 403)
|
|
281
|
+
return "forbidden";
|
|
282
|
+
if (statusCode === 404)
|
|
283
|
+
return "not_found";
|
|
284
|
+
if (statusCode === 408)
|
|
285
|
+
return "request_timeout";
|
|
286
|
+
if (statusCode === 429)
|
|
287
|
+
return "rate_limited";
|
|
288
|
+
if (statusCode >= 400 && statusCode < 500)
|
|
289
|
+
return "client_error";
|
|
290
|
+
if (statusCode >= 500 && statusCode <= 599)
|
|
291
|
+
return "backend_error";
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
function operationalErrorFacts(error) {
|
|
295
|
+
if (!error || typeof error !== "object")
|
|
296
|
+
return { errorClass: "unknown_error" };
|
|
297
|
+
const record = error;
|
|
298
|
+
const statusCode = typeof record.statusCode === "number" ? record.statusCode : undefined;
|
|
299
|
+
const name = record.name;
|
|
300
|
+
const errorClass = name === "TimeoutError"
|
|
301
|
+
? "request_timeout"
|
|
302
|
+
: name === "AbortError"
|
|
303
|
+
? "abort_error"
|
|
304
|
+
: statusErrorCode(statusCode)
|
|
305
|
+
? "api_error"
|
|
306
|
+
: "unknown_error";
|
|
307
|
+
return {
|
|
308
|
+
errorClass,
|
|
309
|
+
errorCode: statusErrorCode(statusCode) ?? (errorClass === "request_timeout" ? "request_timeout" : "unknown_error"),
|
|
310
|
+
statusCode,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function isHandledMcpError(output) {
|
|
314
|
+
return !!output && typeof output === "object" && output.isError === true;
|
|
315
|
+
}
|
|
228
316
|
/**
|
|
229
317
|
* Wraps a tool handler fn and sends a Langfuse trace+span for the call.
|
|
230
318
|
*
|
|
@@ -236,7 +324,7 @@ export function buildTracePayload(p) {
|
|
|
236
324
|
* @param fn Async tool handler to wrap
|
|
237
325
|
* @returns Result of fn
|
|
238
326
|
*/
|
|
239
|
-
export async function traceMCPTool(toolName,
|
|
327
|
+
export async function traceMCPTool(toolName, _input, fn) {
|
|
240
328
|
const config = resolveConfig();
|
|
241
329
|
// No config — pass through silently
|
|
242
330
|
if (!config) {
|
|
@@ -247,16 +335,20 @@ export async function traceMCPTool(toolName, input, fn) {
|
|
|
247
335
|
const startTime = new Date();
|
|
248
336
|
let result;
|
|
249
337
|
let isError = false;
|
|
250
|
-
let
|
|
338
|
+
let errorFacts = {};
|
|
251
339
|
let output;
|
|
252
340
|
try {
|
|
253
341
|
result = await fn();
|
|
254
342
|
output = result;
|
|
343
|
+
if (isHandledMcpError(result)) {
|
|
344
|
+
isError = true;
|
|
345
|
+
errorFacts = { errorClass: "handled_mcp_error", errorCode: "tool_error" };
|
|
346
|
+
}
|
|
255
347
|
return result;
|
|
256
348
|
}
|
|
257
349
|
catch (err) {
|
|
258
350
|
isError = true;
|
|
259
|
-
|
|
351
|
+
errorFacts = operationalErrorFacts(err);
|
|
260
352
|
throw err;
|
|
261
353
|
}
|
|
262
354
|
finally {
|
|
@@ -274,37 +366,32 @@ export async function traceMCPTool(toolName, input, fn) {
|
|
|
274
366
|
// Fire-and-forget — build and send async, never awaited
|
|
275
367
|
void (async () => {
|
|
276
368
|
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
369
|
const batch = buildTracePayload({
|
|
283
370
|
toolName,
|
|
284
|
-
input,
|
|
285
|
-
output,
|
|
286
371
|
isError,
|
|
287
|
-
|
|
372
|
+
...errorFacts,
|
|
288
373
|
startTime,
|
|
289
374
|
endTime,
|
|
290
375
|
traceId,
|
|
291
376
|
spanId,
|
|
292
|
-
userIdHashed,
|
|
293
|
-
clientName: _sessionContext.clientName,
|
|
294
377
|
mcpVersion: _sessionContext.mcpVersion,
|
|
295
378
|
stub,
|
|
296
379
|
});
|
|
297
380
|
await sendBatch(config, batch);
|
|
298
381
|
}
|
|
299
382
|
catch (langfuseErr) {
|
|
300
|
-
// Fail-open
|
|
301
|
-
|
|
302
|
-
|
|
383
|
+
// Fail-open, but never serialize the provider/runtime exception text.
|
|
384
|
+
const facts = operationalErrorFacts(langfuseErr);
|
|
385
|
+
log({
|
|
303
386
|
level: "warn",
|
|
304
|
-
message:
|
|
387
|
+
message: "Langfuse trace failed (non-blocking)",
|
|
305
388
|
operation: "langfuse_trace",
|
|
306
|
-
|
|
307
|
-
|
|
389
|
+
error: {
|
|
390
|
+
message: "MCP operation failed",
|
|
391
|
+
code: "telemetry_error",
|
|
392
|
+
statusCode: facts.statusCode,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
308
395
|
}
|
|
309
396
|
})();
|
|
310
397
|
}
|
package/dist/openai-profile.d.ts
CHANGED
|
@@ -15,11 +15,31 @@
|
|
|
15
15
|
* The full MCP server (157 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:
|
|
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 ["frihet:workspace.manage"];
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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). */
|