@frihet/mcp-server 1.16.6 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +92 -31
  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 +549 -0
  8. package/dist/capability-truth.d.ts +29 -0
  9. package/dist/capability-truth.js +254 -0
  10. package/dist/client-interface.d.ts +22 -14
  11. package/dist/client.d.ts +114 -18
  12. package/dist/client.js +488 -56
  13. package/dist/demo-client.d.ts +16 -13
  14. package/dist/demo-client.js +171 -17
  15. package/dist/demo-fixtures.d.ts +3 -3
  16. package/dist/demo-fixtures.js +7 -7
  17. package/dist/fiscal-aliases.d.ts +44 -0
  18. package/dist/fiscal-aliases.js +62 -0
  19. package/dist/index.js +41 -56
  20. package/dist/logger.d.ts +2 -2
  21. package/dist/logger.js +140 -8
  22. package/dist/observability.d.ts +18 -29
  23. package/dist/observability.js +192 -105
  24. package/dist/openai-profile.d.ts +25 -21
  25. package/dist/openai-profile.js +1166 -156
  26. package/dist/openai-review-contract.d.ts +15 -6
  27. package/dist/openai-review-contract.js +715 -41
  28. package/dist/openai-review-oauth.d.ts +47 -0
  29. package/dist/openai-review-oauth.js +73 -3
  30. package/dist/prompts/register-all.d.ts +2 -0
  31. package/dist/prompts/register-all.js +2 -0
  32. package/dist/public-capability-contract.d.ts +39 -0
  33. package/dist/public-capability-contract.js +186 -0
  34. package/dist/redaction.d.ts +10 -0
  35. package/dist/redaction.js +74 -0
  36. package/dist/resources/register-all.js +6 -12
  37. package/dist/server-composition.d.ts +16 -0
  38. package/dist/server-composition.js +44 -0
  39. package/dist/tool-exposure.d.ts +25 -22
  40. package/dist/tool-exposure.js +199 -77
  41. package/dist/tools/accountingClose.d.ts +14 -5
  42. package/dist/tools/accountingClose.js +41 -23
  43. package/dist/tools/backend-availability.d.ts +3 -3
  44. package/dist/tools/backend-availability.js +3 -3
  45. package/dist/tools/clients.js +9 -9
  46. package/dist/tools/crm.js +22 -21
  47. package/dist/tools/deposits.js +30 -11
  48. package/dist/tools/einvoice.d.ts +1 -1
  49. package/dist/tools/expenses.d.ts +16 -0
  50. package/dist/tools/expenses.js +47 -27
  51. package/dist/tools/hr.d.ts +2 -3
  52. package/dist/tools/hr.js +8 -9
  53. package/dist/tools/intelligence.js +2 -2
  54. package/dist/tools/invoices.js +150 -47
  55. package/dist/tools/payroll.d.ts +5 -8
  56. package/dist/tools/payroll.js +19 -32
  57. package/dist/tools/permissions.d.ts +4 -7
  58. package/dist/tools/permissions.js +11 -14
  59. package/dist/tools/products.js +6 -6
  60. package/dist/tools/quotes.js +109 -24
  61. package/dist/tools/shared.d.ts +1431 -75
  62. package/dist/tools/shared.js +386 -76
  63. package/dist/tools/team.js +18 -14
  64. package/dist/tools/vendors.js +9 -9
  65. package/dist/tools/webhooks.js +35 -30
  66. package/dist/types.d.ts +28 -4
  67. package/docs/agent-onboarding.json +381 -0
  68. package/package.json +30 -7
