🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@frihet/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@frihet/mcp-server - npm Package Compare versions

Comparing version
1.16.5
to
1.16.6
+40
dist/openai-review-contract.d.ts
/**
* Fail-closed contract capture for the ChatGPT-reviewed MCP surface.
*
* This intentionally uses a real McpServer, real Client, in-memory MCP
* transport, and the production registration path. It therefore freezes what
* tools/list serializes, not an approximation of registration config objects.
*/
export declare const OPENAI_REVIEW_CONTRACT_VERSION = 1;
export declare const OPENAI_REVIEW_BUSINESS_TOOL_COUNT = 53;
export declare const OPENAI_REVIEW_DISCOVERY_TOOLS: readonly ["describe_tool", "list_tool_groups", "search_tools"];
export declare const OPENAI_REVIEW_TOTAL_TOOL_COUNT = 56;
export type JsonValue = null | boolean | number | string | JsonValue[] | {
[key: string]: JsonValue;
};
export interface OpenAIReviewTool extends Record<string, JsonValue> {
name: string;
}
export interface OpenAIReviewContract {
contractVersion: number;
tools: OpenAIReviewTool[];
prompts: JsonValue[];
resources: JsonValue[];
oauth: Record<string, JsonValue>;
}
export interface OpenAIReviewMcpSurface {
tools: OpenAIReviewTool[];
prompts: JsonValue[];
resources: JsonValue[];
}
/** Capture the exact OpenAI grouped surface over a real tools/list request. */
export declare function captureOpenAIReviewMcpSurface(): Promise<OpenAIReviewMcpSurface>;
export declare function buildOpenAIReviewContract(surface: OpenAIReviewMcpSurface, oauth: Record<string, JsonValue>): OpenAIReviewContract;
/** Canonicalize object-key and tools/list order only; arrays remain semantic. */
export declare function canonicalizeOpenAIReviewContract(contract: OpenAIReviewContract): OpenAIReviewContract;
export declare function serializeOpenAIReviewContract(contract: OpenAIReviewContract): string;
/**
* Fail closed on any semantic drift from the reviewed descriptor snapshot.
*/
export declare function assertOpenAIReviewContract(actual: OpenAIReviewContract, expected: OpenAIReviewContract): void;
//# sourceMappingURL=openai-review-contract.d.ts.map
/**
* Fail-closed contract capture for the ChatGPT-reviewed MCP surface.
*
* This intentionally uses a real McpServer, real Client, in-memory MCP
* transport, and the production registration path. It therefore freezes what
* tools/list serializes, not an approximation of registration config objects.
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { applyOpenAIReviewProfiles, OPENAI_REVIEWED_TOOL_ALLOWLIST, } from "./openai-profile.js";
import { SENSITIVE_FIELD_NAMES } from "./redaction.js";
import { registerAllPrompts } from "./prompts/register-all.js";
import { registerAllResources } from "./resources/register-all.js";
import { registerAllTools } from "./tools/register-all.js";
export const OPENAI_REVIEW_CONTRACT_VERSION = 1;
export const OPENAI_REVIEW_BUSINESS_TOOL_COUNT = 53;
export const OPENAI_REVIEW_DISCOVERY_TOOLS = [
"describe_tool",
"list_tool_groups",
"search_tools",
];
export const OPENAI_REVIEW_TOTAL_TOOL_COUNT = 56;
function makeRegistrationClient() {
return new Proxy({}, {
get: () => async () => ({ data: [], total: 0, limit: 0, offset: 0 }),
});
}
function isMethodNotFound(error) {
return (typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === -32601);
}
async function listPromptsOrEmpty(client) {
const prompts = [];
let cursor;
try {
do {
const page = await client.listPrompts(cursor ? { cursor } : undefined);
prompts.push(...page.prompts);
cursor = page.nextCursor;
} while (cursor);
}
catch (error) {
if (isMethodNotFound(error))
return [];
throw error;
}
return prompts;
}
async function listResourcesOrEmpty(client) {
const resources = [];
let cursor;
try {
do {
const page = await client.listResources(cursor ? { cursor } : undefined);
resources.push(...page.resources);
cursor = page.nextCursor;
} while (cursor);
}
catch (error) {
if (isMethodNotFound(error))
return [];
throw error;
}
return resources;
}
/** Capture the exact OpenAI grouped surface over a real tools/list request. */
export async function captureOpenAIReviewMcpSurface() {
const server = new McpServer({
name: "frihet-openai-review-freeze",
version: "1.0.0",
});
applyOpenAIReviewProfiles(server);
registerAllTools(server, makeRegistrationClient());
registerAllResources(server);
registerAllPrompts(server);
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "frihet-openai-review-freeze-client", version: "1.0.0" }, { capabilities: {} });
try {
await Promise.all([
server.connect(serverTransport),
client.connect(clientTransport),
]);
const tools = [];
let cursor;
do {
const page = await client.listTools(cursor ? { cursor } : undefined);
tools.push(...page.tools);
cursor = page.nextCursor;
} while (cursor);
const [prompts, resources] = await Promise.all([
listPromptsOrEmpty(client),
listResourcesOrEmpty(client),
]);
return { tools, prompts, resources };
}
finally {
await Promise.allSettled([client.close(), server.close()]);
}
}
export function buildOpenAIReviewContract(surface, oauth) {
return {
contractVersion: OPENAI_REVIEW_CONTRACT_VERSION,
tools: surface.tools,
prompts: surface.prompts,
resources: surface.resources,
oauth,
};
}
function canonicalizeValue(value) {
if (Array.isArray(value))
return value.map(canonicalizeValue);
if (value !== null && typeof value === "object") {
return Object.fromEntries(Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, child]) => [key, canonicalizeValue(child)]));
}
return value;
}
/** Canonicalize object-key and tools/list order only; arrays remain semantic. */
export function canonicalizeOpenAIReviewContract(contract) {
const canonical = canonicalizeValue(contract);
canonical.tools.sort((left, right) => left.name.localeCompare(right.name));
return canonical;
}
export function serializeOpenAIReviewContract(contract) {
return `${JSON.stringify(canonicalizeOpenAIReviewContract(contract), null, 2)}\n`;
}
function schemaSensitivePaths(tools) {
const sensitive = new Set(SENSITIVE_FIELD_NAMES.map((field) => field.toLowerCase()));
// `documentNumber` is an invoice sequence identifier on these two tools,
// not the guest/customer identity-document field covered by the shared
// redaction policy. Keep the exception exact so any future occurrence is
// still rejected by default.
const nonSensitiveBusinessPaths = new Set([
"create_invoice.inputSchema.properties.documentNumber",
"update_invoice.inputSchema.properties.documentNumber",
]);
const paths = new Set();
const visit = (value, path) => {
if (value === undefined || value === null || typeof value !== "object")
return;
if (Array.isArray(value)) {
value.forEach((child, index) => visit(child, `${path}[${index}]`));
return;
}
for (const [key, child] of Object.entries(value)) {
const childPath = `${path}.${key}`;
if (sensitive.has(key.toLowerCase()) &&
!nonSensitiveBusinessPaths.has(childPath)) {
paths.add(childPath);
}
visit(child, childPath);
}
};
for (const tool of tools) {
visit(tool.inputSchema, `${tool.name}.inputSchema`);
visit(tool.outputSchema, `${tool.name}.outputSchema`);
}
return paths;
}
function firstDifference(expected, actual, path = "$") {
if (Object.is(expected, actual))
return undefined;
if (Array.isArray(expected) || Array.isArray(actual)) {
if (!Array.isArray(expected) || !Array.isArray(actual)) {
return { path, expected, actual };
}
if (expected.length !== actual.length)
return { path: `${path}.length`, expected: expected.length, actual: actual.length };
for (let index = 0; index < expected.length; index += 1) {
const difference = firstDifference(expected[index], actual[index], `${path}[${index}]`);
if (difference)
return difference;
}
return undefined;
}
if (expected !== null &&
actual !== null &&
typeof expected === "object" &&
typeof actual === "object") {
const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort();
for (const key of keys) {
const difference = firstDifference(expected[key], actual[key], `${path}.${key}`);
if (difference)
return difference;
}
return undefined;
}
return { path, expected, actual };
}
function preview(value) {
const serialized = JSON.stringify(value);
if (serialized === undefined)
return "<missing>";
return serialized.length > 180 ? `${serialized.slice(0, 177)}...` : serialized;
}
/**
* Fail closed on any semantic drift from the reviewed descriptor snapshot.
*/
export function assertOpenAIReviewContract(actual, expected) {
if (actual.contractVersion !== OPENAI_REVIEW_CONTRACT_VERSION) {
throw new Error(`Unsupported OpenAI review contract version: ${actual.contractVersion}`);
}
if (actual.tools.length !== OPENAI_REVIEW_TOTAL_TOOL_COUNT) {
throw new Error(`OpenAI review surface must expose exactly ${OPENAI_REVIEW_TOTAL_TOOL_COUNT} tools; got ${actual.tools.length}`);
}
const names = actual.tools.map((tool) => tool.name);
if (new Set(names).size !== names.length) {
throw new Error("OpenAI review surface contains duplicate tool names");
}
const discovery = new Set(OPENAI_REVIEW_DISCOVERY_TOOLS);
const businessNames = names.filter((name) => !discovery.has(name));
if (businessNames.length !== OPENAI_REVIEW_BUSINESS_TOOL_COUNT) {
throw new Error(`OpenAI review surface must expose exactly ${OPENAI_REVIEW_BUSINESS_TOOL_COUNT} business tools; got ${businessNames.length}`);
}
const missingDiscovery = OPENAI_REVIEW_DISCOVERY_TOOLS.filter((name) => !names.includes(name));
if (missingDiscovery.length > 0) {
throw new Error(`Missing discovery tools: ${missingDiscovery.join(", ")}`);
}
const leaks = businessNames.filter((name) => !OPENAI_REVIEWED_TOOL_ALLOWLIST.has(name));
if (leaks.length > 0) {
throw new Error(`Non-reviewed tools leaked into OpenAI surface: ${leaks.join(", ")}`);
}
if (actual.prompts.length !== 0 || actual.resources.length !== 0) {
throw new Error(`OpenAI review surface must expose 0 prompts and 0 resources; got ${actual.prompts.length} prompts and ${actual.resources.length} resources`);
}
const sensitivePaths = [...schemaSensitivePaths(actual.tools)];
if (sensitivePaths.length > 0) {
throw new Error(`Sensitive schema fields are forbidden in the OpenAI review surface: ${sensitivePaths.join(", ")}`);
}
const expectedSensitivePaths = [...schemaSensitivePaths(expected.tools)];
if (expectedSensitivePaths.length > 0) {
throw new Error(`Frozen OpenAI review snapshot contains sensitive schema fields: ${expectedSensitivePaths.join(", ")}`);
}
const canonicalExpected = canonicalizeOpenAIReviewContract(expected);
const canonicalActual = canonicalizeOpenAIReviewContract(actual);
const difference = firstDifference(canonicalExpected, canonicalActual);
if (difference) {
throw new Error(`OpenAI review descriptor drift at ${difference.path}: expected ${preview(difference.expected)}, got ${preview(difference.actual)}`);
}
}
//# sourceMappingURL=openai-review-contract.js.map
/**
* OAuth contract inputs shared by the real Worker and the OpenAI descriptor
* freeze gate. Values in this module are public protocol metadata, not secrets.
*/
export declare const OPENAI_REVIEW_ORIGIN = "https://openai-mcp.frihet.io";
/**
* Byte-compatible extraction of the options previously inlined in the Worker.
* index.ts spreads this object into the real OAuthProvider constructor.
*/
export declare const OAUTH_PROVIDER_REVIEW_OPTIONS: {
apiRoute: string;
authorizeEndpoint: string;
tokenEndpoint: string;
clientRegistrationEndpoint: string;
scopesSupported: string[];
accessTokenTTL: number;
refreshTokenTTL: number;
allowPlainPKCE: false;
};
/**
* Materialize the public metadata generated by workers-oauth-provider 0.3.0
* from the same options used by the Worker. The dependency version is pinned
* separately in the canonical descriptor snapshot.
*/
export declare function buildOpenAIReviewOAuthContract(origin?: string): {
authorizationServer: {
issuer: string;
authorization_endpoint: string;
token_endpoint: string;
registration_endpoint: string;
scopes_supported: string[];
response_types_supported: string[];
response_modes_supported: string[];
grant_types_supported: string[];
token_endpoint_auth_methods_supported: string[];
revocation_endpoint: string;
code_challenge_methods_supported: string[];
client_id_metadata_document_supported: boolean;
};
protectedResource: {
resource: string;
authorization_servers: string[];
scopes_supported: string[];
bearer_methods_supported: string[];
};
wwwAuthenticate: {
resourceMetadataUrl: string;
missingTokenHeader: string;
};
};
//# sourceMappingURL=openai-review-oauth.d.ts.map
/**
* OAuth contract inputs shared by the real Worker and the OpenAI descriptor
* freeze gate. Values in this module are public protocol metadata, not secrets.
*/
export const OPENAI_REVIEW_ORIGIN = "https://openai-mcp.frihet.io";
/**
* Byte-compatible extraction of the options previously inlined in the Worker.
* index.ts spreads this object into the real OAuthProvider constructor.
*/
export const OAUTH_PROVIDER_REVIEW_OPTIONS = {
apiRoute: "/mcp",
authorizeEndpoint: "/authorize",
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
scopesSupported: ["read", "write"],
accessTokenTTL: 3600,
refreshTokenTTL: 2592000,
allowPlainPKCE: false,
};
function endpoint(origin, path) {
return new URL(path, new URL(origin).origin).toString();
}
/**
* Materialize the public metadata generated by workers-oauth-provider 0.3.0
* from the same options used by the Worker. The dependency version is pinned
* separately in the canonical descriptor snapshot.
*/
export function buildOpenAIReviewOAuthContract(origin = OPENAI_REVIEW_ORIGIN) {
const normalizedOrigin = new URL(origin).origin;
const authorizationEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.authorizeEndpoint);
const tokenEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.tokenEndpoint);
const registrationEndpoint = endpoint(normalizedOrigin, OAUTH_PROVIDER_REVIEW_OPTIONS.clientRegistrationEndpoint);
const resourceMetadataUrl = endpoint(normalizedOrigin, "/.well-known/oauth-protected-resource");
return {
authorizationServer: {
issuer: normalizedOrigin,
authorization_endpoint: authorizationEndpoint,
token_endpoint: tokenEndpoint,
registration_endpoint: registrationEndpoint,
scopes_supported: [...OAUTH_PROVIDER_REVIEW_OPTIONS.scopesSupported],
response_types_supported: ["code"],
response_modes_supported: ["query"],
grant_types_supported: ["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported: [
"client_secret_basic",
"client_secret_post",
"none",
],
revocation_endpoint: tokenEndpoint,
code_challenge_methods_supported: ["S256"],
client_id_metadata_document_supported: false,
},
protectedResource: {
resource: normalizedOrigin,
authorization_servers: [normalizedOrigin],
scopes_supported: [...OAUTH_PROVIDER_REVIEW_OPTIONS.scopesSupported],
bearer_methods_supported: ["header"],
},
wwwAuthenticate: {
resourceMetadataUrl,
missingTokenHeader: `Bearer realm="OAuth", resource_metadata="${resourceMetadataUrl}", ` +
'error="invalid_token", error_description="Missing or invalid access token"',
},
};
}
//# sourceMappingURL=openai-review-oauth.js.map
+32
-2

