char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
0d9ceae1e
char/aliceai-t5-moe
1import { Client } from '@modelcontextprotocol/sdk/client' ; 2import { CORS_PROXY } from '$lib/constants' ; 3import { MCPConnectionPhase , MCPTransportType } from '$lib/enums' ; 4import { MCPService } from '$lib/services/mcp.service' ; 5import type { MCPConnection , MCPConnectionLog , MCPServerConfig } from '$lib/types' ; 6import { afterEach , describe , expect , it , vi } from 'vitest' ; 7 8type DiagnosticFetchFactory = ( 9serverName :string , 10config :MCPServerConfig , 11baseInit :RequestInit , 12targetUrl :URL , 13useProxy :boolean , 14onLog ?:( log :MCPConnectionLog ) => void 15) => { fetch :typeof fetch ; disable :() => void }; 16 17const createDiagnosticFetch = ( 18config :MCPServerConfig , 19onLog ?:( log :MCPConnectionLog ) => void , 20baseInit :RequestInit = {}, 21useProxy = false 22) => 23( 24MCPService as unknown as { createDiagnosticFetch :DiagnosticFetchFactory } 25). createDiagnosticFetch ( 'test-server' , config , baseInit , new URL ( config . url ), useProxy , onLog ); 26 27describe ( 'MCPService' , () => { 28afterEach (() => { 29vi . restoreAllMocks (); 30vi . unstubAllGlobals (); 31}); 32 33it ( 'stops transport phase logging after handshake diagnostics are disabled' , async () => { 34const logs :MCPConnectionLog [] = []; 35const response = new Response ( '{}' , { 36headers :{ 'content-type' :'application/json' }, 37status :200 38}); 39 40vi . stubGlobal ( 'fetch' , vi . fn (). mockResolvedValue ( response )); 41 42const config :MCPServerConfig = { 43transport :MCPTransportType . STREAMABLE_HTTP , 44url :'https://example.com/mcp' 45}; 46const controller = createDiagnosticFetch ( config , ( log ) => logs . push ( log )); 47 48await controller . fetch ( config . url , { body :'{}' , method :'POST' }); 49expect ( logs ). toHaveLength ( 2 ); 50expect ( logs . every (( log ) => log . message . includes ( 'https://example.com/mcp' ))). toBe ( true ); 51 52controller . disable (); 53await controller . fetch ( config . url , { body :'{}' , method :'POST' }); 54 55expect ( logs ). toHaveLength ( 2 ); 56}); 57 58it ( 'redacts all configured custom headers in diagnostic request logs' , async () => { 59const logs :MCPConnectionLog [] = []; 60const response = new Response ( '{}' , { 61headers :{ 'content-type' :'application/json' }, 62status :200 63}); 64 65vi . stubGlobal ( 'fetch' , vi . fn (). mockResolvedValue ( response )); 66 67const config :MCPServerConfig = { 68headers :{ 69'x-auth-token' :'secret-token' , 70'x-vendor-api-key' :'secret-key' 71}, 72transport :MCPTransportType . STREAMABLE_HTTP , 73url :'https://example.com/mcp' 74}; 75const controller = createDiagnosticFetch ( config , ( log ) => logs . push ( log ), { 76headers :config . headers 77}); 78 79await controller . fetch ( config . url , { 80body :'{}' , 81headers :{ 'content-type' :'application/json' }, 82method :'POST' 83}); 84 85expect ( logs ). toHaveLength ( 2 ); 86expect ( logs [ 0 ]. details ). toMatchObject ({ 87request :{ 88headers :{ 89'content-type' :'application/json' , 90'x-auth-token' :'[redacted]' , 91'x-vendor-api-key' :'[redacted]' 92} 93} 94}); 95}); 96 97it ( 'wraps dynamic request headers when using the CORS proxy' , async () => { 98const logs :MCPConnectionLog [] = []; 99const proxiedAuthToken = ` ${ CORS_PROXY . HEADER_PREFIX } x-auth-token` ; 100const proxiedContentType = ` ${ CORS_PROXY . HEADER_PREFIX } content-type` ; 101const proxiedSessionId = ` ${ CORS_PROXY . HEADER_PREFIX } mcp-session-id` ; 102const response = new Response ( '{}' , { 103headers :{ 'content-type' :'application/json' }, 104status :200 105}); 106const fetchMock = vi . fn (). mockResolvedValue ( response ); 107 108vi . stubGlobal ( 'fetch' , fetchMock ); 109 110const config :MCPServerConfig = { 111transport :MCPTransportType . STREAMABLE_HTTP , 112url :'https://example.com/mcp' , 113useProxy :true 114}; 115const controller = createDiagnosticFetch ( 116config , 117( log ) => logs . push ( log ), 118{ 119headers :{ 120authorization :'Bearer llama-server-key' , 121[ proxiedAuthToken ] :'target-token' 122} 123}, 124true 125); 126 127await controller . fetch ( 'http://localhost:8080/cors-proxy?url=https%3A%2F%2Fexample.com%2Fmcp' , { 128body :'{}' , 129headers :{ 130'content-type' :'application/json' , 131'mcp-session-id' :'session-request-12345' 132}, 133method :'POST' 134}); 135 136const sentHeaders = fetchMock . mock . calls [ 0 ]?.[ 1 ]?. headers as Headers ; 137 138expect ( sentHeaders . get ( 'authorization' )). toBe ( 'Bearer llama-server-key' ); 139expect ( sentHeaders . get ( proxiedAuthToken )). toBe ( 'target-token' ); 140expect ( sentHeaders . get ( proxiedContentType )). toBe ( 'application/json' ); 141expect ( sentHeaders . get ( proxiedSessionId )). toBe ( 'session-request-12345' ); 142expect ( sentHeaders . has ( 'content-type' )). toBe ( false ); 143expect ( sentHeaders . has ( 'mcp-session-id' )). toBe ( false ); 144expect ( logs [ 0 ]. details ). toMatchObject ({ 145request :{ 146headers :{ 147authorization :'[redacted]' , 148[ proxiedAuthToken ] :'[redacted]' , 149[ proxiedSessionId ] :'....12345' 150} 151} 152}); 153}); 154 155it ( 'DELETE request with CORS proxy should return a fake 200 response' , async () => { 156const logs :MCPConnectionLog [] = []; 157const fetchMock = vi . fn (); 158 159vi . stubGlobal ( 'fetch' , fetchMock ); 160 161const config :MCPServerConfig = { 162transport :MCPTransportType . STREAMABLE_HTTP , 163url :'https://example.com/mcp' , 164useProxy :true 165}; 166const controller = createDiagnosticFetch ( config , ( log ) => logs . push ( log ), {}, true ); 167const response = await controller . fetch ( 168'http://localhost:8080/cors-proxy?url=https%3A%2F%2Fexample.com%2Fmcp' , 169{ method :'DELETE' } 170); 171 172expect ( fetchMock ). not . toHaveBeenCalled (); 173expect ( response . status ). toBe ( 200 ); 174expect ( logs . at ( - 1 )?. details ). toMatchObject ({ 175response :{ isFake :true , status :200 } 176}); 177}); 178 179it ( 'partially redacts mcp-session-id in diagnostic request and response logs' , async () => { 180const logs :MCPConnectionLog [] = []; 181const response = new Response ( '{}' , { 182headers :{ 183'content-type' :'application/json' , 184'mcp-session-id' :'session-response-67890' 185}, 186status :200 187}); 188 189vi . stubGlobal ( 'fetch' , vi . fn (). mockResolvedValue ( response )); 190 191const config :MCPServerConfig = { 192transport :MCPTransportType . STREAMABLE_HTTP , 193url :'https://example.com/mcp' 194}; 195const controller = createDiagnosticFetch ( config , ( log ) => logs . push ( log )); 196 197await controller . fetch ( config . url , { 198body :'{}' , 199headers :{ 200'content-type' :'application/json' , 201'mcp-session-id' :'session-request-12345' 202}, 203method :'POST' 204}); 205 206expect ( logs ). toHaveLength ( 2 ); 207expect ( logs [ 0 ]. details ). toMatchObject ({ 208request :{ 209headers :{ 210'content-type' :'application/json' , 211'mcp-session-id' :'....12345' 212} 213} 214}); 215expect ( logs [ 1 ]. details ). toMatchObject ({ 216response :{ 217headers :{ 218'content-type' :'application/json' , 219'mcp-session-id' :'....67890' 220} 221} 222}); 223}); 224 225it ( 'extracts JSON-RPC methods without logging the raw request body' , async () => { 226const logs :MCPConnectionLog [] = []; 227const response = new Response ( '{}' , { 228headers :{ 'content-type' :'application/json' }, 229status :200 230}); 231 232vi . stubGlobal ( 'fetch' , vi . fn (). mockResolvedValue ( response )); 233 234const config :MCPServerConfig = { 235transport :MCPTransportType . STREAMABLE_HTTP , 236url :'https://example.com/mcp' 237}; 238const controller = createDiagnosticFetch ( config , ( log ) => logs . push ( log )); 239 240await controller . fetch ( config . url , { 241body :JSON . stringify ([ 242{ id :1 , jsonrpc :'2.0' , method :'initialize' }, 243{ jsonrpc :'2.0' , method :'notifications/initialized' } 244]), 245method :'POST' 246}); 247 248expect ( logs [ 0 ]. details ). toMatchObject ({ 249request :{ 250body :{ 251kind :'string' , 252size :expect . any ( Number ) 253}, 254jsonRpcMethods :[ 'initialize' , 'notifications/initialized' ], 255method :'POST' 256} 257}); 258}); 259 260it ( 'adds a CORS hint to Failed to fetch diagnostic log messages' , async () => { 261const logs :MCPConnectionLog [] = []; 262const fetchError = new TypeError ( 'Failed to fetch' ); 263 264vi . stubGlobal ( 'fetch' , vi . fn (). mockRejectedValue ( fetchError )); 265 266const config :MCPServerConfig = { 267transport :MCPTransportType . STREAMABLE_HTTP , 268url :'http://localhost:8000/mcp' 269}; 270const controller = createDiagnosticFetch ( config , ( log ) => logs . push ( log )); 271 272await expect ( controller . fetch ( config . url , { body :'{}' , method :'POST' })). rejects . toThrow ( 273'Failed to fetch' 274); 275 276expect ( logs ). toHaveLength ( 2 ); 277expect ( logs [ 1 ]. message ). toBe ( 278'HTTP POST http://localhost:8000/mcp failed: Failed to fetch (check CORS?)' 279); 280}); 281 282it ( 'detaches phase error logging after the initialize handshake completes' , async () => { 283const phaseLogs :Array <{ phase :MCPConnectionPhase ; log :MCPConnectionLog }> = []; 284const stopPhaseLogging = vi . fn (); 285 286let emitClientError :(( error :Error ) => void ) | undefined ; 287 288vi . spyOn ( MCPService , 'createTransport' ). mockReturnValue ({ 289 stopPhaseLogging, 290transport :{} as never , 291type :MCPTransportType . WEBSOCKET 292}); 293vi . spyOn ( MCPService , 'listTools' ). mockResolvedValue ([]); 294vi . spyOn ( Client . prototype , 'getServerVersion' ). mockReturnValue ( undefined ); 295vi . spyOn ( Client . prototype , 'getServerCapabilities' ). mockReturnValue ( undefined ); 296vi . spyOn ( Client . prototype , 'getInstructions' ). mockReturnValue ( undefined ); 297vi . spyOn ( Client . prototype , 'connect' ). mockImplementation ( async function ( this :Client ) { 298emitClientError = ( error :Error ) => this . onerror ?.( error ); 299this . onerror ?.( new Error ( 'handshake protocol error' )); 300}); 301 302await MCPService . connect ( 303'test-server' , 304{ 305transport :MCPTransportType . WEBSOCKET , 306url :'ws://example.com/mcp' 307}, 308undefined , 309undefined , 310( phase , log ) => phaseLogs . push ({ log, phase}) 311); 312 313expect ( stopPhaseLogging ). toHaveBeenCalledTimes ( 1 ); 314expect ( 315phaseLogs . filter ( 316({ log, phase}) => 317phase === MCPConnectionPhase . ERROR && 318log . message === 'Protocol error: handshake protocol error' 319) 320). toHaveLength ( 1 ); 321 322emitClientError ?.( new Error ( 'runtime protocol error' )); 323 324expect ( 325phaseLogs . filter ( 326({ log, phase}) => 327phase === MCPConnectionPhase . ERROR && 328log . message === 'Protocol error: runtime protocol error' 329) 330). toHaveLength ( 0 ); 331}); 332 333it ( 'falls back to structuredContent when content array is empty' , async () => { 334const connection = { 335client :{ 336callTool :vi . fn (). mockResolvedValue ({ 337content :[], 338structuredContent :{ accounts :[{ id :1 }], total :1 } 339}) 340}, 341requestTimeoutMs :9000 , 342serverName :'test-server' 343} as unknown as MCPConnection ; 344const result = await MCPService . callTool ( connection , { arguments :{}, name :'tool' }); 345 346expect ( result . isError ). toBe ( false ); 347expect ( result . content ). toBe ( '{"accounts":[{"id":1}],"total":1}' ); 348}); 349});