@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
@@ -3,6 +3,47 @@
3
3
  * freeze gate. Values in this module are public protocol metadata, not secrets.
4
4
  */
5
5
  export declare const OPENAI_REVIEW_ORIGIN = "https://openai-mcp.frihet.io";
6
+ export declare const FULL_MCP_ORIGIN = "https://mcp.frihet.io";
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[];
39
+ /** RFC 7636 S256 challenges are the 32-byte SHA-256 digest encoded as
40
+ * unpadded base64url, which is always exactly 43 characters. */
41
+ export declare function isValidS256CodeChallenge(value: string): boolean;
42
+ /** RFC 7636 code_verifier ABNF: 43–128 unreserved URI characters. */
43
+ export declare function isValidPKCECodeVerifier(value: string): boolean;
44
+ export type FrihetAccessProfile = "openai" | "full";
45
+ /** Fail closed if a Worker deploy omits or mistypes its profile binding. */
46
+ export declare function resolveFrihetAccessProfile(value: string | undefined): FrihetAccessProfile;
6
47
  /**
7
48
  * Byte-compatible extraction of the options previously inlined in the Worker.
8
49
  * index.ts spreads this object into the real OAuthProvider constructor.
@@ -16,11 +57,60 @@ export declare const OAUTH_PROVIDER_REVIEW_OPTIONS: {
16
57
  accessTokenTTL: number;
17
58
  refreshTokenTTL: number;
18
59
  allowPlainPKCE: false;
60
+ resourceMetadata: {
61
+ resource: string;
62
+ authorization_servers: string[];
63
+ scopes_supported: string[];
64
+ bearer_methods_supported: string[];
65
+ resource_name: string;
66
+ };
67
+ };
68
+ export type OAuthBoundaryError = "invalid_target" | "invalid_scope";
69
+ export type OAuthBoundaryResult = {
70
+ ok: true;
71
+ } | {
72
+ ok: false;
73
+ error: OAuthBoundaryError;
74
+ description: string;
75
+ };
76
+ interface OAuthBoundaryParameters {
77
+ resource?: string | string[];
78
+ scope?: string | string[];
79
+ /** The authorization request creates the grant and must carry the canonical
80
+ * resource. Later token/refresh requests may omit it because the provider
81
+ * inherits the already-validated value stored on that grant. */
82
+ requireResource: boolean;
83
+ /** Authorization requests must include the advertised scope. Token refresh
84
+ * requests may omit it because the provider reuses the validated grant. */
85
+ requireScope: boolean;
86
+ }
87
+ export declare function hasExactOpenAIReviewScopes(value: unknown): value is string[];
88
+ /**
89
+ * Validate the two OAuth values that form the reviewed-host authorization
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.
94
+ */
95
+ export declare function validateOAuthBoundary(parameters: OAuthBoundaryParameters, acceptedResources: readonly string[]): OAuthBoundaryResult;
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;
19
108
  };
20
109
  /**
21
110
  * Materialize the public metadata generated by workers-oauth-provider 0.3.0
22
- * from the same options used by the Worker. The dependency version is pinned
23
- * 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.
24
114
  */