@@ -28,2 +28,30 @@ /**

/**
* Origin marker sent on every request so the backend can tell an MCP-driven
* create apart from a direct-API one.
*
* The backend classifier (`detectApiInvoiceSource`, Frihet-ERP
* functions/src/publicApi.ts) matches `mcp` / `frihet-mcp` / `@frihet/mcp`
* across three headers: `x-frihet-source`, `x-frihet-client` and `user-agent`.
* We send TWO markers because they survive different network paths:
*
* - `X-Frihet-Source: mcp` is the explicit, documented marker. It reaches the
* backend when the client talks to the Cloud Function directly (custom
* `baseUrl`, self-hosted proxy).
* - `User-Agent` is what actually reaches the backend on the DEFAULT baseUrl:
* `api.frihet.io` is fronted by workers/api-proxy/worker.js, whose
* `ALLOWED_REQUEST_HEADERS` allowlist forwards `user-agent` but drops
* `x-frihet-source` (verified live: /agents.json answers 200 at
* api.frihet.io and 401 at the Cloud Function, so the Worker is in path).
* Without the UA the source header is a phantom — set, then stripped at the
* edge, and every MCP invoice still lands as `source: 'api'`.
*
* Deliberately carries no version: the version lives in package.json only
* (see src/index.ts PKG_VERSION) and a second hardcoded copy is exactly the
* drift `scripts/audit-mcp-refs.mjs` exists to catch.
*
* Pinned on the wire by src/__tests__/source-header-contract.test.ts.
*/
const SOURCE_MARKER = "mcp";
const SOURCE_USER_AGENT = "frihet-mcp-server";
/**
* Fresh idempotency key, always a syntactically valid UUID v4.

@@ -106,2 +134,4 @@ *

Accept: "application/json",
"X-Frihet-Source": SOURCE_MARKER,
"User-Agent": SOURCE_USER_AGENT,
};

@@ -668,6 +698,6 @@ if (resolvedIdempotencyKey) {

async getBusinessContext() {
return this.request("GET", "/context");
return this.requestUnwrapped("GET", "/context");
}
async getMonthlySummary(month) {
return this.request("GET", "/monthly", undefined, {
return this.requestUnwrapped("GET", "/monthly", undefined, {
month,

@@ -674,0 +704,0 @@ });

+38
-12

@@ -260,7 +260,32 @@ /**

return {
businessName: "Demo Studio SL",
fiscalRegime: "autonomo",
currency: "EUR",
country: "ES",
metrics: { openInvoices: 4, overdueInvoices: 1, totalClients: demoClients.length, totalProducts: demoProducts.length },
business: {
name: "Demo Studio SL",
fiscalZone: "IVA",
currency: "EUR",
language: "es",
country: "ES",
},
defaults: { taxRate: 21, irpfRate: 15, dueDays: 30, currency: "EUR" },
plan: {
name: "free",
invoices: { used: 5, limit: 999 },
expenses: { used: demoExpenses.length, limit: 5 },
aiMessages: { used: 2, limit: 30 },
},
series: [{ id: "default", prefix: "F", current: 5, year: 2026 }],
recentActivity: {
lastInvoice: { number: "F-2026-005", date: "2026-07-18", client: "Acme Studio" },
lastExpense: { date: "2026-07-17", vendor: "Demo Supplies", amount: 405.6 },
overdueCount: 1,
overdueAmount: 640,
unpaidCount: 2,
},
topClients: [{ name: "Acme Studio", totalRevenue: 2200, invoiceCount: 2 }],
currentMonth: {
revenue: 3960.4,
expenses: 405.6,
profit: 3554.8,
invoiceCount: 5,
expenseCount: demoExpenses.length,
},
...READ_STAMP,

@@ -271,9 +296,10 @@ };

return {
month: month ?? "2026-07",
revenue: 3960.4,
expenses: 405.6,
net: 3554.8,
invoiceCount: 5,
expenseCount: demoExpenses.length,
currency: "EUR",
period: month ?? "2026-07",
revenue: { total: 3960.4, taxBase: 3273.06, tax: 687.34, irpf: 0 },
expenses: { total: 405.6, deductible: 405.6, tax: 70.4 },
profit: { gross: 3554.8, net: 2937.86 },
invoices: { created: 5, sent: 1, paid: 3, overdue: 1 },
topClients: [{ name: "Acme Studio", totalRevenue: 2200, invoiceCount: 2 }],
byCategory: { supplies: 405.6 },
taxLiability: { vatPayable: 616.94, irpfRetained: 0, estimatedModel303: 616.94 },
...READ_STAMP,

@@ -280,0 +306,0 @@ };

@@ -66,4 +66,12 @@ /**

export declare const OPENAI_REVIEWED_TOOL_ALLOWLIST: ReadonlySet<string>;
/**
* Apply the exact profile composition used by the ChatGPT review Worker.
*
* Keep this helper as the single source for the security-sensitive ordering:
* grouped exposure first (with the frozen allow-list), OpenAI profile second.
* Tool registration happens afterwards through registerAllTools().
*/
export declare function applyOpenAIReviewProfiles(server: any): void;
/** Number of resources excluded in OpenAI mode (for logging). */
export declare const OPENAI_EXCLUDED_RESOURCE_COUNT: number;
//# sourceMappingURL=openai-profile.d.ts.map

