| 1 | using System.Reflection; |
| 2 | |
| 3 | namespace CascadeIDE.Services; |
| 4 | |
| 5 | /// <summary> |
| 6 | /// MCP tool names for Cursor: only <c>[A-Za-z0-9_]</c>; combined server+tool length ≤ 60 (conservative cap on tool part). |
| 7 | /// <see cref="IdeCommands"/> <c>command_id</c> may contain <c>.</c> — wire name uses <c>_</c>. |
| 8 | /// </summary> |
| 9 | internal static class IdeMcpToolNaming |
| 10 | { |
| 11 | /// <summary>Max length of full tool name (<c>ide_*</c>). Leaves room for server names like <c>cascade-ide-debug</c>.</summary> |
| 12 | public const int MaxToolNameLength = 45; |
| 13 | |
| 14 | private static readonly Dictionary<string, string> CommandIdToToolName; |
| 15 | private static readonly Dictionary<string, string> ToolNameToCommandId; |
| 16 | |
| 17 | static IdeMcpToolNaming() |
| 18 | { |
| 19 | CommandIdToToolName = new Dictionary<string, string>(StringComparer.Ordinal); |
| 20 | ToolNameToCommandId = new Dictionary<string, string>(StringComparer.Ordinal); |
| 21 | |
| 22 | var type = typeof(IdeCommands); |
| 23 | foreach (var f in type.GetFields(BindingFlags.Public | BindingFlags.Static)) |
| 24 | { |
| 25 | if (f.FieldType != typeof(string)) |
| 26 | continue; |
| 27 | var commandId = (string?)f.GetValue(null); |
| 28 | if (string.IsNullOrWhiteSpace(commandId)) |
| 29 | continue; |
| 30 | Register(commandId); |
| 31 | } |
| 32 | |
| 33 | // Shorter wire name (combined server+tool ≤ 60 in Cursor). |
| 34 | RegisterAlias(IdeCommands.ChatToggleProductSpineInAgentContext, "chat_toggle_spine_ctx"); |
| 35 | } |
| 36 | |
| 37 | public static string ToToolName(string commandId) |
| 38 | { |
| 39 | if (CommandIdToToolName.TryGetValue(commandId, out var name)) |
| 40 | return name; |
| 41 | return Register(commandId); |
| 42 | } |
| 43 | |
| 44 | public static bool TryToCommandId(string toolName, out string commandId) |
| 45 | { |
| 46 | if (ToolNameToCommandId.TryGetValue(toolName, out var id)) |
| 47 | { |
| 48 | commandId = id; |
| 49 | return true; |
| 50 | } |
| 51 | |
| 52 | commandId = ""; |
| 53 | return false; |
| 54 | } |
| 55 | |
| 56 | /// <summary>Skip auto-proxy when no stable short name fits Cursor limits.</summary> |
| 57 | public static bool IsSupportedAutoProxyToolName(string commandId) => |
| 58 | ToToolName(commandId).Length <= MaxToolNameLength; |
| 59 | |
| 60 | private static void RegisterAlias(string commandId, string suffixAfterIde) |
| 61 | { |
| 62 | var toolName = "ide_" + suffixAfterIde; |
| 63 | if (toolName.Length > MaxToolNameLength) |
| 64 | throw new InvalidOperationException($"MCP tool name too long: {toolName}"); |
| 65 | |
| 66 | CommandIdToToolName[commandId] = toolName; |
| 67 | ToolNameToCommandId[toolName] = commandId; |
| 68 | } |
| 69 | |
| 70 | private static string Register(string commandId) |
| 71 | { |
| 72 | if (CommandIdToToolName.TryGetValue(commandId, out var existing)) |
| 73 | return existing; |
| 74 | |
| 75 | var suffix = commandId.Replace('.', '_'); |
| 76 | var toolName = "ide_" + suffix; |
| 77 | CommandIdToToolName[commandId] = toolName; |
| 78 | ToolNameToCommandId[toolName] = commandId; |
| 79 | return toolName; |
| 80 | } |
| 81 | } |
| 82 | |