cherry-studio/packages/shared/mcp.ts
LiuVaayne 6d15b0dfd1
feat(mcp): add MCP Hub server for multi-server tool orchestration (#12192)
* feat(mcp): add hub server type definitions

- Add 'hub' to BuiltinMCPServerNames enum as '@cherry/hub'
- Create GeneratedTool, SearchQuery, ExecInput, ExecOutput types
- Add ExecutionContext and ConsoleMethods interfaces

Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e
Co-authored-by: Amp <amp@ampcode.com>

* feat(mcp): implement hub server core components

- generator.ts: Convert MCP tools to JS functions with JSDoc
- tool-registry.ts: In-memory cache with 10-min TTL
- search.ts: Comma-separated keyword search with ranking
- runtime.ts: Code execution with parallel/settle/console helpers

Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e
Co-authored-by: Amp <amp@ampcode.com>

* feat(mcp): integrate hub server with MCP infrastructure

- Create HubServer class with search/exec tools
- Implement mcp-bridge for calling tools via MCPService
- Register hub server in factory with dependency injection
- Initialize hub dependencies in MCPService constructor
- Add hub server description label for i18n

Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e
Co-authored-by: Amp <amp@ampcode.com>

* test(mcp): add unit tests for hub server

- generator.test.ts: Test schema conversion and JSDoc generation
- search.test.ts: Test keyword matching, ranking, and limits
- runtime.test.ts: Test code execution, helpers, and error handling

Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e
Co-authored-by: Amp <amp@ampcode.com>

* docs(mcp): add hub server documentation

- Document search/exec tool usage and parameters
- Explain configuration and caching behavior
- Include architecture diagram and file structure

Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e
Co-authored-by: Amp <amp@ampcode.com>

* ♻️ refactor(hub): simplify dependency injection for HubServer

- Remove HubServerDependencies interface and setHubServerDependencies from factory
- Add initHubBridge() to mcp-bridge for direct initialization
- Make HubServer constructor parameterless (uses pre-initialized bridge)
- MCPService now calls initHubBridge() directly instead of factory setter
- Add integration tests for full search → exec flow

* 📝 docs(hub): add comments explaining why hub is not in builtin list

- Add JSDoc to HubServer class explaining its purpose and design
- Add comment to builtinMCPServers explaining hub exclusion
- Hub is a meta-server for LLM code mode, auto-enabled internally

*  feat: add available tools section to HUB_MODE_SYSTEM_PROMPT

- Add shared utility for generating MCP tool function names (serverName_toolName format)
- Update hub server to use consistent function naming across search, exec and prompt
- Add fetchAllActiveServerTools to ApiService for renderer process
- Update parameterBuilder to include available tools in auto/hub mode prompt
- Use CacheService for 1-minute tools caching in hub server
- Remove ToolRegistry in favor of direct fetching with caching
- Update search ranking to include server name matching
- Fix tests to use new naming format

Amp-Thread-ID: https://ampcode.com/threads/T-019b6971-d5c9-7719-9245-a89390078647
Co-authored-by: Amp <amp@ampcode.com>

* ♻️ refactor: consolidate MCP tool name utilities into shared module

- 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

*  feat: isolate hub exec worker and filter disabled tools

* 🐛 fix: inline hub worker source

* 🐛 fix: sync hub tool cache and map

* Update import path for buildFunctionCallToolName in BaseService

*  feat: refine hub mode system prompt

* 🐛 fix: propagate hub tool errors

* 📝 docs: clarify hub exec return

*  feat(hub): improve prompts and tool descriptions for better LLM success rate

- Rewrite HUB_MODE_SYSTEM_PROMPT_BASE with Critical Rules section
- Add Common Mistakes to Avoid section with examples
- Update exec tool description with IMPORTANT return requirement
- Improve search tool description clarity
- Simplify generator output with return reminder in header
- Add per-field @param JSDoc with required/optional markers

Fixes issue where LLMs forgot to return values from exec code

* ♻️ refactor(hub): return empty string when no tools available

*  feat(hub): add dedicated AUTO_MODE_SYSTEM_PROMPT for auto mode

- Create self-contained prompt teaching XML tool_use format
- Only shows search/exec tools (no generic examples)
- Add complete workflow example with common mistakes
- Update parameterBuilder to use getAutoModeSystemPrompt()
- User prompt comes first, then auto mode instructions
- Skip hub prompt when no tools available

* ♻️ refactor: move hub prompts to dedicated prompts-code-mode.ts

- Create src/renderer/src/config/prompts-code-mode.ts
- Move HUB_MODE_SYSTEM_PROMPT_BASE and AUTO_MODE_SYSTEM_PROMPT_BASE
- Move getHubModeSystemPrompt() and getAutoModeSystemPrompt()
- Extract shared buildToolsSection() helper
- Update parameterBuilder.ts import

* ♻️ refactor: add mcpMode support to promptToolUsePlugin

- Add mcpMode parameter to PromptToolUseConfig and defaultBuildSystemPrompt
- Pass mcpMode through middleware config to plugin builder
- Consolidate getAutoModeSystemPrompt into getHubModeSystemPrompt
- Update parameterBuilder to use getHubModeSystemPrompt

* ♻️ refactor: move getHubModeSystemPrompt to shared package

- Create @cherrystudio/shared workspace package with exports
- Move getHubModeSystemPrompt and ToolInfo to packages/shared/prompts
- Add @cherrystudio/shared dependency to @cherrystudio/ai-core
- Update promptToolUsePlugin to import from shared package
- Update renderer prompts-code-mode.ts to re-export from shared
- Add toolSetToToolInfoArray converter for type compatibility

* Revert "♻️ refactor: move getHubModeSystemPrompt to shared package"

This reverts commit 894b2fd487.

* Remove duplicate Tool Use Examples header from system prompt

* fix: add handleModeChange call in MCPToolsButton for manual mode activation

* style: update AssistantMCPSettings to use min-height instead of overflow for better layout control

* feat(i18n): add MCP server modes and truncate messages in multiple languages

- Introduced new "mode" options for MCP servers: auto, disabled, and manual with corresponding descriptions and labels.
- Added translations for "base64DataTruncated" and "truncated" messages across various language files.
- Enhanced user experience by providing clearer feedback on data truncation.

* Normalize tool names for search and exec in parser

* Clarify tool usage rules in code mode prompts and examples

* Clarify code execution instructions and update example usage

* refactor: simplify JSDoc description handling by removing unnecessary truncation

* refactor: optimize listAllActiveServerTools method to use Promise.allSettled for improved error handling and performance

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2026-01-07 16:35:51 +08:00

117 lines
3.6 KiB
TypeScript

/**
* 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
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+(.)/g, (_, char) => char.toUpperCase())
.replace(/[^a-zA-Z0-9]/g, '')
if (result && !/^[a-zA-Z_]/.test(result)) {
result = '_' + result
}
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<string>
}
/**
* 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>
): string {
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
})
}