@@ -23,5 +23,6 @@ /**

*/
import { z } from "zod";
import { z } from "zod/v4";
import { MCP_RESOURCE_COUNT } from "./resources/register-all.js";
import { SENSITIVE_FIELD_NAMES, deepRedact, redactText } from "./redaction.js";
import { applyToolExposureProfile } from "./tool-exposure.js";
const PROFILE = {

@@ -168,2 +169,13 @@ // -- OpenAI-reviewed core surface ----------------------------------------

stripInputFields: {
// Projection is useful to direct MCP clients, but unnecessary in the
// reviewed ChatGPT surface. OpenAI's scanner treats a comma-delimited
// free-form string as ambiguous (array vs JSON vs CSV), so omit it and
// return the full, redacted record shape instead.
list_invoices: ["fields"],
search_invoices: ["fields"],
list_expenses: ["fields"],
list_clients: ["fields"],
list_products: ["fields"],
list_quotes: ["fields"],
list_vendors: ["fields"],
create_client: ["taxId"], // NIF/CIF/VAT — government-issued identifier

@@ -207,7 +219,3 @@ update_client: ["taxId"],

function stripSensitiveOutputSchema(schema, fields) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const def = schema?._def;
const typeName = def?.typeName;
if (typeName === "ZodObject") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (schema instanceof z.ZodObject) {
const shape = schema.shape;

@@ -228,9 +236,23 @@ const newShape = {};

return schema;
let rebuilt = z.object(newShape);
if (typeof def.description === "string")
rebuilt = rebuilt.describe(def.description);
const base = z.object(newShape);
// Zod v4 stores passthrough/strict behavior in `catchall`. Preserve it so
// redacting one declared field never changes validation of unrelated API
// fields. The previous implementation inspected Zod v3's `_def.typeName`,
// which is absent in v4 and silently returned the original schema.
const catchall = schema._def.catchall;
let rebuilt;
if (catchall instanceof z.ZodUnknown)
rebuilt = base.passthrough();
else if (catchall instanceof z.ZodNever)
rebuilt = base.strict();
else if (catchall)
rebuilt = base.catchall(catchall);
else
rebuilt = base;
if (typeof schema.description === "string")
rebuilt = rebuilt.describe(schema.description);
return rebuilt;
}
if (typeName === "ZodArray") {
const inner = def.type;
if (schema instanceof z.ZodArray) {
const inner = schema.element;
const stripped = stripSensitiveOutputSchema(inner, fields);

@@ -240,13 +262,13 @@ if (stripped === inner)

let rebuilt = z.array(stripped);
if (typeof def.description === "string")
rebuilt = rebuilt.describe(def.description);
if (typeof schema.description === "string")
rebuilt = rebuilt.describe(schema.description);
return rebuilt;
}
if (typeName === "ZodOptional") {
const inner = def.innerType;
if (schema instanceof z.ZodOptional) {
const inner = schema.unwrap();
const stripped = stripSensitiveOutputSchema(inner, fields);
return stripped === inner ? schema : z.optional(stripped);
}
if (typeName === "ZodNullable") {
const inner = def.innerType;
if (schema instanceof z.ZodNullable) {
const inner = schema.unwrap();
const stripped = stripSensitiveOutputSchema(inner, fields);

@@ -440,2 +462,16 @@ return stripped === inner ? schema : z.nullable(stripped);

export const OPENAI_REVIEWED_TOOL_ALLOWLIST = PROFILE.includeTools;
/**
* Apply the exact profile composition used by the ChatGPT review Worker.
*
* Keep this helper as the single source for the security-sensitive ordering:
* grouped exposure first (with the frozen allow-list), OpenAI profile second.
* Tool registration happens afterwards through registerAllTools().
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function applyOpenAIReviewProfiles(server) {
applyToolExposureProfile(server, {
allowlist: OPENAI_REVIEWED_TOOL_ALLOWLIST,
});
applyOpenAIProfile(server);
}
/** Number of resources excluded in OpenAI mode (for logging). */

@@ -442,0 +478,0 @@ export const OPENAI_EXCLUDED_RESOURCE_COUNT = PROFILE.excludeResources

@@ -13,3 +13,94 @@ /**

import { z } from "zod/v4";
import { withToolLogging, formatRecord, getContent, mutateContent, openObjectOutput, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, } from "./shared.js";
import { withToolLogging, formatRecord, getContent, mutateContent, invoiceItemOutput, openObjectOutput, READ_ONLY_ANNOTATIONS, CREATE_ANNOTATIONS, } from "./shared.js";
const usageLimitOutput = z.union([z.number(), z.literal("unlimited")]);
const businessContextOutput = z.object({
business: z.object({
name: z.string(),
taxId: z.string().optional(),
fiscalZone: z.string(),
currency: z.string(),
language: z.string(),
country: z.string(),
}).passthrough(),
defaults: z.object({
taxRate: z.number(),
irpfRate: z.number(),
dueDays: z.number(),
currency: z.string(),
}).passthrough(),
plan: z.object({
name: z.string(),
invoices: z.object({ used: z.number(), limit: usageLimitOutput }),
expenses: z.object({ used: z.number(), limit: usageLimitOutput }),
aiMessages: z.object({ used: z.number(), limit: usageLimitOutput }),
}).passthrough(),
series: z.array(z.object({
id: z.string().optional(),
prefix: z.string().optional(),
current: z.number().optional(),
year: z.number().optional(),
}).passthrough()),
recentActivity: z.object({
lastInvoice: z.object({
number: z.string().optional(),
date: z.string().optional(),
client: z.string().optional(),
}).passthrough().nullable(),
lastExpense: z.object({
date: z.string().optional(),
vendor: z.string().optional(),
amount: z.number(),
}).passthrough().nullable(),
overdueCount: z.number(),
overdueAmount: z.number(),
unpaidCount: z.number(),
}).passthrough(),
topClients: z.array(z.object({
name: z.string(),
totalRevenue: z.number(),
invoiceCount: z.number(),
}).passthrough()),
currentMonth: z.object({
revenue: z.number(),
expenses: z.number(),
profit: z.number(),
invoiceCount: z.number(),
expenseCount: z.number(),
}).passthrough(),
}).passthrough().describe("Business context snapshot: workspace profile, fiscal setup, plan usage, recent activity and current-month performance");
const monthlySummaryOutput = z.object({
period: z.string(),
revenue: z.object({
total: z.number(),
taxBase: z.number(),
tax: z.number(),
irpf: z.number(),
}).passthrough(),
expenses: z.object({
total: z.number(),
deductible: z.number(),
tax: z.number(),
}).passthrough(),
profit: z.object({
gross: z.number(),
net: z.number(),
}).passthrough(),
invoices: z.object({
created: z.number(),
sent: z.number(),
paid: z.number(),
overdue: z.number(),
}).passthrough(),
topClients: z.array(z.object({
name: z.string(),
totalRevenue: z.number(),
invoiceCount: z.number(),
}).passthrough()),
byCategory: z.record(z.string(), z.number()),
taxLiability: z.object({
vatPayable: z.number(),
irpfRetained: z.number(),
estimatedModel303: z.number(),
}).passthrough(),
}).passthrough().describe("Monthly financial summary with revenue, expenses, profit, invoice status, top clients and estimated tax liability");
export function registerIntelligenceTools(server, client) {

@@ -26,3 +117,3 @@ // -- get_business_context --

inputSchema: {},
outputSchema: openObjectOutput("Business context snapshot: workspace profile, fiscal setup, recent activity / Contexto de negocio: perfil, configuración fiscal y actividad reciente"),
outputSchema: businessContextOutput,
}, async () => withToolLogging("get_business_context", async () => {

@@ -51,3 +142,3 @@ const result = await client.getBusinessContext();

},
outputSchema: openObjectOutput("Monthly P&L summary: revenue, expenses, profit, tax liability, invoice stats / Resumen mensual: ingresos, gastos, beneficio, impuestos"),
outputSchema: monthlySummaryOutput,
}, async ({ month }) => withToolLogging("get_monthly_summary", async () => {

@@ -107,3 +198,3 @@ const result = await client.getMonthlySummary(month);

},
outputSchema: openObjectOutput("The newly created draft invoice, cloned from the original / La nueva factura borrador, clonada de la original"),
outputSchema: invoiceItemOutput.describe("The newly created draft invoice, cloned from the original / La nueva factura borrador, clonada de la original"),
}, async ({ id, newIssueDate, newDueDate }) => withToolLogging("duplicate_invoice", async () => {

@@ -110,0 +201,0 @@ // 1. Fetch the original invoice (returns the FULL raw stored document —

{
"name": "@frihet/mcp-server",
"version": "1.16.5",
"version": "1.16.6",
"description": "AI-native MCP server for Frihet ERP — 157 tools: invoicing, expenses, CRM, banking, POS + ES/EU fiscal compliance (VeriFactu, TicketBAI, Facturae). Zero-install at mcp.frihet.io. Works with Claude, ChatGPT, Cursor, Windsurf, Cline & any MCP client.",

@@ -12,3 +12,5 @@ "type": "module",

"build": "tsc",
"test": "npm run build && node --test dist/__tests__/openai-profile.test.js dist/__tests__/tool-exposure.test.js dist/__tests__/einvoice-tools.test.js dist/__tests__/einvoice-day4-tools.test.js dist/__tests__/stay-tools.test.js dist/__tests__/pos-tools.test.js dist/__tests__/kitchen-tools.test.js dist/__tests__/banking-tools.test.js dist/__tests__/banking-client-contract.test.js dist/__tests__/pagination-cursor-param.test.js dist/__tests__/fiscal-tools.test.js dist/__tests__/time-tools.test.js dist/__tests__/recurring-tools.test.js dist/__tests__/team-tools.test.js dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js dist/__tests__/audit-server-version.test.js dist/__tests__/openai-grouped-compose.test.js dist/__tests__/contract.test.js dist/__tests__/observability-redaction.test.js dist/__tests__/intelligence-duplicate-invoice.test.js dist/__tests__/get-envelope-unwrap-regression.test.js dist/__tests__/mutation-unwrap-and-schema-regression.test.js dist/__tests__/schema-envelope-guard.test.js dist/__tests__/demo-mode.test.js dist/__tests__/idempotency-key-contract.test.js",
"test": "npm run build && node --test dist/__tests__/openai-profile.test.js dist/__tests__/tool-exposure.test.js dist/__tests__/einvoice-tools.test.js dist/__tests__/einvoice-day4-tools.test.js dist/__tests__/stay-tools.test.js dist/__tests__/pos-tools.test.js dist/__tests__/kitchen-tools.test.js dist/__tests__/banking-tools.test.js dist/__tests__/banking-client-contract.test.js dist/__tests__/pagination-cursor-param.test.js dist/__tests__/fiscal-tools.test.js dist/__tests__/time-tools.test.js dist/__tests__/recurring-tools.test.js dist/__tests__/team-tools.test.js dist/__tests__/d4b-hr-payroll-onboarding-tools.test.js dist/__tests__/audit-server-version.test.js dist/__tests__/openai-grouped-compose.test.js dist/__tests__/openai-review-descriptor.test.js dist/__tests__/contract.test.js dist/__tests__/observability-redaction.test.js dist/__tests__/intelligence-duplicate-invoice.test.js dist/__tests__/get-envelope-unwrap-regression.test.js dist/__tests__/mutation-unwrap-and-schema-regression.test.js dist/__tests__/schema-envelope-guard.test.js dist/__tests__/demo-mode.test.js dist/__tests__/idempotency-key-contract.test.js dist/__tests__/source-header-contract.test.js",
"test:openai-review-descriptor": "npm run build && node --test dist/__tests__/openai-review-descriptor.test.js",
"gate:openai-review-descriptor": "npm run build && node scripts/check-openai-review-descriptor.mjs",
"start": "node dist/index.js",

@@ -15,0 +17,0 @@ "postinstall": "node scripts/postinstall.js || true",

@@ -50,3 +50,3 @@ <p align="center">

> **Tool count:** the package (1.16.5) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).
> **Tool count:** the package (1.16.6) ships all 157 tools, same as the remote endpoint (`mcp.frihet.io`).

@@ -53,0 +53,0 @@ ---