@@ -3,6 +3,16 @@
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
+ /** RFC 7636 S256 challenges are the 32-byte SHA-256 digest encoded as
9
+ * unpadded base64url, which is always exactly 43 characters. */
10
+ export declare function isValidS256CodeChallenge(value: string): boolean;
11
+ /** RFC 7636 code_verifier ABNF: 43–128 unreserved URI characters. */
12
+ export declare function isValidPKCECodeVerifier(value: string): boolean;
13
+ export type FrihetAccessProfile = "openai" | "full";
14
+ /** Fail closed if a Worker deploy omits or mistypes its profile binding. */
15
+ export declare function resolveFrihetAccessProfile(value: string | undefined): FrihetAccessProfile;
6
16
  /**
7
17
  * Byte-compatible extraction of the options previously inlined in the Worker.
8
18
  * index.ts spreads this object into the real OAuthProvider constructor.
@@ -16,7 +26,42 @@ export declare const OAUTH_PROVIDER_REVIEW_OPTIONS: {
16
26
  accessTokenTTL: number;
17
27
  refreshTokenTTL: number;
18
28
  allowPlainPKCE: false;
29
+ resourceMetadata: {
30
+ resource: string;
31
+ authorization_servers: string[];
32
+ scopes_supported: string[];
33
+ bearer_methods_supported: string[];
34
+ resource_name: string;
35
+ };
36
+ };
37
+ export type OAuthBoundaryError = "invalid_target" | "invalid_scope";
38
+ export type OAuthBoundaryResult = {
39
+ ok: true;
40
+ } | {
41
+ ok: false;
42
+ error: OAuthBoundaryError;
43
+ description: string;
19
44
  };
45
+ interface OAuthBoundaryParameters {
46
+ resource?: string | string[];
47
+ scope?: string | string[];
48
+ /** The authorization request creates the grant and must carry the canonical
49
+ * resource. Later token/refresh requests may omit it because the provider
50
+ * inherits the already-validated value stored on that grant. */
51
+ requireResource: boolean;
52
+ /** Authorization requests must include the advertised scope. Token refresh
53
+ * requests may omit it because the provider reuses the validated grant. */
54
+ requireScope: boolean;
55
+ }
56
+ /**
57
+ * 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.
62
+ */
63
+ export declare function validateOAuthBoundary(parameters: OAuthBoundaryParameters, expectedResource: string): OAuthBoundaryResult;
64
+ export declare function buildOpenAIUnauthorizedChallenge(origin?: string): string;
20
65
  /**
21
66
  * Materialize the public metadata generated by workers-oauth-provider 0.3.0
22
67
  * from the same options used by the Worker. The dependency version is pinned
@@ -42,10 +87,12 @@ export declare function buildOpenAIReviewOAuthContract(origin?: string): {
42
87
  authorization_servers: string[];
43
88
  scopes_supported: string[];
44
89
  bearer_methods_supported: string[];
90
+ resource_name: string;
45
91
  };
46
92
  wwwAuthenticate: {
47
93
  resourceMetadataUrl: string;
48
94
  missingTokenHeader: string;
49
95
  };
50
96
  };
97
+ export {};
51
98
  //# sourceMappingURL=openai-review-oauth.d.ts.map
@@ -3,6 +3,25 @@
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
+ /** RFC 7636 S256 challenges are the 32-byte SHA-256 digest encoded as
9
+ * unpadded base64url, which is always exactly 43 characters. */
10
+ export function isValidS256CodeChallenge(value) {
11
+ return /^[A-Za-z0-9_-]{43}$/u.test(value);
12
+ }
13
+ /** RFC 7636 code_verifier ABNF: 43–128 unreserved URI characters. */
14
+ export function isValidPKCECodeVerifier(value) {
15
+ return /^[A-Za-z0-9._~-]{43,128}$/u.test(value);
16
+ }
17
+ /** Fail closed if a Worker deploy omits or mistypes its profile binding. */
18
+ export function resolveFrihetAccessProfile(value) {
19
+ if (value === "true")
20
+ return "openai";
21
+ if (value === "false")
22
+ return "full";
23
+ throw new Error("FRIHET_OPENAI_MODE must be explicitly set to true or false");
24
+ }
6
25
  /**
7
26
  * Byte-compatible extraction of the options previously inlined in the Worker.
8
27
  * index.ts spreads this object into the real OAuthProvider constructor.
@@ -12,14 +31,65 @@ export const OAUTH_PROVIDER_REVIEW_OPTIONS = {
12
31
  authorizeEndpoint: "/authorize",
13
32
  tokenEndpoint: "/token",
14
33
  clientRegistrationEndpoint: "/register",
15
- scopesSupported: ["read", "write"],
34
+ // This provider does not implement per-tool OAuth authorization. Advertise
35
+ // one honest connector-wide scope instead of implying that `read` prevents
36
+ // writes. Every mutating reviewed tool still requires confirm=true.
37
+ scopesSupported: [FRIHET_CONNECTOR_SCOPE],
16
38
  accessTokenTTL: 3600,
17
39
  refreshTokenTTL: 2592000,
18
40
  allowPlainPKCE: false,
41
+ resourceMetadata: {
42
+ resource: OPENAI_REVIEW_ORIGIN,
43
+ authorization_servers: [OPENAI_REVIEW_ORIGIN],
44
+ scopes_supported: [FRIHET_CONNECTOR_SCOPE],
45
+ bearer_methods_supported: ["header"],
46
+ resource_name: "Frihet ChatGPT connector",
47
+ },
19
48
  };
49
+ /**
50
+ * Validate the two OAuth values that form the reviewed-host authorization
51
+ * boundary. RFC 8707 resource comparison is exact by design: accepting an
52
+ * arbitrary absolute URI would let a token minted by one Frihet Worker target
53
+ * the other Worker. Unknown or empty scopes are rejected instead of silently
54
+ * downscoping to an empty token.
55
+ */
56
+ export function validateOAuthBoundary(parameters, expectedResource) {
57
+ if (parameters.resource === undefined && !parameters.requireResource) {
58
+ // The provider will inherit the exact resource stored on the grant.
59
+ }
60
+ else if (typeof parameters.resource !== "string"
61
+ || parameters.resource !== expectedResource) {
62
+ return {
63
+ ok: false,
64
+ error: "invalid_target",
65
+ description: "The OAuth resource must exactly match this Frihet MCP server.",
66
+ };
67
+ }
68
+ if (parameters.scope === undefined && !parameters.requireScope)
69
+ return { ok: true };
70
+ const scopes = Array.isArray(parameters.scope)
71
+ ? parameters.scope
72
+ : typeof parameters.scope === "string"
73
+ ? parameters.scope.split(/\s+/u).filter(Boolean)
74
+ : [];
75
+ if (scopes.length !== 1 || scopes[0] !== FRIHET_CONNECTOR_SCOPE) {
76
+ return {
77
+ ok: false,
78
+ error: "invalid_scope",
79
+ description: `The only supported OAuth scope is ${FRIHET_CONNECTOR_SCOPE}.`,
80
+ };
81
+ }
82
+ return { ok: true };
83
+ }
20
84
  function endpoint(origin, path) {
21
85
  return new URL(path, new URL(origin).origin).toString();
22
86
  }
