@@ -8,9 +8,19 @@ import {
88 MCP_TOOL_SEPARATOR ,
99 MCP_TOOL_PREFIX ,
1010 HYPHEN_ENCODING ,
11+ MAX_TOOL_NAME_LENGTH ,
12+ HASH_SUFFIX_LENGTH ,
13+ mcpToolNameRegistry ,
14+ clearMcpToolNameRegistry ,
15+ computeHashSuffix ,
1116} from "../mcp-name"
1217
1318describe ( "mcp-name utilities" , ( ) => {
19+ // Clear the registry before each test to ensure isolation
20+ beforeEach ( ( ) => {
21+ clearMcpToolNameRegistry ( )
22+ } )
23+
1424 describe ( "constants" , ( ) => {
1525 it ( "should have correct separator and prefix" , ( ) => {
1626 expect ( MCP_TOOL_SEPARATOR ) . toBe ( "--" )
@@ -20,6 +30,14 @@ describe("mcp-name utilities", () => {
2030 it ( "should have correct hyphen encoding" , ( ) => {
2131 expect ( HYPHEN_ENCODING ) . toBe ( "___" )
2232 } )
33+
34+ it ( "should have correct max tool name length" , ( ) => {
35+ expect ( MAX_TOOL_NAME_LENGTH ) . toBe ( 64 )
36+ } )
37+
38+ it ( "should have correct hash suffix length" , ( ) => {
39+ expect ( HASH_SUFFIX_LENGTH ) . toBe ( 8 )
40+ } )
2341 } )
2442
2543 describe ( "isMcpTool" , ( ) => {
@@ -146,12 +164,52 @@ describe("mcp-name utilities", () => {
146164 expect ( buildMcpToolName ( "server@name" , "tool!name" ) ) . toBe ( "mcp--servername--toolname" )
147165 } )
148166
149- it ( "should truncate long names to 64 characters" , ( ) => {
167+ it ( "should truncate long names to 64 characters with hash suffix " , ( ) => {
150168 const longServer = "a" . repeat ( 50 )
151169 const longTool = "b" . repeat ( 50 )
152170 const result = buildMcpToolName ( longServer , longTool )
153171 expect ( result . length ) . toBeLessThanOrEqual ( 64 )
172+ expect ( result . length ) . toBe ( 64 )
154173 expect ( result . startsWith ( "mcp--" ) ) . toBe ( true )
174+ // Should end with underscore + 8 char hash suffix
175+ expect ( result ) . toMatch ( / _ [ a - f 0 - 9 ] { 8 } $ / )
176+ } )
177+
178+ it ( "should use hash suffix for long names and register them" , ( ) => {
179+ const longServer = "a" . repeat ( 50 )
180+ const longTool = "b" . repeat ( 50 )
181+ const result = buildMcpToolName ( longServer , longTool )
182+
183+ // The shortened name should be registered
184+ expect ( mcpToolNameRegistry . has ( result ) ) . toBe ( true )
185+ const registered = mcpToolNameRegistry . get ( result )
186+ expect ( registered ) . toEqual ( {
187+ serverName : longServer ,
188+ toolName : longTool ,
189+ } )
190+ } )
191+
192+ it ( "should produce deterministic hash suffixes" , ( ) => {
193+ const longServer = "a" . repeat ( 50 )
194+ const longTool = "b" . repeat ( 50 )
195+ // Build the same name twice
196+ clearMcpToolNameRegistry ( )
197+ const result1 = buildMcpToolName ( longServer , longTool )
198+ clearMcpToolNameRegistry ( )
199+ const result2 = buildMcpToolName ( longServer , longTool )
200+ // Should produce identical results
201+ expect ( result1 ) . toBe ( result2 )
202+ } )
203+
204+ it ( "should produce unique hash suffixes for different tools" , ( ) => {
205+ const longServer = "a" . repeat ( 50 )
206+ const result1 = buildMcpToolName ( longServer , "tool1_" + "x" . repeat ( 40 ) )
207+ const result2 = buildMcpToolName ( longServer , "tool2_" + "y" . repeat ( 40 ) )
208+ // Both should be truncated
209+ expect ( result1 . length ) . toBe ( 64 )
210+ expect ( result2 . length ) . toBe ( 64 )
211+ // Should have different hash suffixes
212+ expect ( result1 ) . not . toBe ( result2 )
155213 } )
156214
157215 it ( "should handle names starting with numbers" , ( ) => {
@@ -347,4 +405,119 @@ describe("mcp-name utilities", () => {
347405 } )
348406 } )
349407 } )
408+
409+ describe ( "computeHashSuffix" , ( ) => {
410+ it ( "should compute deterministic hash for the same inputs" , ( ) => {
411+ const hash1 = computeHashSuffix ( "server" , "tool" )
412+ const hash2 = computeHashSuffix ( "server" , "tool" )
413+ expect ( hash1 ) . toBe ( hash2 )
414+ } )
415+
416+ it ( "should return 8-character hex string" , ( ) => {
417+ const hash = computeHashSuffix ( "server" , "tool" )
418+ expect ( hash ) . toHaveLength ( 8 )
419+ expect ( hash ) . toMatch ( / ^ [ a - f 0 - 9 ] { 8 } $ / )
420+ } )
421+
422+ it ( "should produce different hashes for different inputs" , ( ) => {
423+ const hash1 = computeHashSuffix ( "server1" , "tool" )
424+ const hash2 = computeHashSuffix ( "server2" , "tool" )
425+ expect ( hash1 ) . not . toBe ( hash2 )
426+ } )
427+ } )
428+
429+ describe ( "hash suffix roundtrip - fixes issue #10766" , ( ) => {
430+ it ( "should preserve original names through roundtrip with long hyphenated tool names" , ( ) => {
431+ // This is the exact scenario from issue #10766
432+ // Tool name with many hyphens that exceeds 64 chars when encoded
433+ const serverName = "abcdefghij-kl-mnop-qrs-tuv"
434+ const toolName = "wxyz-abcd-efghijk-lmno"
435+
436+ // Build the tool name
437+ const builtName = buildMcpToolName ( serverName , toolName )
438+
439+ // Should be truncated to 64 chars with hash suffix
440+ expect ( builtName . length ) . toBe ( 64 )
441+ expect ( builtName ) . toMatch ( / _ [ a - f 0 - 9 ] { 8 } $ / )
442+
443+ // The critical fix: parsing should return the ORIGINAL names
444+ const parsed = parseMcpToolName ( builtName )
445+ expect ( parsed ) . toEqual ( {
446+ serverName : serverName , // Original with hyphens!
447+ toolName : toolName , // Original with hyphens!
448+ } )
449+ } )
450+
451+ it ( "should not corrupt hyphen encoding when truncation is needed" , ( ) => {
452+ // Long server and tool names that would cause truncation mid-encoding
453+ const serverName = "very-long-server-name-with-many-hyphens"
454+ const toolName = "another-long-tool-name-with-hyphens"
455+
456+ // Build the tool name
457+ const builtName = buildMcpToolName ( serverName , toolName )
458+
459+ // Should be truncated to 64 chars
460+ expect ( builtName . length ) . toBe ( 64 )
461+
462+ // Parse should return original names (via registry lookup)
463+ const parsed = parseMcpToolName ( builtName )
464+ expect ( parsed ) . toEqual ( {
465+ serverName : serverName ,
466+ toolName : toolName ,
467+ } )
468+ } )
469+
470+ it ( "should work correctly when names do not need truncation" , ( ) => {
471+ // Short names that don't need truncation
472+ const serverName = "server"
473+ const toolName = "get-data"
474+
475+ const builtName = buildMcpToolName ( serverName , toolName )
476+
477+ // Should NOT have hash suffix
478+ expect ( builtName ) . toBe ( "mcp--server--get___data" )
479+ expect ( builtName . length ) . toBeLessThan ( 64 )
480+
481+ // Normal decode path should work
482+ const parsed = parseMcpToolName ( builtName )
483+ expect ( parsed ) . toEqual ( {
484+ serverName : "server" ,
485+ toolName : "get-data" , // Hyphen decoded from ___
486+ } )
487+ } )
488+
489+ it ( "should handle the registry lookup for shortened names" , ( ) => {
490+ const serverName = "a" . repeat ( 30 )
491+ const toolName = "b" . repeat ( 30 ) + "-hyphen"
492+
493+ // Build registers the shortened name
494+ const builtName = buildMcpToolName ( serverName , toolName )
495+
496+ // Verify it's in the registry
497+ expect ( mcpToolNameRegistry . has ( builtName ) ) . toBe ( true )
498+
499+ // Parse uses the registry to get original names
500+ const parsed = parseMcpToolName ( builtName )
501+ expect ( parsed ) . toEqual ( {
502+ serverName : serverName ,
503+ toolName : toolName ,
504+ } )
505+ } )
506+ } )
507+
508+ describe ( "clearMcpToolNameRegistry" , ( ) => {
509+ it ( "should clear all registered tool names" , ( ) => {
510+ // Register some tool names via buildMcpToolName
511+ const longServer = "a" . repeat ( 50 )
512+ const longTool = "b" . repeat ( 50 )
513+ buildMcpToolName ( longServer , longTool )
514+
515+ expect ( mcpToolNameRegistry . size ) . toBeGreaterThan ( 0 )
516+
517+ // Clear the registry
518+ clearMcpToolNameRegistry ( )
519+
520+ expect ( mcpToolNameRegistry . size ) . toBe ( 0 )
521+ } )
522+ } )
350523} )
0 commit comments