11import { describe , it , expect } from "vitest" ;
2- import { sanitizeKey , normalizePath , buildClientUserAgent } from "./utils.js" ;
2+ import {
3+ sanitizeKey ,
4+ normalizePath ,
5+ buildClientUserAgent ,
6+ type MCPClientInfo
7+ } from "./utils.js" ;
38
49describe ( "sanitizeKey" , ( ) => {
5- it ( "replaces unsafe characters with underscores" , ( ) => {
6- expect ( sanitizeKey ( "foo/bar/baz " ) ) . toBe ( "foo_bar_baz " ) ;
10+ it ( "should replace unsafe characters with underscores" , ( ) => {
11+ expect ( sanitizeKey ( "foo/bar" ) ) . toBe ( "foo_bar " ) ;
712 expect ( sanitizeKey ( "foo:bar" ) ) . toBe ( "foo_bar" ) ;
8- expect ( sanitizeKey ( "foo@bar.com " ) ) . toBe ( "foo_bar_com " ) ;
13+ expect ( sanitizeKey ( "foo@bar" ) ) . toBe ( "foo_bar " ) ;
914 } ) ;
1015
11- it ( "collapses multiple underscores" , ( ) => {
16+ it ( "should collapse multiple underscores" , ( ) => {
1217 expect ( sanitizeKey ( "foo//bar" ) ) . toBe ( "foo_bar" ) ;
13- expect ( sanitizeKey ( "foo___bar " ) ) . toBe ( "foo_bar" ) ;
18+ expect ( sanitizeKey ( "foo:::bar " ) ) . toBe ( "foo_bar" ) ;
1419 } ) ;
1520
16- it ( "strips leading and trailing underscores" , ( ) => {
17- expect ( sanitizeKey ( "_foo_ " ) ) . toBe ( "foo" ) ;
18- expect ( sanitizeKey ( "__foo__ " ) ) . toBe ( "foo" ) ;
21+ it ( "should trim leading/ trailing underscores" , ( ) => {
22+ expect ( sanitizeKey ( "/foo " ) ) . toBe ( "foo" ) ;
23+ expect ( sanitizeKey ( "foo/ " ) ) . toBe ( "foo" ) ;
1924 } ) ;
2025
21- it ( "preserves safe characters" , ( ) => {
22- expect ( sanitizeKey ( "foo-bar_baz123" ) ) . toBe ( "foo-bar_baz123" ) ;
26+ it ( "should keep valid characters" , ( ) => {
27+ expect ( sanitizeKey ( "foo-bar_baz" ) ) . toBe ( "foo-bar_baz" ) ;
28+ expect ( sanitizeKey ( "FooBar123" ) ) . toBe ( "FooBar123" ) ;
2329 } ) ;
2430} ) ;
2531
@@ -29,63 +35,102 @@ describe("normalizePath", () => {
2935 expect ( normalizePath ( "./foo/bar" ) ) . toBe ( "foo/bar" ) ;
3036 } ) ;
3137
32- it ( "removes leading slashes " , ( ) => {
38+ it ( "removes leading / " , ( ) => {
3339 expect ( normalizePath ( "/src" ) ) . toBe ( "src" ) ;
34- expect ( normalizePath ( "//src" ) ) . toBe ( "src" ) ;
40+ expect ( normalizePath ( "/// src" ) ) . toBe ( "src" ) ;
3541 } ) ;
3642
37- it ( "removes trailing slashes " , ( ) => {
43+ it ( "removes trailing / " , ( ) => {
3844 expect ( normalizePath ( "src/" ) ) . toBe ( "src" ) ;
39- expect ( normalizePath ( "src//" ) ) . toBe ( "src" ) ;
45+ expect ( normalizePath ( "src/// " ) ) . toBe ( "src" ) ;
4046 } ) ;
4147
4248 it ( "collapses multiple slashes" , ( ) => {
4349 expect ( normalizePath ( "src//lib" ) ) . toBe ( "src/lib" ) ;
44- expect ( normalizePath ( "a/// b//c" ) ) . toBe ( "a/b/c" ) ;
50+ expect ( normalizePath ( "a//b//c" ) ) . toBe ( "a/b/c" ) ;
4551 } ) ;
4652
47- it ( "returns empty string for root representations " , ( ) => {
53+ it ( "handles root paths " , ( ) => {
4854 expect ( normalizePath ( "./" ) ) . toBe ( "" ) ;
4955 expect ( normalizePath ( "/" ) ) . toBe ( "" ) ;
5056 expect ( normalizePath ( "" ) ) . toBe ( "" ) ;
5157 } ) ;
58+
59+ it ( "handles complex paths" , ( ) => {
60+ expect ( normalizePath ( "./src//lib/" ) ) . toBe ( "src/lib" ) ;
61+ expect ( normalizePath ( "///a//b//c///" ) ) . toBe ( "a/b/c" ) ;
62+ } ) ;
5263} ) ;
5364
5465describe ( "buildClientUserAgent" , ( ) => {
55- it ( "builds CLI user agent " , ( ) => {
66+ it ( "builds CLI User-Agent " , ( ) => {
5667 const ua = buildClientUserAgent ( "cli" ) ;
57- expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. c l i \/ \d + \. \d + \. \d + / ) ;
58- } ) ;
59-
60- it ( "builds SDK user agent" , ( ) => {
61- const ua = buildClientUserAgent ( "sdk" ) ;
62- expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. s d k \/ \d + \. \d + \. \d + / ) ;
68+ expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. c l i \/ [ \d . ] + / ) ;
6369 } ) ;
6470
65- it ( "builds MCP user agent without client info " , ( ) => {
71+ it ( "builds MCP User-Agent " , ( ) => {
6672 const ua = buildClientUserAgent ( "mcp" ) ;
67- expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. m c p \/ \d + \. \d + \. \d + $ / ) ;
68- } ) ;
69-
70- it ( "builds MCP user agent with client info" , ( ) => {
71- const ua = buildClientUserAgent ( "mcp" , { name : "claude-desktop" , version : "1.0.0" } ) ;
72- expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. m c p \/ \d + \. \d + \. \d + \/ c l a u d e - d e s k t o p \/ 1 \. 0 \. 0 $ / ) ;
73- } ) ;
74-
75- it ( "builds MCP user agent with client name only" , ( ) => {
76- const ua = buildClientUserAgent ( "mcp" , { name : "cursor" } ) ;
77- expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. m c p \/ \d + \. \d + \. \d + \/ c u r s o r $ / ) ;
73+ expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. m c p \/ [ \d . ] + / ) ;
7874 } ) ;
7975
80- it ( "sanitizes MCP client info - spaces replaced with dashes" , ( ) => {
81- const ua = buildClientUserAgent ( "mcp" , { name : "My App" , version : "1.2.3" } ) ;
82- // Space replaced with -
83- expect ( ua ) . toMatch ( / \/ M y - A p p \/ 1 \. 2 \. 3 $ / ) ;
76+ it ( "builds SDK User-Agent" , ( ) => {
77+ const ua = buildClientUserAgent ( "sdk" ) ;
78+ expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. s d k \/ [ \d . ] + / ) ;
79+ } ) ;
80+
81+ it ( "appends MCP client info with name and version" , ( ) => {
82+ const mcpClientInfo : MCPClientInfo = {
83+ name : "claude-desktop" ,
84+ version : "1.0.0"
85+ } ;
86+ const ua = buildClientUserAgent ( "mcp" , mcpClientInfo ) ;
87+ expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. m c p \/ [ \d . ] + \/ c l a u d e - d e s k t o p \/ 1 \. 0 \. 0 $ / ) ;
88+ } ) ;
89+
90+ it ( "appends MCP client info with name only" , ( ) => {
91+ const mcpClientInfo : MCPClientInfo = {
92+ name : "cursor"
93+ } ;
94+ const ua = buildClientUserAgent ( "mcp" , mcpClientInfo ) ;
95+ expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. m c p \/ [ \d . ] + \/ c u r s o r $ / ) ;
96+ } ) ;
97+
98+ it ( "sanitizes MCP client name per RFC 9110" , ( ) => {
99+ const mcpClientInfo : MCPClientInfo = {
100+ name : "My App" , // space not allowed
101+ version : "1.0"
102+ } ;
103+ const ua = buildClientUserAgent ( "mcp" , mcpClientInfo ) ;
104+ expect ( ua ) . toContain ( "/My-App/" ) ; // space replaced with -
105+ } ) ;
106+
107+ it ( "truncates long client names to 32 chars" , ( ) => {
108+ const mcpClientInfo : MCPClientInfo = {
109+ name : "a" . repeat ( 50 ) ,
110+ version : "1.0"
111+ } ;
112+ const ua = buildClientUserAgent ( "mcp" , mcpClientInfo ) ;
113+ // Should contain 32 'a's, not 50
114+ expect ( ua ) . toContain ( "/" + "a" . repeat ( 32 ) + "/" ) ;
115+ } ) ;
116+
117+ it ( "truncates long versions to 8 chars" , ( ) => {
118+ const mcpClientInfo : MCPClientInfo = {
119+ name : "app" ,
120+ version : "1.2.3-beta.4" // 12 chars
121+ } ;
122+ const ua = buildClientUserAgent ( "mcp" , mcpClientInfo ) ;
123+ // Version should be truncated to 8 chars: "1.2.3-be"
124+ expect ( ua ) . toMatch ( / \/ a p p \/ 1 \. 2 \. 3 - b e $ / ) ;
84125 } ) ;
85126
86- it ( "truncates long version strings" , ( ) => {
87- const ua = buildClientUserAgent ( "mcp" , { name : "app" , version : "1.2.3-beta.1" } ) ;
88- // Version truncated to 8 chars: "1.2.3-be"
89- expect ( ua ) . toMatch ( / \/ a p p \/ 1 \. 2 \. 3 - b e $ / ) ;
127+ it ( "ignores MCP client info for non-MCP products" , ( ) => {
128+ const mcpClientInfo : MCPClientInfo = {
129+ name : "should-be-ignored" ,
130+ version : "1.0"
131+ } ;
132+ const ua = buildClientUserAgent ( "cli" , mcpClientInfo ) ;
133+ expect ( ua ) . not . toContain ( "should-be-ignored" ) ;
134+ expect ( ua ) . toMatch ( / ^ a u g m e n t \. c t x c \. c l i \/ [ \d . ] + $ / ) ;
90135 } ) ;
91136} ) ;
0 commit comments