87
+ export function buildOpenAIUnauthorizedChallenge(origin = OPENAI_REVIEW_ORIGIN) {
88
+ const resourceMetadataUrl = endpoint(new URL(origin).origin, "/.well-known/oauth-protected-resource");
89
+ return `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", ` +
90
+ `scope="${FRIHET_CONNECTOR_SCOPE}", error="invalid_token", ` +
91
+ 'error_description="Missing or invalid access token"';
92
+ }
23
93
  /**
24
94
  * Materialize the public metadata generated by workers-oauth-provider 0.3.0
25
95
  * from the same options used by the Worker. The dependency version is pinned
@@ -55,11 +125,11 @@ export function buildOpenAIReviewOAuthContract(origin = OPENAI_REVIEW_ORIGIN) {
55
125
  authorization_servers: [normalizedOrigin],
56
126
  scopes_supported: [...OAUTH_PROVIDER_REVIEW_OPTIONS.scopesSupported],
57
127
  bearer_methods_supported: ["header"],
128
+ resource_name: OAUTH_PROVIDER_REVIEW_OPTIONS.resourceMetadata.resource_name,
58
129
  },
59
130
  wwwAuthenticate: {
60
131
  resourceMetadataUrl,
61
- missingTokenHeader: `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", ` +
62
- 'error="invalid_token", error_description="Missing or invalid access token"',
132
+ missingTokenHeader: buildOpenAIUnauthorizedChallenge(normalizedOrigin),
63
133
  },
64
134
  };
65
135
  }
@@ -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", {
@@ -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
@@ -0,0 +1,186 @@
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 { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { CAPABILITY_META_KEY, } from "./capability-truth.js";
11
+ import { FISCAL_MODELO_ALIASES } from "./fiscal-aliases.js";
12
+ import { MCP_PROMPT_COUNT } from "./prompts/register-all.js";
13
+ import { localMcpSurfaceComposition, registerMcpSurface, remoteMcpSurfaceComposition, } from "./server-composition.js";
14
+ import { GROUPED_META_TOOL_COUNT } from "./tool-exposure.js";
15
+ export const PUBLIC_CAPABILITY_CONTRACT_VERSION = 2;
16
+ function makeRegistrationClient() {
17
+ return new Proxy({}, {
18
+ get: () => async () => ({ data: [], total: 0, limit: 0, offset: 0 }),
19
+ });
20
+ }
21
+ function isMethodNotFound(error) {
22
+ return (typeof error === "object" &&
23
+ error !== null &&
24
+ "code" in error &&
25
+ error.code === -32601);
26
+ }
27
+ async function listPromptsOrEmpty(client) {
28
+ try {
29
+ const result = await client.listPrompts();
30
+ return result.prompts.map((prompt) => prompt.name).sort();
31
+ }
32
+ catch (error) {
33
+ if (isMethodNotFound(error))
34
+ return [];
35
+ throw error;
36
+ }
37
+ }
38
+ async function listResourcesOrEmpty(client) {
39
+ try {
40
+ const result = await client.listResources();
41
+ return result.resources.map((resource) => resource.uri).sort();
42
+ }
43
+ catch (error) {
44
+ if (isMethodNotFound(error))
45
+ return [];
46
+ throw error;
47
+ }
48
+ }
49
+ function compactDiscoveryTool(value) {
50
+ if (typeof value !== "object" || value === null) {
51
+ throw new Error("Grouped discovery returned a non-object tool result");
52
+ }
53
+ const record = value;
54
+ if (typeof record.name !== "string") {
55
+ throw new Error("Grouped discovery result omitted the tool name");
56
+ }
57
+ return {
58
+ name: record.name,
59
+ ...(record.annotations && typeof record.annotations === "object"
60
+ ? { annotations: record.annotations }
61
+ : {}),
62
+ ...(record.capability && typeof record.capability === "object"
63
+ ? { capability: record.capability }
64
+ : {}),
65
+ };
66
+ }
67
+ async function captureDiscoveryTruth(client) {
68
+ const searched = await client.callTool({
69
+ name: "search_tools",
70
+ arguments: { query: "send_invoice", limit: 1 },
71
+ });
72
+ const searchTools = searched.structuredContent?.tools;
73
+ if (!Array.isArray(searchTools) || searchTools.length !== 1) {
74
+ throw new Error("search_tools did not return the pinned send_invoice result");
75
+ }
76
+ const described = await client.callTool({
77
+ name: "describe_tool",
78
+ arguments: { name: "send_invoice" },
79
+ });
80
+ return {
81
+ searchTools: compactDiscoveryTool(searchTools[0]),
82
+ describeTool: compactDiscoveryTool(described.structuredContent),
83
+ };
84
+ }
85
+ async function captureSurface(configure, captureDiscovery = false) {
86
+ const server = new McpServer({ name: "public-capability-capture", version: "1.0.0" });
87
+ configure(server, makeRegistrationClient());
88
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
89
+ const client = new Client({ name: "public-capability-capture-client", version: "1.0.0" }, { capabilities: {} });
90
+ try {
91
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
92
+ const listed = await client.listTools();
93
+ const tools = listed.tools
94
+ .map((tool) => {
95
+ const meta = tool._meta;
96
+ const capability = meta?.[CAPABILITY_META_KEY];
97
+ return {
98
+ name: tool.name,
99
+ ...(tool.annotations
100
+ ? { annotations: tool.annotations }
101
+ : {}),
102
+ ...(capability ? { capability } : {}),
103
+ };
104
+ })
105
+ .sort((left, right) => left.name.localeCompare(right.name));
106
+ const [resources, prompts] = await Promise.all([
107
+ listResourcesOrEmpty(client),
108
+ listPromptsOrEmpty(client),
109
+ ]);
110
+ const discovery = captureDiscovery
111
+ ? await captureDiscoveryTruth(client)
112
+ : undefined;
113
+ return { tools, resources, prompts, ...(discovery ? { discovery } : {}) };
114
+ }
115
+ finally {
116
+ await Promise.allSettled([client.close(), server.close()]);
117
+ }
118
+ }
119
+ export async function capturePublicCapabilityContract() {
120
+ const localFull = await captureSurface((server, client) => {
121
+ registerMcpSurface(server, client, localMcpSurfaceComposition(false, false));
122
+ });
123
+ const remoteGrouped = await captureSurface((server, client) => {
124
+ registerMcpSurface(server, client, remoteMcpSurfaceComposition(false, true));
125
+ }, true);
126
+ const openaiFull = await captureSurface((server, client) => {
127
+ registerMcpSurface(server, client, remoteMcpSurfaceComposition(true, false));
128
+ });
129
+ const aliases = new Set(Object.keys(FISCAL_MODELO_ALIASES));
130
+ const discovery = new Set(["list_tool_groups", "search_tools", "describe_tool"]);
131
+ const canonicalOperations = remoteGrouped.tools.filter((tool) => !aliases.has(tool.name) && !discovery.has(tool.name)).length;
132
+ if (localFull.prompts.length !== MCP_PROMPT_COUNT ||
133
+ remoteGrouped.prompts.length !== MCP_PROMPT_COUNT) {
134
+ throw new Error(`Prompt count drift: registered ${localFull.prompts.length}/${remoteGrouped.prompts.length}, expected ${MCP_PROMPT_COUNT}`);
135
+ }
136
+ return {
137
+ contractVersion: PUBLIC_CAPABILITY_CONTRACT_VERSION,
138
+ catalogue: {
139
+ canonicalOperations,
140
+ aliasNames: aliases.size,
141
+ discoveryNames: GROUPED_META_TOOL_COUNT,
142
+ },
143
+ surfaces: { localFull, remoteGrouped, openaiFull },
144
+ };
145
+ }
146
+ function canonicalize(contract) {
147
+ const clone = structuredClone(contract);
148
+ for (const surface of Object.values(clone.surfaces)) {
149
+ surface.tools.sort((left, right) => left.name.localeCompare(right.name));
150
+ surface.resources.sort();
151
+ surface.prompts.sort();
152
+ }
153
+ return clone;
154
+ }
155
+ export function serializePublicCapabilityContract(contract) {
156
+ return `${JSON.stringify(canonicalize(contract), null, 2)}\n`;
157
+ }
158
+ export function assertPublicCapabilityContract(actual, expected) {
159
+ if (actual.contractVersion !== PUBLIC_CAPABILITY_CONTRACT_VERSION) {
160
+ throw new Error(`Unsupported public capability contract ${actual.contractVersion}`);
161
+ }
162
+ const fullSurfaces = [actual.surfaces.localFull, actual.surfaces.remoteGrouped];
163
+ for (const surface of fullSurfaces) {
164
+ for (const tool of surface.tools) {
165
+ assertFullToolTruth(tool);
166
+ }
167
+ }
168
+ const serializedActual = serializePublicCapabilityContract(actual);
169
+ const serializedExpected = serializePublicCapabilityContract(expected);
170
+ if (serializedActual !== serializedExpected) {
171
+ throw new Error("Public capability contract drift; regenerate and review the runtime capture");
172
+ }
173
+ }
174
+ function assertFullToolTruth(tool) {
175
+ if (!tool.capability) {
176
+ throw new Error(`Full-surface tool ${tool.name} lacks ${CAPABILITY_META_KEY}`);
177
+ }
178
+ if (tool.capability.callability === "available") {
179
+ throw new Error(`Tool ${tool.name} conflates registration with availability`);
180
+ }
181
+ if (tool.capability.externalInteraction &&
182
+ tool.annotations?.openWorldHint !== true) {
183
+ throw new Error(`External tool ${tool.name} must set openWorldHint=true`);
184
+ }
185
+ }
186
+ //# sourceMappingURL=public-capability-contract.js.map
@@ -17,6 +17,16 @@
17
17
  */
