@frihet/mcp-server 1.17.0 → 2.0.1

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 (87) hide show
  1. package/README.md +106 -348
  2. package/dist/agent-onboarding.d.ts +1 -1
  3. package/dist/agent-onboarding.js +3 -3
  4. package/dist/canary/mcp-harness.d.ts +1 -1
  5. package/dist/canary/mcp-harness.js +4 -1
  6. package/dist/capability-truth.js +20 -12
  7. package/dist/claude-candidate-contract.d.ts +482 -0
  8. package/dist/claude-candidate-contract.js +87 -0
  9. package/dist/claude-output-privacy.d.ts +17 -0
  10. package/dist/claude-output-privacy.js +249 -0
  11. package/dist/claude-profile.d.ts +18 -0
  12. package/dist/claude-profile.js +260 -0
  13. package/dist/client-interface.d.ts +26 -20
  14. package/dist/client.d.ts +65 -29
  15. package/dist/client.js +278 -167
  16. package/dist/demo-client.d.ts +28 -21
  17. package/dist/demo-client.js +32 -14
  18. package/dist/deposit-contract.d.ts +29 -0
  19. package/dist/deposit-contract.js +76 -0
  20. package/dist/fetch-no-redirect.d.ts +3 -0
  21. package/dist/fetch-no-redirect.js +16 -0
  22. package/dist/fiscal-aliases.d.ts +2 -2
  23. package/dist/fiscal-aliases.js +2 -2
  24. package/dist/fiscal-period.d.ts +26 -0
  25. package/dist/fiscal-period.js +31 -0
  26. package/dist/google-profile.d.ts +19 -0
  27. package/dist/google-profile.js +122 -0
  28. package/dist/index.js +31 -7
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +2 -2
  31. package/dist/muse-profile.d.ts +19 -0
  32. package/dist/muse-profile.js +122 -0
  33. package/dist/observability.js +3 -2
  34. package/dist/openai-profile.d.ts +2 -2
  35. package/dist/openai-profile.js +37 -11
  36. package/dist/openai-review-contract.d.ts +1 -1
  37. package/dist/openai-review-contract.js +4 -5
  38. package/dist/openai-review-oauth.d.ts +107 -7
  39. package/dist/openai-review-oauth.js +161 -17
  40. package/dist/prompts/register-all.js +32 -78
  41. package/dist/redaction.d.ts +5 -0
  42. package/dist/redaction.js +9 -0
  43. package/dist/resources/register-all.js +150 -54
  44. package/dist/retry-after.d.ts +19 -0
  45. package/dist/retry-after.js +84 -0
  46. package/dist/server-composition.d.ts +6 -2
  47. package/dist/server-composition.js +46 -5
  48. package/dist/tool-exposure.d.ts +7 -5
  49. package/dist/tool-exposure.js +21 -13
  50. package/dist/tools/audit_gl.js +8 -8
  51. package/dist/tools/backend-availability.d.ts +8 -29
  52. package/dist/tools/backend-availability.js +31 -30
  53. package/dist/tools/bank_rules.js +66 -15
  54. package/dist/tools/banking.js +5 -13
  55. package/dist/tools/clients.js +254 -6
  56. package/dist/tools/deposits.js +107 -97
  57. package/dist/tools/einvoice.d.ts +17 -3
  58. package/dist/tools/einvoice.js +59 -12
  59. package/dist/tools/fiscal.d.ts +3 -4
  60. package/dist/tools/fiscal.js +93 -60
  61. package/dist/tools/gestoria.js +15 -7
  62. package/dist/tools/hr.js +41 -11
  63. package/dist/tools/igic.d.ts +9 -8
  64. package/dist/tools/igic.js +34 -64
  65. package/dist/tools/impuesto_sociedades.d.ts +4 -5
  66. package/dist/tools/impuesto_sociedades.js +16 -37
  67. package/dist/tools/intelligence.js +164 -10
  68. package/dist/tools/invoices.js +109 -42
  69. package/dist/tools/kitchen.js +19 -9
  70. package/dist/tools/quotes.js +86 -25
  71. package/dist/tools/recurring.js +26 -25
  72. package/dist/tools/register-all.d.ts +2 -2
  73. package/dist/tools/register-all.js +9 -6
  74. package/dist/tools/search.d.ts +19 -0
  75. package/dist/tools/search.js +84 -0
  76. package/dist/tools/shared.d.ts +18 -38
  77. package/dist/tools/shared.js +96 -30
  78. package/dist/tools/team.js +8 -16
  79. package/dist/tools/time.js +4 -12
  80. package/dist/tools/vendors.js +242 -6
  81. package/dist/types.d.ts +3 -1
  82. package/dist/workflows/daily-operational-brief.d.ts +163 -0
  83. package/dist/workflows/daily-operational-brief.js +881 -0
  84. package/docs/agent-onboarding.json +15 -5
  85. package/package.json +26 -12
  86. package/dist/tools/onboarding.d.ts +0 -19
  87. package/dist/tools/onboarding.js +0 -64
