Roo-Code/src/utils/mcp-name.ts

190 lines
6.4 KiB
TypeScript

/**
* Utilities for sanitizing MCP server and tool names to conform to
* API function name requirements across all providers.
*/
/**
* Separator used between MCP prefix, server name, and tool name.
* We use "--" (double hyphen) because:
* 1. It's allowed by all providers (dashes are permitted in function names)
* 2. It won't conflict with underscores in sanitized server/tool names
* 3. It's unique enough to be a reliable delimiter for parsing
*/
export const MCP_TOOL_SEPARATOR = "--"
/**
* Prefix for all MCP tool function names.
*/
export const MCP_TOOL_PREFIX = "mcp"
/**
* Normalize a string for comparison by treating hyphens and underscores as equivalent.
* This is used to match tool names when models convert hyphens to underscores.
*
* @param name - The name to normalize
* @returns The normalized name with all hyphens converted to underscores
*/
export function normalizeForComparison(name: string): string {
return name.replace(/-/g, "_")
}
/**
* Normalize an MCP tool name by converting underscore separators back to hyphens.
* This handles the case where models (especially Claude) convert hyphens to underscores
* in tool names when using native tool calling.
*
* For example: "mcp__server__tool" -> "mcp--server--tool"
*
* This function uses fuzzy matching - it treats hyphens and underscores as equivalent
* when normalizing the separator pattern.
*
* @param toolName - The tool name that may have underscore separators
* @returns The normalized tool name with hyphen separators
*/
export function normalizeMcpToolName(toolName: string): string {
// Normalize for comparison to detect MCP tools regardless of separator style
const normalized = normalizeForComparison(toolName)
// Only normalize if it looks like an MCP tool (starts with mcp__)
if (normalized.startsWith("mcp__")) {
// Find the pattern: mcp{sep}server{sep}tool where sep is -- or __
// We need to convert the separators while preserving the rest
// First, try to parse assuming all separators are underscores
// Pattern: mcp__server__tool or mcp__server__tool_with_underscores
const parts = toolName.split(/__|--/)
if (parts.length >= 3 && parts[0].toLowerCase() === "mcp") {
// Reconstruct with proper -- separators
const serverName = parts[1]
const toolNamePart = parts.slice(2).join("--") // Rejoin in case tool name had separator
return `${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR}${serverName}${MCP_TOOL_SEPARATOR}${toolNamePart}`
}
}
return toolName
}
/**
* Check if a tool name is an MCP tool (starts with the MCP prefix and separator).
* Uses fuzzy matching to handle both hyphen and underscore separators.
*
* @param toolName - The tool name to check
* @returns true if the tool name starts with "mcp--" or "mcp__", false otherwise
*/
export function isMcpTool(toolName: string): boolean {
const normalized = normalizeForComparison(toolName)
return normalized.startsWith(`${MCP_TOOL_PREFIX}__`)
}
/**
* Sanitize a name to be safe for use in API function names.
* This removes special characters and ensures the name starts correctly.
*
* Note: Hyphens are preserved since they are valid in function names.
* Models may convert hyphens to underscores, but we handle this with
* fuzzy matching when parsing tool names.
*
* @param name - The original name (e.g., MCP server name or tool name)
* @returns A sanitized name that conforms to API requirements
*/
export function sanitizeMcpName(name: string): string {
if (!name) {
return "_"
}
// Replace spaces with underscores first
let sanitized = name.replace(/\s+/g, "_")
// Only allow alphanumeric, underscores, and hyphens
sanitized = sanitized.replace(/[^a-zA-Z0-9_\-]/g, "")
// Replace any double-hyphen sequences with single hyphen to avoid separator conflicts
sanitized = sanitized.replace(/--+/g, "-")
// Ensure the name starts with a letter or underscore
if (sanitized.length > 0 && !/^[a-zA-Z_]/.test(sanitized)) {
sanitized = "_" + sanitized
}
// If empty after sanitization, use a placeholder
if (!sanitized) {
sanitized = "_unnamed"
}
return sanitized
}
/**
* Build a full MCP tool function name from server and tool names.
* The format is: mcp--{sanitized_server_name}--{sanitized_tool_name}
*
* The total length is capped at 64 characters to conform to API limits.
*
* @param serverName - The MCP server name
* @param toolName - The tool name
* @returns A sanitized function name in the format mcp--serverName--toolName
*/
export function buildMcpToolName(serverName: string, toolName: string): string {
const sanitizedServer = sanitizeMcpName(serverName)
const sanitizedTool = sanitizeMcpName(toolName)
// Build the full name: mcp--{server}--{tool}
const fullName = `${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR}${sanitizedServer}${MCP_TOOL_SEPARATOR}${sanitizedTool}`
// Truncate if necessary (max 64 chars for Gemini)
if (fullName.length > 64) {
return fullName.slice(0, 64)
}
return fullName
}
/**
* Parse an MCP tool function name back into server and tool names.
* This handles both hyphen and underscore separators using fuzzy matching.
*
* @param mcpToolName - The full MCP tool name (e.g., "mcp--weather--get_forecast" or "mcp__weather__get_forecast")
* @returns An object with serverName and toolName, or null if parsing fails
*/
export function parseMcpToolName(mcpToolName: string): { serverName: string; toolName: string } | null {
// Normalize the name to handle both separator styles
const normalizedName = normalizeMcpToolName(mcpToolName)
const prefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR
if (!normalizedName.startsWith(prefix)) {
return null
}
// Remove the "mcp--" prefix
const remainder = normalizedName.slice(prefix.length)
// Split on the separator to get server and tool names
const separatorIndex = remainder.indexOf(MCP_TOOL_SEPARATOR)
if (separatorIndex === -1) {
return null
}
const serverName = remainder.slice(0, separatorIndex)
const toolName = remainder.slice(separatorIndex + MCP_TOOL_SEPARATOR.length)
if (!serverName || !toolName) {
return null
}
return {
serverName,
toolName,
}
}
/**
* Check if two tool names match using fuzzy comparison.
* Treats hyphens and underscores as equivalent.
*
* @param name1 - First tool name
* @param name2 - Second tool name
* @returns true if the names match (treating - and _ as equivalent)
*/
export function toolNamesMatch(name1: string, name2: string): boolean {
return normalizeForComparison(name1) === normalizeForComparison(name2)
}