@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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the optional public API base URL accepted by the stdio entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* The returned base is canonical and always ends in `/v1`. Validation happens
|
|
5
|
+
* before a FrihetClient is constructed, so an API key cannot be sent to an
|
|
6
|
+
* attacker-controlled origin through the public environment override.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizePublicApiBaseUrl(value: string): string;
|
|
9
|
+
//# sourceMappingURL=api-origin.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the optional public API base URL accepted by the stdio entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* The returned base is canonical and always ends in `/v1`. Validation happens
|
|
5
|
+
* before a FrihetClient is constructed, so an API key cannot be sent to an
|
|
6
|
+
* attacker-controlled origin through the public environment override.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizePublicApiBaseUrl(value) {
|
|
9
|
+
if (value !== value.trim()) {
|
|
10
|
+
throw new Error("FRIHET_API_URL must not contain surrounding whitespace");
|
|
11
|
+
}
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = new URL(value);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error("FRIHET_API_URL must be a valid URL");
|
|
18
|
+
}
|
|
19
|
+
if (parsed.protocol !== "https:") {
|
|
20
|
+
throw new Error("FRIHET_API_URL must use https");
|
|
21
|
+
}
|
|
22
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
23
|
+
if (hostname.split(".").some((label) => label.length === 0)
|
|
24
|
+
|| !(hostname === "frihet.io" || hostname.endsWith(".frihet.io"))) {
|
|
25
|
+
throw new Error("FRIHET_API_URL hostname must be under frihet.io");
|
|
26
|
+
}
|
|
27
|
+
if (parsed.username || parsed.password) {
|
|
28
|
+
throw new Error("FRIHET_API_URL must not contain URL credentials");
|
|
29
|
+
}
|
|
30
|
+
if (parsed.port) {
|
|
31
|
+
throw new Error("FRIHET_API_URL must use the default HTTPS port");
|
|
32
|
+
}
|
|
33
|
+
if (parsed.search || parsed.hash) {
|
|
34
|
+
throw new Error("FRIHET_API_URL must not contain a query or fragment");
|
|
35
|
+
}
|
|
36
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "/v1" && parsed.pathname !== "/v1/") {
|
|
37
|
+
throw new Error("FRIHET_API_URL path must be / or /v1");
|
|
38
|
+
}
|
|
39
|
+
return `https://${hostname}/v1`;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=api-origin.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP SDK Compatibility & Golden Baseline Canary Harness.
|
|
3
|
+
*
|
|
4
|
+
* Deterministically verifies @frihet/mcp-server SDK v1 compatibility,
|
|
5
|
+
* golden contract, tool enumeration, JSON schema validity, pagination parameters,
|
|
6
|
+
* typed error contracts, capability metadata, and representative READ operations
|
|
7
|
+
* against @modelcontextprotocol/sdk v1.x before v2.0 migration.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: This is an in-process SDK client/server compatibility harness.
|
|
10
|
+
* Model Context Protocol Inspector 2.3.0 is documented as a reference authority,
|
|
11
|
+
* while this harness executes in-process via @modelcontextprotocol/sdk v1.x client
|
|
12
|
+
* with zero daemons to provide deterministic CI verification.
|
|
13
|
+
*
|
|
14
|
+
* Zero production writes. Zero credential leaks.
|
|
15
|
+
*/
|
|
16
|
+
export declare const HARNESS_CONTRACT_VERSION = 1;
|
|
17
|
+
export declare const INSPECTOR_PINNED_VERSION = "2.3.0";
|
|
18
|
+
export declare const CANONICAL_OPERATIONS_COUNT = 157;
|
|
19
|
+
export declare const FISCAL_ALIASES_COUNT: number;
|
|
20
|
+
export type CheckStatus = "PASS" | "FAIL" | "UNSUPPORTED" | "NOT_EXERCISED";
|
|
21
|
+
export type OverallStatus = "PASS" | "PASS_WITH_GAPS" | "FAIL";
|
|
22
|
+
export interface HarnessCheckResult {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
category: "initialize" | "tools" | "resources" | "prompts" | "reads" | "pagination" | "errors" | "mutations" | "auth";
|
|
26
|
+
status: CheckStatus;
|
|
27
|
+
detail: string;
|
|
28
|
+
durationMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ToolSnapshot {
|
|
31
|
+
name: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
inputSchema: Record<string, unknown>;
|
|
35
|
+
hasOutputSchema: boolean;
|
|
36
|
+
annotations: Record<string, unknown>;
|
|
37
|
+
capabilityMeta?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export interface McpBaselineReport {
|
|
40
|
+
contractVersion: number;
|
|
41
|
+
inspectorPinnedVersion: string;
|
|
42
|
+
harnessMode: "in-process-sdk-client";
|
|
43
|
+
protocolVersion: string | null;
|
|
44
|
+
serverInfo: {
|
|
45
|
+
name: string;
|
|
46
|
+
version: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
};
|
|
49
|
+
summary: {
|
|
50
|
+
totalTools: number;
|
|
51
|
+
canonicalOperations: number;
|
|
52
|
+
fiscalAliases: number;
|
|
53
|
+
resources: number;
|
|
54
|
+
prompts: number;
|
|
55
|
+
checks: {
|
|
56
|
+
total: number;
|
|
57
|
+
pass: number;
|
|
58
|
+
fail: number;
|
|
59
|
+
unsupported: number;
|
|
60
|
+
notExercised: number;
|
|
61
|
+
};
|
|
62
|
+
overallStatus: OverallStatus;
|
|
63
|
+
};
|
|
64
|
+
checkMatrix: HarnessCheckResult[];
|
|
65
|
+
tools: ToolSnapshot[];
|
|
66
|
+
resources: string[];
|
|
67
|
+
prompts: string[];
|
|
68
|
+
sanitized: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Executes the complete compatibility harness in-process and returns
|
|
72
|
+
* a structured, sanitized baseline report.
|
|
73
|
+
*/
|
|
74
|
+
export declare function runMcpBaseline(): Promise<McpBaselineReport>;
|
|
75
|
+
export declare function serializeMcpBaseline(report: McpBaselineReport): string;
|
|
76
|
+
export declare function assertMcpBaseline(actual: McpBaselineReport, expected: McpBaselineReport): void;
|
|
77
|
+
//# sourceMappingURL=mcp-harness.d.ts.map
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP SDK Compatibility & Golden Baseline Canary Harness.
|
|
3
|
+
*
|
|
4
|
+
* Deterministically verifies @frihet/mcp-server SDK v1 compatibility,
|
|
5
|
+
* golden contract, tool enumeration, JSON schema validity, pagination parameters,
|
|
6
|
+
* typed error contracts, capability metadata, and representative READ operations
|
|
7
|
+
* against @modelcontextprotocol/sdk v1.x before v2.0 migration.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: This is an in-process SDK client/server compatibility harness.
|
|
10
|
+
* Model Context Protocol Inspector 2.3.0 is documented as a reference authority,
|
|
11
|
+
* while this harness executes in-process via @modelcontextprotocol/sdk v1.x client
|
|
12
|
+
* with zero daemons to provide deterministic CI verification.
|
|
13
|
+
*
|
|
14
|
+
* Zero production writes. Zero credential leaks.
|
|
15
|
+
*/
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
18
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
19
|
+
import { DemoFrihetClient } from "../demo-client.js";
|
|
20
|
+
import { registerMcpSurface, localMcpSurfaceComposition } from "../server-composition.js";
|
|
21
|
+
import { CAPABILITY_META_KEY } from "../capability-truth.js";
|
|
22
|
+
import { FISCAL_MODELO_ALIASES } from "../fiscal-aliases.js";
|
|
23
|
+
export const HARNESS_CONTRACT_VERSION = 1;
|
|
24
|
+
export const INSPECTOR_PINNED_VERSION = "2.3.0";
|
|
25
|
+
export const CANONICAL_OPERATIONS_COUNT = 157;
|
|
26
|
+
export const FISCAL_ALIASES_COUNT = Object.keys(FISCAL_MODELO_ALIASES).length;
|
|
27
|
+
/**
|
|
28
|
+
* Executes the complete compatibility harness in-process and returns
|
|
29
|
+
* a structured, sanitized baseline report.
|
|
30
|
+
*/
|
|
31
|
+
export async function runMcpBaseline() {
|
|
32
|
+
const checkMatrix = [];
|
|
33
|
+
const addCheck = (c) => {
|
|
34
|
+
checkMatrix.push(c);
|
|
35
|
+
};
|
|
36
|
+
const demoClient = new DemoFrihetClient();
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: "frihet-erp",
|
|
39
|
+
version: "1.16.6",
|
|
40
|
+
description: "AI-native MCP server for Frihet ERP",
|
|
41
|
+
});
|
|
42
|
+
const composition = localMcpSurfaceComposition(false, false);
|
|
43
|
+
registerMcpSurface(server, demoClient, composition);
|
|
44
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
45
|
+
const client = new Client({
|
|
46
|
+
name: "frihet-mcp-canary-client",
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
}, {
|
|
49
|
+
capabilities: {
|
|
50
|
+
roots: { listChanged: true },
|
|
51
|
+
sampling: {},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const toolSnapshots = [];
|
|
55
|
+
const resourceUris = [];
|
|
56
|
+
const promptNames = [];
|
|
57
|
+
let serverInfo = { name: "unknown", version: "unknown", description: "" };
|
|
58
|
+
try {
|
|
59
|
+
// 1. Initialize & Handshake
|
|
60
|
+
const initStart = Date.now();
|
|
61
|
+
await Promise.all([
|
|
62
|
+
server.connect(serverTransport),
|
|
63
|
+
client.connect(clientTransport),
|
|
64
|
+
]);
|
|
65
|
+
const sVersion = client.getServerVersion();
|
|
66
|
+
if (sVersion) {
|
|
67
|
+
serverInfo = {
|
|
68
|
+
name: sVersion.name,
|
|
69
|
+
version: sVersion.version,
|
|
70
|
+
description: "AI-native MCP server for Frihet ERP",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Protocol Version Negotiation
|
|
74
|
+
// Note: SDK v1 Client does not expose negotiated protocolVersion property post-handshake.
|
|
75
|
+
// We mark NOT_EXERCISED truthfully rather than asserting a constant.
|
|
76
|
+
addCheck({
|
|
77
|
+
id: "initialize.protocol_negotiation",
|
|
78
|
+
name: "Protocol Version Negotiation",
|
|
79
|
+
category: "initialize",
|
|
80
|
+
status: "NOT_EXERCISED",
|
|
81
|
+
detail: "Protocol version negotiation is encapsulated within SDK v1 client handshake and not directly observable on Client instance post-connect",
|
|
82
|
+
durationMs: Date.now() - initStart,
|
|
83
|
+
});
|
|
84
|
+
// Server Capabilities Declaration
|
|
85
|
+
const caps = client.getServerCapabilities();
|
|
86
|
+
const hasTools = !!caps?.tools;
|
|
87
|
+
const hasResources = !!caps?.resources;
|
|
88
|
+
const hasPrompts = !!caps?.prompts;
|
|
89
|
+
if (hasTools && hasResources && hasPrompts) {
|
|
90
|
+
addCheck({
|
|
91
|
+
id: "initialize.server_capabilities",
|
|
92
|
+
name: "Server Capabilities Declaration",
|
|
93
|
+
category: "initialize",
|
|
94
|
+
status: "PASS",
|
|
95
|
+
detail: "Server declared tools, resources, and prompts capabilities",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
addCheck({
|
|
100
|
+
id: "initialize.server_capabilities",
|
|
101
|
+
name: "Server Capabilities Declaration",
|
|
102
|
+
category: "initialize",
|
|
103
|
+
status: "FAIL",
|
|
104
|
+
detail: `Missing capabilities: tools=${hasTools}, resources=${hasResources}, prompts=${hasPrompts}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 2. Tools Enumeration & Structural Schemas
|
|
108
|
+
const toolsStart = Date.now();
|
|
109
|
+
const toolsList = await client.listTools();
|
|
110
|
+
const tools = toolsList.tools;
|
|
111
|
+
const expectedTotal = CANONICAL_OPERATIONS_COUNT + FISCAL_ALIASES_COUNT;
|
|
112
|
+
const isCountValid = tools.length === expectedTotal;
|
|
113
|
+
addCheck({
|
|
114
|
+
id: "tools.enumeration",
|
|
115
|
+
name: "Exhaustive Tool Enumeration",
|
|
116
|
+
category: "tools",
|
|
117
|
+
status: isCountValid ? "PASS" : "FAIL",
|
|
118
|
+
detail: `Enumerated ${tools.length} tools (${CANONICAL_OPERATIONS_COUNT} canonical + ${FISCAL_ALIASES_COUNT} aliases)`,
|
|
119
|
+
durationMs: Date.now() - toolsStart,
|
|
120
|
+
});
|
|
121
|
+
let nonSnakeCaseCount = 0;
|
|
122
|
+
let invalidInputSchemaCount = 0;
|
|
123
|
+
let missingCapabilityMetaCount = 0;
|
|
124
|
+
for (const tool of tools) {
|
|
125
|
+
if (!/^[a-z0-9]+(_[a-z0-9]+)*$/.test(tool.name)) {
|
|
126
|
+
nonSnakeCaseCount++;
|
|
127
|
+
}
|
|
128
|
+
const inputSchema = tool.inputSchema;
|
|
129
|
+
const isInputSchemaValid = inputSchema !== null &&
|
|
130
|
+
typeof inputSchema === "object" &&
|
|
131
|
+
inputSchema.type === "object" &&
|
|
132
|
+
typeof inputSchema.properties === "object";
|
|
133
|
+
if (!isInputSchemaValid) {
|
|
134
|
+
invalidInputSchemaCount++;
|
|
135
|
+
}
|
|
136
|
+
const meta = tool._meta;
|
|
137
|
+
const hasCap = !!(meta && meta[CAPABILITY_META_KEY]);
|
|
138
|
+
if (!hasCap) {
|
|
139
|
+
missingCapabilityMetaCount++;
|
|
140
|
+
}
|
|
141
|
+
toolSnapshots.push({
|
|
142
|
+
name: tool.name,
|
|
143
|
+
title: tool.title,
|
|
144
|
+
description: tool.description,
|
|
145
|
+
inputSchema: (tool.inputSchema || {}),
|
|
146
|
+
hasOutputSchema: !!tool.outputSchema,
|
|
147
|
+
annotations: (tool.annotations || {}),
|
|
148
|
+
capabilityMeta: meta?.[CAPABILITY_META_KEY],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Sort tool snapshots deterministically by name
|
|
152
|
+
toolSnapshots.sort((a, b) => a.name.localeCompare(b.name));
|
|
153
|
+
addCheck({
|
|
154
|
+
id: "tools.canonical_names",
|
|
155
|
+
name: "Canonical Snake Case Naming",
|
|
156
|
+
category: "tools",
|
|
157
|
+
status: nonSnakeCaseCount === 0 ? "PASS" : "FAIL",
|
|
158
|
+
detail: nonSnakeCaseCount === 0 ? "All tools use valid snake_case names" : `${nonSnakeCaseCount} invalid tool names detected`,
|
|
159
|
+
});
|
|
160
|
+
addCheck({
|
|
161
|
+
id: "tools.input_schemas_structural",
|
|
162
|
+
name: "Structural Input Schema Validation",
|
|
163
|
+
category: "tools",
|
|
164
|
+
status: invalidInputSchemaCount === 0 ? "PASS" : "FAIL",
|
|
165
|
+
detail: invalidInputSchemaCount === 0 ? "All tool inputSchemas declare valid object structure and properties mapping" : `${invalidInputSchemaCount} tools have malformed inputSchemas`,
|
|
166
|
+
});
|
|
167
|
+
addCheck({
|
|
168
|
+
id: "tools.capability_metadata",
|
|
169
|
+
name: "Capability Metadata Truth",
|
|
170
|
+
category: "tools",
|
|
171
|
+
status: missingCapabilityMetaCount === 0 ? "PASS" : "FAIL",
|
|
172
|
+
detail: missingCapabilityMetaCount === 0 ? "All tools carry io.frihet/capability metadata" : `${missingCapabilityMetaCount} tools missing capability metadata`,
|
|
173
|
+
});
|
|
174
|
+
// 3. Resources & Prompts Enumeration
|
|
175
|
+
const resourcesList = await client.listResources();
|
|
176
|
+
for (const r of resourcesList.resources) {
|
|
177
|
+
resourceUris.push(r.uri);
|
|
178
|
+
}
|
|
179
|
+
resourceUris.sort();
|
|
180
|
+
addCheck({
|
|
181
|
+
id: "resources.enumeration",
|
|
182
|
+
name: "Local Resources Enumeration",
|
|
183
|
+
category: "resources",
|
|
184
|
+
status: resourceUris.length === 11 ? "PASS" : "FAIL",
|
|
185
|
+
detail: `Enumerated ${resourceUris.length}/11 resources`,
|
|
186
|
+
});
|
|
187
|
+
const promptsList = await client.listPrompts();
|
|
188
|
+
for (const p of promptsList.prompts) {
|
|
189
|
+
promptNames.push(p.name);
|
|
190
|
+
}
|
|
191
|
+
promptNames.sort();
|
|
192
|
+
addCheck({
|
|
193
|
+
id: "prompts.enumeration",
|
|
194
|
+
name: "Local Prompts Enumeration",
|
|
195
|
+
category: "prompts",
|
|
196
|
+
status: promptNames.length === 10 ? "PASS" : "FAIL",
|
|
197
|
+
detail: `Enumerated ${promptNames.length}/10 prompts`,
|
|
198
|
+
});
|
|
199
|
+
// 4. Safe Representative READ Executions (Demo Mode)
|
|
200
|
+
const readTestCases = [
|
|
201
|
+
{
|
|
202
|
+
tool: "get_business_context",
|
|
203
|
+
args: {},
|
|
204
|
+
validate: (r) => {
|
|
205
|
+
const sc = r.structuredContent;
|
|
206
|
+
return !!(sc && sc.business && typeof sc.business.name === "string");
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
tool: "list_invoices",
|
|
211
|
+
args: { limit: 2 },
|
|
212
|
+
validate: (r) => {
|
|
213
|
+
const sc = r.structuredContent;
|
|
214
|
+
return !!(sc && Array.isArray(sc.data) && typeof sc.total === "number");
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
tool: "get_monthly_summary",
|
|
219
|
+
args: { month: "2026-07" },
|
|
220
|
+
validate: (r) => {
|
|
221
|
+
const sc = r.structuredContent;
|
|
222
|
+
return !!(sc && (typeof sc.period === "string" || sc.revenue !== undefined));
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
tool: "get_quarterly_taxes",
|
|
227
|
+
args: { quarter: "2026-Q1" },
|
|
228
|
+
validate: (r) => {
|
|
229
|
+
const sc = r.structuredContent;
|
|
230
|
+
return !!(sc && typeof sc.quarter === "string");
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
tool: "list_clients",
|
|
235
|
+
args: { limit: 2 },
|
|
236
|
+
validate: (r) => {
|
|
237
|
+
const sc = r.structuredContent;
|
|
238
|
+
return !!(sc && Array.isArray(sc.data));
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
tool: "list_expenses",
|
|
243
|
+
args: { limit: 2 },
|
|
244
|
+
validate: (r) => {
|
|
245
|
+
const sc = r.structuredContent;
|
|
246
|
+
return !!(sc && Array.isArray(sc.data));
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
tool: "list_products",
|
|
251
|
+
args: { limit: 2 },
|
|
252
|
+
validate: (r) => {
|
|
253
|
+
const sc = r.structuredContent;
|
|
254
|
+
return !!(sc && Array.isArray(sc.data));
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
tool: "get_modelo_303_summary",
|
|
259
|
+
args: { period: "2026-Q1" },
|
|
260
|
+
validate: (r) => {
|
|
261
|
+
const sc = r.structuredContent;
|
|
262
|
+
return !!(sc && (sc.model === "303" || sc.period === "2026-Q1"));
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
tool: "permissions_me",
|
|
267
|
+
args: {},
|
|
268
|
+
validate: (r) => {
|
|
269
|
+
const sc = r.structuredContent;
|
|
270
|
+
return !!(sc && typeof sc.role === "string");
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
tool: "verifactu_status",
|
|
275
|
+
args: { invoiceId: "demo_inv_001" },
|
|
276
|
+
validate: (r) => {
|
|
277
|
+
const sc = r.structuredContent;
|
|
278
|
+
return !!(sc && (sc.invoiceId === "demo_inv_001" || typeof sc.status === "string"));
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
for (const testCase of readTestCases) {
|
|
283
|
+
const opStart = Date.now();
|
|
284
|
+
try {
|
|
285
|
+
const rawRes = await client.callTool({
|
|
286
|
+
name: testCase.tool,
|
|
287
|
+
arguments: testCase.args,
|
|
288
|
+
});
|
|
289
|
+
const res = rawRes;
|
|
290
|
+
const content = res.content;
|
|
291
|
+
const isContentValid = Array.isArray(content) && content.length > 0 && content[0].type === "text";
|
|
292
|
+
const isStructuredValid = testCase.validate(res);
|
|
293
|
+
if (isContentValid && isStructuredValid) {
|
|
294
|
+
addCheck({
|
|
295
|
+
id: `reads.${testCase.tool}`,
|
|
296
|
+
name: `Read Execution: ${testCase.tool}`,
|
|
297
|
+
category: "reads",
|
|
298
|
+
status: "PASS",
|
|
299
|
+
detail: "Returned valid content text and schema-valid structuredContent",
|
|
300
|
+
durationMs: Date.now() - opStart,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
addCheck({
|
|
305
|
+
id: `reads.${testCase.tool}`,
|
|
306
|
+
name: `Read Execution: ${testCase.tool}`,
|
|
307
|
+
category: "reads",
|
|
308
|
+
status: "FAIL",
|
|
309
|
+
detail: `Invalid payload shape (content valid: ${isContentValid}, structured valid: ${isStructuredValid})`,
|
|
310
|
+
durationMs: Date.now() - opStart,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
addCheck({
|
|
316
|
+
id: `reads.${testCase.tool}`,
|
|
317
|
+
name: `Read Execution: ${testCase.tool}`,
|
|
318
|
+
category: "reads",
|
|
319
|
+
status: "FAIL",
|
|
320
|
+
detail: `Invocation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
321
|
+
durationMs: Date.now() - opStart,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// 5. Pagination & Cursor Parameters Contract
|
|
326
|
+
const paginatedTools = ["list_invoices", "list_expenses", "list_clients", "list_products", "list_quotes", "list_sales", "list_transactions"];
|
|
327
|
+
let paginatedValid = 0;
|
|
328
|
+
for (const toolName of paginatedTools) {
|
|
329
|
+
const snap = toolSnapshots.find((t) => t.name === toolName);
|
|
330
|
+
if (snap && snap.inputSchema && typeof snap.inputSchema === "object") {
|
|
331
|
+
const props = (snap.inputSchema.properties || {});
|
|
332
|
+
if (props.limit || props.offset || props.cursor) {
|
|
333
|
+
paginatedValid++;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
addCheck({
|
|
338
|
+
id: "pagination.cursor_and_limits",
|
|
339
|
+
name: "Pagination Parameter Contract",
|
|
340
|
+
category: "pagination",
|
|
341
|
+
status: paginatedValid === paginatedTools.length ? "PASS" : "FAIL",
|
|
342
|
+
detail: `${paginatedValid}/${paginatedTools.length} paginated tools declare limit/offset/cursor schema properties`,
|
|
343
|
+
});
|
|
344
|
+
// 6. Typed MCP Error Contracts
|
|
345
|
+
// A. Unknown tool error
|
|
346
|
+
try {
|
|
347
|
+
const res = await client.callTool({
|
|
348
|
+
name: "non_existent_tool_baseline_check",
|
|
349
|
+
arguments: {},
|
|
350
|
+
});
|
|
351
|
+
if (res.isError && Array.isArray(res.content) && res.content[0]?.text?.includes("not found")) {
|
|
352
|
+
addCheck({
|
|
353
|
+
id: "errors.unknown_tool",
|
|
354
|
+
name: "Unknown Tool Error Rejection",
|
|
355
|
+
category: "errors",
|
|
356
|
+
status: "PASS",
|
|
357
|
+
detail: `Rejected unknown tool with typed error: ${res.content[0].text}`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
addCheck({
|
|
362
|
+
id: "errors.unknown_tool",
|
|
363
|
+
name: "Unknown Tool Error Rejection",
|
|
364
|
+
category: "errors",
|
|
365
|
+
status: "FAIL",
|
|
366
|
+
detail: "Unknown tool call succeeded unexpectedly without error flag",
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
const isExpectedError = (typeof err === "object" && err !== null && "code" in err && err.code === -32601) ||
|
|
372
|
+
(err instanceof Error && /tool.*not found/i.test(err.message));
|
|
373
|
+
addCheck({
|
|
374
|
+
id: "errors.unknown_tool",
|
|
375
|
+
name: "Unknown Tool Error Rejection",
|
|
376
|
+
category: "errors",
|
|
377
|
+
status: isExpectedError ? "PASS" : "FAIL",
|
|
378
|
+
detail: `Rejected unknown tool with typed error (-32601): ${err instanceof Error ? err.message : String(err)}`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
// B. Invalid arguments error
|
|
382
|
+
try {
|
|
383
|
+
const res = await client.callTool({
|
|
384
|
+
name: "get_invoice",
|
|
385
|
+
arguments: { id: 12345 }, // Invalid argument type
|
|
386
|
+
});
|
|
387
|
+
if (res.isError) {
|
|
388
|
+
addCheck({
|
|
389
|
+
id: "errors.invalid_arguments",
|
|
390
|
+
name: "Invalid Arguments Validation Rejection",
|
|
391
|
+
category: "errors",
|
|
392
|
+
status: "PASS",
|
|
393
|
+
detail: "Returned isError: true on invalid argument type",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
addCheck({
|
|
398
|
+
id: "errors.invalid_arguments",
|
|
399
|
+
name: "Invalid Arguments Validation Rejection",
|
|
400
|
+
category: "errors",
|
|
401
|
+
status: "FAIL",
|
|
402
|
+
detail: "Server accepted invalid argument type without isError",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
addCheck({
|
|
408
|
+
id: "errors.invalid_arguments",
|
|
409
|
+
name: "Invalid Arguments Validation Rejection",
|
|
410
|
+
category: "errors",
|
|
411
|
+
status: "PASS",
|
|
412
|
+
detail: `Client rejected invalid argument type: ${err instanceof Error ? err.message : String(err)}`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
// 7. Mutating Tool Schemas (Zero-Write Safety Inspection)
|
|
416
|
+
const mutatingTools = [
|
|
417
|
+
"create_invoice",
|
|
418
|
+
"update_invoice",
|
|
419
|
+
"delete_invoice",
|
|
420
|
+
"create_expense",
|
|
421
|
+
"create_client",
|
|
422
|
+
"create_quote",
|
|
423
|
+
"send_invoice",
|
|
424
|
+
"refund_sale",
|
|
425
|
+
"period_close",
|
|
426
|
+
"leave_approve",
|
|
427
|
+
"verifactu_resubmit",
|
|
428
|
+
];
|
|
429
|
+
let mutatingSchemasValid = 0;
|
|
430
|
+
for (const toolName of mutatingTools) {
|
|
431
|
+
const snap = toolSnapshots.find((t) => t.name === toolName);
|
|
432
|
+
if (snap && snap.inputSchema && typeof snap.inputSchema === "object" && snap.annotations.readOnlyHint !== true) {
|
|
433
|
+
mutatingSchemasValid++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
addCheck({
|
|
437
|
+
id: "mutations.zero_write_schema_inspection",
|
|
438
|
+
name: "Mutation Schemas & Side-Effect Safety",
|
|
439
|
+
category: "mutations",
|
|
440
|
+
status: mutatingSchemasValid === mutatingTools.length ? "PASS" : "FAIL",
|
|
441
|
+
detail: `Inspected ${mutatingSchemasValid}/${mutatingTools.length} mutating schemas; zero production writes executed`,
|
|
442
|
+
});
|
|
443
|
+
// 8. Auth Negative Path
|
|
444
|
+
// In-process demo harness runs DemoFrihetClient without HTTP transport.
|
|
445
|
+
// Marked NOT_EXERCISED truthfully.
|
|
446
|
+
addCheck({
|
|
447
|
+
id: "auth.safe_negative_remediation",
|
|
448
|
+
name: "Auth Negative Path Safe Remediation",
|
|
449
|
+
category: "auth",
|
|
450
|
+
status: "NOT_EXERCISED",
|
|
451
|
+
detail: "In-process demo harness operates with DemoFrihetClient; negative Bearer auth remediation requires live HTTP transport test surface",
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
await Promise.allSettled([
|
|
456
|
+
client.close(),
|
|
457
|
+
server.close(),
|
|
458
|
+
]);
|
|
459
|
+
}
|
|
460
|
+
const passCount = checkMatrix.filter((c) => c.status === "PASS").length;
|
|
461
|
+
const failCount = checkMatrix.filter((c) => c.status === "FAIL").length;
|
|
462
|
+
const unsupportedCount = checkMatrix.filter((c) => c.status === "UNSUPPORTED").length;
|
|
463
|
+
const notExercisedCount = checkMatrix.filter((c) => c.status === "NOT_EXERCISED").length;
|
|
464
|
+
const overallStatus = failCount > 0 ? "FAIL" : notExercisedCount > 0 ? "PASS_WITH_GAPS" : "PASS";
|
|
465
|
+
return {
|
|
466
|
+
contractVersion: HARNESS_CONTRACT_VERSION,
|
|
467
|
+
inspectorPinnedVersion: INSPECTOR_PINNED_VERSION,
|
|
468
|
+
harnessMode: "in-process-sdk-client",
|
|
469
|
+
protocolVersion: null,
|
|
470
|
+
serverInfo,
|
|
471
|
+
summary: {
|
|
472
|
+
totalTools: toolSnapshots.length,
|
|
473
|
+
canonicalOperations: CANONICAL_OPERATIONS_COUNT,
|
|
474
|
+
fiscalAliases: FISCAL_ALIASES_COUNT,
|
|
475
|
+
resources: resourceUris.length,
|
|
476
|
+
prompts: promptNames.length,
|
|
477
|
+
checks: {
|
|
478
|
+
total: checkMatrix.length,
|
|
479
|
+
pass: passCount,
|
|
480
|
+
fail: failCount,
|
|
481
|
+
unsupported: unsupportedCount,
|
|
482
|
+
notExercised: notExercisedCount,
|
|
483
|
+
},
|
|
484
|
+
overallStatus,
|
|
485
|
+
},
|
|
486
|
+
checkMatrix,
|
|
487
|
+
tools: toolSnapshots,
|
|
488
|
+
resources: resourceUris,
|
|
489
|
+
prompts: promptNames,
|
|
490
|
+
sanitized: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
export function serializeMcpBaseline(report) {
|
|
494
|
+
// Deep clone and clean timing-dependent fields for deterministic serialization
|
|
495
|
+
const deterministic = {
|
|
496
|
+
...report,
|
|
497
|
+
checkMatrix: report.checkMatrix.map((c) => {
|
|
498
|
+
const { durationMs: _, ...rest } = c;
|
|
499
|
+
return rest;
|
|
500
|
+
}),
|
|
501
|
+
};
|
|
502
|
+
return `${JSON.stringify(deterministic, null, 2)}\n`;
|
|
503
|
+
}
|
|
504
|
+
export function assertMcpBaseline(actual, expected) {
|
|
505
|
+
if (actual.contractVersion !== expected.contractVersion) {
|
|
506
|
+
throw new Error(`Contract version mismatch: actual ${actual.contractVersion} !== expected ${expected.contractVersion}`);
|
|
507
|
+
}
|
|
508
|
+
if (actual.summary.overallStatus === "FAIL" || actual.summary.checks.fail > 0) {
|
|
509
|
+
const failures = actual.checkMatrix.filter((c) => c.status === "FAIL");
|
|
510
|
+
throw new Error(`Baseline failed with ${failures.length} errors: ${failures.map((f) => `${f.id}: ${f.detail}`).join("; ")}`);
|
|
511
|
+
}
|
|
512
|
+
if (actual.summary.overallStatus !== expected.summary.overallStatus) {
|
|
513
|
+
throw new Error(`Overall status mismatch: actual ${actual.summary.overallStatus} !== expected ${expected.summary.overallStatus}`);
|
|
514
|
+
}
|
|
515
|
+
if (actual.summary.totalTools !== expected.summary.totalTools) {
|
|
516
|
+
throw new Error(`Tool count drift: actual ${actual.summary.totalTools} !== expected ${expected.summary.totalTools}`);
|
|
517
|
+
}
|
|
518
|
+
if (actual.summary.canonicalOperations !== expected.summary.canonicalOperations) {
|
|
519
|
+
throw new Error(`Canonical operations drift: actual ${actual.summary.canonicalOperations} !== expected ${expected.summary.canonicalOperations}`);
|
|
520
|
+
}
|
|
521
|
+
if (actual.summary.resources !== expected.summary.resources) {
|
|
522
|
+
throw new Error(`Resource count drift: actual ${actual.summary.resources} !== expected ${expected.summary.resources}`);
|
|
523
|
+
}
|
|
524
|
+
if (actual.summary.prompts !== expected.summary.prompts) {
|
|
525
|
+
throw new Error(`Prompt count drift: actual ${actual.summary.prompts} !== expected ${expected.summary.prompts}`);
|
|
526
|
+
}
|
|
527
|
+
const toolNameSet = new Set();
|
|
528
|
+
for (const tool of actual.tools) {
|
|
529
|
+
if (toolNameSet.has(tool.name)) {
|
|
530
|
+
throw new Error(`Duplicate tool detected in baseline: ${tool.name}`);
|
|
531
|
+
}
|
|
532
|
+
toolNameSet.add(tool.name);
|
|
533
|
+
}
|
|
534
|
+
const actualTools = new Map(actual.tools.map((t) => [t.name, t]));
|
|
535
|
+
const expectedTools = new Map(expected.tools.map((t) => [t.name, t]));
|
|
536
|
+
for (const [name, exp] of expectedTools.entries()) {
|
|
537
|
+
const act = actualTools.get(name);
|
|
538
|
+
if (!act) {
|
|
539
|
+
throw new Error(`Missing expected tool in actual baseline: ${name}`);
|
|
540
|
+
}
|
|
541
|
+
if (JSON.stringify(act.inputSchema) !== JSON.stringify(exp.inputSchema)) {
|
|
542
|
+
throw new Error(`Input schema drift on tool ${name}`);
|
|
543
|
+
}
|
|
544
|
+
if (JSON.stringify(act.annotations) !== JSON.stringify(exp.annotations)) {
|
|
545
|
+
throw new Error(`Annotation drift on tool ${name}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
//# sourceMappingURL=mcp-harness.js.map
|