package/dist/logger.d.ts CHANGED
@@ -41,6 +41,6 @@ export declare function logApiCall(method: string, _path: string, statusCode: nu
41
41
  /**
42
42
  * Log a rate-limit retry.
43
43
  */
44
- export declare function logRetry(method: string, _path: string, retryCount: number, delayMs: number): void;
44
+ export declare function logRetry(method: string, _path: string, retryCount: number, delayMs: number, delaySource?: "retry_after" | "backoff"): void;
45
45
  export {};
46
46
  //# sourceMappingURL=logger.d.ts.map
package/dist/logger.js CHANGED
@@ -211,12 +211,12 @@ export function logApiCall(method, _path, statusCode, durationMs) {
211
211
  /**
212
212
  * Log a rate-limit retry.
213
213
  */
214
- export function logRetry(method, _path, retryCount, delayMs) {
214
+ export function logRetry(method, _path, retryCount, delayMs, delaySource) {
215
215
  log({
216
216
  level: "warn",
217
217
  message: "MCP API retry scheduled",
218
218
  operation: "api_retry",
219
- metadata: { method, retryCount: retryCount + 1, delayMs },
219
+ metadata: { method, retryCount: retryCount + 1, delayMs, delaySource },
220
220
  });
221
221
  }
222
222
  //# sourceMappingURL=logger.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Meta Muse Profile for the Frihet MCP Server.
3
+ *
4
+ * Implements the curated 18-tool financial management surface submitted
5
+ * for the Meta Muse Business Application catalog.
6
+ *
7
+ * Requirements:
8
+ * - Curated catalogue: 18 high-value small business & freelancer tools.
9
+ * - Human-in-the-loop: Invoices and financial documents are created strictly in draft status.
10
+ * - Zero unconfirmed destructive actions (no direct AEAT / VeriFactu fiscal submissions).
11
+ * - Standard output redaction for sensitive fields.
12
+ */
13
+ export declare const MUSE_CANDIDATE_TOOLS: ReadonlySet<string>;
14
+ export declare const MUSE_CANDIDATE_TOOL_COUNT: number;
15
+ export declare const MUSE_INSTRUCTIONS = "Frihet ERP \u2014 natural-language business and financial management for Meta Muse.\nUse get_business_context for complete company health and plan limits.\nQuery and review invoices, expenses, quotes, clients, and products with pagination.\nAll created invoices are drafts. Confirm parameters with the user before creating records.";
16
+ export declare function museToolResult(result: unknown): unknown;
17
+ export declare const MUSE_FORCED_INPUT_VALUES: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
18
+ export declare function applyMuseProfile(server: any): void;
19
+ //# sourceMappingURL=muse-profile.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Meta Muse Profile for the Frihet MCP Server.
3
+ *
4
+ * Implements the curated 18-tool financial management surface submitted
5
+ * for the Meta Muse Business Application catalog.
6
+ *
7
+ * Requirements:
8
+ * - Curated catalogue: 18 high-value small business & freelancer tools.
9
+ * - Human-in-the-loop: Invoices and financial documents are created strictly in draft status.
10
+ * - Zero unconfirmed destructive actions (no direct AEAT / VeriFactu fiscal submissions).
11
+ * - Standard output redaction for sensitive fields.
12
+ */
13
+ import { deepRedact, redactText } from "./redaction.js";
14
+ export const MUSE_CANDIDATE_TOOLS = new Set([
15
+ "get_business_context",
16
+ "get_monthly_summary",
17
+ "get_quarterly_taxes",
18
+ "list_invoices",
19
+ "get_invoice",
20
+ "search_invoices",
21
+ "list_expenses",
22
+ "get_expense",
23
+ "create_expense",
24
+ "list_clients",
25
+ "get_client",
26
+ "create_client",
27
+ "list_products",
28
+ "get_product",
29
+ "list_quotes",
30
+ "get_quote",
31
+ "create_quote",
32
+ "create_invoice",
33
+ ]);
34
+ export const MUSE_CANDIDATE_TOOL_COUNT = MUSE_CANDIDATE_TOOLS.size; // 18
35
+ export const MUSE_INSTRUCTIONS = `Frihet ERP — natural-language business and financial management for Meta Muse.
36
+ Use get_business_context for complete company health and plan limits.
37
+ Query and review invoices, expenses, quotes, clients, and products with pagination.
38
+ All created invoices are drafts. Confirm parameters with the user before creating records.`;
39
+ export function museToolResult(result) {
40
+ if (result === null || typeof result !== "object")
41
+ return result;
42
+ const res = result;
43
+ // Deep-redact structured output if present (in-place deletion of sensitive fields)
44
+ if (res.structuredContent && typeof res.structuredContent === "object") {
45
+ deepRedact(res.structuredContent);
46
+ }
47
+ // Redact text content items
48
+ if (Array.isArray(res.content)) {
49
+ res.content = res.content.map((item) => {
50
+ if (item && typeof item === "object" && "text" in item && typeof item.text === "string") {
51
+ return {
52
+ ...item,
53
+ text: redactText(item.text),
54
+ };
55
+ }
56
+ return item;
57
+ });
58
+ }
59
+ return res;
60
+ }
61
+ export const MUSE_FORCED_INPUT_VALUES = {
62
+ create_invoice: { status: "draft" },
63
+ create_quote: { status: "draft" },
64
+ };
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ export function applyMuseProfile(server) {
67
+ const registerTool = server.registerTool.bind(server);
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ server.registerTool = (name, config, handler) => {
70
+ if (!MUSE_CANDIDATE_TOOLS.has(name))
71
+ return undefined;
72
+ let description = config.description ?? "";
73
+ let inputSchema = config.inputSchema;
74
+ if (name === "create_invoice") {
75
+ description = `${description} [Meta Muse: Generates an invoice strictly in 'draft' status for human review. No irreversible fiscal submission occurs.]`;
76
+ if (inputSchema && typeof inputSchema === "object" && "status" in inputSchema) {
77
+ const { status: _omittedStatus, ...rest } = inputSchema;
78
+ inputSchema = rest;
79
+ }
80
+ }
81
+ else if (name === "create_quote") {
82
+ description = `${description} [Meta Muse: Generates a quote strictly in 'draft' status for human review.]`;
83
+ if (inputSchema && typeof inputSchema === "object" && "status" in inputSchema) {
84
+ const { status: _omittedStatus, ...rest } = inputSchema;
85
+ inputSchema = rest;
86
+ }
87
+ }
88
+ return registerTool(name, {
89
+ ...config,
90
+ description,
91
+ inputSchema,
92
+ annotations: {
93
+ ...config.annotations,
94
+ ...(config.annotations?.readOnlyHint !== true ? { destructiveHint: true } : {}),
95
+ },
96
+ },
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ async (...args) => {
99
+ try {
100
+ let input = args[0];
101
+ if (input && typeof input === "object") {
102
+ const forced = MUSE_FORCED_INPUT_VALUES[name];
103
+ if (forced) {
104
+ input = { ...input, ...forced };
105
+ }
106
+ }
107
+ const res = await handler(input, ...args.slice(1));
108
+ return museToolResult(res);
109
+ }
110
+ catch {
111
+ return museToolResult({
112
+ isError: true,
113
+ _meta: { "io.frihet/operationOutcomeUnknown": true },
114
+ });
115
+ }
116
+ });
117
+ };
118
+ // Meta Muse profile intentionally registers 0 raw prompts or dynamic resources
119
+ server.registerResource = () => undefined;
120
+ server.registerPrompt = () => undefined;
121
+ }
122
+ //# sourceMappingURL=muse-profile.js.map
@@ -20,6 +20,7 @@
20
20
  * Docs: https://langfuse.com/docs/api/reference/overview
21
21
  */
22
22
  import { log } from "./logger.js";
23
+ import { rejectRedirectResponse } from "./fetch-no-redirect.js";
23
24
  const CANONICAL_LANGFUSE_ORIGIN = "https://langfuse.frihet.io";
24
25
  /**
25
26
  * Langfuse receives a Basic Authorization header, so its authority is exact.
@@ -139,14 +140,14 @@ async function sendBatch(config, batch) {
139
140
  const credentials = btoa(`${config.publicKey}:${config.secretKey}`);
140
141
  const resp = await fetch(`${config.baseUrl}/api/public/ingestion`, {
141
142
  method: "POST",
142
- redirect: "error",
143
+ redirect: "manual",
143
144
  headers: {
144
145
  "Content-Type": "application/json",
145
146
  "Authorization": `Basic ${credentials}`,
146
147
  },
147
148
  body: JSON.stringify(batch),
148
149
  signal: AbortSignal.timeout(5000),
149
- });
150
+ }).then(rejectRedirectResponse);
150
151
  if (!resp.ok) {
151
152
  // Never read/log the provider response body. Status is sufficient to
152
153
  // correlate an operational delivery failure without importing arbitrary
@@ -12,7 +12,7 @@
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
18
  * OpenAI-safe mode: 33 reviewed business tools, 0 prompts, 0 resources, and no
@@ -24,7 +24,7 @@
24
24
  /** OAuth requirement advertised on every reviewed business tool. */
25
25
  export declare const OPENAI_OAUTH_SECURITY_SCHEMES: readonly [{
26
26
  readonly type: "oauth2";
27
- readonly scopes: readonly ["frihet:workspace.manage"];
27
+ readonly scopes: readonly string[];
28
28
  }];
29
29
  export declare const OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS: ReadonlySet<string>;
30
30
  export declare const OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS: ReadonlySet<string>;
@@ -12,7 +12,7 @@
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
18
  * OpenAI-safe mode: 33 reviewed business tools, 0 prompts, 0 resources, and no
@@ -26,7 +26,13 @@ import { z } from "zod/v4";
26
26
  import { correctToolAnnotations } from "./capability-truth.js";
27
27
  import { MCP_RESOURCE_COUNT } from "./resources/register-all.js";
28
28
  import { SENSITIVE_FIELD_NAMES, deepRedact, redactText } from "./redaction.js";
29
- import { FRIHET_CONNECTOR_SCOPE } from "./openai-review-oauth.js";
29
+ import { buildOpenAIUnauthorizedChallenge, hasExactOpenAIReviewScopes, OPENAI_REVIEW_OAUTH_SCOPES, } from "./openai-review-oauth.js";
30
+ const REVIEWED_INVOICE_EVIDENCE_BOUNDARY = " Returns internal invoice records only. Invoice status and issue date do not prove external submission or delivery. " +
31
+ "Do not call this tool to check Peppol delivery receipts, EN 16931 validation results, or e-invoicing transmission or acceptance status. " +
32
+ "If the request only concerns that unavailable evidence, explain the limitation without invoking Frihet tools. " +
33
+ "/ Devuelve solo registros internos de facturas. El estado y la fecha de emision no prueban la presentacion o entrega externa. " +
34
+ "No invoques esta herramienta para comprobar acuses de entrega Peppol, resultados de validacion EN 16931 ni estados de transmision o aceptacion de facturacion electronica. " +
35
+ "Si la solicitud solo trata de esas evidencias no disponibles, explica la limitacion sin invocar herramientas de Frihet.";
30
36
  const PROFILE = {
31
37
  // -- OpenAI-reviewed core surface ----------------------------------------
32
38
  //
@@ -125,15 +131,29 @@ const PROFILE = {
125
131
  "and regulated filing data are omitted. " +
126
132
  "/ Obtiene un contexto conciso del espacio de trabajo sin identificadores oficiales, " +
127
133
  "direcciones precisas ni datos de declaraciones reguladas.",
134
+ list_invoices: "List all invoices with optional pagination and filters. " +
135
+ "Returns a paginated list sorted by issue date (newest first). " +
136
+ "Supports filtering by status (draft/sent/paid/overdue/cancelled) and date range. " +
137
+ "Example: status='paid', from='2026-01-01', to='2026-03-31', limit=20 " +
138
+ "/ Lista facturas con paginacion y filtros opcionales. " +
139
+ "Soporta filtrado por estado y rango de fechas." +
140
+ REVIEWED_INVOICE_EVIDENCE_BOUNDARY,
128
141
  get_invoice: "Get one invoice by ID with its stored line items, linked-client context, dates, and status. " +
129
142
  "The response contains only stored document fields; a calculated total may be absent. " +
130
- "/ Obtiene una factura por ID con sus campos almacenados; el total calculado puede no estar presente.",
143
+ "/ Obtiene una factura por ID con sus campos almacenados; el total calculado puede no estar presente." +
144
+ REVIEWED_INVOICE_EVIDENCE_BOUNDARY,
145
+ search_invoices: "Search and filter invoices. Supports filtering by status and date range. " +
146
+ "The query parameter searches across client names and invoice content. " +
147
+ "Example: query='Acme', status='paid', from='2026-01-01', to='2026-03-31' " +
148
+ "/ Busca y filtra facturas. Soporta filtrado por estado y rango de fechas. " +
149
+ "El parametro query busca en nombres de clientes y contenido de facturas." +
150
+ REVIEWED_INVOICE_EVIDENCE_BOUNDARY,
131
151
  get_quote: "Get one quote by ID with its stored line items, linked-client context, dates, and status. " +
132
152
  "The response contains only stored document fields; a calculated total may be absent. " +
133
153
  "/ Obtiene un presupuesto por ID con sus campos almacenados; el total calculado puede no estar presente.",
134
154
  create_invoice: "Prepare a draft invoice that reserves a document number, advances the numbering counter, and may create/link a client; it cannot issue, email, or file it. " +
135
155
  "It requires a client name and at least one line item and always forces draft status. It cannot mark paid, cancel, hash, or submit the invoice to an external filing service. " +
136
- "This invoice counts toward the workspace's monthly invoice usage and may emit invoice-creation analytics to PostHog's EU-hosted analytics service. Active owner-configured webhooks may receive the resulting full business events. Frihet may create in-app and Novu notifications for eligible workspace admins or accountants whose preferences allow them. For a referred workspace's first invoice, Frihet may award activation credits to the referring Frihet account. " +
156
+ "This invoice counts toward the workspace's monthly invoice usage and may emit invoice-creation analytics to PostHog's analytics service. Active owner-configured webhooks may receive the resulting full business events. Frihet may create in-app and Novu notifications for eligible workspace admins or accountants whose preferences allow them. For a referred workspace's first invoice, Frihet may award activation credits to the referring Frihet account. " +
137
157
  "/ Prepara una nueva factura en borrador. Este conector siempre fuerza el estado borrador " +
138
158
  "y reserva un numero interno; puede crear y vincular el cliente si no existe una coincidencia exacta, pero no emite, envia, marca como pagada, cancela, firma ni presenta la factura.",
139
159
  create_expense: "Record an expense on an explicit date; its tax-deductible choice affects Frihet's accounting, files nothing, and a new vendor may persist if the expense fails. " +
@@ -214,6 +234,7 @@ const PROFILE = {
214
234
  "documentNumber",
215
235
  "poNumber",
216
236
  "operationType",
237
+ "fiscalTreatment",
217
238
  ],
218
239
  create_quote: [
219
240
  "clientId",
@@ -281,7 +302,7 @@ const PROFILE = {
281
302
  };
282
303
  /** OAuth requirement advertised on every reviewed business tool. */
283
304
  export const OPENAI_OAUTH_SECURITY_SCHEMES = [
284
- { type: "oauth2", scopes: [FRIHET_CONNECTOR_SCOPE] },
305
+ { type: "oauth2", scopes: [...OPENAI_REVIEW_OAUTH_SCOPES] },
285
306
  ];
286
307
  /**
287
308
  * @modelcontextprotocol/sdk 1.30 accepts unknown registration config fields,
@@ -382,13 +403,12 @@ function installOpenAIToolsListSecurityProjection(server) {
382
403
  || !isRecord(schemes[0])
383
404
  || schemes[0].type !== "oauth2"
384
405
  || !Array.isArray(schemes[0].scopes)
385
- || schemes[0].scopes.length !== 1
386
- || schemes[0].scopes[0] !== FRIHET_CONNECTOR_SCOPE) {
406
+ || !hasExactOpenAIReviewScopes(schemes[0].scopes)) {
387
407
  throw new Error(`OpenAI tool ${String(tool.name)} is missing the reviewed OAuth scope`);
388
408
  }
389
409
  const securitySchemes = [{
390
410
  type: "oauth2",
391
- scopes: [FRIHET_CONNECTOR_SCOPE],
411
+ scopes: [...OPENAI_REVIEW_OAUTH_SCOPES],
392
412
  }];
393
413
  const descriptionSources = schemaDescriptions.get(tool.name);
394
414
  if (!descriptionSources) {
@@ -887,7 +907,7 @@ function customizeReviewedOutputValue(name, value) {
887
907
  }
888
908
  if (name === "create_invoice") {
889
909
  effects.push("The draft counts toward the workspace's monthly invoice usage.");
890
- effects.push("Frihet may have sent invoice-creation usage and activation analytics to PostHog's EU-hosted analytics service.");
910
+ effects.push("Frihet may have sent invoice-creation usage and activation analytics to PostHog's analytics service.");
891
911
  effects.push("Frihet may create in-app and Novu notifications for eligible workspace admins or accountants whose preferences allow them.");
892
912
  effects.push("If this was a referred workspace's first invoice, Frihet may have awarded activation credits to the referring Frihet account.");
893
913
  }
@@ -1064,7 +1084,7 @@ function constrainReviewedDocumentLineItems(schema, name) {
1064
1084
  }
1065
1085
  function reviewedConfirmationDescription(name) {
1066
1086
  if (name === "create_invoice") {
1067
- return "Set true only after the user authorizes creating a numbered draft, advancing the numbering counter, consuming monthly invoice usage, sending invoice-creation analytics to PostHog's EU-hosted analytics service, possibly creating/linking the client, delivering owner-configured webhooks, notifying eligible workspace admins/accountants, and possibly awarding first-use referral credits.";
1087
+ return "Set true only after the user authorizes creating a numbered draft, advancing the numbering counter, consuming monthly invoice usage, sending invoice-creation analytics to PostHog's analytics service, possibly creating/linking the client, delivering owner-configured webhooks, notifying eligible workspace admins/accountants, and possibly awarding first-use referral credits.";
1068
1088
  }
1069
1089
  if (name === "create_quote") {
1070
1090
  return "Set true only after the user authorizes creating a numbered draft, advancing the numbering counter, possibly creating/linking the client, and delivering owner-configured webhooks.";
@@ -1193,7 +1213,7 @@ export function applyOpenAIProfile(server) {
1193
1213
  !config.description.includes("openWorldHint")) {
1194
1214
  const ow = config.annotations?.openWorldHint;
1195
1215
  const openWorldRationale = name === "create_invoice"
1196
- ? "may send invoice analytics to PostHog's EU-hosted analytics service, notify eligible admins/accountants through Novu, award referral credits, and deliver full business events to owner-configured webhook endpoints"
1216
+ ? "may send invoice analytics to PostHog's analytics service, notify eligible admins/accountants through Novu, award referral credits, and deliver full business events to owner-configured webhook endpoints"
1197
1217
  : name === "create_expense"
1198
1218
  ? "may notify eligible admins/accountants through Novu, may award referral credits, and may deliver full business events to owner-configured webhook endpoints"
1199
1219
  : name === "delete_quote"
@@ -1329,6 +1349,12 @@ export function applyOpenAIProfile(server) {
1329
1349
  };
1330
1350
  }
1331
1351
  const result = await handler(handlerInput);
1352
+ if (result.isError === true
1353
+ && result._meta?.["io.frihet/authenticationRequired"] === true) {
1354
+ result._meta = {
1355
+ "mcp/www_authenticate": [buildOpenAIUnauthorizedChallenge()],
1356
+ };
1357
+ }
1332
1358
  // A transport/body failure, a 5xx, or an idempotency-in-progress response
1333
1359
  // after a write was sent is ambiguous: the backend may have committed it
1334
1360
  // even though the caller did not receive a usable success response.
@@ -8,7 +8,7 @@
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
9
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
- export declare const OPENAI_REVIEW_CONTRACT_VERSION = 6;
11
+ export declare const OPENAI_REVIEW_CONTRACT_VERSION = 7;
12
12
  export declare const OPENAI_REVIEW_BUSINESS_TOOL_COUNT = 33;
13
13
  export declare const OPENAI_REVIEW_TOTAL_TOOL_COUNT = 33;
14
14
  export type JsonValue = null | boolean | number | string | JsonValue[] | {
@@ -10,9 +10,9 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { OPENAI_REVIEW_BUSINESS_CONTEXT_TOP_CLIENTS_MAX, OPENAI_REVIEW_DOCUMENT_LINE_ITEM_MAX, OPENAI_REVIEW_FREE_TEXT_WARNING, OPENAI_REVIEW_FREE_TEXT_WARNING_PATHS, OPENAI_REVIEW_LIST_OUTPUT_FIELDS, OPENAI_REVIEW_OFFSET_MAX, OPENAI_REVIEW_PAGINATION_LIMITS, OPENAI_REVIEW_TEXT_INPUT_LIMITS, OPENAI_REVIEW_CONFIRM_REQUIRED_TOOLS, OPENAI_REVIEWED_TOOL_ALLOWLIST, OPENAI_WORKSPACE_WEBHOOK_EVENT_TOOLS, } from "./openai-profile.js";
12
12
  import { SENSITIVE_FIELD_NAMES } from "./redaction.js";
13
- import { FRIHET_CONNECTOR_SCOPE } from "./openai-review-oauth.js";
13
+ import { hasExactOpenAIReviewScopes } from "./openai-review-oauth.js";
14
14
  import { registerMcpSurface, remoteMcpSurfaceComposition, } from "./server-composition.js";
15
- export const OPENAI_REVIEW_CONTRACT_VERSION = 6;
15
+ export const OPENAI_REVIEW_CONTRACT_VERSION = 7;
16
16
  export const OPENAI_REVIEW_BUSINESS_TOOL_COUNT = 33;
17
17
  export const OPENAI_REVIEW_TOTAL_TOOL_COUNT = OPENAI_REVIEW_BUSINESS_TOOL_COUNT;
18
18
  const FORBIDDEN_REVIEW_TOOLS = [
@@ -296,9 +296,8 @@ function assertReviewedOAuthSecuritySchemes(value, path) {
296
296
  || Object.keys(scheme).sort().join(",") !== "scopes,type"
297
297
  || scheme.type !== "oauth2"
298
298
  || !Array.isArray(scheme.scopes)
299
- || scheme.scopes.length !== 1
300
- || scheme.scopes[0] !== FRIHET_CONNECTOR_SCOPE) {
301
- throw new Error(`${path} must require only the reviewed connector OAuth scope`);
299
+ || !hasExactOpenAIReviewScopes(scheme.scopes)) {
300
+ throw new Error(`${path} must require exactly the reviewed OpenAI OAuth scopes`);
302
301
  }
303
302
  }
304
303
  function reviewedInputFieldSchema(tool, path) {
@@ -5,6 +5,37 @@
5
5
  export declare const OPENAI_REVIEW_ORIGIN = "https://openai-mcp.frihet.io";
6
6
  export declare const FULL_MCP_ORIGIN = "https://mcp.frihet.io";
7
7
  export declare const FRIHET_CONNECTOR_SCOPE = "frihet:workspace.manage";
8
+ export declare const OPENAI_REVIEW_OAUTH_SCOPES: readonly string[];
9
+ /**
10
+ * RFC 8707 resource identifier of the reviewed MCP endpoint. Claude's
11
+ * connector guide requires protected-resource metadata to name the connector
12
+ * URL a user enters, including its path; OpenAI documents that ChatGPT takes
13
+ * the `resource` value it sends from that metadata.
14
+ */
15
+ export declare const OPENAI_REVIEW_MCP_RESOURCE = "https://openai-mcp.frihet.io/mcp";
16
+ /** RFC 9728 section 3.1 path-inserted location of the metadata for
17
+ * OPENAI_REVIEW_MCP_RESOURCE. The 401 challenge points here. */
18
+ export declare const OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/mcp";
19
+ /**
20
+ * The only RFC 8707 resource values the reviewed authorization boundary
21
+ * accepts. The canonical MCP URL comes first; the bare origin is the value the
22
+ * root metadata document still publishes (and the only one it published before
23
+ * the path-inserted document existed), so clients using it keep working. Both
24
+ * name this Worker only: the pinned provider matches a token audience by exact
25
+ * origin plus path prefix, so neither value can authorize the full host.
26
+ * The runtime checks that use this set are validateOAuthBoundary (below) and
27
+ * the /token rotation guard in workers/remote-mcp/src/oauth-token-family.ts.
28
+ * The inventory test in workers/remote-mcp/src/__tests__/openai-review-oauth.test.ts
29
+ * lists every runtime line that reads a field named `resource` or `audience`
30
+ * (dot access, string-literal bracket access, or single-line destructuring)
31
+ * and fails on any new such line, whatever it compares or calls. It does not
32
+ * follow a value after it is copied into another variable, multi-line
33
+ * destructuring, or computed property names.
34
+ * Grant props keep `oauthResource: OPENAI_REVIEW_ORIGIN` as the host-binding
35
+ * identity checked at session init and API-key provisioning; that field is not
36
+ * the RFC 8707 value, which the provider stores on the grant itself.
37
+ */
38
+ export declare const OPENAI_REVIEW_OAUTH_RESOURCES: readonly string[];
8
39
  /** RFC 7636 S256 challenges are the 32-byte SHA-256 digest encoded as
9
40
  * unpadded base64url, which is always exactly 43 characters. */
10
41
  export declare function isValidS256CodeChallenge(value: string): boolean;
@@ -53,19 +84,33 @@ interface OAuthBoundaryParameters {
53
84
  * requests may omit it because the provider reuses the validated grant. */
54
85
  requireScope: boolean;
55
86
  }
87
+ export declare function hasExactOpenAIReviewScopes(value: unknown): value is string[];
56
88
  /**
57
89
  * Validate the two OAuth values that form the reviewed-host authorization
58
- * boundary. RFC 8707 resource comparison is exact by design: accepting an
59
- * arbitrary absolute URI would let a token minted by one Frihet Worker target
60
- * the other Worker. Unknown or empty scopes are rejected instead of silently
61
- * downscoping to an empty token.
90
+ * boundary. RFC 8707 resource comparison is exact string membership by design:
91
+ * accepting an arbitrary absolute URI would let a token minted by one Frihet
92
+ * Worker target the other Worker. Unknown or empty scopes are rejected instead
93
+ * of silently downscoping to an empty token.
62
94
  */
63
- export declare function validateOAuthBoundary(parameters: OAuthBoundaryParameters, expectedResource: string): OAuthBoundaryResult;
95
+ export declare function validateOAuthBoundary(parameters: OAuthBoundaryParameters, acceptedResources: readonly string[]): OAuthBoundaryResult;
64
96
  export declare function buildOpenAIUnauthorizedChallenge(origin?: string): string;
97
+ /**
98
+ * Path-inserted protected-resource metadata for the reviewed MCP endpoint,
99
+ * served by the Worker itself (the pinned provider only serves the root
100
+ * document). It differs from the root document only in `resource`.
101
+ */
102
+ export declare function buildReviewedMcpProtectedResourceMetadata(origin?: string): {
103
+ resource: string;
104
+ authorization_servers: string[];
105
+ scopes_supported: string[];
106
+ bearer_methods_supported: string[];
107
+ resource_name: string;
108
+ };
65
109
  /**
66
110
  * Materialize the public metadata generated by workers-oauth-provider 0.3.0
67
- * from the same options used by the Worker. The dependency version is pinned
68
- * separately in the canonical descriptor snapshot.
111
+ * from the same options used by the Worker, plus the path-inserted document
112
+ * the Worker serves itself. The dependency version is pinned separately in
113
+ * the canonical descriptor snapshot.
69
114
  */
70
115
  export declare function buildOpenAIReviewOAuthContract(origin?: string): {
71
116
  authorizationServer: {
@@ -89,10 +134,65 @@ export declare function buildOpenAIReviewOAuthContract(origin?: string): {
89
134
  bearer_methods_supported: string[];
90
135
  resource_name: string;
91
136
  };
137
+ protectedResourceMcp: {
138
+ resource: string;
139
+ authorization_servers: string[];
140
+ scopes_supported: string[];
141
+ bearer_methods_supported: string[];
142
+ resource_name: string;
143
+ };
144
+ openidConfiguration: {
145
+ issuer: string;
146
+ authorization_endpoint: string;
147
+ token_endpoint: string;
148
+ userinfo_endpoint: string;
149
+ registration_endpoint: string;
150
+ scopes_supported: string[];
151
+ response_types_supported: string[];
152
+ response_modes_supported: string[];
153
+ grant_types_supported: string[];
154
+ subject_types_supported: string[];
155
+ token_endpoint_auth_methods_supported: string[];
156
+ code_challenge_methods_supported: string[];
157
+ };
92
158
  wwwAuthenticate: {
93
159
  resourceMetadataUrl: string;
94
160
  missingTokenHeader: string;
95
161
  };
96
162
  };
163
+ interface ReviewedAccessTokenSummary {
164
+ userId?: unknown;
165
+ expiresAt?: unknown;
166
+ audience?: unknown;
167
+ scope?: unknown;
168
+ grant?: {
169
+ scope?: unknown;
170
+ props?: unknown;
171
+ };
172
+ }
173
+ export declare function isVerifiedOpenAIIdentity(value: {
174
+ uid?: unknown;
175
+ email?: unknown;
176
+ emailVerified?: unknown;
177
+ }): value is {
178
+ uid: string;
179
+ email: string;
180
+ emailVerified: true;
181
+ };
182
+ export type OpenAIUserInfo = {
183
+ sub: string;
184
+ email: string;
185
+ email_verified: true;
186
+ };
187
+ /**
188
+ * Validate the provider's opaque access-token summary before serving OIDC
189
+ * UserInfo. The provider already verifies the encrypted token and KV expiry;
190
+ * this second boundary deliberately rechecks every OpenAI-specific binding.
191
+ */
192
+ export declare function buildOpenAIUserInfo(token: ReviewedAccessTokenSummary | null, nowEpochSeconds?: number, boundary?: Readonly<{
193
+ accessProfile: "openai" | "claude";
194
+ origin: string;
195
+ acceptedResources: readonly string[];
196
+ }>): OpenAIUserInfo | undefined;
97
197
  export {};
98
198
  //# sourceMappingURL=openai-review-oauth.d.ts.map