25
115
  export declare function buildOpenAIReviewOAuthContract(origin?: string): {
26
116
  authorizationServer: {
@@ -42,10 +132,67 @@ export declare function buildOpenAIReviewOAuthContract(origin?: string): {
42
132
  authorization_servers: string[];
43
133
  scopes_supported: string[];
44
134
  bearer_methods_supported: string[];
135
+ resource_name: string;
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[];
45
157
  };
46
158
  wwwAuthenticate: {
47
159
  resourceMetadataUrl: string;
48
160
  missingTokenHeader: string;
49
161
  };
50
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;
197
+ export {};
51
198
  //# sourceMappingURL=openai-review-oauth.d.ts.map
@@ -3,6 +3,63 @@
3
3
  * freeze gate. Values in this module are public protocol metadata, not secrets.
4
4
  */
5
5
  export const OPENAI_REVIEW_ORIGIN = "https://openai-mcp.frihet.io";
6
+ export const FULL_MCP_ORIGIN = "https://mcp.frihet.io";
7
+ export const FRIHET_CONNECTOR_SCOPE = "frihet:workspace.manage";
8
+ export const OPENAI_REVIEW_OAUTH_SCOPES = Object.freeze([
9
+ "openid",
10
+ "email",
11
+ FRIHET_CONNECTOR_SCOPE,
12
+ ]);
13
+ /**
14
+ * RFC 8707 resource identifier of the reviewed MCP endpoint. Claude's
15
+ * connector guide requires protected-resource metadata to name the connector
16
+ * URL a user enters, including its path; OpenAI documents that ChatGPT takes
17
+ * the `resource` value it sends from that metadata.
18
+ */
19
+ export const OPENAI_REVIEW_MCP_RESOURCE = `${OPENAI_REVIEW_ORIGIN}/mcp`;
20
+ /** RFC 9728 section 3.1 path-inserted location of the metadata for
21
+ * OPENAI_REVIEW_MCP_RESOURCE. The 401 challenge points here. */
22
+ export const OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/mcp";
23
+ /**
24
+ * The only RFC 8707 resource values the reviewed authorization boundary
25
+ * accepts. The canonical MCP URL comes first; the bare origin is the value the
26
+ * root metadata document still publishes (and the only one it published before
27
+ * the path-inserted document existed), so clients using it keep working. Both
28
+ * name this Worker only: the pinned provider matches a token audience by exact
29
+ * origin plus path prefix, so neither value can authorize the full host.
30
+ * The runtime checks that use this set are validateOAuthBoundary (below) and
31
+ * the /token rotation guard in workers/remote-mcp/src/oauth-token-family.ts.
32
+ * The inventory test in workers/remote-mcp/src/__tests__/openai-review-oauth.test.ts
33
+ * lists every runtime line that reads a field named `resource` or `audience`
34
+ * (dot access, string-literal bracket access, or single-line destructuring)
35
+ * and fails on any new such line, whatever it compares or calls. It does not
36
+ * follow a value after it is copied into another variable, multi-line
37
+ * destructuring, or computed property names.
38
+ * Grant props keep `oauthResource: OPENAI_REVIEW_ORIGIN` as the host-binding
39
+ * identity checked at session init and API-key provisioning; that field is not
40
+ * the RFC 8707 value, which the provider stores on the grant itself.
41
+ */
42
+ export const OPENAI_REVIEW_OAUTH_RESOURCES = Object.freeze([
43
+ OPENAI_REVIEW_MCP_RESOURCE,
44
+ OPENAI_REVIEW_ORIGIN,
45
+ ]);
46
+ /** RFC 7636 S256 challenges are the 32-byte SHA-256 digest encoded as
47
+ * unpadded base64url, which is always exactly 43 characters. */
48
+ export function isValidS256CodeChallenge(value) {
49
+ return /^[A-Za-z0-9_-]{43}$/u.test(value);
50
+ }
51
+ /** RFC 7636 code_verifier ABNF: 43–128 unreserved URI characters. */
52
+ export function isValidPKCECodeVerifier(value) {
53
+ return /^[A-Za-z0-9._~-]{43,128}$/u.test(value);
54
+ }
55
+ /** Fail closed if a Worker deploy omits or mistypes its profile binding. */
56
+ export function resolveFrihetAccessProfile(value) {
57
+ if (value === "true")
58
+ return "openai";
59
+ if (value === "false")
60
+ return "full";
61
+ throw new Error("FRIHET_OPENAI_MODE must be explicitly set to true or false");
62
+ }
6
63
  /**
7
64
  * Byte-compatible extraction of the options previously inlined in the Worker.
8
65
  * index.ts spreads this object into the real OAuthProvider constructor.
@@ -12,25 +69,101 @@ export const OAUTH_PROVIDER_REVIEW_OPTIONS = {
12
69
  authorizeEndpoint: "/authorize",
13
70
  tokenEndpoint: "/token",
14
71
  clientRegistrationEndpoint: "/register",
15
- scopesSupported: ["read", "write"],
72
+ // This provider does not implement per-tool OAuth authorization. Advertise
73
+ // one honest connector-wide scope instead of implying that `read` prevents
74
+ // writes. Where a profile requires confirm=true, that is a client interlock,
75
+ // not an authorization boundary.
76
+ scopesSupported: [...OPENAI_REVIEW_OAUTH_SCOPES],
16
77
  accessTokenTTL: 3600,
17
78
  refreshTokenTTL: 2592000,
18
79
  allowPlainPKCE: false,
80
+ resourceMetadata: {
81
+ resource: OPENAI_REVIEW_ORIGIN,
82
+ authorization_servers: [OPENAI_REVIEW_ORIGIN],
83
+ scopes_supported: [...OPENAI_REVIEW_OAUTH_SCOPES],
84
+ bearer_methods_supported: ["header"],
85
+ resource_name: "Frihet reviewed connector",
86
+ },
19
87
  };
88
+ export function hasExactOpenAIReviewScopes(value) {
89
+ if (!Array.isArray(value) || value.length !== OPENAI_REVIEW_OAUTH_SCOPES.length) {
90
+ return false;
91
+ }
92
+ const scopes = new Set(value);
93
+ return scopes.size === OPENAI_REVIEW_OAUTH_SCOPES.length
94
+ && OPENAI_REVIEW_OAUTH_SCOPES.every((scope) => scopes.has(scope));
95
+ }
96
+ /**
97
+ * Validate the two OAuth values that form the reviewed-host authorization
98
+ * boundary. RFC 8707 resource comparison is exact string membership by design:
99
+ * accepting an arbitrary absolute URI would let a token minted by one Frihet
100
+ * Worker target the other Worker. Unknown or empty scopes are rejected instead
101
+ * of silently downscoping to an empty token.
102
+ */
103
+ export function validateOAuthBoundary(parameters, acceptedResources) {
104
+ if (parameters.resource === undefined && !parameters.requireResource) {
105
+ // The provider will inherit the exact resource stored on the grant.
106
+ }
107
+ else if (typeof parameters.resource !== "string"
108
+ || !acceptedResources.includes(parameters.resource)) {
109
+ return {
110
+ ok: false,
111
+ error: "invalid_target",
112
+ description: "The OAuth resource must exactly match this Frihet MCP server.",
113
+ };
114
+ }
115
+ if (parameters.scope === undefined && !parameters.requireScope)
116
+ return { ok: true };
117
+ const scopes = Array.isArray(parameters.scope)
118
+ ? parameters.scope
119
+ : typeof parameters.scope === "string"
120
+ ? parameters.scope.split(/\s+/u).filter(Boolean)
121
+ : [];
122
+ if (!hasExactOpenAIReviewScopes(scopes)) {
123
+ return {
124
+ ok: false,
125
+ error: "invalid_scope",
126
+ description: `The required OAuth scopes are ${OPENAI_REVIEW_OAUTH_SCOPES.join(" ")}.`,
127
+ };
128
+ }
129
+ return { ok: true };
130
+ }
20
131
  function endpoint(origin, path) {
21
132
  return new URL(path, new URL(origin).origin).toString();
22
133
  }
134
+ export function buildOpenAIUnauthorizedChallenge(origin = OPENAI_REVIEW_ORIGIN) {
135
+ const resourceMetadataUrl = endpoint(new URL(origin).origin, OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH);
136
+ return `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", ` +
137
+ `scope="${OPENAI_REVIEW_OAUTH_SCOPES.join(" ")}", error="invalid_token", ` +
138
+ 'error_description="Missing or invalid access token"';
139
+ }
140
+ /**
141
+ * Path-inserted protected-resource metadata for the reviewed MCP endpoint,
142
+ * served by the Worker itself (the pinned provider only serves the root
143
+ * document). It differs from the root document only in `resource`.
144
+ */
145
+ export function buildReviewedMcpProtectedResourceMetadata(origin = OPENAI_REVIEW_ORIGIN) {
146
+ const normalizedOrigin = new URL(origin).origin;
147
+ return {
148
+ resource: endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.apiRoute),
149
+ authorization_servers: [normalizedOrigin],
150
+ scopes_supported: [...OAUTH_PROVIDER_REVIEW_OPTIONS.scopesSupported],
151
+ bearer_methods_supported: ["header"],
152
+ resource_name: OAUTH_PROVIDER_REVIEW_OPTIONS.resourceMetadata.resource_name,
153
+ };
154
+ }
23
155
  /**
24
156
  * Materialize the public metadata generated by workers-oauth-provider 0.3.0
25
- * from the same options used by the Worker. The dependency version is pinned
26
- * separately in the canonical descriptor snapshot.
157
+ * from the same options used by the Worker, plus the path-inserted document
158
+ * the Worker serves itself. The dependency version is pinned separately in
159
+ * the canonical descriptor snapshot.
27
160
  */
28
161
  export function buildOpenAIReviewOAuthContract(origin = OPENAI_REVIEW_ORIGIN) {
29
162
  const normalizedOrigin = new URL(origin).origin;
30
163
  const authorizationEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.authorizeEndpoint);
31
164
  const tokenEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.tokenEndpoint);
