@frihet/mcp-server 1.16.6 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -31
- package/dist/agent-onboarding.d.ts +98 -0
- package/dist/agent-onboarding.js +275 -0
- package/dist/api-origin.d.ts +9 -0
- package/dist/api-origin.js +41 -0
- package/dist/canary/mcp-harness.d.ts +77 -0
- package/dist/canary/mcp-harness.js +549 -0
- package/dist/capability-truth.d.ts +29 -0
- package/dist/capability-truth.js +254 -0
- package/dist/client-interface.d.ts +22 -14
- package/dist/client.d.ts +114 -18
- package/dist/client.js +488 -56
- package/dist/demo-client.d.ts +16 -13
- package/dist/demo-client.js +171 -17
- package/dist/demo-fixtures.d.ts +3 -3
- package/dist/demo-fixtures.js +7 -7
- package/dist/fiscal-aliases.d.ts +44 -0
- package/dist/fiscal-aliases.js +62 -0
- package/dist/index.js +41 -56
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +140 -8
- package/dist/observability.d.ts +18 -29
- package/dist/observability.js +192 -105
- package/dist/openai-profile.d.ts +25 -21
- package/dist/openai-profile.js +1166 -156
- package/dist/openai-review-contract.d.ts +15 -6
- package/dist/openai-review-contract.js +715 -41
- package/dist/openai-review-oauth.d.ts +47 -0
- package/dist/openai-review-oauth.js +73 -3
- package/dist/prompts/register-all.d.ts +2 -0
- package/dist/prompts/register-all.js +2 -0
- package/dist/public-capability-contract.d.ts +39 -0
- package/dist/public-capability-contract.js +186 -0
- package/dist/redaction.d.ts +10 -0
- package/dist/redaction.js +74 -0
- package/dist/resources/register-all.js +6 -12
- package/dist/server-composition.d.ts +16 -0
- package/dist/server-composition.js +44 -0
- package/dist/tool-exposure.d.ts +25 -22
- package/dist/tool-exposure.js +199 -77
- package/dist/tools/accountingClose.d.ts +14 -5
- package/dist/tools/accountingClose.js +41 -23
- package/dist/tools/backend-availability.d.ts +3 -3
- package/dist/tools/backend-availability.js +3 -3
- package/dist/tools/clients.js +9 -9
- package/dist/tools/crm.js +22 -21
- package/dist/tools/deposits.js +30 -11
- package/dist/tools/einvoice.d.ts +1 -1
- package/dist/tools/expenses.d.ts +16 -0
- package/dist/tools/expenses.js +47 -27
- package/dist/tools/hr.d.ts +2 -3
- package/dist/tools/hr.js +8 -9
- package/dist/tools/intelligence.js +2 -2
- package/dist/tools/invoices.js +150 -47
- package/dist/tools/payroll.d.ts +5 -8
- package/dist/tools/payroll.js +19 -32
- package/dist/tools/permissions.d.ts +4 -7
- package/dist/tools/permissions.js +11 -14
- package/dist/tools/products.js +6 -6
- package/dist/tools/quotes.js +109 -24
- package/dist/tools/shared.d.ts +1431 -75
- package/dist/tools/shared.js +386 -76
- package/dist/tools/team.js +18 -14
- package/dist/tools/vendors.js +9 -9
- package/dist/tools/webhooks.js +35 -30
- package/dist/types.d.ts +28 -4
- package/docs/agent-onboarding.json +381 -0
- package/package.json +30 -7
|
@@ -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
|
-
|
|
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:
|
|
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
|
package/dist/redaction.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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: "
|
|
386
|
-
"/ Resumen
|
|
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
|
-
"
|
|
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
|