18
18
  /** Sentinel left in place of a redacted value in cloned (tracing) payloads. */
19
19
  export declare const REDACTED = "[redacted]";
20
+ /**
21
+ * Allowlist short, single-line human remediation prose for agent display.
22
+ *
23
+ * Server text is omitted unless it matches one of two explicit semantics: an
24
+ * API-key scope instruction matching the ERP contract, or a conservative
25
+ * workspace-owner access instruction. A small character alphabet and the
26
+ * structural checks below are defense in depth. Rejection is intentional: an
27
+ * omitted hint is safer than attempting to redact arbitrary backend prose.
28
+ */
29
+ export declare function sanitizeServerRemediation(value: unknown, errorCode?: string): string | undefined;
20
30
  /**
21
31
  * Field names whose VALUES must never appear in logs, traces, or any surface
22
32
  * outside the process boundary. Includes snake_case + locale synonyms because
package/dist/redaction.js CHANGED
@@ -17,6 +17,64 @@
17
17
  */
18
18
  /** Sentinel left in place of a redacted value in cloned (tracing) payloads. */
19
19
  export const REDACTED = "[redacted]";
20
+ const MAX_SERVER_REMEDIATION_CHARS = 500;
21
+ const SERVER_REMEDIATION_PROSE_ALPHABET = /^[\p{L}\p{N} .,;:!?'"()_/*+\-]+$/u;
22
+ const LIVE_EINVOICE_SCOPE_REMEDIATION = "This API key requires the einvoice:read or einvoice:write scope to access e-invoicing endpoints. Add the scope when creating or updating the key.";
23
+ const OWNER_ACCESS_REMEDIATION = /^(?:Ask (?:a|the|your) workspace owner to (?:grant (?:you )?access to (?:this|the) (?:resource|feature|action)|grant (?:the )?required (?:scope|permission))|Contact (?:a|the|your) workspace owner to request access to (?:this|the) (?:resource|feature|action))\.$/iu;
24
+ const GENERIC_REMEDIATION_MESSAGES = new Set([
25
+ "forbidden",
26
+ "access denied",
27
+ "access forbidden",
28
+ "authorization failed",
29
+ "not authorized",
30
+ ]);
31
+ /**
32
+ * Allowlist short, single-line human remediation prose for agent display.
33
+ *
34
+ * Server text is omitted unless it matches one of two explicit semantics: an
35
+ * API-key scope instruction matching the ERP contract, or a conservative
36
+ * workspace-owner access instruction. A small character alphabet and the
37
+ * structural checks below are defense in depth. Rejection is intentional: an
38
+ * omitted hint is safer than attempting to redact arbitrary backend prose.
39
+ */
40
+ export function sanitizeServerRemediation(value, errorCode = "") {
41
+ if (typeof value !== "string")
42
+ return undefined;
43
+ const remediation = value.trim();
44
+ if (!remediation || remediation.length > MAX_SERVER_REMEDIATION_CHARS)
45
+ return undefined;
46
+ if (!SERVER_REMEDIATION_PROSE_ALPHABET.test(remediation))
47
+ return undefined;
48
+ const normalized = remediation.toLowerCase();
49
+ if (normalized === errorCode.trim().toLowerCase()
50
+ || GENERIC_REMEDIATION_MESSAGES.has(normalized)) {
51
+ return undefined;
52
+ }
53
+ if (remediation !== LIVE_EINVOICE_SCOPE_REMEDIATION
54
+ && !OWNER_ACCESS_REMEDIATION.test(remediation)) {
55
+ return undefined;
56
+ }
57
+ const credentialOrInternalShape = [
58
+ // Frihet API keys are `fri_` plus 32 random bytes encoded base64url (43 chars).
59
+ /\bfri_[A-Za-z0-9_-]{32,128}(?=$|[^A-Za-z0-9_-])/u,
60
+ // Header name is not required for a leaked bearer/basic credential to be dangerous.
61
+ /\b(?:bearer|basic)\s+[A-Za-z0-9+/_-]{8,}={0,2}(?:\s|[.,;!?]|$)/iu,
62
+ /\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|token|client[_ -]?secret|password)\s*(?::|=|\bis\b|\bwas\b)\s*["']?\S+/iu,
63
+ /\b(?:whsec_[A-Za-z0-9_-]+|ya29\.[A-Za-z0-9._-]+)\b/u,
64
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/u,
65
+ /(?:^|\s)at\s+(?:async\s+)?[^\s]+\s*\([^)]*:\d+(?::\d+)?\)/u,
66
+ // Any whitespace-delimited absolute path is outside the remediation contract.
67
+ /(?:^|[\s("'])\/(?:[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)/u,
68
+ /(?:^|[\s("'])[A-Za-z]:\\/u,
69
+ // Relative application/source paths (for example `src/auth.ts`).
70
+ /(?:^|\s)(?:(?:\.\.?\/)?(?:src|lib|dist|build|node_modules|functions|workers)\/)[A-Za-z0-9_./-]+/iu,
71
+ /\b[A-Za-z0-9_./-]+\.(?:c|m)?(?:j|t)sx?(?::\d+(?::\d+)?)?\b/u,
72
+ /\b(?:node_modules|file:\/\/)/iu,
73
+ ];
74
+ if (credentialOrInternalShape.some((pattern) => pattern.test(remediation)))
75
+ return undefined;
76
+ return remediation;
77
+ }
20
78
  /**
21
79
  * Field names whose VALUES must never appear in logs, traces, or any surface
22
80
  * outside the process boundary. Includes snake_case + locale synonyms because
@@ -24,6 +82,8 @@ export const REDACTED = "[redacted]";
24
82
  */
25
83
  export const SENSITIVE_FIELD_NAMES = [
26
84
  "taxId", "tax_id", // Primary field name + snake_case variant
85
+ "taxIdNormalized", "tax_id_normalized", // Search-normalized government tax ID
86
+ "clientTaxId", "client_tax_id", // Per-document client government tax ID
27
87
  "nif", "cif", "vatNumber", // Spanish/EU synonyms for government tax ID
28
88
  "vat_number", "vatId", "vat_id",
29
89
  "secret", // Webhook signing credential
@@ -39,6 +99,20 @@ export const SENSITIVE_FIELD_NAMES = [
39
99
  "requestId", "request_id", "traceId", "trace_id",
40
100
  "sessionId", "session_id", "userId", "user_id",
41
101
  "verifactuHash", "verifactu_hash",
102
+ // Raw Firestore records can contain nested fiscal-submission state and
103
+ // capability/payment identifiers that are not part of any public response
104
+ // contract. Redact the parent objects so future nested fields fail closed.
105
+ "verifactu", "verifactuAnulacion", "verifactu_anulacion",
106
+ "verifactuSubmission", "verifactu_submission",
107
+ "paymentDetails", "payment_details",
108
+ "paymentUrl", "payment_url", "paymentLink", "payment_link",
109
+ "paymentIntentId", "payment_intent_id", "stripePaymentIntentId",
110
+ "publicHash", "public_hash",
111
+ "bankTransactionId", "bank_transaction_id",
112
+ "bankIncomeAuthority", "bank_income_authority",
113
+ // Backend search indexes can embed normalized names, email addresses and
114
+ // government identifiers even when the canonical fields are removed.
115
+ "searchTokens", "search_tokens",
42
116
  ];
43
117
  /** Recursively removes named fields from an object/array tree, IN PLACE. */
44
118
  export function deepRedact(obj, fields = SENSITIVE_FIELD_NAMES) {
@@ -58,13 +58,13 @@ Endpoints:
58
58
  PATCH /quotes/:id — Update quote
59
59
  DELETE /quotes/:id — Delete quote
60
60
 
61
- GET /webhooks — List webhooks (paginated)
61
+ GET /webhooks — List webhooks (unpaginated)
62
62
  GET /webhooks/:id — Get webhook by ID
63
63
  POST /webhooks — Create webhook
64
64
  PATCH /webhooks/:id — Update webhook
65
65
  DELETE /webhooks/:id — Delete webhook
66
66
 
67
- Pagination:
67
+ Pagination (list families that expose limit/offset; webhooks are unpaginated):
68
68
  Query params: limit (1-100, default 50), offset (default 0)
69
69
  Response: { data: [...], total: number, limit: number, offset: number }
70
70
 
@@ -77,7 +77,7 @@ Error responses:
77
77
  500 — Internal server error
78
78
 
79
79
  Content-Type: application/json
80
- All monetary values in EUR (cents not used — decimal euros).
80
+ Monetary values use the currency declared by each record; amounts are decimal major units.
81
81
  Dates in ISO 8601 format (YYYY-MM-DD or full datetime).`;
82
82
  const TAX_RATES = `Spanish Tax Rates by Fiscal Zone
83
83
  ================================
@@ -163,11 +163,6 @@ CANARY ISLANDS (IGIC instead of IVA)
163
163
  Modelo 425 — Annual IGIC summary (same deadline as Modelo 390)
164
164
  Filed with ATC (Administración Tributaria Canaria), NOT AEAT
165
165
 
166
- VERIFACTU (mandatory e-invoicing, phased rollout)
167
- 2026: Voluntary adoption
168
- 2027: Mandatory for large companies
169
- 2028: Mandatory for all businesses
170
-
171
166
  KEY DATES SUMMARY
172
167
  Apr 20 — Q1 filings
173
168
  Jul 20 — Q2 filings
@@ -382,8 +377,8 @@ const COUNTRIES = JSON.stringify([
382
377
  /* ------------------------------------------------------------------ */
383
378
  export function registerAllResources(server, client) {
384
379
  server.registerResource("api-schema", "frihet://api/schema", {
385
- description: "OpenAPI schema summary: all endpoints, authentication method, rate limits, pagination, and error codes. " +
386
- "/ Resumen del esquema OpenAPI: endpoints, autenticación, límites, paginación y errores.",
380
+ description: "Selected core REST endpoint summary with authentication, rate limits, pagination, and error codes. " +
381
+ "/ Resumen de endpoints REST principales, autenticación, límites, paginación y errores.",
387
382
  mimeType: "text/plain",
388
383
  }, async () => ({
389
384
  contents: [
@@ -410,8 +405,7 @@ export function registerAllResources(server, client) {
410
405
  }));
411
406
  server.registerResource("tax-calendar", "frihet://tax/calendar", {
412
407
  description: "Spanish quarterly tax calendar with filing deadlines for Modelo 303, 130, 390, 420 (IGIC), and annual returns. " +
413
- "Includes VeriFactu e-invoicing timeline. " +
414
- "/ Calendario fiscal trimestral español con plazos de presentación de modelos y VeriFactu.",
408
+ "/ Calendario fiscal trimestral español con plazos de presentación de modelos.",
415
409
  mimeType: "text/plain",
416
410
  }, async () => ({
417
411
  contents: [
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Canonical registration path shared by the stdio server, remote Worker, and
3
+ * generated public-surface contract. Transport/auth setup stays with each host;
4
+ * the exposed MCP operations/resources/prompts are composed here exactly once.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { IFrihetClient } from "./client-interface.js";
8
+ export interface McpSurfaceComposition {
9
+ readonly openaiMode: boolean;
10
+ readonly groupedMode: boolean;
11
+ readonly includeDynamicResources: boolean;
12
+ }
13
+ export declare function localMcpSurfaceComposition(openaiMode: boolean, groupedMode: boolean): McpSurfaceComposition;
14
+ export declare function remoteMcpSurfaceComposition(openaiMode: boolean, groupedMode: boolean): McpSurfaceComposition;
15
+ export declare function registerMcpSurface(server: McpServer, client: IFrihetClient, options: McpSurfaceComposition): void;
16
+ //# sourceMappingURL=server-composition.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Canonical registration path shared by the stdio server, remote Worker, and
3
+ * generated public-surface contract. Transport/auth setup stays with each host;
4
+ * the exposed MCP operations/resources/prompts are composed here exactly once.
5
+ */
6
+ import { applyPublicCapabilityTruth, buildLocalDiscoveryCapability, CAPABILITY_META_KEY, } from "./capability-truth.js";
7
+ import { applyFiscalAliases } from "./fiscal-aliases.js";
8
+ import { applyOpenAIProfile, applyOpenAIReviewProfiles, } from "./openai-profile.js";
9
+ import { registerAllPrompts } from "./prompts/register-all.js";
10
+ import { registerAllResources } from "./resources/register-all.js";
11
+ import { applyToolExposureProfile } from "./tool-exposure.js";
12
+ import { registerAllTools } from "./tools/register-all.js";
13
+ export function localMcpSurfaceComposition(openaiMode, groupedMode) {
14
+ return { openaiMode, groupedMode, includeDynamicResources: true };
15
+ }
16
+ export function remoteMcpSurfaceComposition(openaiMode, groupedMode) {
17
+ return { openaiMode, groupedMode, includeDynamicResources: false };
18
+ }
19
+ export function registerMcpSurface(server, client, options) {
20
+ if (options.groupedMode) {
21
+ if (options.openaiMode) {
22
+ applyOpenAIReviewProfiles(server);
23
+ }
24
+ else {
25
+ applyToolExposureProfile(server, {
26
+ capabilityTruth: {
27
+ metaKey: CAPABILITY_META_KEY,
28
+ localDiscovery: buildLocalDiscoveryCapability,
29
+ },
30
+ });
31
+ }
32
+ }
33
+ else if (options.openaiMode) {
34
+ applyOpenAIProfile(server);
35
+ }
36
+ if (!options.openaiMode) {
37
+ applyPublicCapabilityTruth(server);
38
+ }
39
+ registerAllTools(server, client);
40
+ applyFiscalAliases(server);
41
+ registerAllResources(server, options.includeDynamicResources ? client : undefined);
42
+ registerAllPrompts(server);
43
+ }
44
+ //# sourceMappingURL=server-composition.js.map