@@ -101,7 +101,7 @@ function createMockClient(overrides: Partial<McpToolSet> & { serverName: string
101101// --- Tests ---
102102
103103// Import after mocks are set up
104- const { getMcpTools } = await import ( './mcpToolSets' ) ;
104+ const { getMcpTools, sanitizeMcpToolNameForModel } = await import ( './mcpToolSets' ) ;
105105
106106beforeEach ( ( ) => {
107107 vi . clearAllMocks ( ) ;
@@ -114,6 +114,17 @@ beforeEach(() => {
114114 mockRedisSet . mockResolvedValue ( 'OK' ) ;
115115} ) ;
116116
117+ describe ( 'sanitizeMcpToolNameForModel' , ( ) => {
118+ test ( 'replaces provider-invalid characters with underscores' , ( ) => {
119+ expect ( sanitizeMcpToolNameForModel ( 'mcp_backstage__catalog.query-catalog-entities' ) )
120+ . toBe ( 'mcp_backstage__catalog_query_catalog_entities' ) ;
121+ } ) ;
122+
123+ test ( 'returns an underscore for an empty name' , ( ) => {
124+ expect ( sanitizeMcpToolNameForModel ( '' ) ) . toBe ( '_' ) ;
125+ } ) ;
126+ } ) ;
127+
117128describe ( 'getMcpTools' , ( ) => {
118129 test ( 'single server with single tool produces correctly namespaced key' , async ( ) => {
119130 const mockClient = createMockMcpClient ( [
@@ -129,6 +140,58 @@ describe('getMcpTools', () => {
129140 expect ( result . failedServers ) . toEqual ( [ ] ) ;
130141 } ) ;
131142
143+ test ( 'sanitizes model-facing keys for MCP tools with punctuation' , async ( ) => {
144+ const mockClient = createMockMcpClient ( [
145+ { name : 'catalog.query-catalog-entities' , description : 'Query catalog entities' } ,
146+ ] ) ;
147+ mockCreateMCPClient . mockResolvedValue ( mockClient ) ;
148+
149+ const result = await getMcpTools ( [
150+ createMockClient ( { serverId : 'server-backstage' , serverName : 'Backstage' } ) ,
151+ ] ) ;
152+
153+ expect ( Object . keys ( result . tools ) ) . toEqual ( [
154+ 'mcp_backstage__catalog_query_catalog_entities' ,
155+ ] ) ;
156+
157+ const tool = result . tools [ 'mcp_backstage__catalog_query_catalog_entities' ] ;
158+ await expect (
159+ tool . execute ( { filter : 'kind=component' } , { messages : [ ] , toolCallId : 'test' } )
160+ ) . resolves . toEqual ( { content : [ { type : 'text' , text : 'result' } ] } ) ;
161+
162+ expect ( mockServerToolUpsert ) . toHaveBeenCalledWith ( expect . objectContaining ( {
163+ where : {
164+ mcpServerId_toolName : {
165+ mcpServerId : 'server-backstage' ,
166+ toolName : 'catalog.query-catalog-entities' ,
167+ } ,
168+ } ,
169+ } ) ) ;
170+ expect ( mockCaptureEvent ) . toHaveBeenCalledWith ( 'ask_mcp_tool_call_completed' , expect . objectContaining ( {
171+ serverId : 'server-backstage' ,
172+ toolName : 'catalog.query-catalog-entities' ,
173+ success : true ,
174+ } ) ) ;
175+ } ) ;
176+
177+ test ( 'adds stable suffixes when sanitized tool names collide' , async ( ) => {
178+ const mockClient = createMockMcpClient ( [
179+ { name : 'catalog.query' , description : 'Query catalog' } ,
180+ { name : 'catalog_query' , description : 'Query catalog with underscore' } ,
181+ ] ) ;
182+ mockCreateMCPClient . mockResolvedValue ( mockClient ) ;
183+
184+ const result = await getMcpTools ( [
185+ createMockClient ( { serverName : 'Backstage' } ) ,
186+ ] ) ;
187+
188+ const toolNames = Object . keys ( result . tools ) ;
189+ expect ( toolNames ) . toHaveLength ( 2 ) ;
190+ expect ( new Set ( toolNames ) . size ) . toBe ( 2 ) ;
191+ expect ( toolNames ) . not . toContain ( 'mcp_backstage__catalog_query' ) ;
192+ expect ( toolNames . every ( ( toolName ) => / ^ m c p _ b a c k s t a g e _ _ c a t a l o g _ q u e r y _ [ 0 - 9 a - f ] { 8 } $ / . test ( toolName ) ) ) . toBe ( true ) ;
193+ } ) ;
194+
132195 test ( 'multiple servers produce tools with distinct prefixes' , async ( ) => {
133196 const linearClient = createMockMcpClient ( [
134197 { name : 'list_issues' , description : 'List issues' } ,
0 commit comments