32
165
  const registrationEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.clientRegistrationEndpoint);
33
- const resourceMetadataUrl = endpoint(normalizedOrigin, "/.well-known/oauth-protected-resource");
166
+ const resourceMetadataUrl = endpoint(normalizedOrigin, OPENAI_REVIEW_MCP_RESOURCE_METADATA_PATH);
34
167
  return {
35
168
  authorizationServer: {
36
169
  issuer: normalizedOrigin,
@@ -55,12 +188,93 @@ export function buildOpenAIReviewOAuthContract(origin = OPENAI_REVIEW_ORIGIN) {
55
188
  authorization_servers: [normalizedOrigin],
56
189
  scopes_supported: [...OAUTH_PROVIDER_REVIEW_OPTIONS.scopesSupported],
57
190
  bearer_methods_supported: ["header"],
191
+ resource_name: OAUTH_PROVIDER_REVIEW_OPTIONS.resourceMetadata.resource_name,
192
+ },
193
+ protectedResourceMcp: buildReviewedMcpProtectedResourceMetadata(normalizedOrigin),
194
+ openidConfiguration: {
195
+ issuer: normalizedOrigin,
196
+ authorization_endpoint: authorizationEndpoint,
197
+ token_endpoint: tokenEndpoint,
198
+ userinfo_endpoint: endpoint(normalizedOrigin, "/userinfo"),
199
+ registration_endpoint: registrationEndpoint,
200
+ scopes_supported: [...OPENAI_REVIEW_OAUTH_SCOPES],
201
+ response_types_supported: ["code"],
202
+ response_modes_supported: ["query"],
203
+ grant_types_supported: ["authorization_code", "refresh_token"],
204
+ subject_types_supported: ["public"],
205
+ token_endpoint_auth_methods_supported: [
206
+ "client_secret_basic",
207
+ "client_secret_post",
208
+ "none",
209
+ ],
210
+ code_challenge_methods_supported: ["S256"],
58
211
  },
59
212
  wwwAuthenticate: {
60
213
  resourceMetadataUrl,
61
- missingTokenHeader: `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", ` +
62
- 'error="invalid_token", error_description="Missing or invalid access token"',
214
+ missingTokenHeader: buildOpenAIUnauthorizedChallenge(normalizedOrigin),
63
215
  },
64
216
  };
65
217
  }
