/ services / mcp / mcpStringUtils.ts
mcpStringUtils.ts
  1  /**
  2   * Pure string utility functions for MCP tool/server name parsing.
  3   * This file has no heavy dependencies to keep it lightweight for
  4   * consumers that only need string parsing (e.g., permissionValidation).
  5   */
  6  
  7  import { normalizeNameForMCP } from './normalization.js'
  8  
  9  /*
 10   * Extracts MCP server information from a tool name string
 11   * @param toolString The string to parse. Expected format: "mcp__serverName__toolName"
 12   * @returns An object containing server name and optional tool name, or null if not a valid MCP rule
 13   *
 14   * Known limitation: If a server name contains "__", parsing will be incorrect.
 15   * For example, "mcp__my__server__tool" would parse as server="my" and tool="server__tool"
 16   * instead of server="my__server" and tool="tool". This is rare in practice since server
 17   * names typically don't contain double underscores.
 18   */
 19  export function mcpInfoFromString(toolString: string): {
 20    serverName: string
 21    toolName: string | undefined
 22  } | null {
 23    const parts = toolString.split('__')
 24    const [mcpPart, serverName, ...toolNameParts] = parts
 25    if (mcpPart !== 'mcp' || !serverName) {
 26      return null
 27    }
 28    // Join all parts after server name to preserve double underscores in tool names
 29    const toolName =
 30      toolNameParts.length > 0 ? toolNameParts.join('__') : undefined
 31    return { serverName, toolName }
 32  }
 33  
 34  /**
 35   * Generates the MCP tool/command name prefix for a given server
 36   * @param serverName Name of the MCP server
 37   * @returns The prefix string
 38   */
 39  export function getMcpPrefix(serverName: string): string {
 40    return `mcp__${normalizeNameForMCP(serverName)}__`
 41  }
 42  
 43  /**
 44   * Builds a fully qualified MCP tool name from server and tool names.
 45   * Inverse of mcpInfoFromString().
 46   * @param serverName Name of the MCP server (unnormalized)
 47   * @param toolName Name of the tool (unnormalized)
 48   * @returns The fully qualified name, e.g., "mcp__server__tool"
 49   */
 50  export function buildMcpToolName(serverName: string, toolName: string): string {
 51    return `${getMcpPrefix(serverName)}${normalizeNameForMCP(toolName)}`
 52  }
 53  
 54  /**
 55   * Returns the name to use for permission rule matching.
 56   * For MCP tools, uses the fully qualified mcp__server__tool name so that
 57   * deny rules targeting builtins (e.g., "Write") don't match unprefixed MCP
 58   * replacements that share the same display name. Falls back to `tool.name`.
 59   */
 60  export function getToolNameForPermissionCheck(tool: {
 61    name: string
 62    mcpInfo?: { serverName: string; toolName: string }
 63  }): string {
 64    return tool.mcpInfo
 65      ? buildMcpToolName(tool.mcpInfo.serverName, tool.mcpInfo.toolName)
 66      : tool.name
 67  }
 68  
 69  /*
 70   * Extracts the display name from an MCP tool/command name
 71   * @param fullName The full MCP tool/command name (e.g., "mcp__server_name__tool_name")
 72   * @param serverName The server name to remove from the prefix
 73   * @returns The display name without the MCP prefix
 74   */
 75  export function getMcpDisplayName(
 76    fullName: string,
 77    serverName: string,
 78  ): string {
 79    const prefix = `mcp__${normalizeNameForMCP(serverName)}__`
 80    return fullName.replace(prefix, '')
 81  }
 82  
 83  /**
 84   * Extracts just the tool/command display name from a userFacingName
 85   * @param userFacingName The full user-facing name (e.g., "github - Add comment to issue (MCP)")
 86   * @returns The display name without server prefix and (MCP) suffix
 87   */
 88  export function extractMcpToolDisplayName(userFacingName: string): string {
 89    // This is really ugly but our current Tool type doesn't make it easy to have different display names for different purposes.
 90  
 91    // First, remove the (MCP) suffix if present
 92    let withoutSuffix = userFacingName.replace(/\s*\(MCP\)\s*$/, '')
 93  
 94    // Trim the result
 95    withoutSuffix = withoutSuffix.trim()
 96  
 97    // Then, remove the server prefix (everything before " - ")
 98    const dashIndex = withoutSuffix.indexOf(' - ')
 99    if (dashIndex !== -1) {
100      const displayName = withoutSuffix.substring(dashIndex + 3).trim()
101      return displayName
102    }
103  
104    // If no dash found, return the string without (MCP)
105    return withoutSuffix
106  }