From d005d4291f20805f044db8ee99ede7e75251598a Mon Sep 17 00:00:00 2001 From: Vaayne Date: Mon, 29 Dec 2025 20:10:36 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20consolidate=20?= =?UTF-8?q?MCP=20tool=20name=20utilities=20into=20shared=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge buildFunctionCallToolName from src/main/utils/mcp.ts into packages/shared/mcp.ts - Create unified buildMcpToolName base function with options for prefix, delimiter, maxLength, existingNames - Fix toCamelCase to normalize uppercase snake case (MY_SERVER → myServer) - Fix maxLength + existingNames interaction to respect length limit when adding collision suffix - Add comprehensive JSDoc documentation - Update tests and hub.test.ts for new lowercase normalization behavior --- packages/shared/mcp.ts | 111 ++++++-- src/main/__tests__/mcp.test.ts | 240 ++++++++++++++++++ src/main/mcpServers/hub/__tests__/hub.test.ts | 8 +- src/main/services/MCPService.ts | 2 +- src/main/utils/__tests__/mcp.test.ts | 225 ---------------- src/main/utils/mcp.ts | 29 --- 6 files changed, 337 insertions(+), 278 deletions(-) create mode 100644 src/main/__tests__/mcp.test.ts delete mode 100644 src/main/utils/__tests__/mcp.test.ts delete mode 100644 src/main/utils/mcp.ts diff --git a/packages/shared/mcp.ts b/packages/shared/mcp.ts index 7836c00db0..b8e5494f17 100644 --- a/packages/shared/mcp.ts +++ b/packages/shared/mcp.ts @@ -1,13 +1,23 @@ /** * Convert a string to camelCase, ensuring it's a valid JavaScript identifier. + * + * - Normalizes to lowercase first, then capitalizes word boundaries + * - Non-alphanumeric characters are treated as word separators + * - Non-ASCII characters are dropped (ASCII-only output) + * - If result starts with a digit, prefixes with underscore + * + * @example + * toCamelCase('my-server') // 'myServer' + * toCamelCase('MY_SERVER') // 'myServer' + * toCamelCase('123tool') // '_123tool' */ export function toCamelCase(str: string): string { let result = str - .replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase()) - .replace(/^[A-Z]/, (char) => char.toLowerCase()) + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+(.)/g, (_, char) => char.toUpperCase()) .replace(/[^a-zA-Z0-9]/g, '') - // Ensure valid JS identifier: must start with letter or underscore if (result && !/^[a-zA-Z_]/.test(result)) { result = '_' + result } @@ -15,29 +25,92 @@ export function toCamelCase(str: string): string { return result } +export type McpToolNameOptions = { + /** Prefix added before the name (e.g., 'mcp__'). Must be JS-identifier-safe. */ + prefix?: string + /** Delimiter between server and tool parts (e.g., '_' or '__'). Must be JS-identifier-safe. */ + delimiter?: string + /** Maximum length of the final name. Suffix numbers for uniqueness are included in this limit. */ + maxLength?: number + /** Mutable Set for collision detection. The final name will be added to this Set. */ + existingNames?: Set +} + +/** + * Build a valid JavaScript function name from server and tool names. + * Uses camelCase for both parts. + * + * @param serverName - The MCP server name (optional) + * @param toolName - The tool name + * @param options - Configuration options + * @returns A valid JS identifier + */ +export function buildMcpToolName( + serverName: string | undefined, + toolName: string, + options: McpToolNameOptions = {} +): string { + const { prefix = '', delimiter = '_', maxLength, existingNames } = options + + const serverPart = serverName ? toCamelCase(serverName) : '' + const toolPart = toCamelCase(toolName) + const baseName = serverPart ? `${prefix}${serverPart}${delimiter}${toolPart}` : `${prefix}${toolPart}` + + if (!existingNames) { + return maxLength ? truncateToLength(baseName, maxLength) : baseName + } + + let name = maxLength ? truncateToLength(baseName, maxLength) : baseName + let counter = 1 + + while (existingNames.has(name)) { + const suffix = String(counter) + const truncatedBase = maxLength ? truncateToLength(baseName, maxLength - suffix.length) : baseName + name = `${truncatedBase}${suffix}` + counter++ + } + + existingNames.add(name) + return name +} + +function truncateToLength(str: string, maxLength: number): string { + if (str.length <= maxLength) { + return str + } + return str.slice(0, maxLength).replace(/_+$/, '') +} + /** * Generate a unique function name from server name and tool name. * Format: serverName_toolName (camelCase) + * + * @example + * generateMcpToolFunctionName('github', 'search_issues') // 'github_searchIssues' */ export function generateMcpToolFunctionName( serverName: string | undefined, toolName: string, existingNames?: Set ): string { - const serverPrefix = serverName ? toCamelCase(serverName) : '' - const toolNameCamel = toCamelCase(toolName) - const baseName = serverPrefix ? `${serverPrefix}_${toolNameCamel}` : toolNameCamel - - if (!existingNames) { - return baseName - } - - let name = baseName - let counter = 1 - while (existingNames.has(name)) { - name = `${baseName}${counter}` - counter++ - } - existingNames.add(name) - return name + return buildMcpToolName(serverName, toolName, { existingNames }) +} + +/** + * Builds a valid JavaScript function name for MCP tool calls. + * Format: mcp__{serverName}__{toolName} + * + * @param serverName - The MCP server name + * @param toolName - The tool name from the server + * @returns A valid JS identifier in format mcp__{server}__{tool}, max 63 chars + * + * @example + * buildFunctionCallToolName('github', 'search_issues') // 'mcp__github__searchIssues' + */ +export function buildFunctionCallToolName(serverName: string, toolName: string): string { + return buildMcpToolName(serverName, toolName, { + prefix: 'mcp__', + delimiter: '__', + maxLength: 63 + }) } diff --git a/src/main/__tests__/mcp.test.ts b/src/main/__tests__/mcp.test.ts new file mode 100644 index 0000000000..809db3d665 --- /dev/null +++ b/src/main/__tests__/mcp.test.ts @@ -0,0 +1,240 @@ +import { buildFunctionCallToolName, buildMcpToolName, generateMcpToolFunctionName, toCamelCase } from '@shared/mcp' +import { describe, expect, it } from 'vitest' + +describe('toCamelCase', () => { + it('should convert hyphenated strings', () => { + expect(toCamelCase('my-server')).toBe('myServer') + expect(toCamelCase('my-tool-name')).toBe('myToolName') + }) + + it('should convert underscored strings', () => { + expect(toCamelCase('my_server')).toBe('myServer') + expect(toCamelCase('search_issues')).toBe('searchIssues') + }) + + it('should handle mixed delimiters', () => { + expect(toCamelCase('my-server_name')).toBe('myServerName') + }) + + it('should handle leading numbers by prefixing underscore', () => { + expect(toCamelCase('123server')).toBe('_123server') + }) + + it('should handle special characters', () => { + expect(toCamelCase('test@server!')).toBe('testServer') + expect(toCamelCase('tool#name$')).toBe('toolName') + }) + + it('should trim whitespace', () => { + expect(toCamelCase(' server ')).toBe('server') + }) + + it('should handle empty string', () => { + expect(toCamelCase('')).toBe('') + }) + + it('should handle uppercase snake case', () => { + expect(toCamelCase('MY_SERVER')).toBe('myServer') + expect(toCamelCase('SEARCH_ISSUES')).toBe('searchIssues') + }) + + it('should handle mixed case', () => { + expect(toCamelCase('MyServer')).toBe('myserver') + expect(toCamelCase('myTOOL')).toBe('mytool') + }) +}) + +describe('buildMcpToolName', () => { + it('should build basic name with defaults', () => { + expect(buildMcpToolName('github', 'search_issues')).toBe('github_searchIssues') + }) + + it('should handle undefined server name', () => { + expect(buildMcpToolName(undefined, 'search_issues')).toBe('searchIssues') + }) + + it('should apply custom prefix and delimiter', () => { + expect(buildMcpToolName('github', 'search', { prefix: 'mcp__', delimiter: '__' })).toBe('mcp__github__search') + }) + + it('should respect maxLength', () => { + const result = buildMcpToolName('veryLongServerName', 'veryLongToolName', { maxLength: 20 }) + expect(result.length).toBeLessThanOrEqual(20) + }) + + it('should handle collision with existingNames', () => { + const existingNames = new Set(['github_search']) + const result = buildMcpToolName('github', 'search', { existingNames }) + expect(result).toBe('github_search1') + expect(existingNames.has('github_search1')).toBe(true) + }) + + it('should respect maxLength when adding collision suffix', () => { + const existingNames = new Set(['a'.repeat(20)]) + const result = buildMcpToolName('a'.repeat(20), '', { maxLength: 20, existingNames }) + expect(result.length).toBeLessThanOrEqual(20) + expect(existingNames.has(result)).toBe(true) + }) + + it('should handle multiple collisions with maxLength', () => { + const existingNames = new Set(['abcd', 'abc1', 'abc2']) + const result = buildMcpToolName('abcd', '', { maxLength: 4, existingNames }) + expect(result).toBe('abc3') + expect(result.length).toBeLessThanOrEqual(4) + }) +}) + +describe('generateMcpToolFunctionName', () => { + it('should return format serverName_toolName in camelCase', () => { + expect(generateMcpToolFunctionName('github', 'search_issues')).toBe('github_searchIssues') + }) + + it('should handle hyphenated names', () => { + expect(generateMcpToolFunctionName('my-server', 'my-tool')).toBe('myServer_myTool') + }) + + it('should handle undefined server name', () => { + expect(generateMcpToolFunctionName(undefined, 'search_issues')).toBe('searchIssues') + }) + + it('should handle collision detection', () => { + const existingNames = new Set() + const first = generateMcpToolFunctionName('github', 'search', existingNames) + const second = generateMcpToolFunctionName('github', 'search', existingNames) + expect(first).toBe('github_search') + expect(second).toBe('github_search1') + }) +}) + +describe('buildFunctionCallToolName', () => { + describe('basic format', () => { + it('should return format mcp__{server}__{tool} in camelCase', () => { + const result = buildFunctionCallToolName('github', 'search_issues') + expect(result).toBe('mcp__github__searchIssues') + }) + + it('should handle simple server and tool names', () => { + expect(buildFunctionCallToolName('fetch', 'get_page')).toBe('mcp__fetch__getPage') + expect(buildFunctionCallToolName('database', 'query')).toBe('mcp__database__query') + }) + }) + + describe('valid JavaScript identifier', () => { + it('should always start with mcp__ prefix (valid JS identifier start)', () => { + const result = buildFunctionCallToolName('123server', '456tool') + expect(result).toMatch(/^mcp__/) + }) + + it('should handle hyphenated names with camelCase', () => { + const result = buildFunctionCallToolName('my-server', 'my-tool') + expect(result).toBe('mcp__myServer__myTool') + }) + + it('should be a valid JavaScript identifier', () => { + const testCases = [ + ['github', 'create_issue'], + ['my-server', 'fetch-data'], + ['test@server', 'tool#name'], + ['server.name', 'tool.action'] + ] + + for (const [server, tool] of testCases) { + const result = buildFunctionCallToolName(server, tool) + expect(result).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/) + } + }) + }) + + describe('character sanitization', () => { + it('should convert special characters to camelCase boundaries', () => { + expect(buildFunctionCallToolName('my-server', 'my-tool-name')).toBe('mcp__myServer__myToolName') + expect(buildFunctionCallToolName('test@server!', 'tool#name$')).toBe('mcp__testServer__toolName') + expect(buildFunctionCallToolName('server.name', 'tool.action')).toBe('mcp__serverName__toolAction') + }) + + it('should handle spaces', () => { + const result = buildFunctionCallToolName('my server', 'my tool') + expect(result).toBe('mcp__myServer__myTool') + }) + }) + + describe('length constraints', () => { + it('should not exceed 63 characters', () => { + const longServerName = 'a'.repeat(50) + const longToolName = 'b'.repeat(50) + const result = buildFunctionCallToolName(longServerName, longToolName) + expect(result.length).toBeLessThanOrEqual(63) + }) + + it('should not end with underscores after truncation', () => { + const longServerName = 'a'.repeat(30) + const longToolName = 'b'.repeat(30) + const result = buildFunctionCallToolName(longServerName, longToolName) + expect(result).not.toMatch(/_+$/) + expect(result.length).toBeLessThanOrEqual(63) + }) + }) + + describe('edge cases', () => { + it('should handle empty server name', () => { + const result = buildFunctionCallToolName('', 'tool') + expect(result).toBe('mcp__tool') + }) + + it('should handle empty tool name', () => { + const result = buildFunctionCallToolName('server', '') + expect(result).toBe('mcp__server__') + }) + + it('should trim whitespace from names', () => { + const result = buildFunctionCallToolName(' server ', ' tool ') + expect(result).toBe('mcp__server__tool') + }) + + it('should handle mixed case by normalizing to lowercase first', () => { + const result = buildFunctionCallToolName('MyServer', 'MyTool') + expect(result).toBe('mcp__myserver__mytool') + }) + + it('should handle uppercase snake case', () => { + const result = buildFunctionCallToolName('MY_SERVER', 'SEARCH_ISSUES') + expect(result).toBe('mcp__myServer__searchIssues') + }) + }) + + describe('deterministic output', () => { + it('should produce consistent results for same input', () => { + const result1 = buildFunctionCallToolName('github', 'search_repos') + const result2 = buildFunctionCallToolName('github', 'search_repos') + expect(result1).toBe(result2) + }) + + it('should produce different results for different inputs', () => { + const result1 = buildFunctionCallToolName('server1', 'tool') + const result2 = buildFunctionCallToolName('server2', 'tool') + expect(result1).not.toBe(result2) + }) + }) + + describe('real-world scenarios', () => { + it('should handle GitHub MCP server', () => { + expect(buildFunctionCallToolName('github', 'create_issue')).toBe('mcp__github__createIssue') + expect(buildFunctionCallToolName('github', 'search_repositories')).toBe('mcp__github__searchRepositories') + }) + + it('should handle filesystem MCP server', () => { + expect(buildFunctionCallToolName('filesystem', 'read_file')).toBe('mcp__filesystem__readFile') + expect(buildFunctionCallToolName('filesystem', 'write_file')).toBe('mcp__filesystem__writeFile') + }) + + it('should handle hyphenated server names (common in npm packages)', () => { + expect(buildFunctionCallToolName('cherry-fetch', 'get_page')).toBe('mcp__cherryFetch__getPage') + expect(buildFunctionCallToolName('mcp-server-github', 'search')).toBe('mcp__mcpServerGithub__search') + }) + + it('should handle scoped npm package style names', () => { + const result = buildFunctionCallToolName('@anthropic/mcp-server', 'chat') + expect(result).toBe('mcp__AnthropicMcpServer__chat') + }) + }) +}) diff --git a/src/main/mcpServers/hub/__tests__/hub.test.ts b/src/main/mcpServers/hub/__tests__/hub.test.ts index 32b94f8933..657719d3e7 100644 --- a/src/main/mcpServers/hub/__tests__/hub.test.ts +++ b/src/main/mcpServers/hub/__tests__/hub.test.ts @@ -97,10 +97,10 @@ describe('HubServer Integration', () => { expect(searchResult.content).toBeDefined() const searchText = JSON.parse(searchResult.content[0].text) expect(searchText.total).toBeGreaterThan(0) - expect(searchText.tools).toContain('gitHub_searchRepos') + expect(searchText.tools).toContain('github_searchRepos') const execResult = await (hubServer as any).handleExec({ - code: 'return await gitHub_searchRepos({ query: "test" })' + code: 'return await github_searchRepos({ query: "test" })' }) expect(execResult.content).toBeDefined() @@ -114,8 +114,8 @@ describe('HubServer Integration', () => { const execResult = await (hubServer as any).handleExec({ code: ` const results = await parallel( - gitHub_searchRepos({ query: "react" }), - gitHub_getUser({ username: "octocat" }) + github_searchRepos({ query: "react" }), + github_getUser({ username: "octocat" }) ); return results ` diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index b362d56514..d35d895914 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -6,7 +6,6 @@ import { loggerService } from '@logger' import { getMCPServersFromRedux } from '@main/apiServer/utils/mcp' import { createInMemoryMCPServer } from '@main/mcpServers/factory' import { makeSureDirExists, removeEnvProxy } from '@main/utils' -import { buildFunctionCallToolName } from '@main/utils/mcp' import { findCommandInShellEnv, getBinaryName, getBinaryPath, isBinaryExists } from '@main/utils/process' import getLoginShellEnvironment from '@main/utils/shell-env' import { TraceMethod, withSpanFunc } from '@mcp-trace/trace-core' @@ -36,6 +35,7 @@ import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { MCPProgressEvent } from '@shared/config/types' import type { MCPServerLogEntry } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' +import { buildFunctionCallToolName } from '@shared/mcp' import { defaultAppHeaders } from '@shared/utils' import { BuiltinMCPServerNames, diff --git a/src/main/utils/__tests__/mcp.test.ts b/src/main/utils/__tests__/mcp.test.ts deleted file mode 100644 index 706a44bc84..0000000000 --- a/src/main/utils/__tests__/mcp.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { buildFunctionCallToolName } from '../mcp' - -describe('buildFunctionCallToolName', () => { - describe('basic format', () => { - it('should return format mcp__{server}__{tool}', () => { - const result = buildFunctionCallToolName('github', 'search_issues') - expect(result).toBe('mcp__github__search_issues') - }) - - it('should handle simple server and tool names', () => { - expect(buildFunctionCallToolName('fetch', 'get_page')).toBe('mcp__fetch__get_page') - expect(buildFunctionCallToolName('database', 'query')).toBe('mcp__database__query') - expect(buildFunctionCallToolName('cherry_studio', 'search')).toBe('mcp__cherry_studio__search') - }) - }) - - describe('valid JavaScript identifier', () => { - it('should always start with mcp__ prefix (valid JS identifier start)', () => { - const result = buildFunctionCallToolName('123server', '456tool') - expect(result).toMatch(/^mcp__/) - expect(result).toBe('mcp__123server__456tool') - }) - - it('should only contain alphanumeric chars and underscores', () => { - const result = buildFunctionCallToolName('my-server', 'my-tool') - expect(result).toBe('mcp__my_server__my_tool') - expect(result).toMatch(/^[a-zA-Z][a-zA-Z0-9_]*$/) - }) - - it('should be a valid JavaScript identifier', () => { - const testCases = [ - ['github', 'create_issue'], - ['my-server', 'fetch-data'], - ['test@server', 'tool#name'], - ['server.name', 'tool.action'], - ['123abc', 'def456'] - ] - - for (const [server, tool] of testCases) { - const result = buildFunctionCallToolName(server, tool) - // Valid JS identifiers match this pattern - expect(result).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/) - } - }) - }) - - describe('character sanitization', () => { - it('should replace dashes with underscores', () => { - const result = buildFunctionCallToolName('my-server', 'my-tool-name') - expect(result).toBe('mcp__my_server__my_tool_name') - }) - - it('should replace special characters with underscores', () => { - const result = buildFunctionCallToolName('test@server!', 'tool#name$') - expect(result).toBe('mcp__test_server__tool_name') - }) - - it('should replace dots with underscores', () => { - const result = buildFunctionCallToolName('server.name', 'tool.action') - expect(result).toBe('mcp__server_name__tool_action') - }) - - it('should replace spaces with underscores', () => { - const result = buildFunctionCallToolName('my server', 'my tool') - expect(result).toBe('mcp__my_server__my_tool') - }) - - it('should collapse consecutive underscores', () => { - const result = buildFunctionCallToolName('my--server', 'my___tool') - expect(result).toBe('mcp__my_server__my_tool') - expect(result).not.toMatch(/_{3,}/) - }) - - it('should trim leading and trailing underscores from parts', () => { - const result = buildFunctionCallToolName('_server_', '_tool_') - expect(result).toBe('mcp__server__tool') - }) - - it('should handle names with only special characters', () => { - const result = buildFunctionCallToolName('---', '###') - expect(result).toBe('mcp____') - }) - }) - - describe('length constraints', () => { - it('should not exceed 63 characters', () => { - const longServerName = 'a'.repeat(50) - const longToolName = 'b'.repeat(50) - const result = buildFunctionCallToolName(longServerName, longToolName) - - expect(result.length).toBeLessThanOrEqual(63) - }) - - it('should truncate server name to max 20 chars', () => { - const longServerName = 'abcdefghijklmnopqrstuvwxyz' // 26 chars - const result = buildFunctionCallToolName(longServerName, 'tool') - - expect(result).toBe('mcp__abcdefghijklmnopqrst__tool') - expect(result).toContain('abcdefghijklmnopqrst') // First 20 chars - expect(result).not.toContain('uvwxyz') // Truncated - }) - - it('should truncate tool name to max 35 chars', () => { - const longToolName = 'a'.repeat(40) - const result = buildFunctionCallToolName('server', longToolName) - - const expectedTool = 'a'.repeat(35) - expect(result).toBe(`mcp__server__${expectedTool}`) - }) - - it('should not end with underscores after truncation', () => { - // Create a name that would end with underscores after truncation - const longServerName = 'a'.repeat(20) - const longToolName = 'b'.repeat(35) + '___extra' - const result = buildFunctionCallToolName(longServerName, longToolName) - - expect(result).not.toMatch(/_+$/) - expect(result.length).toBeLessThanOrEqual(63) - }) - - it('should handle max length edge case exactly', () => { - // mcp__ (5) + server (20) + __ (2) + tool (35) = 62 chars - const server = 'a'.repeat(20) - const tool = 'b'.repeat(35) - const result = buildFunctionCallToolName(server, tool) - - expect(result.length).toBe(62) - expect(result).toBe(`mcp__${'a'.repeat(20)}__${'b'.repeat(35)}`) - }) - }) - - describe('edge cases', () => { - it('should handle empty server name', () => { - const result = buildFunctionCallToolName('', 'tool') - expect(result).toBe('mcp____tool') - }) - - it('should handle empty tool name', () => { - const result = buildFunctionCallToolName('server', '') - expect(result).toBe('mcp__server__') - }) - - it('should handle both empty names', () => { - const result = buildFunctionCallToolName('', '') - expect(result).toBe('mcp____') - }) - - it('should handle whitespace-only names', () => { - const result = buildFunctionCallToolName(' ', ' ') - expect(result).toBe('mcp____') - }) - - it('should trim whitespace from names', () => { - const result = buildFunctionCallToolName(' server ', ' tool ') - expect(result).toBe('mcp__server__tool') - }) - - it('should handle unicode characters', () => { - const result = buildFunctionCallToolName('服务器', '工具') - // Unicode chars are replaced with underscores, then collapsed - expect(result).toMatch(/^mcp__/) - }) - - it('should handle mixed case', () => { - const result = buildFunctionCallToolName('MyServer', 'MyTool') - expect(result).toBe('mcp__MyServer__MyTool') - }) - }) - - describe('deterministic output', () => { - it('should produce consistent results for same input', () => { - const serverName = 'github' - const toolName = 'search_repos' - - const result1 = buildFunctionCallToolName(serverName, toolName) - const result2 = buildFunctionCallToolName(serverName, toolName) - const result3 = buildFunctionCallToolName(serverName, toolName) - - expect(result1).toBe(result2) - expect(result2).toBe(result3) - }) - - it('should produce different results for different inputs', () => { - const result1 = buildFunctionCallToolName('server1', 'tool') - const result2 = buildFunctionCallToolName('server2', 'tool') - const result3 = buildFunctionCallToolName('server', 'tool1') - const result4 = buildFunctionCallToolName('server', 'tool2') - - expect(result1).not.toBe(result2) - expect(result3).not.toBe(result4) - }) - }) - - describe('real-world scenarios', () => { - it('should handle GitHub MCP server', () => { - expect(buildFunctionCallToolName('github', 'create_issue')).toBe('mcp__github__create_issue') - expect(buildFunctionCallToolName('github', 'search_repositories')).toBe('mcp__github__search_repositories') - expect(buildFunctionCallToolName('github', 'get_pull_request')).toBe('mcp__github__get_pull_request') - }) - - it('should handle filesystem MCP server', () => { - expect(buildFunctionCallToolName('filesystem', 'read_file')).toBe('mcp__filesystem__read_file') - expect(buildFunctionCallToolName('filesystem', 'write_file')).toBe('mcp__filesystem__write_file') - expect(buildFunctionCallToolName('filesystem', 'list_directory')).toBe('mcp__filesystem__list_directory') - }) - - it('should handle hyphenated server names (common in npm packages)', () => { - expect(buildFunctionCallToolName('cherry-fetch', 'get_page')).toBe('mcp__cherry_fetch__get_page') - expect(buildFunctionCallToolName('mcp-server-github', 'search')).toBe('mcp__mcp_server_github__search') - }) - - it('should handle scoped npm package style names', () => { - const result = buildFunctionCallToolName('@anthropic/mcp-server', 'chat') - expect(result).toBe('mcp__anthropic_mcp_server__chat') - }) - - it('should handle tools with long descriptive names', () => { - const result = buildFunctionCallToolName('github', 'search_repositories_by_language_and_stars') - expect(result.length).toBeLessThanOrEqual(63) - expect(result).toMatch(/^mcp__github__search_repositories_by_lan/) - }) - }) -}) diff --git a/src/main/utils/mcp.ts b/src/main/utils/mcp.ts deleted file mode 100644 index 34eb0e63e7..0000000000 --- a/src/main/utils/mcp.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Builds a valid JavaScript function name for MCP tool calls. - * Format: mcp__{server_name}__{tool_name} - * - * @param serverName - The MCP server name - * @param toolName - The tool name from the server - * @returns A valid JS identifier in format mcp__{server}__{tool}, max 63 chars - */ -export function buildFunctionCallToolName(serverName: string, toolName: string): string { - // Sanitize to valid JS identifier chars (alphanumeric + underscore only) - const sanitize = (str: string): string => - str - .trim() - .replace(/[^a-zA-Z0-9]/g, '_') // Replace all non-alphanumeric with underscore - .replace(/_{2,}/g, '_') // Collapse multiple underscores - .replace(/^_+|_+$/g, '') // Trim leading/trailing underscores - - const server = sanitize(serverName).slice(0, 20) // Keep server name short - const tool = sanitize(toolName).slice(0, 35) // More room for tool name - - let name = `mcp__${server}__${tool}` - - // Ensure max 63 chars and clean trailing underscores - if (name.length > 63) { - name = name.slice(0, 63).replace(/_+$/, '') - } - - return name -}