218
+ function isRecord(value) {
219
+ return value !== null && typeof value === "object" && !Array.isArray(value);
220
+ }
221
+ function isVerifiedEmail(email, verified) {
222
+ return verified === true
223
+ && typeof email === "string"
224
+ && email.length >= 3
225
+ && email.length <= 320
226
+ && email === email.trim()
227
+ && !/[\u0000-\u001f\u007f]/u.test(email)
228
+ && /^[^@\s]+@[^@\s]+$/u.test(email);
229
+ }
230
+ export function isVerifiedOpenAIIdentity(value) {
231
+ return typeof value.uid === "string"
232
+ && value.uid.length > 0
233
+ && value.uid.length <= 128
234
+ && isVerifiedEmail(value.email, value.emailVerified);
235
+ }
236
+ /**
237
+ * Validate the provider's opaque access-token summary before serving OIDC
238
+ * UserInfo. The provider already verifies the encrypted token and KV expiry;
239
+ * this second boundary deliberately rechecks every OpenAI-specific binding.
240
+ */
241
+ export function buildOpenAIUserInfo(token, nowEpochSeconds = Math.floor(Date.now() / 1000), boundary = {
242
+ accessProfile: "openai",
243
+ origin: OPENAI_REVIEW_ORIGIN,
244
+ acceptedResources: OPENAI_REVIEW_OAUTH_RESOURCES,
245
+ }) {
246
+ const audience = token?.audience;
247
+ if (!token
248
+ || !Number.isSafeInteger(token.expiresAt)
249
+ || token.expiresAt <= nowEpochSeconds
250
+ || typeof audience !== "string"
251
+ || !boundary.acceptedResources.includes(audience)
252
+ || !hasExactOpenAIReviewScopes(token.scope)
253
+ || !hasExactOpenAIReviewScopes(token.grant?.scope)
254
+ || !isRecord(token.grant?.props)) {
255
+ return undefined;
256
+ }
257
+ const props = token.grant.props;
258
+ const identity = {
259
+ uid: props.userId,
260
+ email: props.email,
261
+ emailVerified: props.emailVerified,
262
+ };
263
+ if (props.accessProfile !== boundary.accessProfile
264
+ || props.authMethod !== "oauth"
265
+ || props.oauthIssuer !== boundary.origin
266
+ || props.oauthResource !== boundary.origin
267
+ || props.oauthAudience !== audience
268
+ || props.oauthScope !== FRIHET_CONNECTOR_SCOPE
269
+ || !hasExactOpenAIReviewScopes(props.oauthScopes)
270
+ || token.userId !== props.userId
271
+ || !isVerifiedOpenAIIdentity(identity)) {
272
+ return undefined;
273
+ }
274
+ return {
275
+ sub: identity.uid,
276
+ email: identity.email,
277
+ email_verified: true,
278
+ };
279
+ }
66
280
  //# sourceMappingURL=openai-review-oauth.js.map
