import { workflowMcpTool } from '@sim/db' import { and, eq, isNull, ne, sql } from 'drizzle-orm' import type { DbOrTx } from '@/lib/db/types' import { MAX_MCP_PARAMETER_SCHEMA_BYTES, MAX_MCP_SERVER_PARAMETER_SCHEMAS_BYTES, MAX_MCP_SERVER_TOOLS_METADATA_BYTES, MAX_MCP_TOOL_DESCRIPTION_BYTES, MAX_MCP_TOOL_NAME_BYTES, } from '@/lib/mcp/constants' function utf8Size(value: string): number { return Buffer.byteLength(value, 'utf-8') } function jsonSize(value: unknown): number | null { try { const json = JSON.stringify(value) return typeof json === 'string' ? utf8Size(json) : null } catch { return null } } export interface McpToolMetadataSizes { toolNameBytes: number toolDescriptionBytes: number parameterSchemaBytes: number } export interface McpToolMetadataUsage { schemaBytes: number metadataBytes: number } export interface McpToolMetadataUsageRow extends McpToolMetadataSizes { id: string } export function getMcpToolMetadataSizes(metadata: { toolName?: string | null toolDescription?: string | null parameterSchema?: unknown }): McpToolMetadataSizes { return { toolNameBytes: metadata.toolName ? utf8Size(metadata.toolName) : 0, toolDescriptionBytes: metadata.toolDescription ? utf8Size(metadata.toolDescription) : 0, parameterSchemaBytes: metadata.parameterSchema !== undefined ? (jsonSize(metadata.parameterSchema) ?? MAX_MCP_PARAMETER_SCHEMA_BYTES + 1) : 0, } } export function addMcpToolMetadataUsage( usage: McpToolMetadataUsage, tool: { toolName?: string | null toolDescription?: string | null parameterSchema?: unknown } ): McpToolMetadataUsage { const sizes = getMcpToolMetadataSizes(tool) return { schemaBytes: usage.schemaBytes + sizes.parameterSchemaBytes, metadataBytes: usage.metadataBytes + sizes.toolNameBytes + sizes.toolDescriptionBytes + sizes.parameterSchemaBytes, } } export function addMcpToolMetadataUsageRow( usage: McpToolMetadataUsage, row: McpToolMetadataUsageRow ): McpToolMetadataUsage { return { schemaBytes: usage.schemaBytes + row.parameterSchemaBytes, metadataBytes: usage.metadataBytes + row.toolNameBytes + row.toolDescriptionBytes + row.parameterSchemaBytes, } } export function subtractMcpToolMetadataUsageRow( usage: McpToolMetadataUsage, row?: McpToolMetadataUsageRow ): McpToolMetadataUsage { if (!row) return usage return { schemaBytes: usage.schemaBytes - row.parameterSchemaBytes, metadataBytes: usage.metadataBytes - row.toolNameBytes - row.toolDescriptionBytes - row.parameterSchemaBytes, } } export function getMcpToolMetadataUsageFromRows( rows: McpToolMetadataUsageRow[] ): McpToolMetadataUsage { return rows.reduce(addMcpToolMetadataUsageRow, { schemaBytes: 0, metadataBytes: 0 }) } export function createMcpToolMetadataUsageRow(tool: { id: string toolName: string toolDescription: string | null parameterSchema: unknown }): McpToolMetadataUsageRow { return { id: tool.id, ...getMcpToolMetadataSizes(tool) } } export function validateMcpServerToolMetadataBudget(usage: McpToolMetadataUsage): string | null { if (usage.schemaBytes > MAX_MCP_SERVER_PARAMETER_SCHEMAS_BYTES) { return `MCP server tool schemas exceed maximum size of ${MAX_MCP_SERVER_PARAMETER_SCHEMAS_BYTES} bytes` } if (usage.metadataBytes > MAX_MCP_SERVER_TOOLS_METADATA_BYTES) { return `MCP server tool metadata exceeds maximum size of ${MAX_MCP_SERVER_TOOLS_METADATA_BYTES} bytes` } return null } export function exceedsMcpServerToolMetadataBudget( usage: McpToolMetadataUsage, tool: { toolName: string; toolDescription: string | null; parameterSchema: unknown } ): boolean { return validateMcpServerToolMetadataBudget(addMcpToolMetadataUsage(usage, tool)) !== null } export async function getMcpServerToolMetadataUsageRows( tx: DbOrTx, serverId: string, excludeToolId?: string ): Promise { const rows = await tx .select({ id: workflowMcpTool.id, toolNameBytes: sql`octet_length(${workflowMcpTool.toolName})`, toolDescriptionBytes: sql`coalesce(octet_length(${workflowMcpTool.toolDescription}), 0)`, parameterSchemaBytes: sql`octet_length(${workflowMcpTool.parameterSchema}::text)`, }) .from(workflowMcpTool) .where( and( eq(workflowMcpTool.serverId, serverId), isNull(workflowMcpTool.archivedAt), excludeToolId ? ne(workflowMcpTool.id, excludeToolId) : undefined ) ) return rows.map((row) => ({ id: row.id, toolNameBytes: Number(row.toolNameBytes) || 0, toolDescriptionBytes: Number(row.toolDescriptionBytes) || 0, parameterSchemaBytes: Number(row.parameterSchemaBytes) || 0, })) } export function validateMcpToolMetadataForStorage(metadata: { toolName?: string | null toolDescription?: string | null parameterSchema?: unknown }): string | null { if (metadata.toolName && utf8Size(metadata.toolName) > MAX_MCP_TOOL_NAME_BYTES) { return `Tool name exceeds maximum size of ${MAX_MCP_TOOL_NAME_BYTES} bytes` } if ( metadata.toolDescription && utf8Size(metadata.toolDescription) > MAX_MCP_TOOL_DESCRIPTION_BYTES ) { return `Tool description exceeds maximum size of ${MAX_MCP_TOOL_DESCRIPTION_BYTES} bytes` } if (metadata.parameterSchema !== undefined) { const parameterSchemaBytes = jsonSize(metadata.parameterSchema) if (parameterSchemaBytes === null) { return 'Tool parameter schema must be JSON serializable' } if (parameterSchemaBytes > MAX_MCP_PARAMETER_SCHEMA_BYTES) { return `Tool parameter schema exceeds maximum size of ${MAX_MCP_PARAMETER_SCHEMA_BYTES} bytes` } } return null }