@frihet/mcp-server 1.16.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -82
- package/dist/agent-onboarding.d.ts +98 -0
- package/dist/agent-onboarding.js +275 -0
- package/dist/api-origin.d.ts +9 -0
- package/dist/api-origin.js +41 -0
- package/dist/canary/mcp-harness.d.ts +77 -0
- package/dist/canary/mcp-harness.js +552 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +262 -0
- package/dist/claude-candidate-contract.d.ts +482 -0
- package/dist/claude-candidate-contract.js +87 -0
- package/dist/claude-output-privacy.d.ts +17 -0
- package/dist/claude-output-privacy.js +240 -0
- package/dist/claude-profile.d.ts +18 -0
- package/dist/claude-profile.js +260 -0
- package/dist/client-interface.d.ts +47 -33
- package/dist/client.d.ts +172 -40
- package/dist/client.js +708 -165
- package/dist/demo-client.d.ts +41 -32
- package/dist/demo-client.js +202 -31
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/deposit-contract.d.ts +29 -0
- package/dist/deposit-contract.js +76 -0
- package/dist/fetch-no-redirect.d.ts +3 -0
- package/dist/fetch-no-redirect.js +16 -0
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/fiscal-period.d.ts +26 -0
- package/dist/fiscal-period.js +31 -0
- package/dist/google-profile.d.ts +19 -0
- package/dist/google-profile.js +122 -0
- package/dist/index.js +68 -59
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/muse-profile.d.ts +19 -0
- package/dist/muse-profile.js +122 -0
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +194 -106
- package/dist/openai-profile.d.ts +26 -22
- package/dist/openai-profile.js +1193 -157
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +714 -41
- package/dist/openai-review-oauth.d.ts +149 -2
- package/dist/openai-review-oauth.js +220 -6
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +34 -78
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +15 -0
- package/dist/redaction.js +83 -0
- package/dist/resources/register-all.js +154 -64
- package/dist/retry-after.d.ts +19 -0
- package/dist/retry-after.js +84 -0
- package/dist/server-composition.d.ts +19 -0
- package/dist/server-composition.js +74 -0
- package/dist/tool-exposure.d.ts +31 -27
- package/dist/tool-exposure.js +211 -87
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/audit_gl.js +8 -8
- package/dist/tools/backend-availability.d.ts +8 -29
- package/dist/tools/backend-availability.js +31 -30
- package/dist/tools/bank_rules.js +66 -15
- package/dist/tools/banking.js +5 -13
- package/dist/tools/clients.js +261 -15
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +114 -85
- package/dist/tools/einvoice.d.ts +16 -2
- package/dist/tools/einvoice.js +59 -12
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/fiscal.d.ts +3 -4
- package/dist/tools/fiscal.js +93 -60
- package/dist/tools/gestoria.js +15 -7
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +49 -20
- package/dist/tools/igic.d.ts +9 -8
- package/dist/tools/igic.js +34 -64
- package/dist/tools/impuesto_sociedades.d.ts +4 -5
- package/dist/tools/impuesto_sociedades.js +16 -37
- package/dist/tools/intelligence.js +166 -12
- package/dist/tools/invoices.js +224 -54
- package/dist/tools/kitchen.js +19 -9
- package/dist/tools/payroll.d.ts +5 -8
- package/dist/tools/payroll.js +19 -32
- package/dist/tools/permissions.d.ts +4 -7
- package/dist/tools/permissions.js +11 -14
- package/dist/tools/products.js +6 -6
- package/dist/tools/quotes.js +172 -26
- package/dist/tools/recurring.js +26 -25
- package/dist/tools/register-all.d.ts +2 -2
- package/dist/tools/register-all.js +9 -6
- package/dist/tools/search.d.ts +19 -0
- package/dist/tools/search.js +84 -0
- package/dist/tools/shared.d.ts +1445 -109
- package/dist/tools/shared.js +478 -102
- package/dist/tools/team.js +24 -28
- package/dist/tools/time.js +4 -12
- package/dist/tools/vendors.js +249 -15
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +31 -5
- package/dist/workflows/daily-operational-brief.d.ts +163 -0
- package/dist/workflows/daily-operational-brief.js +881 -0
- package/docs/agent-onboarding.json +391 -0
- package/package.json +49 -12
- package/dist/tools/onboarding.d.ts +0 -19
- package/dist/tools/onboarding.js +0 -64
package/dist/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,54 @@
|
|
|
18
19
|
*
|
|
19
20
|
* Docs: https://langfuse.com/docs/api/reference/overview
|
|
20
21
|
*/
|
|
21
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
157
|
+
message: "Langfuse ingestion request failed",
|
|
137
158
|
operation: "langfuse_send",
|
|
138
|
-
|
|
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
|
|
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):
|
|
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.
|
|
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,
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
182
|
-
|
|
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.${
|
|
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.${
|
|
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
|
-
|
|
208
|
-
|
|
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
|
|
216
|
-
? { statusMessage:
|
|
217
|
-
: stubbed
|
|
218
|
-
? { statusMessage:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
384
|
+
// Fail-open, but never serialize the provider/runtime exception text.
|
|
385
|
+
const facts = operationalErrorFacts(langfuseErr);
|
|
386
|
+
log({
|
|
303
387
|
level: "warn",
|
|
304
|
-
message:
|
|
388
|
+
message: "Langfuse trace failed (non-blocking)",
|
|
305
389
|
operation: "langfuse_trace",
|
|
306
|
-
|
|
307
|
-
|
|
390
|
+
error: {
|
|
391
|
+
message: "MCP operation failed",
|
|
392
|
+
code: "telemetry_error",
|
|
393
|
+
statusCode: facts.statusCode,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
308
396
|
}
|
|
309
397
|
})();
|
|
310
398
|
}
|
package/dist/openai-profile.d.ts
CHANGED
|
@@ -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 (
|
|
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:
|
|
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
|
-
*
|
|
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). */
|