@@ -6,5 +6,7 @@
6
6
  * using the available Frihet tools to complete each action.
7
7
  */
8
8
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ /** Exact prompt registrations below; runtime capture gates this value. */
10
+ export declare const MCP_PROMPT_COUNT = 10;
9
11
  export declare function registerAllPrompts(server: McpServer): void;
10
12
  //# sourceMappingURL=register-all.d.ts.map
@@ -6,6 +6,8 @@
6
6
  * using the available Frihet tools to complete each action.
7
7
  */
8
8
  import { z } from "zod/v4";
9
+ /** Exact prompt registrations below; runtime capture gates this value. */
10
+ export const MCP_PROMPT_COUNT = 10;
9
11
  export function registerAllPrompts(server) {
10
12
  // -- monthly-close --
11
13
  server.registerPrompt("monthly-close", {
@@ -184,34 +186,21 @@ export function registerAllPrompts(server) {
184
186
  role: "user",
185
187
  content: {
186
188
  type: "text",
187
- text: `Help me follow up on overdue invoices.\n\n` +
188
- `Follow these steps:\n\n` +
189
- `1. FIND OVERDUE INVOICES\n` +
190
- ` - Use list_invoices to find all invoices with status "overdue" or "sent" past their due date\n` +
191
- ` - For each, note: invoice ID, client name, amount, due date, days overdue\n` +
192
- ` - Sort by days overdue (most overdue first)\n\n` +
193
- `2. GROUP BY CLIENT\n` +
194
- ` - Group overdue invoices by client\n` +
195
- ` - Calculate total outstanding per client\n` +
196
- ` - Use get_client to get contact details for each client\n\n` +
197
- `3. DRAFT FOLLOW-UP MESSAGES\n` +
198
- ` For each client, draft a professional payment reminder:\n` +
199
- ` - Tone: Firm but polite. Maintain the business relationship.\n` +
200
- ` - Include: Invoice number(s), amount(s), original due date(s)\n` +
201
- ` - 1-15 days overdue: Friendly reminder, assume it was overlooked\n` +
202
- ` - 16-30 days overdue: Firmer tone, request confirmation of payment date\n` +
203
- ` - 31-60 days overdue: Escalation notice, mention potential late fees\n` +
204
- ` - 60+ days overdue: Final notice, mention potential debt collection\n` +
205
- ` - Provide both English and Spanish versions\n\n` +
206
- `4. SUGGEST ACTIONS\n` +
207
- ` - Recommend which invoices to update to "overdue" status if still "sent"\n` +
208
- ` - Suggest setting up webhook notifications for future overdue invoices\n` +
209
- ` - Flag any clients with multiple overdue invoices (potential bad debt risk)\n\n` +
210
- `5. SUMMARY\n` +
211
- ` - Total overdue amount\n` +
212
- ` - Number of clients affected\n` +
213
- ` - Oldest unpaid invoice\n` +
214
- ` - Recommended priority order for follow-up`,
189
+ text: `Help me prepare overdue invoice follow-ups for review.
190
+
191
+ 1. GATHER RECORDS
192
+ - Call list_invoices without a status filter (it has no partial status) and select sent, partial and overdue records client-side from each returned status. Paginate every result set using the returned pagination fields until exhausted; deduplicate by invoice ID. If a page fails or results are incomplete, disclose that limitation.
193
+ - Use get_invoice when detail is missing. Record invoice ID/number, client, currency, due date and remaining unpaid balance after recorded payments and credits. Do not count full invoice totals as outstanding for partially paid records. If balance cannot be verified, flag it as unknown instead of assuming it.
194
+ - Confirm the as-of date and workspace timezone. Separate not-yet-due and due-today invoices from overdue invoices. Missing or invalid due dates need review, not an invented age. Invoice status alone does not prove delivery.
195
+
196
+ 2. GROUP AND DRAFT
197
+ - Group verified positive overdue balances by client and currency; never sum different currencies or infer conversion rates. Show the source invoices, balances and days overdue.
198
+ - Use get_client to verify contact details. Draft a neutral, professional reminder in English and Spanish, listing invoice numbers, due dates, remaining balances and currencies. Do not assume why payment is late or that a debt is undisputed.
199
+
200
+ 3. USER REVIEW
201
+ - Present drafts, recipients and supporting records for user approval before any actions, including sending messages, changing records or setting reminders.
202
+ - Do not automatically impose late fees, write off balances, stop work, threaten collection, assign collectible probabilities or make business decisions based on age alone. Ask the user to review disputes and their agreed terms.
203
+ - Summarize overdue balances per currency, clients affected, oldest verified overdue invoice and any missing evidence. Keep not-yet-due balances separate.`,
215
204
  },
216
205
  },
217
206
  ],
@@ -458,7 +447,7 @@ export function registerAllPrompts(server) {
458
447
  // -- invoice-aging-review --
459
448
  server.registerPrompt("invoice-aging-review", {
460
449
  title: "Invoice Aging Review",
461
- description: "Accounts receivable aging analysis: group unpaid invoices by aging bucket (0-30, 31-60, 61-90, 90+ days), " +
450
+ description: "Accounts receivable aging analysis: separate not-yet-due balances from overdue buckets by currency, " +
462
451
  "identify top debtors, and suggest collection actions. " +
463
452
  "/ Análisis de antigüedad de cuentas por cobrar: agrupar facturas impagadas por tramos, " +
464
453
  "identificar mayores deudores y sugerir acciones de cobro.",
@@ -468,55 +457,22 @@ export function registerAllPrompts(server) {
468
457
  role: "user",
469
458
  content: {
470
459
  type: "text",
471
- text: `Perform an accounts receivable aging analysis. Follow these steps:\n\n` +
472
- `1. GATHER UNPAID INVOICES\n` +
473
- ` - Use list_invoices to find all invoices with status "sent" or "overdue"\n` +
474
- ` - For each invoice, note: invoice ID/number, client name, amount (with tax),\n` +
475
- ` issue date, due date, and days since due date\n` +
476
- ` - If due date is in the future, calculate days until due (negative = not yet due)\n\n` +
477
- `2. AGING BUCKETS\n` +
478
- ` Group all unpaid invoices into these buckets:\n` +
479
- ` - CURRENT (0-30 days): Not yet due or less than 30 days overdue\n` +
480
- ` - 31-60 DAYS: Between 31 and 60 days overdue\n` +
481
- ` - 61-90 DAYS: Between 61 and 90 days overdue\n` +
482
- ` - 90+ DAYS: More than 90 days overdue (high risk)\n` +
483
- ` For each bucket, show:\n` +
484
- ` - Number of invoices\n` +
485
- ` - Total amount\n` +
486
- ` - Percentage of total outstanding receivables\n` +
487
- ` - List of invoices in that bucket\n\n` +
488
- `3. TOP DEBTORS\n` +
489
- ` - Use get_client for each client with unpaid invoices to get their details\n` +
490
- ` - Rank clients by total outstanding amount (highest first)\n` +
491
- ` - For each top debtor, show:\n` +
492
- ` - Client name and contact info\n` +
493
- ` - Number of unpaid invoices\n` +
494
- ` - Total outstanding amount\n` +
495
- ` - Oldest unpaid invoice date\n` +
496
- ` - Average days overdue\n\n` +
497
- `4. COLLECTION ACTIONS\n` +
498
- ` Suggest specific actions for each bucket:\n` +
499
- ` - CURRENT (0-30 days): No action needed, monitor normally\n` +
500
- ` - 31-60 DAYS: Send a friendly payment reminder (use overdue-followup prompt pattern)\n` +
501
- ` - 61-90 DAYS: Escalate — phone call + formal written reminder,\n` +
502
- ` consider pausing new work for this client\n` +
503
- ` - 90+ DAYS: Final notice before debt collection,\n` +
504
- ` evaluate write-off vs collection agency cost,\n` +
505
- ` stop all new work for this client\n\n` +
506
- `5. WRITE-OFF CANDIDATES\n` +
507
- ` - Flag any invoices 90+ days overdue with amounts under €100 — likely not worth pursuing\n` +
508
- ` - For larger amounts 90+ days, suggest formal demand letter before write-off\n` +
509
- ` - Note: written-off invoices may still be tax-deductible as bad debt\n` +
510
- ` (requires documentation of collection attempts)\n\n` +
511
- `6. SUMMARY DASHBOARD\n` +
512
- ` Present a clear overview:\n` +
513
- ` - Total accounts receivable (all unpaid invoices)\n` +
514
- ` - Breakdown by aging bucket (amount and % of total)\n` +
515
- ` - Weighted collection estimate:\n` +
516
- ` Current: 95% collectible, 31-60: 80%, 61-90: 50%, 90+: 20%\n` +
517
- ` - Expected collectible amount vs total outstanding\n` +
518
- ` - Number of clients with overdue invoices\n` +
519
- ` - Most urgent actions to take (prioritized list)`,
460
+ text: `Prepare an accounts receivable aging review.
461
+
462
+ 1. VERIFY THE DATA
463
+ - Call list_invoices without a status filter (it has no partial status) and select sent, partial and overdue records client-side from each returned status. Paginate every result set using the returned pagination fields until exhausted; deduplicate by invoice ID. Report any failed page or incomplete coverage.
464
+ - Use get_invoice when needed to verify currency, due date and remaining unpaid balance after recorded payments and credits. Do not count full invoice totals as outstanding for partially paid invoices. Mark unverifiable balances as unknown; do not invent totals.
465
+ - Confirm the as-of date and workspace timezone. Exclude settled, draft and cancelled records from receivables. Status alone does not establish delivery or an undisputed debt.
466
+
467
+ 2. AGE VERIFIED BALANCES
468
+ - Keep separate buckets: not-yet-due; due today; 1-30 days overdue; 31-60 days overdue; 61-90 days overdue; more than 90 days overdue. Missing or invalid due dates form an unknown-age bucket.
469
+ - Group by currency. For each bucket, show invoice IDs, count, remaining unpaid balance and percentage of that currency's verified receivables (only when the denominator is positive). Never combine currencies without a user-supplied conversion basis.
470
+ - Use get_client for client details and summarize positive balances by client within each currency, showing oldest due dates and missing evidence.
471
+
472
+ 3. REVIEW BEFORE ACTING
473
+ - Present the per-currency report and possible follow-up drafts using overdue-followup for user approval before any actions. A report is not permission to send, change status or create charges.
474
+ - Do not assign arbitrary collectible probabilities, automatically write off balances, impose late fees, stop work or make business decisions from age or amount alone. Ask for the user's terms, dispute context and decision before proposing an action.
475
+ - Keep not-yet-due amounts separate from overdue exposure and disclose unknown balances and incomplete pagination.`,
520
476
  },
521
477
  },
522
478
  ],
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Generated public-surface contract captured through the real MCP SDK.
3
+ *
4
+ * This is the mechanical source for catalogue, alias, discovery, resource, and
5
+ * prompt truth across the npm/full, remote/grouped, and OpenAI/full profiles.
6
+ */
7
+ import { type PublicCapabilityTruth } from "./capability-truth.js";
8
+ export declare const PUBLIC_CAPABILITY_CONTRACT_VERSION = 2;
9
+ export interface CompactPublicTool {
10
+ name: string;
11
+ annotations?: Record<string, boolean>;
12
+ capability?: PublicCapabilityTruth;
13
+ }
14
+ export interface CompactPublicSurface {
15
+ tools: CompactPublicTool[];
16
+ resources: string[];
17
+ prompts: string[];
18
+ discovery?: {
19
+ searchTools: CompactPublicTool;
20
+ describeTool: CompactPublicTool;
21
+ };
22
+ }
23
+ export interface PublicCapabilityContract {
24
+ contractVersion: number;
25
+ catalogue: {
26
+ canonicalOperations: number;
27
+ aliasNames: number;
28
+ discoveryNames: number;
29
+ };
30
+ surfaces: {
31
+ localFull: CompactPublicSurface;
32
+ remoteGrouped: CompactPublicSurface;
33
+ openaiFull: CompactPublicSurface;
34
+ };
35
+ }
36
+ export declare function capturePublicCapabilityContract(): Promise<PublicCapabilityContract>;
37
+ export declare function serializePublicCapabilityContract(contract: PublicCapabilityContract): string;
38
+ export declare function assertPublicCapabilityContract(actual: PublicCapabilityContract, expected: PublicCapabilityContract): void;
39
+ //# sourceMappingURL=public-capability-contract.d.ts.map