@frontmcp/testing 1.11.1 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +11 -1
  2. package/assertions/mcp-assertions.d.ts +2 -2
  3. package/assertions/mcp-assertions.d.ts.map +1 -1
  4. package/auth/mock-cimd-server.d.ts +12 -2
  5. package/auth/mock-cimd-server.d.ts.map +1 -1
  6. package/client/index.d.ts +1 -1
  7. package/client/index.d.ts.map +1 -1
  8. package/client/mcp-test-client.builder.d.ts +9 -4
  9. package/client/mcp-test-client.builder.d.ts.map +1 -1
  10. package/client/mcp-test-client.d.ts +34 -1
  11. package/client/mcp-test-client.d.ts.map +1 -1
  12. package/client/mcp-test-client.types.d.ts +17 -3
  13. package/client/mcp-test-client.types.d.ts.map +1 -1
  14. package/esm/fixtures/index.mjs +2 -2
  15. package/esm/index.mjs +3 -3
  16. package/esm/package.json +6 -6
  17. package/esm/perf/index.mjs +1 -1
  18. package/fixtures/fixture-types.d.ts +13 -0
  19. package/fixtures/fixture-types.d.ts.map +1 -1
  20. package/fixtures/index.js +3 -3
  21. package/fixtures/test-fixture.d.ts.map +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.d.ts.map +1 -1
  24. package/index.js +7 -7
  25. package/interceptor/interceptor-chain.d.ts +1 -1
  26. package/interceptor/interceptor-chain.d.ts.map +1 -1
  27. package/interceptor/mock-registry.d.ts +1 -1
  28. package/interceptor/mock-registry.d.ts.map +1 -1
  29. package/matchers/index.d.ts +1 -1
  30. package/matchers/index.d.ts.map +1 -1
  31. package/package.json +6 -6
  32. package/perf/index.js +4 -4
  33. package/perf/leak-detector.d.ts.map +1 -1
  34. package/perf/metrics-collector.d.ts +1 -1
  35. package/perf/metrics-collector.d.ts.map +1 -1
  36. package/perf/perf-fixtures.d.ts +1 -1
  37. package/perf/perf-fixtures.d.ts.map +1 -1
  38. package/perf/regression-detector.d.ts +1 -1
  39. package/perf/regression-detector.d.ts.map +1 -1
  40. package/transport/mcp-20260728.d.ts +19 -0
  41. package/transport/mcp-20260728.d.ts.map +1 -0
  42. package/transport/streamable-http.transport.d.ts +12 -0
  43. package/transport/streamable-http.transport.d.ts.map +1 -1
  44. package/transport/transport.interface.d.ts +14 -1
  45. package/transport/transport.interface.d.ts.map +1 -1
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
- "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:true}):target,mod));var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var index_exports={};__export(index_exports,{AssertionError:()=>AssertionError,AuthHeaders:()=>AuthHeaders,BASIC_UI_TOOL_CONFIG:()=>BASIC_UI_TOOL_CONFIG,BaselineStore:()=>BaselineStore,ConnectionError:()=>ConnectionError,DefaultInterceptorChain:()=>DefaultInterceptorChain,DefaultMockRegistry:()=>DefaultMockRegistry,EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS:()=>EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS:()=>EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,EXPECTED_GENERIC_TOOLS_LIST_META_KEYS:()=>EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,EXPECTED_GENERIC_TOOL_CALL_META_KEYS:()=>EXPECTED_GENERIC_TOOL_CALL_META_KEYS,EXPECTED_OPENAI_TOOLS_LIST_META_KEYS:()=>EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,EXPECTED_OPENAI_TOOL_CALL_META_KEYS:()=>EXPECTED_OPENAI_TOOL_CALL_META_KEYS,FULL_UI_TOOL_CONFIG:()=>FULL_UI_TOOL_CONFIG,LeakDetector:()=>LeakDetector,McpAssertions:()=>McpAssertions,McpClient:()=>import_client.Client,McpProtocolError:()=>McpProtocolError,McpStdioClientTransport:()=>import_client2.StdioClientTransport,McpTestClient:()=>McpTestClient,McpTestClientBuilder:()=>McpTestClientBuilder,MetricsCollector:()=>MetricsCollector,MockAPIServer:()=>MockAPIServer,MockCimdServer:()=>MockCimdServer,MockOAuthServer:()=>MockOAuthServer,PLATFORM_DETECTION_PATTERNS:()=>PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl:()=>PerfFixturesImpl,RegressionDetector:()=>RegressionDetector,ReplayRecordingError:()=>ReplayRecordingError,ReportGenerator:()=>ReportGenerator,ServerStartError:()=>ServerStartError,SseTransport:()=>SseTransport,StreamableHttpTransport:()=>StreamableHttpTransport,TestClientError:()=>TestClientError,TestServer:()=>TestServer,TestTokenFactory:()=>TestTokenFactory,TestUsers:()=>TestUsers,TimeoutError:()=>TimeoutError,UIAssertions:()=>UIAssertions,assertNoLeak:()=>assertNoLeak,basicUIToolInputSchema:()=>basicUIToolInputSchema,basicUIToolOutputSchema:()=>basicUIToolOutputSchema,buildUserAgent:()=>buildUserAgent,clearGlobalMeasurements:()=>clearGlobalMeasurements,containsPrompt:()=>containsPrompt,containsResource:()=>containsResource,containsResourceTemplate:()=>containsResourceTemplate,containsTool:()=>containsTool,createLeakDetector:()=>createLeakDetector,createPerfFixtures:()=>createPerfFixtures,createReportGenerator:()=>createReportGenerator,createTestUser:()=>createTestUser,expect:()=>expect,forceFullGc:()=>forceFullGc,forceGc:()=>forceGc,formatBaselineAsComment:()=>formatBaselineAsComment,formatBytes:()=>formatBytes,formatDuration:()=>formatDuration,formatMicroseconds:()=>formatMicroseconds,formatReplayReport:()=>formatReplayReport,fullUIToolInputSchema:()=>fullUIToolInputSchema,fullUIToolOutputSchema:()=>fullUIToolOutputSchema,generateBasicUIToolOutput:()=>generateBasicUIToolOutput,generateFullUIToolOutput:()=>generateFullUIToolOutput,getBaselineStore:()=>getBaselineStore,getForbiddenMetaPrefixes:()=>getForbiddenMetaPrefixes,getGlobalMeasurements:()=>getGlobalMeasurements,getPlatformClientInfo:()=>getPlatformClientInfo,getPlatformMetaNamespace:()=>getPlatformMetaNamespace,getPlatformMimeType:()=>getPlatformMimeType,getPlatformUserAgent:()=>getPlatformUserAgent,getRegressionDetector:()=>getRegressionDetector,getToolCallMetaPrefixes:()=>getToolCallMetaPrefixes,getToolsListMetaPrefixes:()=>getToolsListMetaPrefixes,hasMimeType:()=>hasMimeType,hasTextContent:()=>hasTextContent,httpMock:()=>httpMock,httpResponse:()=>httpResponse,interceptors:()=>interceptors,isError:()=>isError,isExtAppsPlatform:()=>isExtAppsPlatform,isGcAvailable:()=>isGcAvailable,isOpenAIPlatform:()=>isOpenAIPlatform,isSuccessful:()=>isSuccessful,isTcpPortListening:()=>isTcpPortListening,isUiPlatform:()=>isUiPlatform,mcpMatchers:()=>mcpMatchers,mockResponse:()=>mockResponse,parseBaselineFromComment:()=>parseBaselineFromComment,perfTest:()=>perfTest,replaySession:()=>replaySession,saveReports:()=>saveReports,summarizeRegressions:()=>summarizeRegressions,test:()=>test,uiMatchers:()=>uiMatchers});module.exports=__toCommonJS(index_exports);var import_utils3=require("@frontmcp/utils");var DefaultMockRegistry=class{mocks=[];add(mock){const entry={definition:mock,callCount:0,calls:[],remainingUses:mock.times??Infinity};this.mocks.push(entry);return{remove:()=>{const index=this.mocks.indexOf(entry);if(index!==-1){this.mocks.splice(index,1)}},callCount:()=>entry.callCount,calls:()=>[...entry.calls]}}clear(){this.mocks=[]}getAll(){return this.mocks.map(e=>e.definition)}match(request){for(const entry of this.mocks){if(entry.remainingUses<=0)continue;const{definition}=entry;if(definition.method!==request.method)continue;if(definition.params!==void 0){const params=request.params??{};if(typeof definition.params==="function"){if(!definition.params(params))continue}else{if(!this.paramsMatch(definition.params,params))continue}}entry.callCount++;entry.calls.push(request);entry.remainingUses--;return definition}return void 0}paramsMatch(expected,actual){for(const[key,value]of Object.entries(expected)){if(!(key in actual))return false;const actualValue=actual[key];if(Array.isArray(value)){if(!Array.isArray(actualValue))return false;if(value.length!==actualValue.length)return false;for(let i=0;i<value.length;i++){const expectedItem=value[i];const actualItem=actualValue[i];if(typeof expectedItem==="object"&&expectedItem!==null){if(typeof actualItem!=="object"||actualItem===null)return false;if(!this.paramsMatch(expectedItem,actualItem)){return false}}else if(actualItem!==expectedItem){return false}}}else if(typeof value==="object"&&value!==null){if(typeof actualValue!=="object"||actualValue===null)return false;if(!this.paramsMatch(value,actualValue)){return false}}else if(actualValue!==value){return false}}return true}};var mockResponse={success(result,id=1){return{jsonrpc:"2.0",id,result}},error(code,message,data,id=1){return{jsonrpc:"2.0",id,error:{code,message,data}}},toolResult(content,id=1){return{jsonrpc:"2.0",id,result:{content}}},toolsList(tools,id=1){return{jsonrpc:"2.0",id,result:{tools}}},resourcesList(resources,id=1){return{jsonrpc:"2.0",id,result:{resources}}},resourceRead(contents,id=1){return{jsonrpc:"2.0",id,result:{contents}}},errors:{methodNotFound:(method,id=1)=>mockResponse.error(-32601,`Method not found: ${method}`,void 0,id),invalidParams:(message,id=1)=>mockResponse.error(-32602,message,void 0,id),internalError:(message,id=1)=>mockResponse.error(-32603,message,void 0,id),resourceNotFound:(uri,id=1)=>mockResponse.error(-32002,`Resource not found: ${uri}`,{uri},id),toolNotFound:(name,id=1)=>mockResponse.error(-32601,`Tool not found: ${name}`,{name},id),unauthorized:(id=1)=>mockResponse.error(-32001,"Unauthorized",void 0,id),forbidden:(id=1)=>mockResponse.error(-32003,"Forbidden",void 0,id)}};var DefaultInterceptorChain=class{request=[];response=[];mocks;constructor(){this.mocks=new DefaultMockRegistry}addRequestInterceptor(interceptor){this.request.push(interceptor);return()=>{const index=this.request.indexOf(interceptor);if(index!==-1){this.request.splice(index,1)}}}addResponseInterceptor(interceptor){this.response.push(interceptor);return()=>{const index=this.response.indexOf(interceptor);if(index!==-1){this.response.splice(index,1)}}}async processRequest(request,meta){let currentRequest=request;const mockDef=this.mocks.match(request);if(mockDef){if(mockDef.delay&&mockDef.delay>0){await sleep(mockDef.delay)}let mockResponse2;if(typeof mockDef.response==="function"){mockResponse2=await mockDef.response(request)}else{mockResponse2=mockDef.response}return{type:"mock",response:{...mockResponse2,id:request.id??mockResponse2.id}}}for(const interceptor of this.request){const ctx={request:currentRequest,meta};const result=await interceptor(ctx);switch(result.action){case"passthrough":break;case"modify":currentRequest=result.request;break;case"mock":return{type:"mock",response:{...result.response,id:request.id??result.response.id}};case"error":return{type:"error",error:result.error}}}return{type:"continue",request:currentRequest}}async processResponse(request,response,durationMs){let currentResponse=response;for(const interceptor of this.response){const ctx={request,response:currentResponse,durationMs};const result=await interceptor(ctx);switch(result.action){case"passthrough":break;case"modify":currentResponse=result.response;break}}return currentResponse}clear(){this.request=[];this.response=[];this.mocks.clear()}};function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}var interceptors={logger(logFn=console.log){return ctx=>{logFn(`[MCP Request] ${ctx.request.method}`,ctx.request.params);return{action:"passthrough"}}},delay(ms){return async()=>{await sleep(ms);return{action:"passthrough"}}},failWhen(condition,error){return ctx=>{if(condition(ctx)){const err=typeof error==="string"?new Error(error):error;return{action:"error",error:err}}return{action:"passthrough"}}},modifyMethod(method,modifier){return ctx=>{if(ctx.request.method===method){return{action:"modify",request:modifier(ctx.request)}}return{action:"passthrough"}}},responseLogger(logFn=console.log){return ctx=>{const status=ctx.response.error?"ERROR":"OK";logFn(`[MCP Response] ${ctx.request.method} ${status} (${ctx.durationMs}ms)`,ctx.response);return{action:"passthrough"}}},modifyResponse(method,modifier){return ctx=>{if(ctx.request.method===method){return{action:"modify",response:modifier(ctx.response)}}return{action:"passthrough"}}}};function errorName(err){if(typeof err!=="object"||err===null)return void 0;const name=err.name;return typeof name==="string"?name:void 0}function isAbortError(err){return errorName(err)==="AbortError"}function errorMessage(err,fallback="Unknown error"){if(typeof err==="string"&&err)return err;if(typeof err==="object"&&err!==null){const message=err.message;if(typeof message==="string"&&message)return message}return fallback}var interceptedErrors=new WeakSet;function markInterceptedError(err){interceptedErrors.add(err);return err}function isInterceptedError(err){return typeof err==="object"&&err!==null&&interceptedErrors.has(err)}var import_utils=require("@frontmcp/utils");var DEFAULT_TIMEOUT=3e4;var SseTransport=class{baseUrl;entryPath;timeout;authHeaders;publicMode;debug;clientInfo;notificationHandler;interceptors;elicitationHandler;state="disconnected";authToken;sessionId;messageEndpoint;stream;pending=new Map;connectionCount=0;reconnectCount=0;lastRequestHeaders={};constructor(config){this.baseUrl=config.baseUrl.replace(/\/$/,"");this.entryPath=config.entryPath;this.timeout=config.timeout??DEFAULT_TIMEOUT;this.authHeaders=config.auth?.headers??{};this.authToken=config.auth?.token;this.publicMode=config.publicMode??false;this.debug=config.debug??false;this.clientInfo=config.clientInfo;this.notificationHandler=config.notificationHandler;this.interceptors=config.interceptors;this.elicitationHandler=config.elicitationHandler}async connect(){this.state="connecting";this.connectionCount++;try{if(!this.publicMode&&!this.authToken){await this.requestAnonymousToken()}await this.openStream();this.state="connected";this.log(`Connected; messages go to ${this.messageEndpoint}`)}catch(error){this.state="error";this.stream?.abort();this.stream=void 0;throw error}}async request(message){this.ensureConnected();const startTime=Date.now();if(this.interceptors){const intercepted=await this.interceptors.processRequest(message,{timestamp:new Date,transport:"sse",sessionId:this.sessionId});switch(intercepted.type){case"mock":return await this.interceptors.processResponse(message,intercepted.response,Date.now()-startTime);case"error":throw markInterceptedError(intercepted.error);case"continue":message=intercepted.request;break}}let response;if(message.id===void 0){await this.post(JSON.stringify(message));response={jsonrpc:"2.0",id:null,result:void 0}}else{response=await this.postAndAwait(message.id,JSON.stringify(message))}if(this.interceptors){response=await this.interceptors.processResponse(message,response,Date.now()-startTime)}return response}async notify(message){this.ensureConnected();const result=await this.post(JSON.stringify(message));if(!result.ok)this.log(`HTTP ${result.status} on notification: ${result.body}`)}async sendRaw(data){this.ensureConnected();let id;try{const parsed=JSON.parse(data);if(typeof parsed.id==="string"||typeof parsed.id==="number")id=parsed.id}catch{}if(id!==void 0)return this.postAndAwait(id,data);const result=await this.post(data);if(!result.ok){return{jsonrpc:"2.0",id:null,error:{code:-32700,message:"Parse error",data:result.body}}}return{jsonrpc:"2.0",id:null,result:void 0}}async close(){this.closeStream("Transport closed");this.state="disconnected";this.sessionId=void 0;this.messageEndpoint=void 0;this.log("SSE transport closed")}isConnected(){return this.state==="connected"}getState(){return this.state}getSessionId(){return this.sessionId}setAuthToken(token){this.authToken=token}setTimeout(ms){this.timeout=ms}getMessageEndpoint(){return this.messageEndpoint}getConnectionCount(){return this.connectionCount}getReconnectCount(){return this.reconnectCount}getLastRequestHeaders(){return{...this.lastRequestHeaders}}setInterceptors(interceptors2){this.interceptors=interceptors2}getInterceptors(){return this.interceptors}setElicitationHandler(handler){this.elicitationHandler=handler}async simulateDisconnect(){this.closeStream("Simulated disconnect");this.state="disconnected";this.sessionId=void 0;this.messageEndpoint=void 0}async waitForReconnect(timeoutMs){this.reconnectCount++;let timer;try{await Promise.race([this.connect(),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error("Timeout waiting for reconnection")),timeoutMs)})])}finally{clearTimeout(timer)}}async openStream(){const controller=new AbortController;this.stream=controller;const url=this.sseUrl();const{"Content-Type":_contentType,...headers}=this.buildHeaders();this.log(`GET ${url}`);const connectTimer=setTimeout(()=>controller.abort(),this.timeout);let response;try{response=await fetch(url,{method:"GET",headers:{...headers,Accept:"text/event-stream"},signal:controller.signal})}catch(error){clearTimeout(connectTimer);throw new Error(isAbortError(error)?`SSE connection to ${url} timed out after ${this.timeout}ms`:`SSE connection to ${url} failed: ${errorMessage(error)}`,{cause:error})}const isEventStream=response.headers.get("content-type")?.includes("text/event-stream")??false;if(!response.ok||!response.body||!isEventStream){clearTimeout(connectTimer);const body=await response.text().catch(()=>"");throw new Error(`SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body?` \u2014 ${body}`:""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`)}const endpoint=new Promise((resolve,reject)=>{const reader=response.body?.getReader();if(!reader){reject(new Error(`SSE connection to ${url} has no body`));return}void this.readStream(controller,reader,resolve,reject)});try{const data=await endpoint;const messageUrl=new URL(data,url);this.messageEndpoint=messageUrl.toString();this.sessionId=messageUrl.searchParams.get("sessionId")??void 0}finally{clearTimeout(connectTimer)}}async readStream(controller,reader,onEndpoint,onEndpointError){const decoder=new TextDecoder;let buffer="";let endpointSeen=false;try{for(let chunk=await reader.read();!chunk.done;chunk=await reader.read()){const{completeEvents,remainder}=splitSSEEvents(buffer+decoder.decode(chunk.value,{stream:true}));buffer=remainder;for(const text of completeEvents){const event=parseSSEEvent(text);if(!event)continue;if(event.type==="endpoint"){if(!endpointSeen){endpointSeen=true;onEndpoint(event.data)}continue}await this.handleMessage(event.data)}}}catch(error){if(!controller.signal.aborted)this.log("SSE stream failed:",error)}finally{reader.releaseLock()}if(!endpointSeen){onEndpointError(new Error(controller.signal.aborted?`SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms`:`SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`))}if(this.stream===controller){this.stream=void 0;this.rejectPending("SSE stream closed by the server");if(this.state==="connected")this.state="disconnected"}}async handleMessage(data){let message;try{message=JSON.parse(data)}catch{this.log("Ignoring SSE event that is not JSON:",data);return}if(!message||typeof message!=="object")return;if(typeof message.method==="string"){const request=message;if(request.id===void 0||request.id===null){this.notificationHandler?.(request);return}await this.answerServerRequest(request);return}if(message.id!==void 0&&message.id!==null){const pending=this.pending.get(message.id);if(pending){clearTimeout(pending.timer);this.pending.delete(message.id);pending.resolve(message)}}}async answerServerRequest(request){const id=request.id;let reply;if(request.method==="elicitation/create"){reply={jsonrpc:"2.0",id,result:await this.elicit(request.params)}}else if(request.method==="ping"){reply={jsonrpc:"2.0",id,result:{}}}else{reply={jsonrpc:"2.0",id,error:{code:-32601,message:`Method not found: ${request.method}`}}}const result=await this.post(JSON.stringify(reply));if(!result.ok)this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`)}async elicit(params){if(!this.elicitationHandler)return{action:"decline"};try{return await this.elicitationHandler(params)}catch(error){this.log("Elicitation handler error:",error);return{action:"cancel"}}}closeStream(reason){const controller=this.stream;this.stream=void 0;controller?.abort();this.rejectPending(reason)}rejectPending(reason){for(const[id,pending]of this.pending){clearTimeout(pending.timer);pending.resolve({jsonrpc:"2.0",id,error:{code:-32e3,message:reason}})}this.pending.clear()}async postAndAwait(id,body){const answer=new Promise(resolve=>{const timer=setTimeout(()=>{this.pending.delete(id);resolve({jsonrpc:"2.0",id,error:{code:-32e3,message:`Request timeout after ${this.timeout}ms`}})},this.timeout);this.pending.set(id,{resolve,timer})});const result=await this.post(body);if(!result.ok){const pending=this.pending.get(id);if(pending){clearTimeout(pending.timer);this.pending.delete(id)}return{jsonrpc:"2.0",id,error:{code:-32e3,message:`HTTP ${result.status}: ${result.statusText}`,data:result.body}}}return answer}async post(body){const endpoint=this.messageEndpoint;if(!endpoint)throw new Error("SSE transport has no message endpoint. Call connect() first.");const headers=this.buildHeaders();this.lastRequestHeaders=headers;this.log(`POST ${endpoint}`,body);const controller=new AbortController;const timer=setTimeout(()=>controller.abort(),this.timeout);try{const response=await fetch(endpoint,{method:"POST",headers,body,signal:controller.signal});const text=await response.text();return{ok:response.ok,status:response.status,statusText:response.statusText,body:text}}catch(error){if(isAbortError(error)){return{ok:false,status:0,statusText:`timeout after ${this.timeout}ms`,body:""}}throw error}finally{clearTimeout(timer)}}async requestAnonymousToken(){const url=new URL(this.baseUrl);url.pathname=`${(0,import_utils.trimTrailing)(url.pathname,"/")}/oauth/token`;url.search="";const tokenUrl=url.toString();try{const response=await fetch(tokenUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:"anonymous",client_id:crypto.randomUUID(),resource:this.baseUrl})});if(!response.ok){this.log(`No anonymous token (HTTP ${response.status})`);return}const token=await response.json();if(token.access_token)this.authToken=token.access_token}catch(error){this.log("Error requesting anonymous token:",error)}}sseUrl(){const url=new URL(this.baseUrl);const entry=(0,import_utils.trimSlashes)(this.entryPath);const basePath=(0,import_utils.trimTrailing)(url.pathname,"/");url.pathname=entry?`${basePath}/${entry}/sse`:`${basePath}/sse`;return url.toString()}buildHeaders(){const headers={"Content-Type":"application/json",Accept:"application/json, text/event-stream"};if(this.clientInfo)headers["User-Agent"]=`${this.clientInfo.name}/${this.clientInfo.version}`;if(this.authToken)headers["Authorization"]=`Bearer ${this.authToken}`;if(this.sessionId)headers["mcp-session-id"]=this.sessionId;Object.assign(headers,this.authHeaders);return headers}ensureConnected(){if(this.state!=="connected"){throw new Error("Transport not connected. Call connect() first.")}}log(message,data){if(this.debug){console.log(`[SSE] ${message}`,data??"")}}};function splitSSEEvents(buffer){const endsWithCarriageReturn=buffer.endsWith("\r");const normalized=(endsWithCarriageReturn?buffer.slice(0,-1):buffer).replace(/\r\n?/g,"\n");const completeEvents=normalized.split("\n\n");const remainder=completeEvents.pop()??"";return{completeEvents,remainder:endsWithCarriageReturn?`${remainder}\r`:remainder}}function parseSSEEvent(text){let type="message";const data=[];for(const line of text.split("\n")){if(line.startsWith(":"))continue;const colon=line.indexOf(":");const field=colon===-1?line:line.slice(0,colon);const value=colon===-1?"":line.slice(colon+1).replace(/^ /,"");if(field==="event")type=value;else if(field==="data")data.push(value)}return data.length>0?{type,data:data.join("\n")}:void 0}var import_utils2=require("@frontmcp/utils");var DEFAULT_TIMEOUT2=3e4;var NOTIFICATION_STREAM_REOPEN_DELAY_MS=250;var StreamableHttpTransport=class{config;state="disconnected";sessionId;authToken;connectionCount=0;reconnectCount=0;lastRequestHeaders={};interceptors;publicMode;elicitationHandler;notificationHandler;notificationStream;notificationProtocolVersion="";notificationStreamReopenTimer;constructor(config){this.config={baseUrl:config.baseUrl.replace(/\/$/,""),entryPath:config.entryPath,timeout:config.timeout??DEFAULT_TIMEOUT2,auth:config.auth??{},publicMode:config.publicMode??false,debug:config.debug??false,interceptors:config.interceptors,clientInfo:config.clientInfo};this.authToken=config.auth?.token;this.interceptors=config.interceptors;this.publicMode=config.publicMode??false;this.elicitationHandler=config.elicitationHandler;this.notificationHandler=config.notificationHandler}async connect(){this.state="connecting";this.connectionCount++;try{if(this.publicMode){this.log("Public mode: connecting without authentication");this.state="connected";return}if(!this.authToken){await this.requestAnonymousToken()}this.state="connected";this.log("Connected to StreamableHTTP transport")}catch(error){this.state="error";throw error}}async requestAnonymousToken(){const clientId=crypto.randomUUID();const tokenUrl=`${this.config.baseUrl}/oauth/token`;this.log(`Requesting anonymous token from ${tokenUrl}`);try{const response=await fetch(tokenUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:"anonymous",client_id:clientId,resource:this.config.baseUrl})});if(!response.ok){const errorText=await response.text();this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);return}const tokenResponse=await response.json();if(tokenResponse.access_token){this.authToken=tokenResponse.access_token;this.log("Anonymous token acquired successfully")}}catch(error){this.log(`Error requesting anonymous token: ${error}`)}}async request(message){this.ensureConnected();const startTime=Date.now();if(this.interceptors){const interceptResult=await this.interceptors.processRequest(message,{timestamp:new Date,transport:"streamable-http",sessionId:this.sessionId});switch(interceptResult.type){case"mock":{const mockResponse2=await this.interceptors.processResponse(message,interceptResult.response,Date.now()-startTime);return mockResponse2}case"error":throw markInterceptedError(interceptResult.error);case"continue":message=interceptResult.request;break}}const headers=this.buildHeaders();this.lastRequestHeaders=headers;const url=this.mcpUrl();this.log(`POST ${url}`,message);const controller=new AbortController;const timeoutId=setTimeout(()=>controller.abort(),this.config.timeout);try{const response=await fetch(url,{method:"POST",headers,body:JSON.stringify(message),signal:controller.signal});const newSessionId=response.headers.get("mcp-session-id");if(newSessionId){this.sessionId=newSessionId}let jsonResponse;if(!response.ok){const errorText=await response.text();this.log(`HTTP Error ${response.status}: ${errorText}`);jsonResponse={jsonrpc:"2.0",id:message.id??null,error:{code:-32e3,message:`HTTP ${response.status}: ${response.statusText}`,data:errorText}}}else{const contentType=response.headers.get("content-type")??"";if(contentType.includes("text/event-stream")){jsonResponse=await this.handleSSEResponseWithElicitation(response,message)}else{const text=await response.text();this.log("Response:",text);if(!text.trim()){jsonResponse={jsonrpc:"2.0",id:message.id??null,result:void 0}}else{jsonResponse=JSON.parse(text)}}}if(this.interceptors){jsonResponse=await this.interceptors.processResponse(message,jsonResponse,Date.now()-startTime)}clearTimeout(timeoutId);return jsonResponse}catch(error){clearTimeout(timeoutId);if(isAbortError(error)){return{jsonrpc:"2.0",id:message.id??null,error:{code:-32e3,message:`Request timeout after ${this.config.timeout}ms`}}}throw error}}async notify(message){this.ensureConnected();const headers=this.buildHeaders();this.lastRequestHeaders=headers;const url=this.mcpUrl();this.log(`POST ${url} (notification)`,message);const controller=new AbortController;const timeoutId=setTimeout(()=>controller.abort(),this.config.timeout);try{const response=await fetch(url,{method:"POST",headers,body:JSON.stringify(message),signal:controller.signal});clearTimeout(timeoutId);const newSessionId=response.headers.get("mcp-session-id");if(newSessionId){this.sessionId=newSessionId}if(!response.ok){const errorText=await response.text();this.log(`HTTP Error ${response.status} on notification: ${errorText}`)}}catch(error){clearTimeout(timeoutId);if(!isAbortError(error)){throw error}}}async sendRaw(data){this.ensureConnected();const headers=this.buildHeaders();this.lastRequestHeaders=headers;const url=this.mcpUrl();this.log(`POST ${url} (raw)`,data);const controller=new AbortController;const timeoutId=setTimeout(()=>controller.abort(),this.config.timeout);try{const response=await fetch(url,{method:"POST",headers,body:data,signal:controller.signal});clearTimeout(timeoutId);const text=await response.text();if(!text.trim()){return{jsonrpc:"2.0",id:null,error:{code:-32700,message:"Parse error"}}}return JSON.parse(text)}catch(error){clearTimeout(timeoutId);return{jsonrpc:"2.0",id:null,error:{code:-32700,message:"Parse error",data:errorMessage(error)}}}}async openNotificationStream(protocolVersion){this.notificationProtocolVersion=protocolVersion;await this.connectNotificationStream()}async close(){this.closeNotificationStream();this.state="disconnected";this.sessionId=void 0;this.log("StreamableHTTP transport closed")}isConnected(){return this.state==="connected"}getState(){return this.state}getSessionId(){return this.sessionId}setAuthToken(token){this.authToken=token}setTimeout(ms){this.config.timeout=ms}setInterceptors(interceptors2){this.interceptors=interceptors2}getInterceptors(){return this.interceptors}setElicitationHandler(handler){this.elicitationHandler=handler}getConnectionCount(){return this.connectionCount}getReconnectCount(){return this.reconnectCount}getLastRequestHeaders(){return{...this.lastRequestHeaders}}async simulateDisconnect(){this.closeNotificationStream();this.state="disconnected";this.sessionId=void 0}async waitForReconnect(timeoutMs){const deadline=Date.now()+timeoutMs;this.reconnectCount++;await this.connect();while(Date.now()<deadline){if(this.state==="connected"){return}await new Promise(r=>setTimeout(r,50))}throw new Error("Timeout waiting for reconnection")}closeNotificationStream(){clearTimeout(this.notificationStreamReopenTimer);this.notificationStream?.abort();this.notificationStream=void 0}async connectNotificationStream(){if(!this.sessionId||this.notificationStream)return;const controller=new AbortController;this.notificationStream=controller;const{"Content-Type":_contentType,...headers}=this.buildHeaders();const established=fetch(this.mcpUrl(),{method:"GET",headers:{...headers,Accept:"text/event-stream","MCP-Protocol-Version":this.notificationProtocolVersion},signal:controller.signal}).then(async response=>{const isEventStream=response.headers.get("content-type")?.includes("text/event-stream")??false;if(!response.ok||!response.body||!isEventStream){await response.body?.cancel();this.log(`Server offers no notification stream (HTTP ${response.status})`);this.releaseNotificationStream(controller);return}void this.readNotificationStream(controller,response.body.getReader())},error=>{this.log("Failed to open the notification stream:",error);this.releaseNotificationStream(controller)});let establishTimer;const establishDeadline=new Promise(resolve=>{establishTimer=setTimeout(resolve,this.config.timeout)});await Promise.race([established,establishDeadline]);clearTimeout(establishTimer)}releaseNotificationStream(controller){if(this.notificationStream===controller)this.notificationStream=void 0}async readNotificationStream(controller,reader){const decoder=new TextDecoder;let buffer="";try{for(let chunk=await reader.read();!chunk.done;chunk=await reader.read()){const{completeEvents,remainder}=splitSSEEvents2(buffer+decoder.decode(chunk.value,{stream:true}));buffer=remainder;for(const eventText of completeEvents){for(const event of this.parseSSEEvents(eventText,void 0).events){await this.handleSSEEvent(event)}}}}catch(error){this.log("Notification stream ended:",error)}finally{reader.releaseLock()}if(controller.signal.aborted||this.notificationStream!==controller)return;this.releaseNotificationStream(controller);this.notificationStreamReopenTimer=setTimeout(()=>void this.connectNotificationStream(),NOTIFICATION_STREAM_REOPEN_DELAY_MS)}async handleSSEResponseWithElicitation(response,originalRequest){this.log("handleSSEResponseWithElicitation: starting",{requestId:originalRequest.id});const reader=response.body?.getReader();if(!reader){this.log("handleSSEResponseWithElicitation: no response body");return{jsonrpc:"2.0",id:originalRequest.id??null,error:{code:-32e3,message:"No response body"}}}const decoder=new TextDecoder;let buffer="";let finalResponse=null;let sseSessionId;try{let readCount=0;while(true){readCount++;this.log(`handleSSEResponseWithElicitation: reading chunk ${readCount}`);const{done,value}=await reader.read();this.log(`handleSSEResponseWithElicitation: read result`,{done,valueLength:value?.length});if(done){if(buffer.trim()){const parsed=this.parseSSEEvents(buffer,originalRequest.id);for(const event of parsed.events){const handled=await this.handleSSEEvent(event);if(handled.isFinal){finalResponse=handled.response}}if(parsed.sessionId&&!sseSessionId){sseSessionId=parsed.sessionId}}break}buffer+=decoder.decode(value,{stream:true});const eventEndPattern=/\n\n/g;let lastEventEnd=0;let match;while((match=eventEndPattern.exec(buffer))!==null){const eventText=buffer.slice(lastEventEnd,match.index);lastEventEnd=match.index+2;if(eventText.trim()){const parsed=this.parseSSEEvents(eventText,originalRequest.id);for(const event of parsed.events){const handled=await this.handleSSEEvent(event);if(handled.isFinal){finalResponse=handled.response}}if(parsed.sessionId&&!sseSessionId){sseSessionId=parsed.sessionId}}}buffer=buffer.slice(lastEventEnd)}}finally{reader.releaseLock()}if(sseSessionId&&!this.sessionId){this.sessionId=sseSessionId;this.log("Session ID from SSE:",this.sessionId)}if(finalResponse){return finalResponse}return{jsonrpc:"2.0",id:originalRequest.id??null,error:{code:-32e3,message:"No final response received in SSE stream"}}}parseSSEEvents(text,_requestId){const lines=text.split("\n");const events=[];let currentEvent={type:"message",data:[]};let sessionId;for(const line of lines){if(line.startsWith("event: ")){currentEvent.type=line.slice(7)}else if(line.startsWith("data: ")){currentEvent.data.push(line.slice(6))}else if(line==="data:"){currentEvent.data.push("")}else if(line.startsWith("id: ")){const idValue=line.slice(4);currentEvent.id=idValue;const colonIndex=idValue.lastIndexOf(":");if(colonIndex>0){sessionId=idValue.substring(0,colonIndex)}else{sessionId=idValue}}else if(line===""&&currentEvent.data.length>0){events.push({type:currentEvent.type,data:currentEvent.data.join("\n"),id:currentEvent.id});currentEvent={type:"message",data:[]}}}if(currentEvent.data.length>0){events.push({type:currentEvent.type,data:currentEvent.data.join("\n"),id:currentEvent.id})}return{events,sessionId}}async handleSSEEvent(event){this.log("SSE Event:",{type:event.type,data:event.data.slice(0,200)});try{const parsed=JSON.parse(event.data);if("method"in parsed&&parsed.method==="elicitation/create"){await this.handleElicitationRequest(parsed);return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}if("method"in parsed&&parsed.id===void 0){this.notificationHandler?.(parsed);return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}if("result"in parsed||"error"in parsed){return{isFinal:true,response:parsed}}return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}catch{this.log("Failed to parse SSE event data:",event.data);return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}}async handleElicitationRequest(request){const params=request.params;this.log("Elicitation request received:",{mode:params?.mode,message:params?.message?.slice(0,100)});const requestId=request.id;if(requestId===void 0||requestId===null){this.log("Elicitation request has no ID, cannot respond");return}if(!this.elicitationHandler){this.log("No elicitation handler registered, sending error");await this.sendElicitationResponse(requestId,{action:"decline"});return}try{const response=await this.elicitationHandler(params);this.log("Elicitation handler response:",response);await this.sendElicitationResponse(requestId,response)}catch(error){this.log("Elicitation handler error:",error);await this.sendElicitationResponse(requestId,{action:"cancel"})}}async sendElicitationResponse(requestId,response){const headers=this.buildHeaders();const url=this.mcpUrl();const rpcResponse={jsonrpc:"2.0",id:requestId,result:response};this.log("Sending elicitation response:",rpcResponse);try{const fetchResponse=await fetch(url,{method:"POST",headers,body:JSON.stringify(rpcResponse)});if(!fetchResponse.ok){this.log(`Elicitation response HTTP error: ${fetchResponse.status}`)}}catch(error){this.log("Failed to send elicitation response:",error)}}mcpUrl(){const url=new URL(this.config.baseUrl);const entry=(0,import_utils2.trimSlashes)(this.config.entryPath);const basePath=(0,import_utils2.trimTrailing)(url.pathname,"/");url.pathname=entry?`${basePath}/${entry}`:`${basePath}/`;return url.toString()}buildHeaders(){const headers={"Content-Type":"application/json",Accept:"application/json, text/event-stream"};if(this.config.clientInfo){headers["User-Agent"]=`${this.config.clientInfo.name}/${this.config.clientInfo.version}`}if(this.authToken){headers["Authorization"]=`Bearer ${this.authToken}`}if(this.sessionId){headers["mcp-session-id"]=this.sessionId}if(this.config.auth.headers){Object.assign(headers,this.config.auth.headers)}return headers}ensureConnected(){if(this.state!=="connected"){throw new Error("Transport not connected. Call connect() first.")}}log(message,data){if(this.config.debug){console.log(`[StreamableHTTP] ${message}`,data??"")}}parseSSEResponseWithSession(text,requestId){const lines=text.split("\n");const dataLines=[];let sseSessionId;for(const line of lines){if(line.startsWith("data: ")){dataLines.push(line.slice(6))}else if(line==="data:"){dataLines.push("")}else if(line.startsWith("id: ")){const idValue=line.slice(4);const colonIndex=idValue.lastIndexOf(":");if(colonIndex>0){sseSessionId=idValue.substring(0,colonIndex)}else{sseSessionId=idValue}}}if(dataLines.length>0){const jsonData=dataLines.join("\n");try{return{response:JSON.parse(jsonData),sseSessionId}}catch{this.log("Failed to parse SSE data as JSON:",jsonData)}}return{response:{jsonrpc:"2.0",id:requestId??null,error:{code:-32700,message:"Failed to parse SSE response",data:text}},sseSessionId}}};function splitSSEEvents2(buffer){const endsWithCarriageReturn=buffer.endsWith("\r");const normalized=(endsWithCarriageReturn?buffer.slice(0,-1):buffer).replace(/\r\n?/g,"\n");const completeEvents=normalized.split("\n\n");const remainder=completeEvents.pop()??"";return{completeEvents,remainder:endsWithCarriageReturn?`${remainder}\r`:remainder}}var MCP_APPS_EXTENSION_KEY="io.modelcontextprotocol/ui";function getPlatformClientInfo(platform){switch(platform){case"openai":return{name:"ChatGPT",version:"1.0"};case"ext-apps":return{name:"mcp-ext-apps",version:"1.0"};case"claude":return{name:"claude-desktop",version:"1.0"};case"claude-code":return{name:"claude-code",version:"1.0"};case"codex":return{name:"codex-mcp-client",version:"1.0"};case"cursor":return{name:"cursor",version:"1.0"};case"continue":return{name:"continue",version:"1.0"};case"cody":return{name:"cody",version:"1.0"};case"gemini":return{name:"gemini",version:"1.0"};case"generic-mcp":return{name:"generic-mcp-client",version:"1.0"};case"unknown":default:return{name:"mcp-test-client",version:"1.0"}}}function buildUserAgent(clientInfo){return`${clientInfo.name}/${clientInfo.version}`}function getPlatformUserAgent(platform){return buildUserAgent(getPlatformClientInfo(platform))}var PLATFORM_DETECTION_PATTERNS={openai:/chatgpt/i,"ext-apps":/mcp-ext-apps/i,claude:/claude|claude-desktop/i,"claude-code":/claude[- ]code/i,codex:/codex/i,cursor:/cursor/i,continue:/continue/i,cody:/cody/i,gemini:/gemini/i,"generic-mcp":/generic-mcp/i,unknown:/.*/};function getPlatformCapabilities(platform){const baseCapabilities={sampling:{},elicitation:{form:{}}};if(platform==="ext-apps"){return{...baseCapabilities,experimental:{[MCP_APPS_EXTENSION_KEY]:{mimeTypes:["text/html;profile=mcp-app"]}}}}return baseCapabilities}var McpTestClientBuilder=class{config;constructor(config){this.config={...config}}withAuth(auth){this.config.auth={...this.config.auth,...auth};return this}withToken(token){this.config.auth={...this.config.auth,token};return this}withHeaders(headers){this.config.auth={...this.config.auth,headers:{...this.config.auth?.headers,...headers}};return this}withTransport(transport){this.config.transport=transport;return this}withTimeout(timeoutMs){this.config.timeout=timeoutMs;return this}withDebug(enabled=true){this.config.debug=enabled;return this}withPublicMode(enabled=true){this.config.publicMode=enabled;return this}withProtocolVersion(version){this.config.protocolVersion=version;return this}withClientInfo(info){this.config.clientInfo=info;return this}withPlatform(platform){this.config.platform=platform;this.config.clientInfo=getPlatformClientInfo(platform);this.config.capabilities=getPlatformCapabilities(platform);return this}withCapabilities(capabilities){this.config.capabilities=capabilities;return this}withQueryParams(params){this.config.queryParams={...this.config.queryParams,...params};return this}build(){return new McpTestClient(this.config)}async buildAndConnect(){const client=this.build();await client.connect();return client}};var DEFAULT_TIMEOUT3=3e4;var DEFAULT_PROTOCOL_VERSION="2025-06-18";var UNSUPPORTED_PROTOCOL_VERSIONS=["2026-07-28"];var DEFAULT_CLIENT_INFO={name:"@frontmcp/testing",version:"0.4.0"};var MAX_LIST_PAGES=1e3;function isAuthFailure(error){return typeof error?.message==="string"&&/^HTTP (401|403)\b/.test(error.message)}function describeInitializeError(error){const message=error?.message??"Unknown error";const data=error?.data;const detail=typeof data==="string"?data.trim():"";return detail?`${message} - ${detail.slice(0,500)}`:message}function extractEntryPaths(data){let payload=data;if(typeof payload==="string"){try{payload=JSON.parse(payload)}catch{return void 0}}if(!payload||typeof payload!=="object")return void 0;const paths=payload.entryPaths;if(!Array.isArray(paths))return void 0;const strings=paths.filter(p=>typeof p==="string");return strings.length>0?strings:void 0}var McpTestClient=class{config;transport=null;initResult=null;requestIdCounter=0;_lastRequestId=0;_sessionId;triedDiscoveredEntryPath=false;_sessionInfo=null;_authState={isAnonymous:true,scopes:[]};_logs=[];_traces=[];_notifications=[];_progressUpdates=[];collectingProgress=false;progressTokenCounter=0;_interceptors;_elicitationHandler;constructor(config){if(config.protocolVersion&&UNSUPPORTED_PROTOCOL_VERSIONS.includes(config.protocolVersion)){throw new Error(`McpTestClient does not support protocol version ${config.protocolVersion}: it has no initialize handshake. Use ${DEFAULT_PROTOCOL_VERSION} or earlier.`)}this.config={baseUrl:config.baseUrl,entryPath:config.entryPath,transport:config.transport??"streamable-http",auth:config.auth??{},publicMode:config.publicMode??false,timeout:config.timeout??DEFAULT_TIMEOUT3,debug:config.debug??false,protocolVersion:config.protocolVersion??DEFAULT_PROTOCOL_VERSION,clientInfo:config.clientInfo??DEFAULT_CLIENT_INFO,platform:config.platform,capabilities:config.capabilities,queryParams:config.queryParams};if(config.auth?.token){this._authState={isAnonymous:false,token:config.auth.token,scopes:this.parseScopesFromToken(config.auth.token),user:this.parseUserFromToken(config.auth.token)}}this._interceptors=new DefaultInterceptorChain}static create(config){return new McpTestClientBuilder(config)}async connect(){this.log("debug",`Connecting to ${this.config.baseUrl}...`);this.triedDiscoveredEntryPath=false;this.initResult=null;this.transport=this.createTransport();await this.transport.connect();const maxRetries=3;const retryDelayMs=500;let lastError;let reportedEntryPaths;let attemptsMade=0;for(let attempt=1;attempt<=maxRetries;attempt++){attemptsMade=attempt;const initResponse=await this.initialize();if(initResponse.success&&initResponse.data){this.initResult=initResponse.data;break}lastError=describeInitializeError(initResponse.error);reportedEntryPaths=extractEntryPaths(initResponse.error?.data)??reportedEntryPaths;if(reportedEntryPaths?.length===1&&!this.triedDiscoveredEntryPath){this.triedDiscoveredEntryPath=true;const discovered=reportedEntryPaths[0];this.log("info",`Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);this.config.entryPath=discovered;await this.transport.close();this.transport=this.createTransport();await this.transport.connect();continue}if(isAuthFailure(initResponse.error))break;if(attempt<maxRetries){this.log("debug",`MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);await new Promise(resolve=>setTimeout(resolve,retryDelayMs))}}if(!this.initResult){const served=reportedEntryPaths?.length?` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.`:"";throw new Error(`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade===1?"":"s"}: ${lastError}.${served}`)}this._sessionId=this.transport.getSessionId();this._sessionInfo={id:this._sessionId??`session-${Date.now()}`,createdAt:new Date,lastActivityAt:new Date,requestCount:1};await this.transport.notify({jsonrpc:"2.0",method:"notifications/initialized"});await this.transport.openNotificationStream?.(this.initResult.protocolVersion);this.log("info",`Connected to ${this.initResult.serverInfo?.name??"MCP Server"}`);return this.initResult}async disconnect(){if(this.transport){await this.transport.close();this.transport=null}this.initResult=null;this.log("info","Disconnected from MCP server")}async reconnect(options){await this.disconnect();if(options?.sessionId&&this.transport){this._sessionId=options.sessionId}await this.connect()}isConnected(){return this.transport?.isConnected()??false}get sessionId(){return this._sessionId??""}get session(){const info=this._sessionInfo??{id:"",createdAt:new Date,lastActivityAt:new Date,requestCount:0};return{...info,expire:async()=>{this._sessionId=void 0;this._sessionInfo=null}}}get auth(){return this._authState}async authenticate(token){if(typeof token!=="string"||token.trim()===""){throw new Error("authenticate() requires a non-empty token")}const previousAuthState=this._authState;const previousConfigAuth=this.config.auth;this.config.auth={...previousConfigAuth,token};this._authState={isAnonymous:false,token,scopes:this.parseScopesFromToken(token),user:this.parseUserFromToken(token)};const previousTransport=this.transport;if(!previousTransport){this.log("debug","Authentication updated (not connected yet)");return}const previousInit=this.initResult;const previousSessionId=this._sessionId;const previousSessionInfo=this._sessionInfo;try{await this.connect()}catch(err){await this.transport?.close().catch(()=>void 0);this.transport=previousTransport;this.initResult=previousInit;this._sessionId=previousSessionId;this._sessionInfo=previousSessionInfo;this.config.auth=previousConfigAuth;this._authState=previousAuthState;throw err}await previousTransport.close().catch(()=>void 0);this.log("debug","Authentication updated")}get serverInfo(){return{name:this.initResult?.serverInfo?.name??"",version:this.initResult?.serverInfo?.version??""}}get protocolVersion(){return this.initResult?.protocolVersion??""}get instructions(){return this.initResult?.instructions??""}get capabilities(){return this.initResult?.capabilities??{}}hasCapability(name){return!!this.capabilities[name]}tools={list:async()=>this.listAllPages("tools/list","tools",cursor=>this.listTools(cursor),page=>page.tools),call:async(name,args,options={})=>{const response=await this.callTool(name,args,this.progressTokenFor(options));return this.wrapToolResult(response)}};resources={list:async()=>this.listAllPages("resources/list","resources",cursor=>this.listResources(cursor),page=>page.resources),listTemplates:async()=>this.listAllPages("resources/templates/list","resource templates",cursor=>this.listResourceTemplates(cursor),page=>page.resourceTemplates),read:async uri=>{const response=await this.readResource(uri);return this.wrapResourceContent(response)},subscribe:async uri=>{const response=await this.request("resources/subscribe",{uri});if(!response.success)throw new Error(`Failed to subscribe to ${uri}: ${response.error?.message}`)},unsubscribe:async uri=>{const response=await this.request("resources/unsubscribe",{uri});if(!response.success)throw new Error(`Failed to unsubscribe from ${uri}: ${response.error?.message}`)}};prompts={list:async()=>this.listAllPages("prompts/list","prompts",cursor=>this.listPrompts(cursor),page=>page.prompts),get:async(name,args)=>{const response=await this.getPrompt(name,args);return this.wrapPromptResult(response)}};raw={request:async message=>{const transport=this.getConnectedTransport();const start=Date.now();const response=await transport.request(message);this.traceRequest(message.method,message.params,message.id,response,Date.now()-start);return response},notify:async message=>{const transport=this.getConnectedTransport();await transport.notify(message)},sendRaw:async data=>{const transport=this.getConnectedTransport();return transport.sendRaw(data)}};get lastRequestId(){return this._lastRequestId}get transport_info(){return{type:this.config.transport,isConnected:()=>this.transport?.isConnected()??false,messageEndpoint:this.transport?.getMessageEndpoint?.(),connectionCount:this.transport?.getConnectionCount?.()??0,reconnectCount:this.transport?.getReconnectCount?.()??0,lastRequestHeaders:this.transport?.getLastRequestHeaders?.()??{},simulateDisconnect:async()=>{await this.transport?.simulateDisconnect?.()},waitForReconnect:async timeoutMs=>{await this.transport?.waitForReconnect?.(timeoutMs)}}}get transport_(){return this.transport_info}notifications={collect:()=>{return new NotificationCollector(this._notifications)},collectProgress:()=>{this.collectingProgress=true;return new ProgressCollector(this._progressUpdates)},send:async(method,params)=>{await this.raw.notify({jsonrpc:"2.0",method,params})}};onElicitation(handler){this._elicitationHandler=handler;if(this.transport?.setElicitationHandler){this.transport.setElicitationHandler(handler)}this.log("debug","Elicitation handler registered")}clearElicitationHandler(){this._elicitationHandler=void 0;if(this.transport?.setElicitationHandler){this.transport.setElicitationHandler(void 0)}this.log("debug","Elicitation handler cleared")}logs={all:()=>[...this._logs],filter:level=>this._logs.filter(l=>l.level===level),search:text=>this._logs.filter(l=>l.message.includes(text)),last:()=>this._logs[this._logs.length-1],clear:()=>{this._logs=[]}};trace={all:()=>[...this._traces],last:()=>this._traces[this._traces.length-1],clear:()=>{this._traces=[]}};mock={add:mock=>{return this._interceptors.mocks.add(mock)},tool:(name,result,options)=>{return this._interceptors.mocks.add({method:"tools/call",params:{name},response:mockResponse.toolResult([{type:"text",text:typeof result==="string"?result:JSON.stringify(result)}]),times:options?.times,delay:options?.delay})},toolError:(name,code,message,options)=>{return this._interceptors.mocks.add({method:"tools/call",params:{name},response:mockResponse.error(code,message),times:options?.times})},resource:(uri,content,options)=>{const contentObj=typeof content==="string"?{uri,text:content}:{uri,...content};return this._interceptors.mocks.add({method:"resources/read",params:{uri},response:mockResponse.resourceRead([contentObj]),times:options?.times,delay:options?.delay})},resourceError:(uri,options)=>{return this._interceptors.mocks.add({method:"resources/read",params:{uri},response:mockResponse.errors.resourceNotFound(uri),times:options?.times})},toolsList:(tools,options)=>{return this._interceptors.mocks.add({method:"tools/list",response:mockResponse.toolsList(tools),times:options?.times})},resourcesList:(resources,options)=>{return this._interceptors.mocks.add({method:"resources/list",response:mockResponse.resourcesList(resources),times:options?.times})},clear:()=>{this._interceptors.mocks.clear()},all:()=>{return this._interceptors.mocks.getAll()}};intercept={request:interceptor=>{return this._interceptors.addRequestInterceptor(interceptor)},response:interceptor=>{return this._interceptors.addResponseInterceptor(interceptor)},delay:ms=>{return this._interceptors.addRequestInterceptor(async()=>{await new Promise(r=>setTimeout(r,ms));return{action:"passthrough"}})},failMethod:(method,error)=>{return this._interceptors.addRequestInterceptor(ctx=>{if(ctx.request.method===method){return{action:"error",error:new Error(error??`Intercepted: ${method}`)}}return{action:"passthrough"}})},clear:()=>{this._interceptors.request=[];this._interceptors.response=[]},clearAll:()=>{this._interceptors.clear()}};setTimeout(ms){this.config.timeout=ms;if(this.transport){this.transport.setTimeout(ms)}}async initialize(){const capabilities=this.config.capabilities??{sampling:{},elicitation:{form:{}}};return this.request("initialize",{protocolVersion:this.config.protocolVersion,capabilities,clientInfo:this.config.clientInfo})}async listTools(cursor){return this.request("tools/list",cursor?{cursor}:{})}async listAllPages(method,label,fetchPage,itemsOf){const items=[];const seenCursors=new Set;let cursor;for(let page=0;page<MAX_LIST_PAGES;page++){const response=await fetchPage(cursor);if(!response.success||!response.data){throw new Error(`Failed to list ${label}: ${response.error?.message}`)}items.push(...itemsOf(response.data)??[]);const next=response.data.nextCursor;if(!next)return items;if(seenCursors.has(next)){throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`)}seenCursors.add(next);cursor=next}throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`)}async callTool(name,args,progressToken){return this.request("tools/call",{name,arguments:args??{},...progressToken!==void 0&&{_meta:{progressToken}}})}progressTokenFor(options){if(options.progressToken!==void 0)return options.progressToken;return this.collectingProgress?`progress-${++this.progressTokenCounter}`:void 0}recordNotification=notification=>{const timestamp=new Date;this._notifications.push({method:notification.method,params:notification.params,timestamp});if(notification.method!=="notifications/progress")return;const{progress,total,progressToken}=notification.params??{};this._progressUpdates.push({progress:Number(progress),total:typeof total==="number"?total:void 0,progressToken:typeof progressToken==="string"||typeof progressToken==="number"?progressToken:void 0,timestamp})};async listResources(cursor){return this.request("resources/list",cursor?{cursor}:{})}async listResourceTemplates(cursor){return this.request("resources/templates/list",cursor?{cursor}:{})}async readResource(uri){return this.request("resources/read",{uri})}async listPrompts(cursor){return this.request("prompts/list",cursor?{cursor}:{})}async getPrompt(name,args){return this.request("prompts/get",{name,arguments:args??{}})}mcpPathLabel(){const entry=(0,import_utils3.trimSlashes)(this.config.entryPath);return entry?`/${entry}`:"/"}createTransport(){let baseUrl=this.config.baseUrl;if(this.config.queryParams&&Object.keys(this.config.queryParams).length>0){const url=new URL(baseUrl);Object.entries(this.config.queryParams).forEach(([key,value])=>{url.searchParams.set(key,String(value))});baseUrl=url.toString()}switch(this.config.transport){case"streamable-http":return new StreamableHttpTransport({baseUrl,entryPath:this.config.entryPath,timeout:this.config.timeout,auth:this.config.auth,publicMode:this.config.publicMode,debug:this.config.debug,interceptors:this._interceptors,clientInfo:this.config.clientInfo,elicitationHandler:this._elicitationHandler,notificationHandler:this.recordNotification});case"sse":return new SseTransport({baseUrl,entryPath:this.config.entryPath,timeout:this.config.timeout,auth:this.config.auth,publicMode:this.config.publicMode,debug:this.config.debug,interceptors:this._interceptors,clientInfo:this.config.clientInfo,elicitationHandler:this._elicitationHandler,notificationHandler:this.recordNotification});default:throw new Error(`Unknown transport type: ${this.config.transport}`)}}async request(method,params){const transport=this.getConnectedTransport();const id=++this.requestIdCounter;this._lastRequestId=id;const start=Date.now();try{const response=await transport.request({jsonrpc:"2.0",id,method,params});const durationMs=Date.now()-start;this.updateSessionActivity();if("error"in response&&response.error){const error=response.error;this.traceRequest(method,params,id,response,durationMs);return{success:false,error,durationMs,requestId:id}}this.traceRequest(method,params,id,response,durationMs);return{success:true,data:response.result,durationMs,requestId:id}}catch(err){if(isInterceptedError(err))throw err;const durationMs=Date.now()-start;const error={code:-32603,message:errorMessage(err)};return{success:false,error,durationMs,requestId:id}}}getConnectedTransport(){if(!this.transport||!this.transport.isConnected()){throw new Error("Not connected to MCP server. Call connect() first.")}return this.transport}updateSessionActivity(){if(this._sessionInfo){this._sessionInfo.lastActivityAt=new Date;this._sessionInfo.requestCount++}}wrapToolResult(response){const raw=response.data??{content:[]};const isError2=!response.success||raw.isError===true;const meta=raw._meta;const hasUI=meta?.["ui/html"]!==void 0||meta?.["ui/component"]!==void 0;const structuredContent=raw["structuredContent"];return{raw,isSuccess:!isError2,isError:isError2,error:response.error,durationMs:response.durationMs,json(){if(hasUI&&structuredContent!==void 0){return structuredContent}const textContent=raw.content?.find(c=>c.type==="text");if(textContent&&"text"in textContent){return JSON.parse(textContent.text)}throw new Error("No text content to parse as JSON")},text(){const textContent=raw.content?.find(c=>c.type==="text");if(textContent&&"text"in textContent){return textContent.text}return void 0},hasTextContent(){return raw.content?.some(c=>c.type==="text")??false},hasImageContent(){return raw.content?.some(c=>c.type==="image")??false},hasResourceContent(){return raw.content?.some(c=>c.type==="resource")??false},hasToolUI(){return hasUI}}}wrapResourceContent(response){const raw=response.data??{contents:[]};const isError2=!response.success;const firstContent=raw.contents?.[0];return{raw,isSuccess:!isError2,isError:isError2,error:response.error,durationMs:response.durationMs,json(){if(firstContent&&"text"in firstContent){return JSON.parse(firstContent.text)}throw new Error("No text content to parse as JSON")},text(){if(firstContent&&"text"in firstContent){return firstContent.text}return void 0},mimeType(){return firstContent?.mimeType},hasMimeType(type){return firstContent?.mimeType===type}}}wrapPromptResult(response){const raw=response.data??{messages:[]};const isError2=!response.success;return{raw,isSuccess:!isError2,isError:isError2,error:response.error,durationMs:response.durationMs,messages:raw.messages??[],description:raw.description}}log(level,message,data){const entry={level,message,timestamp:new Date,data};this._logs.push(entry);if(this.config.debug){console.log(`[${level.toUpperCase()}] ${message}`,data??"")}}traceRequest(method,params,id,response,durationMs){this._traces.push({request:{method,params,id},response:{result:"result"in response?response.result:void 0,error:"error"in response?response.error:void 0},durationMs,timestamp:new Date})}parseScopesFromToken(token){try{const payload=this.decodeJwtPayload(token);if(!payload)return[];const scope=payload["scope"];const scopes2=payload["scopes"];if(typeof scope==="string"){return scope.split(" ")}if(Array.isArray(scopes2)){return scopes2}return[]}catch{return[]}}parseUserFromToken(token){try{const payload=this.decodeJwtPayload(token);const sub=payload?.["sub"];if(!sub||typeof sub!=="string")return void 0;return{sub,email:payload["email"],name:payload["name"]}}catch{return void 0}}decodeJwtPayload(token){try{const parts=token.split(".");if(parts.length!==3)return null;const payload=Buffer.from(parts[1],"base64url").toString("utf-8");return JSON.parse(payload)}catch{return null}}};var NotificationCollector=class{constructor(notifications){this.notifications=notifications}notifications;get received(){return[...this.notifications]}has(method){return this.notifications.some(n=>n.method===method)}async waitFor(method,timeoutMs){const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){const found=this.notifications.find(n=>n.method===method);if(found)return found;await new Promise(r=>setTimeout(r,50))}throw new Error(`Timeout waiting for notification: ${method}`)}};var ProgressCollector=class{constructor(updates){this.updates=updates}updates;get all(){return[...this.updates]}async waitForComplete(timeoutMs){const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){const last=this.updates[this.updates.length-1];if(last&&last.total!==void 0&&last.progress>=last.total){return}await new Promise(r=>setTimeout(r,50))}throw new Error("Timeout waiting for progress to complete")}};var import_jose=require("jose");var TestTokenFactory=class{issuer;audience;privateKey=null;publicKey=null;jwk=null;keyId;hmacSecret;constructor(options={}){this.issuer=options.issuer??"https://test.frontmcp.local";this.audience=options.audience??"frontmcp-test";this.keyId=`test-key-${Date.now()}`;this.hmacSecret=options.hmacSecret?new TextEncoder().encode(options.hmacSecret):null}get isHmac(){return this.hmacSecret!==null}get alg(){return this.isHmac?"HS256":"RS256"}signingKey(){if(this.hmacSecret)return this.hmacSecret;if(!this.privateKey){throw new Error("Private key not initialized")}return this.privateKey}async ensureKeys(){if(this.isHmac)return;if(this.privateKey&&this.publicKey)return;const{publicKey,privateKey}=await(0,import_jose.generateKeyPair)("RS256",{extractable:true});this.privateKey=privateKey;this.publicKey=publicKey;this.jwk=await(0,import_jose.exportJWK)(publicKey);this.jwk.kid=this.keyId;this.jwk.use="sig";this.jwk.alg="RS256"}async createTestToken(options){await this.ensureKeys();const nowMs=Date.now();const iat=Math.floor(nowMs/1e3);const exp=options.exp??3600;const payload={iss:options.iss??this.issuer,sub:options.sub,aud:options.aud??this.audience,iat,exp:Math.ceil(nowMs/1e3)+exp,scope:options.scopes?.join(" "),...options.claims};const header=this.isHmac?{alg:this.alg,typ:"JWT"}:{alg:this.alg,kid:this.keyId};const token=await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());return token}async createAdminToken(sub="admin-001"){return this.createTestToken({sub,scopes:["admin:*","read","write","delete"],claims:{email:"admin@test.local",name:"Test Admin",role:"admin"}})}async createUserToken(sub="user-001",scopes2=["read","write"]){return this.createTestToken({sub,scopes:scopes2,claims:{email:"user@test.local",name:"Test User",role:"user"}})}async createAnonymousToken(expiresIn){return this.createTestToken({sub:`anon:${Date.now()}`,scopes:["anonymous"],exp:expiresIn,claims:{name:"Anonymous",role:"anonymous"}})}async createExpiredToken(options){await this.ensureKeys();const now=Math.floor(Date.now()/1e3);const payload={iss:this.issuer,sub:options.sub,aud:this.audience,iat:now-7200,exp:now-3600};const header=this.isHmac?{alg:this.alg,typ:"JWT"}:{alg:this.alg,kid:this.keyId};const token=await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());return token}createTokenWithInvalidSignature(options){const now=Math.floor(Date.now()/1e3);const headerObj=this.isHmac?{alg:this.alg,typ:"JWT"}:{alg:this.alg,kid:this.keyId};const header=Buffer.from(JSON.stringify(headerObj)).toString("base64url");const payload=Buffer.from(JSON.stringify({iss:this.issuer,sub:options.sub,aud:this.audience,iat:now,exp:now+3600})).toString("base64url");const signature=Buffer.from("invalid-signature-"+Date.now()).toString("base64url");return`${header}.${payload}.${signature}`}async getPublicJwks(){if(this.isHmac){throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)")}await this.ensureKeys();if(!this.jwk){throw new Error("JWK not initialized")}return{keys:[this.jwk]}}getIssuer(){return this.issuer}getAudience(){return this.audience}};var AuthHeaders={bearer(token){return{Authorization:`Bearer ${token}`}},noAuth(){return{}},mcpRequest(token,sessionId){const headers={"Content-Type":"application/json",Accept:"application/json",Authorization:`Bearer ${token}`};if(sessionId){headers["mcp-session-id"]=sessionId}return headers},publicMode(sessionId){const headers={"Content-Type":"application/json",Accept:"application/json"};if(sessionId){headers["mcp-session-id"]=sessionId}return headers},custom(headerName,headerValue){return{[headerName]:headerValue}}};var TestUsers={admin:{sub:"admin-001",email:"admin@test.local",name:"Test Admin",scopes:["admin:*","read","write","delete"],role:"admin"},user:{sub:"user-001",email:"user@test.local",name:"Test User",scopes:["read","write"],role:"user"},readOnly:{sub:"readonly-001",email:"readonly@test.local",name:"Read Only User",scopes:["read"],role:"readonly"},anonymous:{sub:"anon:001",name:"Anonymous",scopes:["anonymous"],role:"anonymous"},noScopes:{sub:"noscopes-001",email:"noscopes@test.local",name:"No Scopes User",scopes:[],role:"user"},toolsOnly:{sub:"toolsonly-001",email:"toolsonly@test.local",name:"Tools Only User",scopes:["tools:execute"],role:"user"},resourcesOnly:{sub:"resourcesonly-001",email:"resourcesonly@test.local",name:"Resources Only User",scopes:["resources:read"],role:"user"}};function createTestUser(overrides){return{scopes:[],...overrides}}var import_http=require("http");var import_utils4=require("@frontmcp/utils");var MockOAuthServer=class{tokenFactory;options;server=null;_info=null;connections=new Set;authCodes=new Map;refreshTokens=new Map;accessTokenTtlSeconds;refreshTokenTtlSeconds;constructor(tokenFactory2,options={}){this.tokenFactory=tokenFactory2;this.options=options;this.accessTokenTtlSeconds=options.accessTokenTtlSeconds??3600;this.refreshTokenTtlSeconds=options.refreshTokenTtlSeconds??30*24*3600}setAutoApprove(enabled){this.options.autoApprove=enabled}setTestUser(user){this.options.testUser=user}addValidRedirectUri(uri){const uris=this.options.validRedirectUris??[];uris.push(uri);this.options.validRedirectUris=uris}clearStoredTokens(){this.authCodes.clear();this.refreshTokens.clear()}async start(){if(this.server){throw new Error("Mock OAuth server is already running")}const port=this.options.port??0;return new Promise((resolve,reject)=>{const server=(0,import_http.createServer)(this.handleRequest.bind(this));this.server=server;server.on("connection",socket=>{this.connections.add(socket);socket.on("close",()=>this.connections.delete(socket))});server.on("error",err=>{this.log(`Server error: ${err.message}`);reject(err)});server.listen(port,()=>{const address=server.address();if(!address||typeof address==="string"){reject(new Error("Failed to get server address"));return}const actualPort=address.port;const issuer=this.options.issuer??`http://localhost:${actualPort}`;this._info={baseUrl:`http://localhost:${actualPort}`,port:actualPort,issuer,jwksUrl:`http://localhost:${actualPort}/.well-known/jwks.json`};this.log(`Mock OAuth server started at ${this._info.baseUrl}`);resolve(this._info)})})}async stop(){const server=this.server;if(!server){return}for(const socket of this.connections){socket.destroy()}this.connections.clear();return new Promise((resolve,reject)=>{server.close(err=>{if(err){reject(err)}else{this.server=null;this._info=null;this.log("Mock OAuth server stopped");resolve()}})})}get info(){if(!this._info){throw new Error("Mock OAuth server is not running")}return this._info}getTokenFactory(){return this.tokenFactory}async handleRequest(req,res){const fullUrl=req.url??"/";const[urlPath,queryString]=fullUrl.split("?");this.log(`${req.method} ${urlPath}`);res.setHeader("Access-Control-Allow-Origin","*");res.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS");res.setHeader("Access-Control-Allow-Headers","Content-Type, Authorization");if(req.method==="OPTIONS"){res.writeHead(204);res.end();return}try{if(urlPath==="/.well-known/jwks.json"||urlPath==="/.well-known/jwks"){await this.handleJwks(req,res)}else if(urlPath==="/.well-known/openid-configuration"){await this.handleOidcConfig(req,res)}else if(urlPath==="/.well-known/oauth-authorization-server"){await this.handleOAuthMetadata(req,res)}else if(urlPath==="/oauth/authorize"){await this.handleAuthorizeEndpoint(req,res,queryString)}else if(urlPath==="/oauth/token"){await this.handleTokenEndpoint(req,res)}else if(urlPath==="/userinfo"){await this.handleUserInfoEndpoint(req,res)}else if(urlPath==="/oauth/authorize/submit"&&req.method==="POST"){await this.handleAuthorizeSubmit(req,res)}else{res.writeHead(404,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"not_found",error_description:"Endpoint not found"}))}}catch(error){const errorMsg=error instanceof Error?`${error.message}
2
- ${error.stack}`:String(error);this.logError(`Error handling request to ${urlPath}: ${errorMsg}`);res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",error_description:"Internal server error"}))}}async handleJwks(_req,res){const jwks=await this.tokenFactory.getPublicJwks();res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(jwks));this.log("Served JWKS")}async handleOidcConfig(_req,res){const issuer=this._info?.issuer??"http://localhost";const config={issuer,authorization_endpoint:`${issuer}/oauth/authorize`,token_endpoint:`${issuer}/oauth/token`,jwks_uri:`${issuer}/.well-known/jwks.json`,response_types_supported:["code"],subject_types_supported:["public"],id_token_signing_alg_values_supported:["RS256"],scopes_supported:["openid","profile","email"],token_endpoint_auth_methods_supported:["client_secret_basic","client_secret_post","none"],claims_supported:["sub","iss","aud","exp","iat","email","name"],grant_types_supported:["authorization_code","refresh_token","anonymous"]};res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(config));this.log("Served OIDC configuration")}async handleOAuthMetadata(_req,res){const issuer=this._info?.issuer??"http://localhost";const metadata={issuer,authorization_endpoint:`${issuer}/oauth/authorize`,token_endpoint:`${issuer}/oauth/token`,jwks_uri:`${issuer}/.well-known/jwks.json`,response_types_supported:["code"],grant_types_supported:["authorization_code","refresh_token","anonymous"],token_endpoint_auth_methods_supported:["client_secret_basic","client_secret_post","none"],scopes_supported:["openid","profile","email","anonymous"]};res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(metadata));this.log("Served OAuth metadata")}async handleAuthorizeEndpoint(_req,res,queryString){const params=new URLSearchParams(queryString??"");const clientId=params.get("client_id");const redirectUri=params.get("redirect_uri");const responseType=params.get("response_type");const state=params.get("state")??void 0;const scope=params.get("scope")??"openid";const codeChallenge=params.get("code_challenge")??void 0;const codeChallengeMethod=params.get("code_challenge_method")??void 0;if(!clientId||!redirectUri||!responseType){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required parameters: client_id, redirect_uri, response_type"}));return}if(responseType!=="code"){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"unsupported_response_type",error_description:"Only response_type=code is supported"}));return}if(!this.isValidRedirectUri(redirectUri)){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Invalid redirect_uri"}));return}if(this.options.clientId&&clientId!==this.options.clientId){this.redirectWithError(res,redirectUri,"unauthorized_client","Invalid client_id",state);return}if(this.options.autoApprove){const testUser=this.options.testUser;if(!testUser){this.logError("autoApprove is enabled but no testUser configured. Set testUser in MockOAuthServerOptions.");res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",error_description:"autoApprove is enabled but no testUser configured"}));return}const code=this.generateCode();const scopes2=scope.split(" ").filter(Boolean);this.authCodes.set(code,{code,clientId,redirectUri,codeChallenge,codeChallengeMethod,scopes:scopes2,user:testUser,state,expiresAt:Date.now()+5*60*1e3,used:false});const callbackUrl=new URL(redirectUri);callbackUrl.searchParams.set("code",code);if(state){callbackUrl.searchParams.set("state",state)}this.log(`Auto-approved auth request, redirecting with code to ${callbackUrl.origin}${callbackUrl.pathname}`);res.writeHead(302,{Location:callbackUrl.toString()});res.end();return}const html=this.renderLoginPage(clientId,redirectUri,scope,state,codeChallenge,codeChallengeMethod);res.writeHead(200,{"Content-Type":"text/html"});res.end(html)}async handleTokenEndpoint(req,res){const body=await this.readBody(req);const params=new URLSearchParams(body);const authHeader=req.headers["authorization"];let clientSecret=params.get("client_secret")??void 0;if(!clientSecret&&authHeader?.startsWith("Basic ")){const decoded=Buffer.from(authHeader.slice(6),"base64").toString("utf8");const colonIndex=decoded.indexOf(":");if(colonIndex>=0){clientSecret=decoded.slice(colonIndex+1)}}if(this.options.clientSecret&&clientSecret!==this.options.clientSecret){res.writeHead(401,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_client",error_description:"Invalid client_secret"}));return}const grantType=params.get("grant_type");if(grantType==="anonymous"){const token=await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({access_token:token,token_type:"Bearer",expires_in:this.accessTokenTtlSeconds}));this.log("Issued anonymous token")}else if(grantType==="authorization_code"){await this.handleAuthorizationCodeGrant(params,res)}else if(grantType==="refresh_token"){await this.handleRefreshTokenGrant(params,res)}else{res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"unsupported_grant_type",error_description:`Unsupported grant_type: ${grantType}`}))}}async handleAuthorizationCodeGrant(params,res){const code=params.get("code");const redirectUri=params.get("redirect_uri");const clientId=params.get("client_id");const codeVerifier=params.get("code_verifier");if(!code||!redirectUri||!clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required parameters: code, redirect_uri, client_id"}));return}const codeRecord=this.authCodes.get(code);if(!codeRecord){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Authorization code not found or expired"}));return}if(codeRecord.used){this.authCodes.delete(code);res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Authorization code has already been used"}));return}if(codeRecord.expiresAt<Date.now()){this.authCodes.delete(code);res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Authorization code has expired"}));return}if(codeRecord.clientId!==clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"client_id mismatch"}));return}if(codeRecord.redirectUri!==redirectUri){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"redirect_uri mismatch"}));return}if(codeRecord.codeChallenge){if(!codeVerifier){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"code_verifier required"}));return}const method=codeRecord.codeChallengeMethod??"plain";if(method!=="S256"&&method!=="plain"){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:`Unsupported code_challenge_method: ${method}`}));return}const expectedChallenge=await this.computeCodeChallengeAsync(codeVerifier,method);if(expectedChallenge!==codeRecord.codeChallenge){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"PKCE verification failed"}));return}}codeRecord.used=true;const accessToken=await this.tokenFactory.createTestToken({sub:codeRecord.user.sub,scopes:codeRecord.scopes,exp:this.accessTokenTtlSeconds,claims:{email:codeRecord.user.email,name:codeRecord.user.name,...codeRecord.user.claims??{}}});const idToken=await this.tokenFactory.createTestToken({sub:codeRecord.user.sub,exp:this.accessTokenTtlSeconds,claims:{email:codeRecord.user.email,name:codeRecord.user.name,...codeRecord.user.claims??{}}});const refreshToken=this.generateCode();this.refreshTokens.set(refreshToken,{token:refreshToken,clientId,user:codeRecord.user,scopes:codeRecord.scopes,expiresAt:Date.now()+this.refreshTokenTtlSeconds*1e3});this.log(`Issued tokens for user: ${codeRecord.user.sub}`);res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({access_token:accessToken,token_type:"Bearer",expires_in:this.accessTokenTtlSeconds,refresh_token:refreshToken,id_token:idToken,scope:codeRecord.scopes.join(" ")}))}async handleRefreshTokenGrant(params,res){const refreshToken=params.get("refresh_token");const clientId=params.get("client_id");if(!refreshToken||!clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required parameters: refresh_token, client_id"}));return}const tokenRecord=this.refreshTokens.get(refreshToken);if(!tokenRecord){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Refresh token not found or expired"}));return}if(tokenRecord.expiresAt<Date.now()){this.refreshTokens.delete(refreshToken);res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Refresh token has expired"}));return}if(tokenRecord.clientId!==clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"client_id mismatch"}));return}const accessToken=await this.tokenFactory.createTestToken({sub:tokenRecord.user.sub,scopes:tokenRecord.scopes,exp:this.accessTokenTtlSeconds,claims:{email:tokenRecord.user.email,name:tokenRecord.user.name,...tokenRecord.user.claims??{}}});this.refreshTokens.delete(refreshToken);const newRefreshToken=this.generateCode();this.refreshTokens.set(newRefreshToken,{...tokenRecord,token:newRefreshToken,expiresAt:Date.now()+this.refreshTokenTtlSeconds*1e3});this.log(`Refreshed tokens for user: ${tokenRecord.user.sub}`);res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({access_token:accessToken,token_type:"Bearer",expires_in:this.accessTokenTtlSeconds,refresh_token:newRefreshToken,scope:tokenRecord.scopes.join(" ")}))}async handleUserInfoEndpoint(req,res){const authHeader=req.headers["authorization"];if(!authHeader?.startsWith("Bearer ")){res.writeHead(401,{"WWW-Authenticate":'Bearer error="invalid_token"'});res.end(JSON.stringify({error:"invalid_token",error_description:"Missing or invalid Authorization header"}));return}const testUser=this.options.testUser;if(!testUser){this.logError("UserInfo endpoint called but no testUser configured. Set testUser in MockOAuthServerOptions.");res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",error_description:"No test user configured"}));return}res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({sub:testUser.sub,email:testUser.email,name:testUser.name,picture:testUser.picture,...testUser.claims??{}}))}async handleAuthorizeSubmit(req,res){const body=await this.readBody(req);const params=new URLSearchParams(body);const clientId=params.get("client_id");const redirectUri=params.get("redirect_uri");const scope=params.get("scope")??"openid";const state=params.get("state")??void 0;const codeChallenge=params.get("code_challenge")??void 0;const codeChallengeMethod=params.get("code_challenge_method")??void 0;const action=params.get("action");const sub=params.get("sub");const email=params.get("email")??void 0;const name=params.get("name")??void 0;if(!clientId||!redirectUri||!sub){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required fields"}));return}if(!this.isValidRedirectUri(redirectUri)){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Invalid redirect_uri"}));return}if(this.options.clientId&&clientId!==this.options.clientId){this.redirectWithError(res,redirectUri,"unauthorized_client","Invalid client_id",state);return}if(action==="deny"){this.redirectWithError(res,redirectUri,"access_denied","User denied the authorization request",state);return}const testUser={sub,email,name};const code=this.generateCode();const scopes2=scope.split(" ").filter(Boolean);this.authCodes.set(code,{code,clientId,redirectUri,codeChallenge,codeChallengeMethod,scopes:scopes2,user:testUser,state,expiresAt:Date.now()+5*60*1e3,used:false});const callbackUrl=new URL(redirectUri);callbackUrl.searchParams.set("code",code);if(state){callbackUrl.searchParams.set("state",state)}this.log(`Manual auth approved for user: ${sub}, redirecting with code to ${callbackUrl.origin}${callbackUrl.pathname}`);res.writeHead(302,{Location:callbackUrl.toString()});res.end()}readBody(req){return new Promise((resolve,reject)=>{const chunks=[];req.on("data",chunk=>chunks.push(chunk));req.on("end",()=>resolve(Buffer.concat(chunks).toString()));req.on("error",reject)})}log(message){const shouldLog=this.options.debug||process.env["DEBUG"]==="1"||process.env["DEBUG_SERVER"]==="1";if(shouldLog){console.log(`[MockOAuthServer] ${message}`)}}logError(message){console.error(`[MockOAuthServer ERROR] ${message}`)}isValidRedirectUri(redirectUri){const validUris=this.options.validRedirectUris;if(!validUris||validUris.length===0){return true}if(redirectUri.length>2048){return false}for(const pattern of validUris){if(pattern===redirectUri){return true}if(pattern.includes("*")){if(this.matchWildcardPattern(pattern,redirectUri)){return true}}}return false}matchWildcardPattern(pattern,input){const parts=pattern.split("*");let remaining=input;if(!remaining.startsWith(parts[0])){return false}remaining=remaining.slice(parts[0].length);for(let i=1;i<parts.length;i++){const part=parts[i];if(i===parts.length-1){if(!remaining.endsWith(part)){return false}}else{const idx=remaining.indexOf(part);if(idx===-1){return false}remaining=remaining.slice(idx+part.length)}}return true}redirectWithError(res,redirectUri,error,errorDescription,state){const url=new URL(redirectUri);url.searchParams.set("error",error);url.searchParams.set("error_description",errorDescription);if(state){url.searchParams.set("state",state)}res.writeHead(302,{Location:url.toString()});res.end()}async generateCodeAsync(){return(0,import_utils4.base64urlEncode)((0,import_utils4.randomBytes)(32))}generateCode(){const bytes=new Uint8Array(32);crypto.getRandomValues(bytes);const base64=Buffer.from(bytes).toString("base64");return base64.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async computeCodeChallengeAsync(verifier,method){if(method==="S256"){return(0,import_utils4.sha256Base64url)(verifier)}return verifier}renderLoginPage(clientId,redirectUri,scope,state,codeChallenge,codeChallengeMethod){const issuer=this._info?.issuer??"http://localhost";return`<!DOCTYPE html>
1
+ "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:true}):target,mod));var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var index_exports={};__export(index_exports,{AssertionError:()=>AssertionError,AuthHeaders:()=>AuthHeaders,BASIC_UI_TOOL_CONFIG:()=>BASIC_UI_TOOL_CONFIG,BaselineStore:()=>BaselineStore,ConnectionError:()=>ConnectionError,DefaultInterceptorChain:()=>DefaultInterceptorChain,DefaultMockRegistry:()=>DefaultMockRegistry,EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS:()=>EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS:()=>EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,EXPECTED_GENERIC_TOOLS_LIST_META_KEYS:()=>EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,EXPECTED_GENERIC_TOOL_CALL_META_KEYS:()=>EXPECTED_GENERIC_TOOL_CALL_META_KEYS,EXPECTED_OPENAI_TOOLS_LIST_META_KEYS:()=>EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,EXPECTED_OPENAI_TOOL_CALL_META_KEYS:()=>EXPECTED_OPENAI_TOOL_CALL_META_KEYS,FULL_UI_TOOL_CONFIG:()=>FULL_UI_TOOL_CONFIG,LeakDetector:()=>LeakDetector,McpAssertions:()=>McpAssertions,McpClient:()=>import_client.Client,McpProtocolError:()=>McpProtocolError,McpStdioClientTransport:()=>import_client2.StdioClientTransport,McpTestClient:()=>McpTestClient,McpTestClientBuilder:()=>McpTestClientBuilder,MetricsCollector:()=>MetricsCollector,MockAPIServer:()=>MockAPIServer,MockCimdServer:()=>MockCimdServer,MockOAuthServer:()=>MockOAuthServer,PLATFORM_DETECTION_PATTERNS:()=>PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl:()=>PerfFixturesImpl,RegressionDetector:()=>RegressionDetector,ReplayRecordingError:()=>ReplayRecordingError,ReportGenerator:()=>ReportGenerator,ServerStartError:()=>ServerStartError,SseTransport:()=>SseTransport,StreamableHttpTransport:()=>StreamableHttpTransport,TestClientError:()=>TestClientError,TestServer:()=>TestServer,TestTokenFactory:()=>TestTokenFactory,TestUsers:()=>TestUsers,TimeoutError:()=>TimeoutError,UIAssertions:()=>UIAssertions,assertNoLeak:()=>assertNoLeak,basicUIToolInputSchema:()=>basicUIToolInputSchema,basicUIToolOutputSchema:()=>basicUIToolOutputSchema,buildUserAgent:()=>buildUserAgent,clearGlobalMeasurements:()=>clearGlobalMeasurements,containsPrompt:()=>containsPrompt,containsResource:()=>containsResource,containsResourceTemplate:()=>containsResourceTemplate,containsTool:()=>containsTool,createLeakDetector:()=>createLeakDetector,createPerfFixtures:()=>createPerfFixtures,createReportGenerator:()=>createReportGenerator,createTestUser:()=>createTestUser,expect:()=>expect,forceFullGc:()=>forceFullGc,forceGc:()=>forceGc,formatBaselineAsComment:()=>formatBaselineAsComment,formatBytes:()=>formatBytes,formatDuration:()=>formatDuration,formatMicroseconds:()=>formatMicroseconds,formatReplayReport:()=>formatReplayReport,fullUIToolInputSchema:()=>fullUIToolInputSchema,fullUIToolOutputSchema:()=>fullUIToolOutputSchema,generateBasicUIToolOutput:()=>generateBasicUIToolOutput,generateFullUIToolOutput:()=>generateFullUIToolOutput,getBaselineStore:()=>getBaselineStore,getForbiddenMetaPrefixes:()=>getForbiddenMetaPrefixes,getGlobalMeasurements:()=>getGlobalMeasurements,getPlatformClientInfo:()=>getPlatformClientInfo,getPlatformMetaNamespace:()=>getPlatformMetaNamespace,getPlatformMimeType:()=>getPlatformMimeType,getPlatformUserAgent:()=>getPlatformUserAgent,getRegressionDetector:()=>getRegressionDetector,getToolCallMetaPrefixes:()=>getToolCallMetaPrefixes,getToolsListMetaPrefixes:()=>getToolsListMetaPrefixes,hasMimeType:()=>hasMimeType,hasTextContent:()=>hasTextContent,httpMock:()=>httpMock,httpResponse:()=>httpResponse,interceptors:()=>interceptors,isError:()=>isError,isExtAppsPlatform:()=>isExtAppsPlatform,isGcAvailable:()=>isGcAvailable,isOpenAIPlatform:()=>isOpenAIPlatform,isSuccessful:()=>isSuccessful,isTcpPortListening:()=>isTcpPortListening,isUiPlatform:()=>isUiPlatform,mcpMatchers:()=>mcpMatchers,mockResponse:()=>mockResponse,parseBaselineFromComment:()=>parseBaselineFromComment,perfTest:()=>perfTest,replaySession:()=>replaySession,saveReports:()=>saveReports,summarizeRegressions:()=>summarizeRegressions,test:()=>test,uiMatchers:()=>uiMatchers});module.exports=__toCommonJS(index_exports);var import_protocol3=require("@frontmcp/protocol");var import_utils4=require("@frontmcp/utils");var DefaultMockRegistry=class{mocks=[];add(mock){const entry={definition:mock,callCount:0,calls:[],remainingUses:mock.times??Infinity};this.mocks.push(entry);return{remove:()=>{const index=this.mocks.indexOf(entry);if(index!==-1){this.mocks.splice(index,1)}},callCount:()=>entry.callCount,calls:()=>[...entry.calls]}}clear(){this.mocks=[]}getAll(){return this.mocks.map(e=>e.definition)}match(request){for(const entry of this.mocks){if(entry.remainingUses<=0)continue;const{definition}=entry;if(definition.method!==request.method)continue;if(definition.params!==void 0){const params=request.params??{};if(typeof definition.params==="function"){if(!definition.params(params))continue}else{if(!this.paramsMatch(definition.params,params))continue}}entry.callCount++;entry.calls.push(request);entry.remainingUses--;return definition}return void 0}paramsMatch(expected,actual){for(const[key,value]of Object.entries(expected)){if(!(key in actual))return false;const actualValue=actual[key];if(Array.isArray(value)){if(!Array.isArray(actualValue))return false;if(value.length!==actualValue.length)return false;for(let i=0;i<value.length;i++){const expectedItem=value[i];const actualItem=actualValue[i];if(typeof expectedItem==="object"&&expectedItem!==null){if(typeof actualItem!=="object"||actualItem===null)return false;if(!this.paramsMatch(expectedItem,actualItem)){return false}}else if(actualItem!==expectedItem){return false}}}else if(typeof value==="object"&&value!==null){if(typeof actualValue!=="object"||actualValue===null)return false;if(!this.paramsMatch(value,actualValue)){return false}}else if(actualValue!==value){return false}}return true}};var mockResponse={success(result,id=1){return{jsonrpc:"2.0",id,result}},error(code,message,data,id=1){return{jsonrpc:"2.0",id,error:{code,message,data}}},toolResult(content,id=1){return{jsonrpc:"2.0",id,result:{content}}},toolsList(tools,id=1){return{jsonrpc:"2.0",id,result:{tools}}},resourcesList(resources,id=1){return{jsonrpc:"2.0",id,result:{resources}}},resourceRead(contents,id=1){return{jsonrpc:"2.0",id,result:{contents}}},errors:{methodNotFound:(method,id=1)=>mockResponse.error(-32601,`Method not found: ${method}`,void 0,id),invalidParams:(message,id=1)=>mockResponse.error(-32602,message,void 0,id),internalError:(message,id=1)=>mockResponse.error(-32603,message,void 0,id),resourceNotFound:(uri,id=1)=>mockResponse.error(-32002,`Resource not found: ${uri}`,{uri},id),toolNotFound:(name,id=1)=>mockResponse.error(-32601,`Tool not found: ${name}`,{name},id),unauthorized:(id=1)=>mockResponse.error(-32001,"Unauthorized",void 0,id),forbidden:(id=1)=>mockResponse.error(-32003,"Forbidden",void 0,id)}};var DefaultInterceptorChain=class{request=[];response=[];mocks;constructor(){this.mocks=new DefaultMockRegistry}addRequestInterceptor(interceptor){this.request.push(interceptor);return()=>{const index=this.request.indexOf(interceptor);if(index!==-1){this.request.splice(index,1)}}}addResponseInterceptor(interceptor){this.response.push(interceptor);return()=>{const index=this.response.indexOf(interceptor);if(index!==-1){this.response.splice(index,1)}}}async processRequest(request,meta){let currentRequest=request;const mockDef=this.mocks.match(request);if(mockDef){if(mockDef.delay&&mockDef.delay>0){await sleep(mockDef.delay)}let mockResponse2;if(typeof mockDef.response==="function"){mockResponse2=await mockDef.response(request)}else{mockResponse2=mockDef.response}return{type:"mock",response:{...mockResponse2,id:request.id??mockResponse2.id}}}for(const interceptor of this.request){const ctx={request:currentRequest,meta};const result=await interceptor(ctx);switch(result.action){case"passthrough":break;case"modify":currentRequest=result.request;break;case"mock":return{type:"mock",response:{...result.response,id:request.id??result.response.id}};case"error":return{type:"error",error:result.error}}}return{type:"continue",request:currentRequest}}async processResponse(request,response,durationMs){let currentResponse=response;for(const interceptor of this.response){const ctx={request,response:currentResponse,durationMs};const result=await interceptor(ctx);switch(result.action){case"passthrough":break;case"modify":currentResponse=result.response;break}}return currentResponse}clear(){this.request=[];this.response=[];this.mocks.clear()}};function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}var interceptors={logger(logFn=console.log){return ctx=>{logFn(`[MCP Request] ${ctx.request.method}`,ctx.request.params);return{action:"passthrough"}}},delay(ms){return async()=>{await sleep(ms);return{action:"passthrough"}}},failWhen(condition,error){return ctx=>{if(condition(ctx)){const err=typeof error==="string"?new Error(error):error;return{action:"error",error:err}}return{action:"passthrough"}}},modifyMethod(method,modifier){return ctx=>{if(ctx.request.method===method){return{action:"modify",request:modifier(ctx.request)}}return{action:"passthrough"}}},responseLogger(logFn=console.log){return ctx=>{const status=ctx.response.error?"ERROR":"OK";logFn(`[MCP Response] ${ctx.request.method} ${status} (${ctx.durationMs}ms)`,ctx.response);return{action:"passthrough"}}},modifyResponse(method,modifier){return ctx=>{if(ctx.request.method===method){return{action:"modify",response:modifier(ctx.response)}}return{action:"passthrough"}}}};function errorName(err){if(typeof err!=="object"||err===null)return void 0;const name=err.name;return typeof name==="string"?name:void 0}function isAbortError(err){return errorName(err)==="AbortError"}function errorMessage(err,fallback="Unknown error"){if(typeof err==="string"&&err)return err;if(typeof err==="object"&&err!==null){const message=err.message;if(typeof message==="string"&&message)return message}return fallback}var interceptedErrors=new WeakSet;function markInterceptedError(err){interceptedErrors.add(err);return err}function isInterceptedError(err){return typeof err==="object"&&err!==null&&interceptedErrors.has(err)}var import_utils=require("@frontmcp/utils");var DEFAULT_TIMEOUT=3e4;var SseTransport=class{baseUrl;entryPath;timeout;authHeaders;publicMode;debug;clientInfo;notificationHandler;interceptors;elicitationHandler;state="disconnected";authToken;sessionId;messageEndpoint;stream;pending=new Map;connectionCount=0;reconnectCount=0;lastRequestHeaders={};constructor(config){this.baseUrl=config.baseUrl.replace(/\/$/,"");this.entryPath=config.entryPath;this.timeout=config.timeout??DEFAULT_TIMEOUT;this.authHeaders=config.auth?.headers??{};this.authToken=config.auth?.token;this.publicMode=config.publicMode??false;this.debug=config.debug??false;this.clientInfo=config.clientInfo;this.notificationHandler=config.notificationHandler;this.interceptors=config.interceptors;this.elicitationHandler=config.elicitationHandler}async connect(){this.state="connecting";this.connectionCount++;try{if(!this.publicMode&&!this.authToken){await this.requestAnonymousToken()}await this.openStream();this.state="connected";this.log(`Connected; messages go to ${this.messageEndpoint}`)}catch(error){this.state="error";this.stream?.abort();this.stream=void 0;throw error}}async request(message){this.ensureConnected();const startTime=Date.now();if(this.interceptors){const intercepted=await this.interceptors.processRequest(message,{timestamp:new Date,transport:"sse",sessionId:this.sessionId});switch(intercepted.type){case"mock":return await this.interceptors.processResponse(message,intercepted.response,Date.now()-startTime);case"error":throw markInterceptedError(intercepted.error);case"continue":message=intercepted.request;break}}let response;if(message.id===void 0){await this.post(JSON.stringify(message));response={jsonrpc:"2.0",id:null,result:void 0}}else{response=await this.postAndAwait(message.id,JSON.stringify(message))}if(this.interceptors){response=await this.interceptors.processResponse(message,response,Date.now()-startTime)}return response}async notify(message){this.ensureConnected();const result=await this.post(JSON.stringify(message));if(!result.ok)this.log(`HTTP ${result.status} on notification: ${result.body}`)}async sendRaw(data){this.ensureConnected();let id;try{const parsed=JSON.parse(data);if(typeof parsed.id==="string"||typeof parsed.id==="number")id=parsed.id}catch{}if(id!==void 0)return this.postAndAwait(id,data);const result=await this.post(data);if(!result.ok){return{jsonrpc:"2.0",id:null,error:{code:-32700,message:"Parse error",data:result.body}}}return{jsonrpc:"2.0",id:null,result:void 0}}async close(){this.closeStream("Transport closed");this.state="disconnected";this.sessionId=void 0;this.messageEndpoint=void 0;this.log("SSE transport closed")}isConnected(){return this.state==="connected"}getState(){return this.state}getSessionId(){return this.sessionId}setAuthToken(token){this.authToken=token}setTimeout(ms){this.timeout=ms}getMessageEndpoint(){return this.messageEndpoint}getConnectionCount(){return this.connectionCount}getReconnectCount(){return this.reconnectCount}getLastRequestHeaders(){return{...this.lastRequestHeaders}}setInterceptors(interceptors2){this.interceptors=interceptors2}getInterceptors(){return this.interceptors}setElicitationHandler(handler){this.elicitationHandler=handler}async simulateDisconnect(){this.closeStream("Simulated disconnect");this.state="disconnected";this.sessionId=void 0;this.messageEndpoint=void 0}async waitForReconnect(timeoutMs){this.reconnectCount++;let timer;try{await Promise.race([this.connect(),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error("Timeout waiting for reconnection")),timeoutMs)})])}finally{clearTimeout(timer)}}async openStream(){const controller=new AbortController;this.stream=controller;const url=this.sseUrl();const{"Content-Type":_contentType,...headers}=this.buildHeaders();this.log(`GET ${url}`);const connectTimer=setTimeout(()=>controller.abort(),this.timeout);let response;try{response=await fetch(url,{method:"GET",headers:{...headers,Accept:"text/event-stream"},signal:controller.signal})}catch(error){clearTimeout(connectTimer);throw new Error(isAbortError(error)?`SSE connection to ${url} timed out after ${this.timeout}ms`:`SSE connection to ${url} failed: ${errorMessage(error)}`,{cause:error})}const isEventStream=response.headers.get("content-type")?.includes("text/event-stream")??false;if(!response.ok||!response.body||!isEventStream){clearTimeout(connectTimer);const body=await response.text().catch(()=>"");throw new Error(`SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body?` \u2014 ${body}`:""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`)}const endpoint=new Promise((resolve,reject)=>{const reader=response.body?.getReader();if(!reader){reject(new Error(`SSE connection to ${url} has no body`));return}void this.readStream(controller,reader,resolve,reject)});try{const data=await endpoint;const messageUrl=new URL(data,url);this.messageEndpoint=messageUrl.toString();this.sessionId=messageUrl.searchParams.get("sessionId")??void 0}finally{clearTimeout(connectTimer)}}async readStream(controller,reader,onEndpoint,onEndpointError){const decoder=new TextDecoder;let buffer="";let endpointSeen=false;try{for(let chunk=await reader.read();!chunk.done;chunk=await reader.read()){const{completeEvents,remainder}=splitSSEEvents(buffer+decoder.decode(chunk.value,{stream:true}));buffer=remainder;for(const text of completeEvents){const event=parseSSEEvent(text);if(!event)continue;if(event.type==="endpoint"){if(!endpointSeen){endpointSeen=true;onEndpoint(event.data)}continue}await this.handleMessage(event.data)}}}catch(error){if(!controller.signal.aborted)this.log("SSE stream failed:",error)}finally{reader.releaseLock()}if(!endpointSeen){onEndpointError(new Error(controller.signal.aborted?`SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms`:`SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`))}if(this.stream===controller){this.stream=void 0;this.rejectPending("SSE stream closed by the server");if(this.state==="connected")this.state="disconnected"}}async handleMessage(data){let message;try{message=JSON.parse(data)}catch{this.log("Ignoring SSE event that is not JSON:",data);return}if(!message||typeof message!=="object")return;if(typeof message.method==="string"){const request=message;if(request.id===void 0||request.id===null){this.notificationHandler?.(request);return}await this.answerServerRequest(request);return}if(message.id!==void 0&&message.id!==null){const pending=this.pending.get(message.id);if(pending){clearTimeout(pending.timer);this.pending.delete(message.id);pending.resolve(message)}}}async answerServerRequest(request){const id=request.id;let reply;if(request.method==="elicitation/create"){reply={jsonrpc:"2.0",id,result:await this.elicit(request.params)}}else if(request.method==="ping"){reply={jsonrpc:"2.0",id,result:{}}}else{reply={jsonrpc:"2.0",id,error:{code:-32601,message:`Method not found: ${request.method}`}}}const result=await this.post(JSON.stringify(reply));if(!result.ok)this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`)}async elicit(params){if(!this.elicitationHandler)return{action:"decline"};try{return await this.elicitationHandler(params)}catch(error){this.log("Elicitation handler error:",error);return{action:"cancel"}}}closeStream(reason){const controller=this.stream;this.stream=void 0;controller?.abort();this.rejectPending(reason)}rejectPending(reason){for(const[id,pending]of this.pending){clearTimeout(pending.timer);pending.resolve({jsonrpc:"2.0",id,error:{code:-32e3,message:reason}})}this.pending.clear()}async postAndAwait(id,body){const answer=new Promise(resolve=>{const timer=setTimeout(()=>{this.pending.delete(id);resolve({jsonrpc:"2.0",id,error:{code:-32e3,message:`Request timeout after ${this.timeout}ms`}})},this.timeout);this.pending.set(id,{resolve,timer})});const result=await this.post(body);if(!result.ok){const pending=this.pending.get(id);if(pending){clearTimeout(pending.timer);this.pending.delete(id)}return{jsonrpc:"2.0",id,error:{code:-32e3,message:`HTTP ${result.status}: ${result.statusText}`,data:result.body}}}return answer}async post(body){const endpoint=this.messageEndpoint;if(!endpoint)throw new Error("SSE transport has no message endpoint. Call connect() first.");const headers=this.buildHeaders();this.lastRequestHeaders=headers;this.log(`POST ${endpoint}`,body);const controller=new AbortController;const timer=setTimeout(()=>controller.abort(),this.timeout);try{const response=await fetch(endpoint,{method:"POST",headers,body,signal:controller.signal});const text=await response.text();return{ok:response.ok,status:response.status,statusText:response.statusText,body:text}}catch(error){if(isAbortError(error)){return{ok:false,status:0,statusText:`timeout after ${this.timeout}ms`,body:""}}throw error}finally{clearTimeout(timer)}}async requestAnonymousToken(){const url=new URL(this.baseUrl);url.pathname=`${(0,import_utils.trimTrailing)(url.pathname,"/")}/oauth/token`;url.search="";const tokenUrl=url.toString();try{const response=await fetch(tokenUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:"anonymous",client_id:crypto.randomUUID(),resource:this.baseUrl})});if(!response.ok){this.log(`No anonymous token (HTTP ${response.status})`);return}const token=await response.json();if(token.access_token)this.authToken=token.access_token}catch(error){this.log("Error requesting anonymous token:",error)}}sseUrl(){const url=new URL(this.baseUrl);const entry=(0,import_utils.trimSlashes)(this.entryPath);const basePath=(0,import_utils.trimTrailing)(url.pathname,"/");url.pathname=entry?`${basePath}/${entry}/sse`:`${basePath}/sse`;return url.toString()}buildHeaders(){const headers={"Content-Type":"application/json",Accept:"application/json, text/event-stream"};if(this.clientInfo)headers["User-Agent"]=`${this.clientInfo.name}/${this.clientInfo.version}`;if(this.authToken)headers["Authorization"]=`Bearer ${this.authToken}`;if(this.sessionId)headers["mcp-session-id"]=this.sessionId;Object.assign(headers,this.authHeaders);return headers}ensureConnected(){if(this.state!=="connected"){throw new Error("Transport not connected. Call connect() first.")}}log(message,data){if(this.debug){console.log(`[SSE] ${message}`,data??"")}}};function splitSSEEvents(buffer){const endsWithCarriageReturn=buffer.endsWith("\r");const normalized=(endsWithCarriageReturn?buffer.slice(0,-1):buffer).replace(/\r\n?/g,"\n");const completeEvents=normalized.split("\n\n");const remainder=completeEvents.pop()??"";return{completeEvents,remainder:endsWithCarriageReturn?`${remainder}\r`:remainder}}function parseSSEEvent(text){let type="message";const data=[];for(const line of text.split("\n")){if(line.startsWith(":"))continue;const colon=line.indexOf(":");const field=colon===-1?line:line.slice(0,colon);const value=colon===-1?"":line.slice(colon+1).replace(/^ /,"");if(field==="event")type=value;else if(field==="data")data.push(value)}return data.length>0?{type,data:data.join("\n")}:void 0}var import_protocol2=require("@frontmcp/protocol");var import_utils3=require("@frontmcp/utils");var import_protocol=require("@frontmcp/protocol");var import_utils2=require("@frontmcp/utils");var SENTINEL_PREFIX="=?base64?";var SENTINEL_SUFFIX="?=";var NAME_PARAM_BY_METHOD={"tools/call":"name","prompts/get":"name","resources/read":"uri"};function isStatelessRequest(protocolVersion,method){if(protocolVersion===import_protocol.PROTOCOL_2026_07_28)return true;return import_protocol.MCP_20260728_ADDED_METHODS.includes(method??"")}function encodeHeaderValue(value){const needsEncoding=/[^\x20-\x7e]/.test(value)||value!==value.trim()||value.startsWith(SENTINEL_PREFIX);if(!needsEncoding)return value;return`${SENTINEL_PREFIX}${(0,import_utils2.base64Encode)(new TextEncoder().encode(value))}${SENTINEL_SUFFIX}`}function withRequestMeta(message,meta){const params=message.params??{};const callerMeta=params["_meta"];return{...message,params:{...params,_meta:{...meta,...callerMeta}}}}function revisionHeaders(message,toolInputSchema){const headers={"MCP-Protocol-Version":import_protocol.PROTOCOL_2026_07_28};if(!message)return headers;headers["Mcp-Method"]=message.method;const nameParam=NAME_PARAM_BY_METHOD[message.method];const name=nameParam?message.params?.[nameParam]:void 0;if(typeof name==="string")headers["Mcp-Name"]=encodeHeaderValue(name);if(message.method==="tools/call"){Object.assign(headers,paramHeaders(toolInputSchema,message.params?.["arguments"]))}return headers}function paramHeaders(schema,args){const headers={};const properties=schema?.properties??{};const values=typeof args==="object"&&args!==null?args:{};for(const[key,property]of Object.entries(properties)){const value=values[key];const headerName=property?.["x-mcp-header"];if(typeof headerName==="string"&&headerName&&isHeaderValue(value)){headers[`Mcp-Param-${headerName}`]=encodeHeaderValue(String(value))}Object.assign(headers,paramHeaders(property,value))}return headers}function isHeaderValue(value){return typeof value==="string"||typeof value==="boolean"||Number.isSafeInteger(value)}function jsonRpcErrorIn(body){try{const parsed=JSON.parse(body);const error=parsed.error;if(typeof error?.code!=="number"||typeof error.message!=="string")return void 0;return{jsonrpc:"2.0",id:parsed.id??null,error}}catch{return void 0}}function acknowledgedFilterIn(eventData){try{const message=JSON.parse(eventData);if(message.method!==import_protocol.SUBSCRIPTIONS_ACKNOWLEDGED_METHOD)return void 0;return message.params?.notifications??{}}catch{return void 0}}var DEFAULT_TIMEOUT2=3e4;var NOTIFICATION_STREAM_REOPEN_DELAY_MS=250;var StreamableHttpTransport=class{config;state="disconnected";sessionId;authToken;connectionCount=0;reconnectCount=0;lastRequestHeaders={};interceptors;publicMode;elicitationHandler;notificationHandler;notificationStream;notificationProtocolVersion="";notificationStreamReopenTimer;subscriptionStream;protocolVersion;requestMeta;toolInputSchemas=new Map;constructor(config){this.config={baseUrl:config.baseUrl.replace(/\/$/,""),entryPath:config.entryPath,timeout:config.timeout??DEFAULT_TIMEOUT2,auth:config.auth??{},publicMode:config.publicMode??false,debug:config.debug??false,interceptors:config.interceptors,clientInfo:config.clientInfo};this.authToken=config.auth?.token;this.interceptors=config.interceptors;this.publicMode=config.publicMode??false;this.elicitationHandler=config.elicitationHandler;this.notificationHandler=config.notificationHandler;this.protocolVersion=config.protocolVersion;this.requestMeta={[import_protocol2.MCP_20260728_META.protocolVersion]:import_protocol2.PROTOCOL_2026_07_28,...config.clientInfo&&{[import_protocol2.MCP_20260728_META.clientInfo]:config.clientInfo},[import_protocol2.MCP_20260728_META.clientCapabilities]:config.capabilities??{},...config.logLevel&&{[import_protocol2.MCP_20260728_META.logLevel]:config.logLevel}}}async connect(){this.state="connecting";this.connectionCount++;try{if(this.publicMode){this.log("Public mode: connecting without authentication");this.state="connected";return}if(!this.authToken){await this.requestAnonymousToken()}this.state="connected";this.log("Connected to StreamableHTTP transport")}catch(error){this.state="error";throw error}}async requestAnonymousToken(){const clientId=crypto.randomUUID();const tokenUrl=`${this.config.baseUrl}/oauth/token`;this.log(`Requesting anonymous token from ${tokenUrl}`);try{const response=await fetch(tokenUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grant_type:"anonymous",client_id:clientId,resource:this.config.baseUrl})});if(!response.ok){const errorText=await response.text();this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);return}const tokenResponse=await response.json();if(tokenResponse.access_token){this.authToken=tokenResponse.access_token;this.log("Anonymous token acquired successfully")}}catch(error){this.log(`Error requesting anonymous token: ${error}`)}}async request(message){this.ensureConnected();const startTime=Date.now();if(this.interceptors){const interceptResult=await this.interceptors.processRequest(message,{timestamp:new Date,transport:"streamable-http",sessionId:this.sessionId});switch(interceptResult.type){case"mock":{const mockResponse2=await this.interceptors.processResponse(message,interceptResult.response,Date.now()-startTime);return mockResponse2}case"error":throw markInterceptedError(interceptResult.error);case"continue":message=interceptResult.request;break}}const stateless=isStatelessRequest(this.protocolVersion,message.method);const outgoing=stateless?withRequestMeta(message,this.requestMeta):message;const headers=this.buildHeaders(stateless,outgoing);this.lastRequestHeaders=headers;const url=this.mcpUrl();this.log(`POST ${url}`,outgoing);const controller=new AbortController;const timeoutId=setTimeout(()=>controller.abort(),this.config.timeout);try{const response=await fetch(url,{method:"POST",headers,body:JSON.stringify(outgoing),signal:controller.signal});const newSessionId=response.headers.get("mcp-session-id");if(newSessionId){this.sessionId=newSessionId}let jsonResponse;if(!response.ok){const errorText=await response.text();this.log(`HTTP Error ${response.status}: ${errorText}`);const rpcError=stateless&&!isAuthStatus(response.status)?jsonRpcErrorIn(errorText):void 0;jsonResponse=rpcError??{jsonrpc:"2.0",id:message.id??null,error:{code:-32e3,message:`HTTP ${response.status}: ${response.statusText}`,data:errorText}}}else{const contentType=response.headers.get("content-type")??"";if(contentType.includes("text/event-stream")){jsonResponse=await this.handleSSEResponseWithElicitation(response,message)}else{const text=await response.text();this.log("Response:",text);if(!text.trim()){jsonResponse={jsonrpc:"2.0",id:message.id??null,result:void 0}}else{jsonResponse=JSON.parse(text)}}}if(stateless&&message.method==="tools/list")this.rememberToolInputSchemas(jsonResponse.result);if(this.interceptors){jsonResponse=await this.interceptors.processResponse(message,jsonResponse,Date.now()-startTime)}clearTimeout(timeoutId);return jsonResponse}catch(error){clearTimeout(timeoutId);if(isAbortError(error)){return{jsonrpc:"2.0",id:message.id??null,error:{code:-32e3,message:`Request timeout after ${this.config.timeout}ms`}}}throw error}}async notify(message){this.ensureConnected();const headers=this.buildHeaders(isStatelessRequest(this.protocolVersion,message.method),message);this.lastRequestHeaders=headers;const url=this.mcpUrl();this.log(`POST ${url} (notification)`,message);const controller=new AbortController;const timeoutId=setTimeout(()=>controller.abort(),this.config.timeout);try{const response=await fetch(url,{method:"POST",headers,body:JSON.stringify(message),signal:controller.signal});clearTimeout(timeoutId);const newSessionId=response.headers.get("mcp-session-id");if(newSessionId){this.sessionId=newSessionId}if(!response.ok){const errorText=await response.text();this.log(`HTTP Error ${response.status} on notification: ${errorText}`)}}catch(error){clearTimeout(timeoutId);if(!isAbortError(error)){throw error}}}async sendRaw(data){this.ensureConnected();const headers=this.buildHeaders(isStatelessRequest(this.protocolVersion,void 0));this.lastRequestHeaders=headers;const url=this.mcpUrl();this.log(`POST ${url} (raw)`,data);const controller=new AbortController;const timeoutId=setTimeout(()=>controller.abort(),this.config.timeout);try{const response=await fetch(url,{method:"POST",headers,body:data,signal:controller.signal});clearTimeout(timeoutId);const text=await response.text();if(!text.trim()){return{jsonrpc:"2.0",id:null,error:{code:-32700,message:"Parse error"}}}return JSON.parse(text)}catch(error){clearTimeout(timeoutId);return{jsonrpc:"2.0",id:null,error:{code:-32700,message:"Parse error",data:errorMessage(error)}}}}async openNotificationStream(protocolVersion){this.notificationProtocolVersion=protocolVersion;await this.connectNotificationStream()}async listen(id,notifications){this.ensureConnected();this.closeSubscriptionStream();const controller=new AbortController;this.subscriptionStream=controller;const message=withRequestMeta({jsonrpc:"2.0",id,method:"subscriptions/listen",params:{notifications}},this.requestMeta);let ackTimer;const acknowledged=await new Promise(resolve=>{ackTimer=setTimeout(()=>resolve(void 0),this.config.timeout);void this.readSubscriptionStream(controller,message,resolve)});clearTimeout(ackTimer);if(!acknowledged&&this.subscriptionStream===controller)this.closeSubscriptionStream();return acknowledged}async close(){this.closeNotificationStream();this.closeSubscriptionStream();this.state="disconnected";this.sessionId=void 0;this.log("StreamableHTTP transport closed")}isConnected(){return this.state==="connected"}getState(){return this.state}getSessionId(){return this.sessionId}setAuthToken(token){this.authToken=token}setTimeout(ms){this.config.timeout=ms}setInterceptors(interceptors2){this.interceptors=interceptors2}getInterceptors(){return this.interceptors}setElicitationHandler(handler){this.elicitationHandler=handler}getConnectionCount(){return this.connectionCount}getReconnectCount(){return this.reconnectCount}getLastRequestHeaders(){return{...this.lastRequestHeaders}}async simulateDisconnect(){this.closeNotificationStream();this.closeSubscriptionStream();this.state="disconnected";this.sessionId=void 0}async waitForReconnect(timeoutMs){const deadline=Date.now()+timeoutMs;this.reconnectCount++;await this.connect();while(Date.now()<deadline){if(this.state==="connected"){return}await new Promise(r=>setTimeout(r,50))}throw new Error("Timeout waiting for reconnection")}closeNotificationStream(){clearTimeout(this.notificationStreamReopenTimer);this.notificationStream?.abort();this.notificationStream=void 0}async connectNotificationStream(){if(!this.sessionId||this.notificationStream)return;const controller=new AbortController;this.notificationStream=controller;const{"Content-Type":_contentType,...headers}=this.buildHeaders();const established=fetch(this.mcpUrl(),{method:"GET",headers:{...headers,Accept:"text/event-stream","MCP-Protocol-Version":this.notificationProtocolVersion},signal:controller.signal}).then(async response=>{const isEventStream=response.headers.get("content-type")?.includes("text/event-stream")??false;if(!response.ok||!response.body||!isEventStream){await response.body?.cancel();this.log(`Server offers no notification stream (HTTP ${response.status})`);this.releaseNotificationStream(controller);return}void this.readNotificationStream(controller,response.body.getReader())},error=>{this.log("Failed to open the notification stream:",error);this.releaseNotificationStream(controller)});let establishTimer;const establishDeadline=new Promise(resolve=>{establishTimer=setTimeout(resolve,this.config.timeout)});await Promise.race([established,establishDeadline]);clearTimeout(establishTimer)}releaseNotificationStream(controller){if(this.notificationStream===controller)this.notificationStream=void 0}closeSubscriptionStream(){this.subscriptionStream?.abort();this.subscriptionStream=void 0}async readSubscriptionStream(controller,message,acknowledge){try{const response=await fetch(this.mcpUrl(),{method:"POST",headers:this.buildHeaders(true,message),body:JSON.stringify(message),signal:controller.signal});const isEventStream=response.headers.get("content-type")?.includes("text/event-stream")??false;if(!response.ok||!response.body||!isEventStream){await response.body?.cancel();this.log(`Server opened no subscriptions/listen stream (HTTP ${response.status})`)}else{await this.readEventStream(response.body.getReader(),async event=>{const filter=acknowledgedFilterIn(event.data);if(filter)acknowledge(filter);else await this.handleSSEEvent(event)})}}catch(error){this.log("subscriptions/listen failed:",error)}acknowledge(void 0)}async readNotificationStream(controller,reader){await this.readEventStream(reader,event=>this.handleSSEEvent(event));if(controller.signal.aborted||this.notificationStream!==controller)return;this.releaseNotificationStream(controller);this.notificationStreamReopenTimer=setTimeout(()=>void this.connectNotificationStream(),NOTIFICATION_STREAM_REOPEN_DELAY_MS)}async readEventStream(reader,onEvent){const decoder=new TextDecoder;let buffer="";try{for(let chunk=await reader.read();!chunk.done;chunk=await reader.read()){const{completeEvents,remainder}=splitSSEEvents2(buffer+decoder.decode(chunk.value,{stream:true}));buffer=remainder;for(const eventText of completeEvents){for(const event of this.parseSSEEvents(eventText,void 0).events){await onEvent(event)}}}}catch(error){this.log("Event stream ended:",error)}finally{reader.releaseLock()}}rememberToolInputSchemas(result){const tools=result?.tools;if(!Array.isArray(tools))return;for(const tool of tools){if(typeof tool?.name==="string")this.toolInputSchemas.set(tool.name,tool.inputSchema)}}async handleSSEResponseWithElicitation(response,originalRequest){this.log("handleSSEResponseWithElicitation: starting",{requestId:originalRequest.id});const reader=response.body?.getReader();if(!reader){this.log("handleSSEResponseWithElicitation: no response body");return{jsonrpc:"2.0",id:originalRequest.id??null,error:{code:-32e3,message:"No response body"}}}const decoder=new TextDecoder;let buffer="";let finalResponse=null;let sseSessionId;try{let readCount=0;while(true){readCount++;this.log(`handleSSEResponseWithElicitation: reading chunk ${readCount}`);const{done,value}=await reader.read();this.log(`handleSSEResponseWithElicitation: read result`,{done,valueLength:value?.length});if(done){if(buffer.trim()){const parsed=this.parseSSEEvents(buffer,originalRequest.id);for(const event of parsed.events){const handled=await this.handleSSEEvent(event);if(handled.isFinal){finalResponse=handled.response}}if(parsed.sessionId&&!sseSessionId){sseSessionId=parsed.sessionId}}break}buffer+=decoder.decode(value,{stream:true});const eventEndPattern=/\n\n/g;let lastEventEnd=0;let match;while((match=eventEndPattern.exec(buffer))!==null){const eventText=buffer.slice(lastEventEnd,match.index);lastEventEnd=match.index+2;if(eventText.trim()){const parsed=this.parseSSEEvents(eventText,originalRequest.id);for(const event of parsed.events){const handled=await this.handleSSEEvent(event);if(handled.isFinal){finalResponse=handled.response}}if(parsed.sessionId&&!sseSessionId){sseSessionId=parsed.sessionId}}}buffer=buffer.slice(lastEventEnd)}}finally{reader.releaseLock()}if(sseSessionId&&!this.sessionId){this.sessionId=sseSessionId;this.log("Session ID from SSE:",this.sessionId)}if(finalResponse){return finalResponse}return{jsonrpc:"2.0",id:originalRequest.id??null,error:{code:-32e3,message:"No final response received in SSE stream"}}}parseSSEEvents(text,_requestId){const lines=text.split("\n");const events=[];let currentEvent={type:"message",data:[]};let sessionId;for(const line of lines){if(line.startsWith("event: ")){currentEvent.type=line.slice(7)}else if(line.startsWith("data: ")){currentEvent.data.push(line.slice(6))}else if(line==="data:"){currentEvent.data.push("")}else if(line.startsWith("id: ")){const idValue=line.slice(4);currentEvent.id=idValue;const colonIndex=idValue.lastIndexOf(":");if(colonIndex>0){sessionId=idValue.substring(0,colonIndex)}else{sessionId=idValue}}else if(line===""&&currentEvent.data.length>0){events.push({type:currentEvent.type,data:currentEvent.data.join("\n"),id:currentEvent.id});currentEvent={type:"message",data:[]}}}if(currentEvent.data.length>0){events.push({type:currentEvent.type,data:currentEvent.data.join("\n"),id:currentEvent.id})}return{events,sessionId}}async handleSSEEvent(event){this.log("SSE Event:",{type:event.type,data:event.data.slice(0,200)});try{const parsed=JSON.parse(event.data);if("method"in parsed&&parsed.method==="elicitation/create"){await this.handleElicitationRequest(parsed);return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}if("method"in parsed&&parsed.id===void 0){this.notificationHandler?.(parsed);return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}if("result"in parsed||"error"in parsed){return{isFinal:true,response:parsed}}return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}catch{this.log("Failed to parse SSE event data:",event.data);return{isFinal:false,response:{jsonrpc:"2.0",id:null,result:void 0}}}}async handleElicitationRequest(request){const params=request.params;this.log("Elicitation request received:",{mode:params?.mode,message:params?.message?.slice(0,100)});const requestId=request.id;if(requestId===void 0||requestId===null){this.log("Elicitation request has no ID, cannot respond");return}if(!this.elicitationHandler){this.log("No elicitation handler registered, sending error");await this.sendElicitationResponse(requestId,{action:"decline"});return}try{const response=await this.elicitationHandler(params);this.log("Elicitation handler response:",response);await this.sendElicitationResponse(requestId,response)}catch(error){this.log("Elicitation handler error:",error);await this.sendElicitationResponse(requestId,{action:"cancel"})}}async sendElicitationResponse(requestId,response){const headers=this.buildHeaders();const url=this.mcpUrl();const rpcResponse={jsonrpc:"2.0",id:requestId,result:response};this.log("Sending elicitation response:",rpcResponse);try{const fetchResponse=await fetch(url,{method:"POST",headers,body:JSON.stringify(rpcResponse)});if(!fetchResponse.ok){this.log(`Elicitation response HTTP error: ${fetchResponse.status}`)}}catch(error){this.log("Failed to send elicitation response:",error)}}mcpUrl(){const url=new URL(this.config.baseUrl);const entry=(0,import_utils3.trimSlashes)(this.config.entryPath);const basePath=(0,import_utils3.trimTrailing)(url.pathname,"/");url.pathname=entry?`${basePath}/${entry}`:`${basePath}/`;return url.toString()}buildHeaders(stateless=false,message){const headers={"Content-Type":"application/json",Accept:"application/json, text/event-stream"};if(this.config.clientInfo){headers["User-Agent"]=`${this.config.clientInfo.name}/${this.config.clientInfo.version}`}if(this.authToken){headers["Authorization"]=`Bearer ${this.authToken}`}if(this.sessionId&&!stateless){headers["mcp-session-id"]=this.sessionId}if(stateless){const toolName=message?.params?.["name"];Object.assign(headers,revisionHeaders(message,typeof toolName==="string"?this.toolInputSchemas.get(toolName):void 0))}if(this.config.auth.headers){Object.assign(headers,this.config.auth.headers)}return headers}ensureConnected(){if(this.state!=="connected"){throw new Error("Transport not connected. Call connect() first.")}}log(message,data){if(this.config.debug){console.log(`[StreamableHTTP] ${message}`,data??"")}}parseSSEResponseWithSession(text,requestId){const lines=text.split("\n");const dataLines=[];let sseSessionId;for(const line of lines){if(line.startsWith("data: ")){dataLines.push(line.slice(6))}else if(line==="data:"){dataLines.push("")}else if(line.startsWith("id: ")){const idValue=line.slice(4);const colonIndex=idValue.lastIndexOf(":");if(colonIndex>0){sseSessionId=idValue.substring(0,colonIndex)}else{sseSessionId=idValue}}}if(dataLines.length>0){const jsonData=dataLines.join("\n");try{return{response:JSON.parse(jsonData),sseSessionId}}catch{this.log("Failed to parse SSE data as JSON:",jsonData)}}return{response:{jsonrpc:"2.0",id:requestId??null,error:{code:-32700,message:"Failed to parse SSE response",data:text}},sseSessionId}}};function isAuthStatus(status){return status===401||status===403}function splitSSEEvents2(buffer){const endsWithCarriageReturn=buffer.endsWith("\r");const normalized=(endsWithCarriageReturn?buffer.slice(0,-1):buffer).replace(/\r\n?/g,"\n");const completeEvents=normalized.split("\n\n");const remainder=completeEvents.pop()??"";return{completeEvents,remainder:endsWithCarriageReturn?`${remainder}\r`:remainder}}var MCP_APPS_EXTENSION_KEY="io.modelcontextprotocol/ui";function getPlatformClientInfo(platform){switch(platform){case"openai":return{name:"ChatGPT",version:"1.0"};case"ext-apps":return{name:"mcp-ext-apps",version:"1.0"};case"claude":return{name:"claude-desktop",version:"1.0"};case"claude-code":return{name:"claude-code",version:"1.0"};case"codex":return{name:"codex-mcp-client",version:"1.0"};case"cursor":return{name:"cursor",version:"1.0"};case"continue":return{name:"continue",version:"1.0"};case"cody":return{name:"cody",version:"1.0"};case"gemini":return{name:"gemini",version:"1.0"};case"generic-mcp":return{name:"generic-mcp-client",version:"1.0"};case"unknown":default:return{name:"mcp-test-client",version:"1.0"}}}function buildUserAgent(clientInfo){return`${clientInfo.name}/${clientInfo.version}`}function getPlatformUserAgent(platform){return buildUserAgent(getPlatformClientInfo(platform))}var PLATFORM_DETECTION_PATTERNS={openai:/chatgpt/i,"ext-apps":/mcp-ext-apps/i,claude:/claude|claude-desktop/i,"claude-code":/claude[- ]code/i,codex:/codex/i,cursor:/cursor/i,continue:/continue/i,cody:/cody/i,gemini:/gemini/i,"generic-mcp":/generic-mcp/i,unknown:/.*/};function getPlatformCapabilities(platform){const baseCapabilities={sampling:{},elicitation:{form:{}}};if(platform==="ext-apps"){return{...baseCapabilities,experimental:{[MCP_APPS_EXTENSION_KEY]:{mimeTypes:["text/html;profile=mcp-app"]}}}}return baseCapabilities}var McpTestClientBuilder=class{config;constructor(config){this.config={...config}}withAuth(auth){this.config.auth={...this.config.auth,...auth};return this}withToken(token){this.config.auth={...this.config.auth,token};return this}withHeaders(headers){this.config.auth={...this.config.auth,headers:{...this.config.auth?.headers,...headers}};return this}withTransport(transport){this.config.transport=transport;return this}withTimeout(timeoutMs){this.config.timeout=timeoutMs;return this}withDebug(enabled=true){this.config.debug=enabled;return this}withPublicMode(enabled=true){this.config.publicMode=enabled;return this}withProtocolVersion(version){this.config.protocolVersion=version;return this}withMcpLogLevel(level){this.config.mcpLogLevel=level;return this}withClientInfo(info){this.config.clientInfo=info;return this}withPlatform(platform){this.config.platform=platform;this.config.clientInfo=getPlatformClientInfo(platform);this.config.capabilities=getPlatformCapabilities(platform);return this}withCapabilities(capabilities){this.config.capabilities=capabilities;return this}withQueryParams(params){this.config.queryParams={...this.config.queryParams,...params};return this}build(){return new McpTestClient(this.config)}async buildAndConnect(){const client=this.build();await client.connect();return client}};var DEFAULT_TIMEOUT3=3e4;var DEFAULT_PROTOCOL_VERSION="2025-06-18";var DEFAULT_MCP_LOG_LEVEL="debug";var MAX_INPUT_ROUNDS=10;var LIST_CHANGED_SUBSCRIPTIONS={toolsListChanged:true,promptsListChanged:true,resourcesListChanged:true};var DEFAULT_CLIENT_INFO={name:"@frontmcp/testing",version:"0.4.0"};var MAX_LIST_PAGES=1e3;function isAuthFailure(error){return typeof error?.message==="string"&&/^HTTP (401|403)\b/.test(error.message)}function describeInitializeError(error){const message=error?.message??"Unknown error";const data=error?.data;const detail=typeof data==="string"?data.trim():"";return detail?`${message} - ${detail.slice(0,500)}`:message}function initializeResultFrom(discovered){const serverInfo=discovered._meta?.[import_protocol3.MCP_20260728_META.serverInfo]??{name:"",version:""};return{protocolVersion:import_protocol3.PROTOCOL_2026_07_28,capabilities:discovered.capabilities,serverInfo,...discovered.instructions!==void 0&&{instructions:discovered.instructions}}}function isInputRequired(result){return result?.resultType==="input_required"}function extractEntryPaths(data){let payload=data;if(typeof payload==="string"){try{payload=JSON.parse(payload)}catch{return void 0}}if(!payload||typeof payload!=="object")return void 0;const paths=payload.entryPaths;if(!Array.isArray(paths))return void 0;const strings=paths.filter(p=>typeof p==="string");return strings.length>0?strings:void 0}var McpTestClient=class{config;resourceSubscriptions=new Set;toolsListed=false;transport=null;initResult=null;requestIdCounter=0;_lastRequestId=0;_sessionId;triedDiscoveredEntryPath=false;_sessionInfo=null;_authState={isAnonymous:true,scopes:[]};_logs=[];_traces=[];_notifications=[];_progressUpdates=[];collectingProgress=false;progressTokenCounter=0;_interceptors;_elicitationHandler;constructor(config){if(config.protocolVersion===import_protocol3.PROTOCOL_2026_07_28&&config.transport==="sse"){throw new Error(`Protocol version ${import_protocol3.PROTOCOL_2026_07_28} has no HTTP+SSE transport: use 'streamable-http', or a protocol version before it with 'sse'.`)}this.config={baseUrl:config.baseUrl,entryPath:config.entryPath,transport:config.transport??"streamable-http",auth:config.auth??{},publicMode:config.publicMode??false,timeout:config.timeout??DEFAULT_TIMEOUT3,debug:config.debug??false,protocolVersion:config.protocolVersion??DEFAULT_PROTOCOL_VERSION,mcpLogLevel:config.mcpLogLevel??DEFAULT_MCP_LOG_LEVEL,clientInfo:config.clientInfo??DEFAULT_CLIENT_INFO,platform:config.platform,capabilities:config.capabilities,queryParams:config.queryParams};if(config.auth?.token){this._authState={isAnonymous:false,token:config.auth.token,scopes:this.parseScopesFromToken(config.auth.token),user:this.parseUserFromToken(config.auth.token)}}this._interceptors=new DefaultInterceptorChain}static create(config){return new McpTestClientBuilder(config)}async connect(){this.log("debug",`Connecting to ${this.config.baseUrl}...`);this.triedDiscoveredEntryPath=false;this.initResult=null;this.toolsListed=false;this.transport=this.createTransport();await this.transport.connect();const maxRetries=3;const retryDelayMs=500;let lastError;let reportedEntryPaths;let attemptsMade=0;for(let attempt=1;attempt<=maxRetries;attempt++){attemptsMade=attempt;const initResponse=this.isStateless?await this.discoverServer():await this.initialize();if(initResponse.success&&initResponse.data){this.initResult=initResponse.data;break}lastError=describeInitializeError(initResponse.error);reportedEntryPaths=extractEntryPaths(initResponse.error?.data)??reportedEntryPaths;if(reportedEntryPaths?.length===1&&!this.triedDiscoveredEntryPath){this.triedDiscoveredEntryPath=true;const discovered=reportedEntryPaths[0];this.log("info",`Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);this.config.entryPath=discovered;await this.transport.close();this.transport=this.createTransport();await this.transport.connect();continue}if(isAuthFailure(initResponse.error))break;if(attempt<maxRetries){this.log("debug",`MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);await new Promise(resolve=>setTimeout(resolve,retryDelayMs))}}if(!this.initResult){const served=reportedEntryPaths?.length?` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.`:"";throw new Error(`Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade===1?"":"s"}: ${lastError}.${served}`)}this._sessionId=this.transport.getSessionId();this._sessionInfo={id:this._sessionId??`session-${Date.now()}`,createdAt:new Date,lastActivityAt:new Date,requestCount:1};if(this.isStateless){await this.listen()}else{await this.transport.notify({jsonrpc:"2.0",method:"notifications/initialized"});await this.transport.openNotificationStream?.(this.initResult.protocolVersion);await this.setServerLogLevel()}this.log("info",`Connected to ${this.initResult.serverInfo?.name??"MCP Server"}`);return this.initResult}async disconnect(){if(this.transport){await this.transport.close();this.transport=null}this.initResult=null;this.log("info","Disconnected from MCP server")}async reconnect(options){await this.disconnect();if(options?.sessionId&&this.transport){this._sessionId=options.sessionId}await this.connect()}isConnected(){return this.transport?.isConnected()??false}get sessionId(){return this._sessionId??""}get session(){const info=this._sessionInfo??{id:"",createdAt:new Date,lastActivityAt:new Date,requestCount:0};return{...info,expire:async()=>{this._sessionId=void 0;this._sessionInfo=null}}}get auth(){return this._authState}async authenticate(token){if(typeof token!=="string"||token.trim()===""){throw new Error("authenticate() requires a non-empty token")}const previousAuthState=this._authState;const previousConfigAuth=this.config.auth;this.config.auth={...previousConfigAuth,token};this._authState={isAnonymous:false,token,scopes:this.parseScopesFromToken(token),user:this.parseUserFromToken(token)};const previousTransport=this.transport;if(!previousTransport){this.log("debug","Authentication updated (not connected yet)");return}const previousInit=this.initResult;const previousSessionId=this._sessionId;const previousSessionInfo=this._sessionInfo;try{await this.connect()}catch(err){await this.transport?.close().catch(()=>void 0);this.transport=previousTransport;this.initResult=previousInit;this._sessionId=previousSessionId;this._sessionInfo=previousSessionInfo;this.config.auth=previousConfigAuth;this._authState=previousAuthState;throw err}await previousTransport.close().catch(()=>void 0);this.log("debug","Authentication updated")}get serverInfo(){return{name:this.initResult?.serverInfo?.name??"",version:this.initResult?.serverInfo?.version??""}}get protocolVersion(){return this.initResult?.protocolVersion??""}get instructions(){return this.initResult?.instructions??""}get capabilities(){return this.initResult?.capabilities??{}}hasCapability(name){return!!this.capabilities[name]}async discover(){const response=await this.request("server/discover",{});if(!response.success||!response.data)throw new Error(`server/discover failed: ${response.error?.message}`);return response.data}tools={list:async()=>{const tools=await this.listAllPages("tools/list","tools",cursor=>this.listTools(cursor),page=>page.tools);this.toolsListed=true;return tools},call:async(name,args,options={})=>{await this.learnToolSchemas();const response=await this.callTool(name,args,this.progressTokenFor(options));return this.wrapToolResult(response)}};resources={list:async()=>this.listAllPages("resources/list","resources",cursor=>this.listResources(cursor),page=>page.resources),listTemplates:async()=>this.listAllPages("resources/templates/list","resource templates",cursor=>this.listResourceTemplates(cursor),page=>page.resourceTemplates),read:async uri=>{const response=await this.readResource(uri);return this.wrapResourceContent(response)},subscribe:async uri=>{if(this.isStateless)return this.listenToResource(uri);const response=await this.request("resources/subscribe",{uri});if(!response.success)throw new Error(`Failed to subscribe to ${uri}: ${response.error?.message}`)},unsubscribe:async uri=>{if(this.isStateless){this.resourceSubscriptions.delete(uri);await this.listen();return}const response=await this.request("resources/unsubscribe",{uri});if(!response.success)throw new Error(`Failed to unsubscribe from ${uri}: ${response.error?.message}`)}};prompts={list:async()=>this.listAllPages("prompts/list","prompts",cursor=>this.listPrompts(cursor),page=>page.prompts),get:async(name,args)=>{const response=await this.getPrompt(name,args);return this.wrapPromptResult(response)}};raw={request:async message=>{const transport=this.getConnectedTransport();const start=Date.now();const response=await transport.request(message);this.traceRequest(message.method,message.params,message.id,response,Date.now()-start);return response},notify:async message=>{const transport=this.getConnectedTransport();await transport.notify(message)},sendRaw:async data=>{const transport=this.getConnectedTransport();return transport.sendRaw(data)}};get lastRequestId(){return this._lastRequestId}get transport_info(){return{type:this.config.transport,isConnected:()=>this.transport?.isConnected()??false,messageEndpoint:this.transport?.getMessageEndpoint?.(),connectionCount:this.transport?.getConnectionCount?.()??0,reconnectCount:this.transport?.getReconnectCount?.()??0,lastRequestHeaders:this.transport?.getLastRequestHeaders?.()??{},simulateDisconnect:async()=>{await this.transport?.simulateDisconnect?.()},waitForReconnect:async timeoutMs=>{await this.transport?.waitForReconnect?.(timeoutMs)}}}get transport_(){return this.transport_info}notifications={collect:()=>{return new NotificationCollector(this._notifications)},collectProgress:()=>{this.collectingProgress=true;return new ProgressCollector(this._progressUpdates)},send:async(method,params)=>{await this.raw.notify({jsonrpc:"2.0",method,params})}};onElicitation(handler){this._elicitationHandler=handler;if(this.transport?.setElicitationHandler){this.transport.setElicitationHandler(handler)}this.log("debug","Elicitation handler registered")}clearElicitationHandler(){this._elicitationHandler=void 0;if(this.transport?.setElicitationHandler){this.transport.setElicitationHandler(void 0)}this.log("debug","Elicitation handler cleared")}logs={all:()=>[...this._logs],filter:level=>this._logs.filter(l=>l.level===level),search:text=>this._logs.filter(l=>l.message.includes(text)),last:()=>this._logs[this._logs.length-1],clear:()=>{this._logs=[]}};trace={all:()=>[...this._traces],last:()=>this._traces[this._traces.length-1],clear:()=>{this._traces=[]}};mock={add:mock=>{return this._interceptors.mocks.add(mock)},tool:(name,result,options)=>{return this._interceptors.mocks.add({method:"tools/call",params:{name},response:mockResponse.toolResult([{type:"text",text:typeof result==="string"?result:JSON.stringify(result)}]),times:options?.times,delay:options?.delay})},toolError:(name,code,message,options)=>{return this._interceptors.mocks.add({method:"tools/call",params:{name},response:mockResponse.error(code,message),times:options?.times})},resource:(uri,content,options)=>{const contentObj=typeof content==="string"?{uri,text:content}:{uri,...content};return this._interceptors.mocks.add({method:"resources/read",params:{uri},response:mockResponse.resourceRead([contentObj]),times:options?.times,delay:options?.delay})},resourceError:(uri,options)=>{return this._interceptors.mocks.add({method:"resources/read",params:{uri},response:mockResponse.errors.resourceNotFound(uri),times:options?.times})},toolsList:(tools,options)=>{return this._interceptors.mocks.add({method:"tools/list",response:mockResponse.toolsList(tools),times:options?.times})},resourcesList:(resources,options)=>{return this._interceptors.mocks.add({method:"resources/list",response:mockResponse.resourcesList(resources),times:options?.times})},clear:()=>{this._interceptors.mocks.clear()},all:()=>{return this._interceptors.mocks.getAll()}};intercept={request:interceptor=>{return this._interceptors.addRequestInterceptor(interceptor)},response:interceptor=>{return this._interceptors.addResponseInterceptor(interceptor)},delay:ms=>{return this._interceptors.addRequestInterceptor(async()=>{await new Promise(r=>setTimeout(r,ms));return{action:"passthrough"}})},failMethod:(method,error)=>{return this._interceptors.addRequestInterceptor(ctx=>{if(ctx.request.method===method){return{action:"error",error:new Error(error??`Intercepted: ${method}`)}}return{action:"passthrough"}})},clear:()=>{this._interceptors.request=[];this._interceptors.response=[]},clearAll:()=>{this._interceptors.clear()}};setTimeout(ms){this.config.timeout=ms;if(this.transport){this.transport.setTimeout(ms)}}get isStateless(){return this.config.protocolVersion===import_protocol3.PROTOCOL_2026_07_28}get clientCapabilities(){return this.config.capabilities??{sampling:{},elicitation:{form:{}}}}async initialize(){return this.request("initialize",{protocolVersion:this.config.protocolVersion,capabilities:this.clientCapabilities,clientInfo:this.config.clientInfo})}async discoverServer(){const response=await this.request("server/discover",{});const data=response.data&&initializeResultFrom(response.data);return{...response,data}}async setServerLogLevel(){const level=this.config.mcpLogLevel;if(level===false||!this.hasCapability("logging"))return;const response=await this.request("logging/setLevel",{level});if(!response.success)this.log("warn",`logging/setLevel ${level} failed: ${response.error?.message}`)}async listen(){const resourceSubscriptions=[...this.resourceSubscriptions];const acknowledged=await this.transport?.listen?.(++this.requestIdCounter,{...LIST_CHANGED_SUBSCRIPTIONS,...resourceSubscriptions.length>0&&{resourceSubscriptions}});if(!acknowledged)this.log("warn","The server opened no subscriptions/listen stream");return acknowledged}async listenToResource(uri){this.resourceSubscriptions.add(uri);const acknowledged=await this.listen();if(acknowledged?.resourceSubscriptions?.includes(uri))return;this.resourceSubscriptions.delete(uri);throw new Error(`Failed to subscribe to ${uri}: the server did not acknowledge it in subscriptions/listen`)}async learnToolSchemas(){if(!this.isStateless||this.toolsListed)return;this.toolsListed=true;await this.tools.list().catch(error=>{this.log("warn",`Listing the tools before the first call failed: ${errorMessage(error)}`)})}async listTools(cursor){return this.request("tools/list",cursor?{cursor}:{})}async listAllPages(method,label,fetchPage,itemsOf){const items=[];const seenCursors=new Set;let cursor;for(let page=0;page<MAX_LIST_PAGES;page++){const response=await fetchPage(cursor);if(!response.success||!response.data){throw new Error(`Failed to list ${label}: ${response.error?.message}`)}items.push(...itemsOf(response.data)??[]);const next=response.data.nextCursor;if(!next)return items;if(seenCursors.has(next)){throw new Error(`Failed to list ${label}: ${method} returned the cursor "${next}" twice`)}seenCursors.add(next);cursor=next}throw new Error(`Failed to list ${label}: ${method} did not finish within ${MAX_LIST_PAGES} pages`)}async callTool(name,args,progressToken){return this.request("tools/call",{name,arguments:args??{},...progressToken!==void 0&&{_meta:{progressToken}}})}progressTokenFor(options){if(options.progressToken!==void 0)return options.progressToken;return this.collectingProgress?`progress-${++this.progressTokenCounter}`:void 0}recordNotification=notification=>{const timestamp=new Date;this._notifications.push({method:notification.method,params:notification.params,timestamp});if(notification.method!=="notifications/progress")return;const{progress,total,progressToken,message}=notification.params??{};this._progressUpdates.push({progress:Number(progress),total:typeof total==="number"?total:void 0,message:typeof message==="string"?message:void 0,progressToken:typeof progressToken==="string"||typeof progressToken==="number"?progressToken:void 0,timestamp})};async listResources(cursor){return this.request("resources/list",cursor?{cursor}:{})}async listResourceTemplates(cursor){return this.request("resources/templates/list",cursor?{cursor}:{})}async readResource(uri){return this.request("resources/read",{uri})}async listPrompts(cursor){return this.request("prompts/list",cursor?{cursor}:{})}async getPrompt(name,args){return this.request("prompts/get",{name,arguments:args??{}})}mcpPathLabel(){const entry=(0,import_utils4.trimSlashes)(this.config.entryPath);return entry?`/${entry}`:"/"}createTransport(){let baseUrl=this.config.baseUrl;if(this.config.queryParams&&Object.keys(this.config.queryParams).length>0){const url=new URL(baseUrl);Object.entries(this.config.queryParams).forEach(([key,value])=>{url.searchParams.set(key,String(value))});baseUrl=url.toString()}switch(this.config.transport){case"streamable-http":return new StreamableHttpTransport({baseUrl,entryPath:this.config.entryPath,timeout:this.config.timeout,auth:this.config.auth,publicMode:this.config.publicMode,debug:this.config.debug,interceptors:this._interceptors,clientInfo:this.config.clientInfo,elicitationHandler:this._elicitationHandler,notificationHandler:this.recordNotification,protocolVersion:this.config.protocolVersion,capabilities:this.clientCapabilities,logLevel:this.config.mcpLogLevel||void 0});case"sse":return new SseTransport({baseUrl,entryPath:this.config.entryPath,timeout:this.config.timeout,auth:this.config.auth,publicMode:this.config.publicMode,debug:this.config.debug,interceptors:this._interceptors,clientInfo:this.config.clientInfo,elicitationHandler:this._elicitationHandler,notificationHandler:this.recordNotification});default:throw new Error(`Unknown transport type: ${this.config.transport}`)}}async request(method,params){let response=await this.send(method,params);for(let round=1;response.success&&isInputRequired(response.data);round++){const{inputRequests,requestState}=response.data;const inputResponses=round>MAX_INPUT_ROUNDS?`the server asked for input more than ${MAX_INPUT_ROUNDS} times`:await this.answerInputRequests(inputRequests);if(typeof inputResponses==="string"){return{...response,success:false,data:void 0,error:{code:-32603,message:`${method}: ${inputResponses}`}}}const retried=await this.send(method,{...params,inputResponses,...requestState!==void 0&&{requestState}});response={...retried,durationMs:response.durationMs+retried.durationMs}}return response}async answerInputRequests(inputRequests={}){const answers={};for(const[key,inputRequest]of Object.entries(inputRequests)){if(inputRequest.method!=="elicitation/create"){return`the server asked for ${inputRequest.method}, which McpTestClient doesn't answer`}answers[key]={...await this.answerElicitation(inputRequest.params??{})}}return answers}async answerElicitation(params){if(!this._elicitationHandler)return{action:"decline"};try{return await this._elicitationHandler({mode:"form",...params})}catch(error){this.log("warn",`The elicitation handler threw: ${errorMessage(error)}`);return{action:"cancel"}}}async send(method,params){const transport=this.getConnectedTransport();const id=++this.requestIdCounter;this._lastRequestId=id;const start=Date.now();try{const response=await transport.request({jsonrpc:"2.0",id,method,params});const durationMs=Date.now()-start;this.updateSessionActivity();if("error"in response&&response.error){const error=response.error;this.traceRequest(method,params,id,response,durationMs);return{success:false,error,durationMs,requestId:id}}this.traceRequest(method,params,id,response,durationMs);return{success:true,data:response.result,durationMs,requestId:id}}catch(err){if(isInterceptedError(err))throw err;const durationMs=Date.now()-start;const error={code:-32603,message:errorMessage(err)};return{success:false,error,durationMs,requestId:id}}}getConnectedTransport(){if(!this.transport||!this.transport.isConnected()){throw new Error("Not connected to MCP server. Call connect() first.")}return this.transport}updateSessionActivity(){if(this._sessionInfo){this._sessionInfo.lastActivityAt=new Date;this._sessionInfo.requestCount++}}wrapToolResult(response){const raw=response.data??{content:[]};const isError2=!response.success||raw.isError===true;const meta=raw._meta;const hasUI=meta?.["ui/html"]!==void 0||meta?.["ui/component"]!==void 0;const structuredContent=raw["structuredContent"];return{raw,isSuccess:!isError2,isError:isError2,error:response.error,durationMs:response.durationMs,json(){if(hasUI&&structuredContent!==void 0){return structuredContent}const textContent=raw.content?.find(c=>c.type==="text");if(textContent&&"text"in textContent){return JSON.parse(textContent.text)}throw new Error("No text content to parse as JSON")},text(){const textContent=raw.content?.find(c=>c.type==="text");if(textContent&&"text"in textContent){return textContent.text}return void 0},hasTextContent(){return raw.content?.some(c=>c.type==="text")??false},hasImageContent(){return raw.content?.some(c=>c.type==="image")??false},hasResourceContent(){return raw.content?.some(c=>c.type==="resource")??false},hasToolUI(){return hasUI}}}wrapResourceContent(response){const raw=response.data??{contents:[]};const isError2=!response.success;const firstContent=raw.contents?.[0];return{raw,isSuccess:!isError2,isError:isError2,error:response.error,durationMs:response.durationMs,json(){if(firstContent&&"text"in firstContent){return JSON.parse(firstContent.text)}throw new Error("No text content to parse as JSON")},text(){if(firstContent&&"text"in firstContent){return firstContent.text}return void 0},mimeType(){return firstContent?.mimeType},hasMimeType(type){return firstContent?.mimeType===type}}}wrapPromptResult(response){const raw=response.data??{messages:[]};const isError2=!response.success;return{raw,isSuccess:!isError2,isError:isError2,error:response.error,durationMs:response.durationMs,messages:raw.messages??[],description:raw.description}}log(level,message,data){const entry={level,message,timestamp:new Date,data};this._logs.push(entry);if(this.config.debug){console.log(`[${level.toUpperCase()}] ${message}`,data??"")}}traceRequest(method,params,id,response,durationMs){this._traces.push({request:{method,params,id},response:{result:"result"in response?response.result:void 0,error:"error"in response?response.error:void 0},durationMs,timestamp:new Date})}parseScopesFromToken(token){try{const payload=this.decodeJwtPayload(token);if(!payload)return[];const scope=payload["scope"];const scopes2=payload["scopes"];if(typeof scope==="string"){return scope.split(" ")}if(Array.isArray(scopes2)){return scopes2}return[]}catch{return[]}}parseUserFromToken(token){try{const payload=this.decodeJwtPayload(token);const sub=payload?.["sub"];if(!sub||typeof sub!=="string")return void 0;return{sub,email:payload["email"],name:payload["name"]}}catch{return void 0}}decodeJwtPayload(token){try{const parts=token.split(".");if(parts.length!==3)return null;const payload=Buffer.from(parts[1],"base64url").toString("utf-8");return JSON.parse(payload)}catch{return null}}};var NotificationCollector=class{constructor(notifications){this.notifications=notifications}notifications;get received(){return[...this.notifications]}has(method){return this.notifications.some(n=>n.method===method)}async waitFor(method,timeoutMs){const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){const found=this.notifications.find(n=>n.method===method);if(found)return found;await new Promise(r=>setTimeout(r,50))}throw new Error(`Timeout waiting for notification: ${method}`)}};var ProgressCollector=class{constructor(updates){this.updates=updates}updates;get all(){return[...this.updates]}async waitForComplete(timeoutMs){const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){const last=this.updates[this.updates.length-1];if(last&&last.total!==void 0&&last.progress>=last.total){return}await new Promise(r=>setTimeout(r,50))}throw new Error("Timeout waiting for progress to complete")}};var import_jose=require("jose");var TestTokenFactory=class{issuer;audience;privateKey=null;publicKey=null;jwk=null;keyId;hmacSecret;constructor(options={}){this.issuer=options.issuer??"https://test.frontmcp.local";this.audience=options.audience??"frontmcp-test";this.keyId=`test-key-${Date.now()}`;this.hmacSecret=options.hmacSecret?new TextEncoder().encode(options.hmacSecret):null}get isHmac(){return this.hmacSecret!==null}get alg(){return this.isHmac?"HS256":"RS256"}signingKey(){if(this.hmacSecret)return this.hmacSecret;if(!this.privateKey){throw new Error("Private key not initialized")}return this.privateKey}async ensureKeys(){if(this.isHmac)return;if(this.privateKey&&this.publicKey)return;const{publicKey,privateKey}=await(0,import_jose.generateKeyPair)("RS256",{extractable:true});this.privateKey=privateKey;this.publicKey=publicKey;this.jwk=await(0,import_jose.exportJWK)(publicKey);this.jwk.kid=this.keyId;this.jwk.use="sig";this.jwk.alg="RS256"}async createTestToken(options){await this.ensureKeys();const nowMs=Date.now();const iat=Math.floor(nowMs/1e3);const exp=options.exp??3600;const payload={iss:options.iss??this.issuer,sub:options.sub,aud:options.aud??this.audience,iat,exp:Math.ceil(nowMs/1e3)+exp,scope:options.scopes?.join(" "),...options.claims};const header=this.isHmac?{alg:this.alg,typ:"JWT"}:{alg:this.alg,kid:this.keyId};const token=await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());return token}async createAdminToken(sub="admin-001"){return this.createTestToken({sub,scopes:["admin:*","read","write","delete"],claims:{email:"admin@test.local",name:"Test Admin",role:"admin"}})}async createUserToken(sub="user-001",scopes2=["read","write"]){return this.createTestToken({sub,scopes:scopes2,claims:{email:"user@test.local",name:"Test User",role:"user"}})}async createAnonymousToken(expiresIn){return this.createTestToken({sub:`anon:${Date.now()}`,scopes:["anonymous"],exp:expiresIn,claims:{name:"Anonymous",role:"anonymous"}})}async createExpiredToken(options){await this.ensureKeys();const now=Math.floor(Date.now()/1e3);const payload={iss:this.issuer,sub:options.sub,aud:this.audience,iat:now-7200,exp:now-3600};const header=this.isHmac?{alg:this.alg,typ:"JWT"}:{alg:this.alg,kid:this.keyId};const token=await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());return token}createTokenWithInvalidSignature(options){const now=Math.floor(Date.now()/1e3);const headerObj=this.isHmac?{alg:this.alg,typ:"JWT"}:{alg:this.alg,kid:this.keyId};const header=Buffer.from(JSON.stringify(headerObj)).toString("base64url");const payload=Buffer.from(JSON.stringify({iss:this.issuer,sub:options.sub,aud:this.audience,iat:now,exp:now+3600})).toString("base64url");const signature=Buffer.from("invalid-signature-"+Date.now()).toString("base64url");return`${header}.${payload}.${signature}`}async getPublicJwks(){if(this.isHmac){throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)")}await this.ensureKeys();if(!this.jwk){throw new Error("JWK not initialized")}return{keys:[this.jwk]}}getIssuer(){return this.issuer}getAudience(){return this.audience}};var AuthHeaders={bearer(token){return{Authorization:`Bearer ${token}`}},noAuth(){return{}},mcpRequest(token,sessionId){const headers={"Content-Type":"application/json",Accept:"application/json",Authorization:`Bearer ${token}`};if(sessionId){headers["mcp-session-id"]=sessionId}return headers},publicMode(sessionId){const headers={"Content-Type":"application/json",Accept:"application/json"};if(sessionId){headers["mcp-session-id"]=sessionId}return headers},custom(headerName,headerValue){return{[headerName]:headerValue}}};var TestUsers={admin:{sub:"admin-001",email:"admin@test.local",name:"Test Admin",scopes:["admin:*","read","write","delete"],role:"admin"},user:{sub:"user-001",email:"user@test.local",name:"Test User",scopes:["read","write"],role:"user"},readOnly:{sub:"readonly-001",email:"readonly@test.local",name:"Read Only User",scopes:["read"],role:"readonly"},anonymous:{sub:"anon:001",name:"Anonymous",scopes:["anonymous"],role:"anonymous"},noScopes:{sub:"noscopes-001",email:"noscopes@test.local",name:"No Scopes User",scopes:[],role:"user"},toolsOnly:{sub:"toolsonly-001",email:"toolsonly@test.local",name:"Tools Only User",scopes:["tools:execute"],role:"user"},resourcesOnly:{sub:"resourcesonly-001",email:"resourcesonly@test.local",name:"Resources Only User",scopes:["resources:read"],role:"user"}};function createTestUser(overrides){return{scopes:[],...overrides}}var import_http=require("http");var import_utils5=require("@frontmcp/utils");var MockOAuthServer=class{tokenFactory;options;server=null;_info=null;connections=new Set;authCodes=new Map;refreshTokens=new Map;accessTokenTtlSeconds;refreshTokenTtlSeconds;constructor(tokenFactory2,options={}){this.tokenFactory=tokenFactory2;this.options=options;this.accessTokenTtlSeconds=options.accessTokenTtlSeconds??3600;this.refreshTokenTtlSeconds=options.refreshTokenTtlSeconds??30*24*3600}setAutoApprove(enabled){this.options.autoApprove=enabled}setTestUser(user){this.options.testUser=user}addValidRedirectUri(uri){const uris=this.options.validRedirectUris??[];uris.push(uri);this.options.validRedirectUris=uris}clearStoredTokens(){this.authCodes.clear();this.refreshTokens.clear()}async start(){if(this.server){throw new Error("Mock OAuth server is already running")}const port=this.options.port??0;return new Promise((resolve,reject)=>{const server=(0,import_http.createServer)(this.handleRequest.bind(this));this.server=server;server.on("connection",socket=>{this.connections.add(socket);socket.on("close",()=>this.connections.delete(socket))});server.on("error",err=>{this.log(`Server error: ${err.message}`);reject(err)});server.listen(port,()=>{const address=server.address();if(!address||typeof address==="string"){reject(new Error("Failed to get server address"));return}const actualPort=address.port;const issuer=this.options.issuer??`http://localhost:${actualPort}`;this._info={baseUrl:`http://localhost:${actualPort}`,port:actualPort,issuer,jwksUrl:`http://localhost:${actualPort}/.well-known/jwks.json`};this.log(`Mock OAuth server started at ${this._info.baseUrl}`);resolve(this._info)})})}async stop(){const server=this.server;if(!server){return}for(const socket of this.connections){socket.destroy()}this.connections.clear();return new Promise((resolve,reject)=>{server.close(err=>{if(err){reject(err)}else{this.server=null;this._info=null;this.log("Mock OAuth server stopped");resolve()}})})}get info(){if(!this._info){throw new Error("Mock OAuth server is not running")}return this._info}getTokenFactory(){return this.tokenFactory}async handleRequest(req,res){const fullUrl=req.url??"/";const[urlPath,queryString]=fullUrl.split("?");this.log(`${req.method} ${urlPath}`);res.setHeader("Access-Control-Allow-Origin","*");res.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS");res.setHeader("Access-Control-Allow-Headers","Content-Type, Authorization");if(req.method==="OPTIONS"){res.writeHead(204);res.end();return}try{if(urlPath==="/.well-known/jwks.json"||urlPath==="/.well-known/jwks"){await this.handleJwks(req,res)}else if(urlPath==="/.well-known/openid-configuration"){await this.handleOidcConfig(req,res)}else if(urlPath==="/.well-known/oauth-authorization-server"){await this.handleOAuthMetadata(req,res)}else if(urlPath==="/oauth/authorize"){await this.handleAuthorizeEndpoint(req,res,queryString)}else if(urlPath==="/oauth/token"){await this.handleTokenEndpoint(req,res)}else if(urlPath==="/userinfo"){await this.handleUserInfoEndpoint(req,res)}else if(urlPath==="/oauth/authorize/submit"&&req.method==="POST"){await this.handleAuthorizeSubmit(req,res)}else{res.writeHead(404,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"not_found",error_description:"Endpoint not found"}))}}catch(error){const errorMsg=error instanceof Error?`${error.message}
2
+ ${error.stack}`:String(error);this.logError(`Error handling request to ${urlPath}: ${errorMsg}`);res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",error_description:"Internal server error"}))}}async handleJwks(_req,res){const jwks=await this.tokenFactory.getPublicJwks();res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(jwks));this.log("Served JWKS")}async handleOidcConfig(_req,res){const issuer=this._info?.issuer??"http://localhost";const config={issuer,authorization_endpoint:`${issuer}/oauth/authorize`,token_endpoint:`${issuer}/oauth/token`,jwks_uri:`${issuer}/.well-known/jwks.json`,response_types_supported:["code"],subject_types_supported:["public"],id_token_signing_alg_values_supported:["RS256"],scopes_supported:["openid","profile","email"],token_endpoint_auth_methods_supported:["client_secret_basic","client_secret_post","none"],claims_supported:["sub","iss","aud","exp","iat","email","name"],grant_types_supported:["authorization_code","refresh_token","anonymous"]};res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(config));this.log("Served OIDC configuration")}async handleOAuthMetadata(_req,res){const issuer=this._info?.issuer??"http://localhost";const metadata={issuer,authorization_endpoint:`${issuer}/oauth/authorize`,token_endpoint:`${issuer}/oauth/token`,jwks_uri:`${issuer}/.well-known/jwks.json`,response_types_supported:["code"],grant_types_supported:["authorization_code","refresh_token","anonymous"],token_endpoint_auth_methods_supported:["client_secret_basic","client_secret_post","none"],scopes_supported:["openid","profile","email","anonymous"]};res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(metadata));this.log("Served OAuth metadata")}async handleAuthorizeEndpoint(_req,res,queryString){const params=new URLSearchParams(queryString??"");const clientId=params.get("client_id");const redirectUri=params.get("redirect_uri");const responseType=params.get("response_type");const state=params.get("state")??void 0;const scope=params.get("scope")??"openid";const codeChallenge=params.get("code_challenge")??void 0;const codeChallengeMethod=params.get("code_challenge_method")??void 0;if(!clientId||!redirectUri||!responseType){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required parameters: client_id, redirect_uri, response_type"}));return}if(responseType!=="code"){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"unsupported_response_type",error_description:"Only response_type=code is supported"}));return}if(!this.isValidRedirectUri(redirectUri)){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Invalid redirect_uri"}));return}if(this.options.clientId&&clientId!==this.options.clientId){this.redirectWithError(res,redirectUri,"unauthorized_client","Invalid client_id",state);return}if(this.options.autoApprove){const testUser=this.options.testUser;if(!testUser){this.logError("autoApprove is enabled but no testUser configured. Set testUser in MockOAuthServerOptions.");res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",error_description:"autoApprove is enabled but no testUser configured"}));return}const code=this.generateCode();const scopes2=scope.split(" ").filter(Boolean);this.authCodes.set(code,{code,clientId,redirectUri,codeChallenge,codeChallengeMethod,scopes:scopes2,user:testUser,state,expiresAt:Date.now()+5*60*1e3,used:false});const callbackUrl=new URL(redirectUri);callbackUrl.searchParams.set("code",code);if(state){callbackUrl.searchParams.set("state",state)}this.log(`Auto-approved auth request, redirecting with code to ${callbackUrl.origin}${callbackUrl.pathname}`);res.writeHead(302,{Location:callbackUrl.toString()});res.end();return}const html=this.renderLoginPage(clientId,redirectUri,scope,state,codeChallenge,codeChallengeMethod);res.writeHead(200,{"Content-Type":"text/html"});res.end(html)}async handleTokenEndpoint(req,res){const body=await this.readBody(req);const params=new URLSearchParams(body);const authHeader=req.headers["authorization"];let clientSecret=params.get("client_secret")??void 0;if(!clientSecret&&authHeader?.startsWith("Basic ")){const decoded=Buffer.from(authHeader.slice(6),"base64").toString("utf8");const colonIndex=decoded.indexOf(":");if(colonIndex>=0){clientSecret=decoded.slice(colonIndex+1)}}if(this.options.clientSecret&&clientSecret!==this.options.clientSecret){res.writeHead(401,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_client",error_description:"Invalid client_secret"}));return}const grantType=params.get("grant_type");if(grantType==="anonymous"){const token=await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({access_token:token,token_type:"Bearer",expires_in:this.accessTokenTtlSeconds}));this.log("Issued anonymous token")}else if(grantType==="authorization_code"){await this.handleAuthorizationCodeGrant(params,res)}else if(grantType==="refresh_token"){await this.handleRefreshTokenGrant(params,res)}else{res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"unsupported_grant_type",error_description:`Unsupported grant_type: ${grantType}`}))}}async handleAuthorizationCodeGrant(params,res){const code=params.get("code");const redirectUri=params.get("redirect_uri");const clientId=params.get("client_id");const codeVerifier=params.get("code_verifier");if(!code||!redirectUri||!clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required parameters: code, redirect_uri, client_id"}));return}const codeRecord=this.authCodes.get(code);if(!codeRecord){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Authorization code not found or expired"}));return}if(codeRecord.used){this.authCodes.delete(code);res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Authorization code has already been used"}));return}if(codeRecord.expiresAt<Date.now()){this.authCodes.delete(code);res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Authorization code has expired"}));return}if(codeRecord.clientId!==clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"client_id mismatch"}));return}if(codeRecord.redirectUri!==redirectUri){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"redirect_uri mismatch"}));return}if(codeRecord.codeChallenge){if(!codeVerifier){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"code_verifier required"}));return}const method=codeRecord.codeChallengeMethod??"plain";if(method!=="S256"&&method!=="plain"){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:`Unsupported code_challenge_method: ${method}`}));return}const expectedChallenge=await this.computeCodeChallengeAsync(codeVerifier,method);if(expectedChallenge!==codeRecord.codeChallenge){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"PKCE verification failed"}));return}}codeRecord.used=true;const accessToken=await this.tokenFactory.createTestToken({sub:codeRecord.user.sub,scopes:codeRecord.scopes,exp:this.accessTokenTtlSeconds,claims:{email:codeRecord.user.email,name:codeRecord.user.name,...codeRecord.user.claims??{}}});const idToken=await this.tokenFactory.createTestToken({sub:codeRecord.user.sub,exp:this.accessTokenTtlSeconds,claims:{email:codeRecord.user.email,name:codeRecord.user.name,...codeRecord.user.claims??{}}});const refreshToken=this.generateCode();this.refreshTokens.set(refreshToken,{token:refreshToken,clientId,user:codeRecord.user,scopes:codeRecord.scopes,expiresAt:Date.now()+this.refreshTokenTtlSeconds*1e3});this.log(`Issued tokens for user: ${codeRecord.user.sub}`);res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({access_token:accessToken,token_type:"Bearer",expires_in:this.accessTokenTtlSeconds,refresh_token:refreshToken,id_token:idToken,scope:codeRecord.scopes.join(" ")}))}async handleRefreshTokenGrant(params,res){const refreshToken=params.get("refresh_token");const clientId=params.get("client_id");if(!refreshToken||!clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required parameters: refresh_token, client_id"}));return}const tokenRecord=this.refreshTokens.get(refreshToken);if(!tokenRecord){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Refresh token not found or expired"}));return}if(tokenRecord.expiresAt<Date.now()){this.refreshTokens.delete(refreshToken);res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"Refresh token has expired"}));return}if(tokenRecord.clientId!==clientId){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_grant",error_description:"client_id mismatch"}));return}const accessToken=await this.tokenFactory.createTestToken({sub:tokenRecord.user.sub,scopes:tokenRecord.scopes,exp:this.accessTokenTtlSeconds,claims:{email:tokenRecord.user.email,name:tokenRecord.user.name,...tokenRecord.user.claims??{}}});this.refreshTokens.delete(refreshToken);const newRefreshToken=this.generateCode();this.refreshTokens.set(newRefreshToken,{...tokenRecord,token:newRefreshToken,expiresAt:Date.now()+this.refreshTokenTtlSeconds*1e3});this.log(`Refreshed tokens for user: ${tokenRecord.user.sub}`);res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({access_token:accessToken,token_type:"Bearer",expires_in:this.accessTokenTtlSeconds,refresh_token:newRefreshToken,scope:tokenRecord.scopes.join(" ")}))}async handleUserInfoEndpoint(req,res){const authHeader=req.headers["authorization"];if(!authHeader?.startsWith("Bearer ")){res.writeHead(401,{"WWW-Authenticate":'Bearer error="invalid_token"'});res.end(JSON.stringify({error:"invalid_token",error_description:"Missing or invalid Authorization header"}));return}const testUser=this.options.testUser;if(!testUser){this.logError("UserInfo endpoint called but no testUser configured. Set testUser in MockOAuthServerOptions.");res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",error_description:"No test user configured"}));return}res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify({sub:testUser.sub,email:testUser.email,name:testUser.name,picture:testUser.picture,...testUser.claims??{}}))}async handleAuthorizeSubmit(req,res){const body=await this.readBody(req);const params=new URLSearchParams(body);const clientId=params.get("client_id");const redirectUri=params.get("redirect_uri");const scope=params.get("scope")??"openid";const state=params.get("state")??void 0;const codeChallenge=params.get("code_challenge")??void 0;const codeChallengeMethod=params.get("code_challenge_method")??void 0;const action=params.get("action");const sub=params.get("sub");const email=params.get("email")??void 0;const name=params.get("name")??void 0;if(!clientId||!redirectUri||!sub){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Missing required fields"}));return}if(!this.isValidRedirectUri(redirectUri)){res.writeHead(400,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"invalid_request",error_description:"Invalid redirect_uri"}));return}if(this.options.clientId&&clientId!==this.options.clientId){this.redirectWithError(res,redirectUri,"unauthorized_client","Invalid client_id",state);return}if(action==="deny"){this.redirectWithError(res,redirectUri,"access_denied","User denied the authorization request",state);return}const testUser={sub,email,name};const code=this.generateCode();const scopes2=scope.split(" ").filter(Boolean);this.authCodes.set(code,{code,clientId,redirectUri,codeChallenge,codeChallengeMethod,scopes:scopes2,user:testUser,state,expiresAt:Date.now()+5*60*1e3,used:false});const callbackUrl=new URL(redirectUri);callbackUrl.searchParams.set("code",code);if(state){callbackUrl.searchParams.set("state",state)}this.log(`Manual auth approved for user: ${sub}, redirecting with code to ${callbackUrl.origin}${callbackUrl.pathname}`);res.writeHead(302,{Location:callbackUrl.toString()});res.end()}readBody(req){return new Promise((resolve,reject)=>{const chunks=[];req.on("data",chunk=>chunks.push(chunk));req.on("end",()=>resolve(Buffer.concat(chunks).toString()));req.on("error",reject)})}log(message){const shouldLog=this.options.debug||process.env["DEBUG"]==="1"||process.env["DEBUG_SERVER"]==="1";if(shouldLog){console.log(`[MockOAuthServer] ${message}`)}}logError(message){console.error(`[MockOAuthServer ERROR] ${message}`)}isValidRedirectUri(redirectUri){const validUris=this.options.validRedirectUris;if(!validUris||validUris.length===0){return true}if(redirectUri.length>2048){return false}for(const pattern of validUris){if(pattern===redirectUri){return true}if(pattern.includes("*")){if(this.matchWildcardPattern(pattern,redirectUri)){return true}}}return false}matchWildcardPattern(pattern,input){const parts=pattern.split("*");let remaining=input;if(!remaining.startsWith(parts[0])){return false}remaining=remaining.slice(parts[0].length);for(let i=1;i<parts.length;i++){const part=parts[i];if(i===parts.length-1){if(!remaining.endsWith(part)){return false}}else{const idx=remaining.indexOf(part);if(idx===-1){return false}remaining=remaining.slice(idx+part.length)}}return true}redirectWithError(res,redirectUri,error,errorDescription,state){const url=new URL(redirectUri);url.searchParams.set("error",error);url.searchParams.set("error_description",errorDescription);if(state){url.searchParams.set("state",state)}res.writeHead(302,{Location:url.toString()});res.end()}async generateCodeAsync(){return(0,import_utils5.base64urlEncode)((0,import_utils5.randomBytes)(32))}generateCode(){const bytes=new Uint8Array(32);crypto.getRandomValues(bytes);const base64=Buffer.from(bytes).toString("base64");return base64.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}async computeCodeChallengeAsync(verifier,method){if(method==="S256"){return(0,import_utils5.sha256Base64url)(verifier)}return verifier}renderLoginPage(clientId,redirectUri,scope,state,codeChallenge,codeChallengeMethod){const issuer=this._info?.issuer??"http://localhost";return`<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8">
@@ -75,7 +75,7 @@ ${error.stack}`:String(error);this.logError(`Error handling request to ${urlPath
75
75
  </form>
76
76
  </div>
77
77
  </body>
78
- </html>`}escapeHtml(text){return text.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}};var import_http2=require("http");var MockAPIServer=class{options;server=null;_info=null;routes;constructor(options){this.options=options;this.routes=options.routes??[];for(const route of this.routes){this.validateRoute(route)}}async start(){if(this.server){throw new Error("Mock API server is already running")}const port=this.options.port??0;return new Promise((resolve,reject)=>{const server=(0,import_http2.createServer)(this.handleRequest.bind(this));this.server=server;server.on("error",err=>{this.log(`Server error: ${err.message}`);reject(err)});server.listen(port,()=>{const address=server.address();if(!address||typeof address==="string"){reject(new Error("Failed to get server address"));return}const actualPort=address.port;this._info={baseUrl:`http://localhost:${actualPort}`,port:actualPort,specUrl:`http://localhost:${actualPort}/openapi.json`};this.log(`Mock API server started at ${this._info.baseUrl}`);resolve(this._info)})})}async stop(){const server=this.server;if(!server){return}return new Promise((resolve,reject)=>{server.close(err=>{if(err){reject(err)}else{this.server=null;this._info=null;this.log("Mock API server stopped");resolve()}})})}get info(){if(!this._info){throw new Error("Mock API server is not running")}return this._info}addRoute(route){this.validateRoute(route);this.routes.push(route)}clearRoutes(){this.routes=[]}validateRoute(route){const method=route.method??"UNKNOWN";const path=route.path??"<unknown>";const hasHandler=typeof route.handler==="function";const hasResponse=route.response!=null;if(hasHandler&&hasResponse){throw new Error(`Mock route ${method} ${path} must define either 'handler' or 'response', not both`)}if(!hasHandler&&!hasResponse){throw new Error(`Mock route ${method} ${path} must define either 'handler' or 'response'`)}}async handleRequest(req,res){const url=req.url??"/";const method=(req.method??"GET").toUpperCase();this.log(`${method} ${url}`);res.setHeader("Access-Control-Allow-Origin","*");res.setHeader("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, PATCH, OPTIONS");res.setHeader("Access-Control-Allow-Headers","Content-Type, Authorization");if(method==="OPTIONS"){res.writeHead(204);res.end();return}try{if(url==="/openapi.json"||url==="/openapi.yaml"){res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(this.options.openApiSpec));this.log("Served OpenAPI spec");return}const matchResult=this.findRoute(method,url);if(matchResult){const{route,params}=matchResult;if(route.handler){let body;if(["POST","PUT","PATCH"].includes(method)){body=await this.parseBody(req)}const queryString=url.includes("?")?url.split("?")[1]:"";const query={};if(queryString){const searchParams=new URLSearchParams(queryString);searchParams.forEach((value,key)=>{query[key]=value})}const mockReq={url,method,headers:this.normalizeHeaders(req.headers),params,query,body};const mockRes={json:(responseBody,status=200)=>{res.writeHead(status,{"Content-Type":"application/json"});res.end(JSON.stringify(responseBody))},send:(responseBody,status=200,headers={})=>{res.writeHead(status,{"Content-Type":"application/json",...headers});res.end(JSON.stringify(responseBody))}};await route.handler(mockReq,mockRes);this.log(`Matched route with handler: ${method} ${route.path}`);return}if(route.response){const status=route.response.status??200;const headers={"Content-Type":"application/json",...route.response.headers};res.writeHead(status,headers);res.end(JSON.stringify(route.response.body));this.log(`Matched route: ${method} ${route.path} -> ${status}`);return}}res.writeHead(404,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"not_found",message:`No mock for ${method} ${url}`}))}catch(error){this.log(`Error handling request: ${error}`);res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",message:"Internal server error"}))}}findRoute(method,url){const path=url.split("?")[0];for(const route of this.routes){if(route.method!==method)continue;if(route.path===path){return{route,params:{}}}const routeParts=route.path.split("/");const urlParts=path.split("/");if(routeParts.length!==urlParts.length)continue;const params={};let match=true;for(let i=0;i<routeParts.length;i++){if(routeParts[i].startsWith(":")){params[routeParts[i].slice(1)]=urlParts[i]}else if(routeParts[i]!==urlParts[i]){match=false;break}}if(match){return{route,params}}}return void 0}async parseBody(req){const maxBodyBytes=this.options.maxBodyBytes??1*1024*1024;const bodyTimeoutMs=this.options.bodyTimeoutMs??1e4;return new Promise((resolve,reject)=>{let body="";let bodyBytes=0;const onData=chunk=>{bodyBytes+=chunk.length;if(bodyBytes>maxBodyBytes){cleanup();req.destroy();reject(new Error(`Request body too large (>${maxBodyBytes} bytes)`));return}body+=chunk.toString()};const cleanup=()=>{req.off("data",onData);req.off("end",onEnd);req.off("error",onError);clearTimeout(timeout)};const timeout=setTimeout(()=>{cleanup();req.destroy();reject(new Error(`Request body read timed out after ${bodyTimeoutMs}ms`))},bodyTimeoutMs);const onEnd=()=>{cleanup();try{resolve(body?JSON.parse(body):void 0)}catch{resolve(body)}};const onError=error=>{cleanup();reject(error)};req.on("data",onData);req.once("end",onEnd);req.once("error",onError)})}normalizeHeaders(headers){const normalized={};for(const[key,value]of Object.entries(headers)){normalized[key.toLowerCase()]=Array.isArray(value)?value[0]:value}return normalized}log(message){if(this.options.debug){console.log(`[MockAPIServer] ${message}`)}}};var import_http3=require("http");var MockCimdServer=class{options;server=null;_info=null;clients=new Map;customResponses=new Map;constructor(options={}){this.options=options}async start(){if(this.server){throw new Error("Mock CIMD server is already running")}const port=this.options.port??0;return new Promise((resolve,reject)=>{const server=(0,import_http3.createServer)(this.handleRequest.bind(this));this.server=server;server.on("error",err=>{this.log(`Server error: ${err.message}`);reject(err)});server.listen(port,()=>{const address=server.address();if(!address||typeof address==="string"){reject(new Error("Failed to get server address"));return}const actualPort=address.port;this._info={baseUrl:`http://localhost:${actualPort}`,port:actualPort};this.log(`Mock CIMD server started at ${this._info.baseUrl}`);resolve(this._info)})})}async stop(){const server=this.server;if(!server){return}return new Promise((resolve,reject)=>{server.close(err=>{if(err){reject(err)}else{this.server=null;this._info=null;this.log("Mock CIMD server stopped");resolve()}})})}get info(){if(!this._info){throw new Error("Mock CIMD server is not running")}return this._info}registerClient(options){if(!this._info){throw new Error("Mock CIMD server is not running. Call start() first.")}const path=options.path??this.nameToPath(options.name);const fullPath=`/clients/${path}/metadata.json`;const clientId=`${this._info.baseUrl}${fullPath}`;const document={client_id:clientId,client_name:options.name,redirect_uris:options.redirectUris??["http://localhost:3000/callback"],token_endpoint_auth_method:options.tokenEndpointAuthMethod??"none",grant_types:options.grantTypes??["authorization_code"],response_types:options.responseTypes??["code"],...options.clientUri&&{client_uri:options.clientUri},...options.logoUri&&{logo_uri:options.logoUri},...options.scope&&{scope:options.scope},...options.contacts&&{contacts:options.contacts}};this.clients.set(fullPath,{path:fullPath,document});this.log(`Registered client: ${options.name} at ${fullPath}`);return clientId}getClientId(name){if(!this._info){throw new Error("Mock CIMD server is not running")}const path=this.nameToPath(name);const fullPath=`/clients/${path}/metadata.json`;const client=this.clients.get(fullPath);if(!client){throw new Error(`Client "${name}" not found. Register it first.`)}return client.document.client_id}registerInvalidDocument(path,document){if(!this._info){throw new Error("Mock CIMD server is not running")}const fullPath=path.startsWith("/")?path:`/${path}`;this.clients.set(fullPath,{path:fullPath,document});this.log(`Registered invalid document at ${fullPath}`)}registerFetchError(path,statusCode,body){const fullPath=path.startsWith("/")?path:`/${path}`;this.customResponses.set(fullPath,{status:statusCode,body});this.log(`Registered error response at ${fullPath}: ${statusCode}`)}removeClient(name){const path=this.nameToPath(name);const fullPath=`/clients/${path}/metadata.json`;this.clients.delete(fullPath);this.log(`Removed client: ${name}`)}clear(){this.clients.clear();this.customResponses.clear();this.log("Cleared all clients and custom responses")}handleRequest(req,res){const url=req.url??"/";const method=(req.method??"GET").toUpperCase();this.log(`${method} ${url}`);res.setHeader("Access-Control-Allow-Origin","*");res.setHeader("Access-Control-Allow-Methods","GET, OPTIONS");res.setHeader("Access-Control-Allow-Headers","Content-Type, Accept");if(method==="OPTIONS"){res.writeHead(204);res.end();return}if(method!=="GET"){res.writeHead(405,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"method_not_allowed"}));return}const customResponse=this.customResponses.get(url);if(customResponse){res.writeHead(customResponse.status,{"Content-Type":"application/json"});res.end(customResponse.body?JSON.stringify(customResponse.body):"");this.log(`Returned custom error response: ${customResponse.status}`);return}const client=this.clients.get(url);if(client){res.writeHead(200,{"Content-Type":"application/json","Cache-Control":"max-age=3600"});res.end(JSON.stringify(client.document));this.log(`Served client metadata: ${client.document.client_name}`);return}res.writeHead(404,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"not_found",message:`No client at ${url}`}));this.log(`Client not found: ${url}`)}nameToPath(name){return name.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}log(message){if(this.options.debug){console.log(`[MockCimdServer] ${message}`)}}};var import_child_process=require("child_process");var import_net2=require("net");var import_utils5=require("@frontmcp/utils");var TestClientError=class extends Error{constructor(message){super(message);this.name="TestClientError";Object.setPrototypeOf(this,new.target.prototype)}};var ConnectionError=class extends TestClientError{constructor(message,cause){super(message);this.cause=cause;this.name="ConnectionError";Object.setPrototypeOf(this,new.target.prototype)}cause};var TimeoutError=class extends TestClientError{constructor(message,timeoutMs){super(message);this.timeoutMs=timeoutMs;this.name="TimeoutError";Object.setPrototypeOf(this,new.target.prototype)}timeoutMs};var McpProtocolError=class extends TestClientError{constructor(message,code,data){super(message);this.code=code;this.data=data;this.name="McpProtocolError";Object.setPrototypeOf(this,new.target.prototype)}code;data};var ServerStartError=class extends TestClientError{constructor(message,cause){super(message);this.cause=cause;this.name="ServerStartError";Object.setPrototypeOf(this,new.target.prototype)}cause};var AssertionError=class extends TestClientError{constructor(message){super(message);this.name="AssertionError";Object.setPrototypeOf(this,new.target.prototype)}};var ReplayRecordingError=class extends TestClientError{constructor(file,line,reason){super(`${file} line ${line} is not a call record: ${reason}`);this.file=file;this.line=line;this.name="ReplayRecordingError";Object.setPrototypeOf(this,new.target.prototype)}file;line};var import_net=require("net");var E2E_PORT_RANGES={"demo-e2e-public":{start:5e4,size:10},"demo-e2e-cache":{start:50010,size:10},"demo-e2e-config":{start:50020,size:10},"demo-e2e-direct":{start:50030,size:10},"demo-e2e-errors":{start:50040,size:10},"demo-e2e-hooks":{start:50050,size:10},"demo-e2e-multiapp":{start:50060,size:10},"demo-e2e-notifications":{start:50070,size:10},"demo-e2e-providers":{start:50080,size:10},"demo-e2e-standalone":{start:50090,size:10},"demo-e2e-orchestrated":{start:50100,size:10},"demo-e2e-transparent":{start:50110,size:10},"demo-e2e-cimd":{start:50120,size:10},"demo-e2e-remote-proxy":{start:50130,size:10},"demo-e2e-skills":{start:50200,size:10},"demo-e2e-remote":{start:50210,size:10},"demo-e2e-openapi":{start:50220,size:10},"demo-e2e-ui":{start:50230,size:10},"demo-e2e-codecall":{start:50240,size:10},"demo-e2e-remember":{start:50250,size:10},"demo-e2e-elicitation":{start:50260,size:10},"demo-e2e-agents":{start:50270,size:10},"demo-e2e-transport-recreation":{start:50280,size:10},"demo-e2e-jobs":{start:50290,size:10},"demo-e2e-redis":{start:50300,size:10},"demo-e2e-serverless":{start:50310,size:10},"demo-e2e-uipack":{start:50320,size:10},"demo-e2e-agent-adapters":{start:50330,size:10},"demo-e2e-guard":{start:50340,size:10},"demo-e2e-cli-exec":{start:50350,size:10},"demo-e2e-env-awareness":{start:50360,size:10},"demo-e2e-esm":{start:50370,size:10},"demo-e2e-feature-flags":{start:50380,size:10},"demo-e2e-resource-providers":{start:50390,size:10},"esm-package-server":{start:50400,size:10},"esm-package-server-hot-reload":{start:50410,size:10},"esm-package-server-cli":{start:50420,size:10},"demo-e2e-unix-socket":{start:50430,size:10},"demo-e2e-distributed":{start:50440,size:20},"demo-e2e-protocol-20260728":{start:50460,size:20},"demo-e2e-testing":{start:50480,size:20},"demo-e2e-answer-shaping":{start:50500,size:10},"mock-oauth":{start:50900,size:10},"mock-api":{start:50910,size:10},"mock-cimd":{start:50920,size:10},default:{start:51e3,size:100}};var reservedPorts=new Map;var lockedPorts=new Set;var LOCK_MAX_AGE_MS=10*60*1e3;function lockDir(){try{const os=require("node:os");const path=require("node:path");return path.join(os.tmpdir(),"frontmcp-testing-ports")}catch{return void 0}}function pidIsAlive(pid){try{process.kill(pid,0);return true}catch(error){return error.code==="EPERM"}}function acquirePortLock(port){if(lockedPorts.has(port))return false;let fs;let file;try{fs=require("node:fs");const dir=lockDir();if(!dir)return true;fs.mkdirSync(dir,{recursive:true});file=require("node:path").join(dir,`${port}.lock`)}catch{return true}const payload=JSON.stringify({pid:process.pid,at:Date.now()});for(let attempt=0;attempt<2;attempt++){try{fs.writeFileSync(file,payload,{flag:"wx"});lockedPorts.add(port);return true}catch(error){if(error.code!=="EEXIST")return true;try{const held=JSON.parse(fs.readFileSync(file,"utf8"));const fresh=typeof held.at==="number"&&Date.now()-held.at<LOCK_MAX_AGE_MS;if(fresh&&typeof held.pid==="number"&&pidIsAlive(held.pid))return false;fs.unlinkSync(file)}catch{}}}return false}function releasePortLock(port){if(!lockedPorts.delete(port))return;try{const fs=require("node:fs");const dir=lockDir();if(dir)fs.unlinkSync(require("node:path").join(dir,`${port}.lock`))}catch{}}var projectPortIndex=new Map;function getPortRange(project){const key=project;if(key in E2E_PORT_RANGES){return E2E_PORT_RANGES[key]}return E2E_PORT_RANGES.default}async function reservePort(project,preferredPort){const range=getPortRange(project);const handle=port=>({port,releaseSocket:async()=>{await releasePort(port)},release:async()=>{await releasePort(port);releasePortLock(port)}});const wantsSpecificPort=preferredPort!==void 0&&Number.isInteger(preferredPort)&&preferredPort>0;if(wantsSpecificPort){const reservation=await tryReservePort(preferredPort,project);if(reservation){return handle(preferredPort)}console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`)}let index=projectPortIndex.get(project)??0;const maxAttempts=range.size;for(let attempt=0;attempt<maxAttempts;attempt++){const port=range.start+index%range.size;index=(index+1)%range.size;if(reservedPorts.has(port)||lockedPorts.has(port)){continue}const reservation=await tryReservePort(port,project);if(reservation){projectPortIndex.set(project,index);return handle(port)}}for(let port=51e3;port<52e3;port++){if(reservedPorts.has(port)||lockedPorts.has(port))continue;if(await tryReservePort(port,project)){return handle(port)}}throw new Error(`[PortRegistry] Could not reserve a port for ${project}. Range: ${range.start}-${range.start+range.size-1}. Currently reserved: ${Array.from(reservedPorts.keys()).join(", ")}`)}async function tryReservePort(port,project){if(!acquirePortLock(port)){return false}return new Promise(resolve=>{const server=(0,import_net.createServer)();server.once("error",()=>{releasePortLock(port);resolve(false)});server.listen(port,()=>{reservedPorts.set(port,{port,project,holder:server,reservedAt:Date.now()});resolve(true)})})}async function releasePort(port){const reservation=reservedPorts.get(port);if(!reservation){return}return new Promise(resolve=>{reservation.holder.close(()=>{reservedPorts.delete(port);resolve()})})}var SERVER_EXIT_TIMEOUT_MS=12e3;var DEBUG_SERVER=process.env["DEBUG_SERVER"]==="1"||process.env["DEBUG"]==="1";var TestServer=class _TestServer{process=null;options;_info;logs=[];portRelease=null;portReleaseSocket=null;constructor(options,port,portRelease,portReleaseSocket){this.options={port,project:options.project,command:options.command??"",cwd:options.cwd??process.cwd(),env:options.env??{},startupTimeout:options.startupTimeout??3e4,healthCheckPath:options.healthCheckPath??"/health",debug:options.debug??DEBUG_SERVER};this.portRelease=portRelease??null;this.portReleaseSocket=portReleaseSocket??portRelease??null;this._info={baseUrl:`http://localhost:${port}`,port}}static async start(options){const project=options.project??"default";const maxAttempts=3;for(let attempt=1;attempt<=maxAttempts;attempt++){const{port,release,releaseSocket}=await reservePort(project,options.port);const server=new _TestServer(options,port,release,releaseSocket);try{await server.startProcess();return server}catch(error){try{await server.stop()}catch(cleanupError){if(options.debug||DEBUG_SERVER){const msg=cleanupError instanceof Error?cleanupError.message:String(cleanupError);console.warn(`[TestServer] Cleanup failed after startup error: ${msg}`)}}const isEADDRINUSE=error instanceof Error&&(error.message.includes("EADDRINUSE")||server.getLogs().some(l=>l.includes("EADDRINUSE")));if(isEADDRINUSE&&attempt<maxAttempts){const delayMs=attempt*500;if(options.debug||DEBUG_SERVER){console.warn(`[TestServer] EADDRINUSE on port ${port}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts})`)}await sleep2(delayMs);continue}throw error}}throw new Error(`[TestServer] Failed to start after ${maxAttempts} attempts`)}static async startNx(project,options={}){if(!/^[\w-]+$/.test(project)){throw new Error(`Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`)}const maxAttempts=3;for(let attempt=1;attempt<=maxAttempts;attempt++){const{port,release,releaseSocket}=await reservePort(project,options.port);const serverOptions={...options,port,project,command:`npx nx serve ${project} --port ${port}`,cwd:options.cwd??process.cwd()};const server=new _TestServer(serverOptions,port,release,releaseSocket);try{await server.startProcess();return server}catch(error){try{await server.stop()}catch(cleanupError){if(options.debug||DEBUG_SERVER){const msg=cleanupError instanceof Error?cleanupError.message:String(cleanupError);console.warn(`[TestServer] Cleanup failed after startup error: ${msg}`)}}const isEADDRINUSE=error instanceof Error&&(error.message.includes("EADDRINUSE")||server.getLogs().some(l=>l.includes("EADDRINUSE")));if(isEADDRINUSE&&attempt<maxAttempts){const delayMs=attempt*500;if(options.debug||DEBUG_SERVER){console.warn(`[TestServer] EADDRINUSE on port ${port}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts})`)}await sleep2(delayMs);continue}throw error}}throw new Error(`[TestServer] Failed to start after ${maxAttempts} attempts`)}static connect(baseUrl){const url=new URL(baseUrl);const port=parseInt(url.port,10)||(url.protocol==="https:"?443:80);const server=new _TestServer({command:"",port},port);server._info={baseUrl:baseUrl.replace(/\/$/,""),port};return server}get info(){return{...this._info}}async stop(){try{await this.stopProcess()}finally{if(this.portRelease){await this.portRelease();this.portRelease=null}this.portReleaseSocket=null}}async stopProcess(){if(this.process){this.log("Stopping server...");const serverPid=this._info.pid!==this._info.shellPid?this._info.pid:void 0;if(this.process.exitCode!==null||this.process.signalCode!==null){this.log(`Server already exited (code: ${this.process.exitCode}, signal: ${this.process.signalCode})`);this.process=null;if(serverPid!==void 0)signalIfRunning(serverPid,"SIGTERM");await this.waitForServerGone(serverPid);return}const pid=this.process.pid;try{if(pid!==void 0){process.kill(-pid,"SIGTERM")}else{this.process.kill("SIGTERM")}}catch{this.process.kill("SIGTERM")}const exitPromise=new Promise(resolve=>{if(this.process){this.process.once("exit",()=>resolve())}else{resolve()}});const killTimeout=setTimeout(()=>{if(this.process){this.log("Force killing server after timeout...");const killPid=this.process.pid;try{if(killPid!==void 0){process.kill(-killPid,"SIGKILL")}else{this.process.kill("SIGKILL")}}catch{this.process.kill("SIGKILL")}}},5e3);await exitPromise;clearTimeout(killTimeout);this.process=null;await this.waitForServerGone(serverPid);this.log("Server stopped")}}async waitForServerGone(serverPid){const deadline=Date.now()+SERVER_EXIT_TIMEOUT_MS;while(Date.now()<deadline){const serverRunning=serverPid!==void 0&&pidIsAlive(serverPid);if(!serverRunning&&!await acceptsConnections(this._info.port))return;await sleep2(100)}if(serverPid!==void 0&&pidIsAlive(serverPid)){this.log("Force killing server process after timeout...");signalIfRunning(serverPid,"SIGKILL")}}async waitForReady(timeout){const timeoutMs=timeout??this.options.startupTimeout;const deadline=Date.now()+timeoutMs;const checkInterval=100;while(Date.now()<deadline){try{const response=await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`,{method:"GET",signal:AbortSignal.timeout(1e3)});if(response.ok||response.status===404){this.log("Server is ready");return}}catch{}await sleep2(checkInterval)}throw new Error(`Server did not become ready within ${timeoutMs}ms`)}async restart(){await this.stopProcess();await this.startProcess()}getLogs(){return[...this.logs]}clearLogs(){this.logs=[]}async startProcess(){if(!this.options.command){await this.waitForReady();return}this.log(`Starting server: ${this.options.command}`);const env={...process.env,...this.options.env,PORT:String(this.options.port)};const runtimeEnv=withWorkspaceProtocolFallback(env,this.options.cwd);if(this.portReleaseSocket){await this.portReleaseSocket();this.portReleaseSocket=null;await sleep2(300)}this.process=(0,import_child_process.spawn)(this.options.command,[],{cwd:this.options.cwd,env:runtimeEnv,shell:true,detached:true,stdio:["pipe","pipe","pipe"]});this._info.pid=this.process.pid;this._info.shellPid=this.process.pid;let processExited=false;let exitCode=null;let exitError=null;this.process.stdout?.on("data",data=>{const text=data.toString();this.logs.push(text);if(this.options.debug){console.log("[SERVER]",text)}});this.process.stderr?.on("data",data=>{const text=data.toString();this.logs.push(`[ERROR] ${text}`);if(this.options.debug){console.error("[SERVER ERROR]",text)}});this.process.on("error",err=>{this.logs.push(`[SPAWN ERROR] ${err.message}`);exitError=err;if(this.options.debug){console.error("[SERVER SPAWN ERROR]",err)}});this.process.once("exit",code=>{processExited=true;exitCode=code;this.log(`Server process exited with code ${code}`)});try{await this.waitForReadyWithExitDetection(()=>{if(exitError){return{exited:true,error:exitError}}if(processExited){const allLogs=this.logs.join("\n");const errorLogs=this.logs.filter(l=>l.includes("[ERROR]")||l.toLowerCase().includes("error")).join("\n");return{exited:true,error:new ServerStartError(`Server process exited unexpectedly with code ${exitCode}.
78
+ </html>`}escapeHtml(text){return text.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}};var import_http2=require("http");var MockAPIServer=class{options;server=null;_info=null;routes;constructor(options){this.options=options;this.routes=options.routes??[];for(const route of this.routes){this.validateRoute(route)}}async start(){if(this.server){throw new Error("Mock API server is already running")}const port=this.options.port??0;return new Promise((resolve,reject)=>{const server=(0,import_http2.createServer)(this.handleRequest.bind(this));this.server=server;server.on("error",err=>{this.log(`Server error: ${err.message}`);reject(err)});server.listen(port,()=>{const address=server.address();if(!address||typeof address==="string"){reject(new Error("Failed to get server address"));return}const actualPort=address.port;this._info={baseUrl:`http://localhost:${actualPort}`,port:actualPort,specUrl:`http://localhost:${actualPort}/openapi.json`};this.log(`Mock API server started at ${this._info.baseUrl}`);resolve(this._info)})})}async stop(){const server=this.server;if(!server){return}return new Promise((resolve,reject)=>{server.close(err=>{if(err){reject(err)}else{this.server=null;this._info=null;this.log("Mock API server stopped");resolve()}})})}get info(){if(!this._info){throw new Error("Mock API server is not running")}return this._info}addRoute(route){this.validateRoute(route);this.routes.push(route)}clearRoutes(){this.routes=[]}validateRoute(route){const method=route.method??"UNKNOWN";const path=route.path??"<unknown>";const hasHandler=typeof route.handler==="function";const hasResponse=route.response!=null;if(hasHandler&&hasResponse){throw new Error(`Mock route ${method} ${path} must define either 'handler' or 'response', not both`)}if(!hasHandler&&!hasResponse){throw new Error(`Mock route ${method} ${path} must define either 'handler' or 'response'`)}}async handleRequest(req,res){const url=req.url??"/";const method=(req.method??"GET").toUpperCase();this.log(`${method} ${url}`);res.setHeader("Access-Control-Allow-Origin","*");res.setHeader("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, PATCH, OPTIONS");res.setHeader("Access-Control-Allow-Headers","Content-Type, Authorization");if(method==="OPTIONS"){res.writeHead(204);res.end();return}try{if(url==="/openapi.json"||url==="/openapi.yaml"){res.writeHead(200,{"Content-Type":"application/json"});res.end(JSON.stringify(this.options.openApiSpec));this.log("Served OpenAPI spec");return}const matchResult=this.findRoute(method,url);if(matchResult){const{route,params}=matchResult;if(route.handler){let body;if(["POST","PUT","PATCH"].includes(method)){body=await this.parseBody(req)}const queryString=url.includes("?")?url.split("?")[1]:"";const query={};if(queryString){const searchParams=new URLSearchParams(queryString);searchParams.forEach((value,key)=>{query[key]=value})}const mockReq={url,method,headers:this.normalizeHeaders(req.headers),params,query,body};const mockRes={json:(responseBody,status=200)=>{res.writeHead(status,{"Content-Type":"application/json"});res.end(JSON.stringify(responseBody))},send:(responseBody,status=200,headers={})=>{res.writeHead(status,{"Content-Type":"application/json",...headers});res.end(JSON.stringify(responseBody))}};await route.handler(mockReq,mockRes);this.log(`Matched route with handler: ${method} ${route.path}`);return}if(route.response){const status=route.response.status??200;const headers={"Content-Type":"application/json",...route.response.headers};res.writeHead(status,headers);res.end(JSON.stringify(route.response.body));this.log(`Matched route: ${method} ${route.path} -> ${status}`);return}}res.writeHead(404,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"not_found",message:`No mock for ${method} ${url}`}))}catch(error){this.log(`Error handling request: ${error}`);res.writeHead(500,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"server_error",message:"Internal server error"}))}}findRoute(method,url){const path=url.split("?")[0];for(const route of this.routes){if(route.method!==method)continue;if(route.path===path){return{route,params:{}}}const routeParts=route.path.split("/");const urlParts=path.split("/");if(routeParts.length!==urlParts.length)continue;const params={};let match=true;for(let i=0;i<routeParts.length;i++){if(routeParts[i].startsWith(":")){params[routeParts[i].slice(1)]=urlParts[i]}else if(routeParts[i]!==urlParts[i]){match=false;break}}if(match){return{route,params}}}return void 0}async parseBody(req){const maxBodyBytes=this.options.maxBodyBytes??1*1024*1024;const bodyTimeoutMs=this.options.bodyTimeoutMs??1e4;return new Promise((resolve,reject)=>{let body="";let bodyBytes=0;const onData=chunk=>{bodyBytes+=chunk.length;if(bodyBytes>maxBodyBytes){cleanup();req.destroy();reject(new Error(`Request body too large (>${maxBodyBytes} bytes)`));return}body+=chunk.toString()};const cleanup=()=>{req.off("data",onData);req.off("end",onEnd);req.off("error",onError);clearTimeout(timeout)};const timeout=setTimeout(()=>{cleanup();req.destroy();reject(new Error(`Request body read timed out after ${bodyTimeoutMs}ms`))},bodyTimeoutMs);const onEnd=()=>{cleanup();try{resolve(body?JSON.parse(body):void 0)}catch{resolve(body)}};const onError=error=>{cleanup();reject(error)};req.on("data",onData);req.once("end",onEnd);req.once("error",onError)})}normalizeHeaders(headers){const normalized={};for(const[key,value]of Object.entries(headers)){normalized[key.toLowerCase()]=Array.isArray(value)?value[0]:value}return normalized}log(message){if(this.options.debug){console.log(`[MockAPIServer] ${message}`)}}};var import_http3=require("http");var MockCimdServer=class{options;server=null;_info=null;clients=new Map;customResponses=new Map;constructor(options={}){this.options=options}async start(){if(this.server){throw new Error("Mock CIMD server is already running")}const port=this.options.port??0;return new Promise((resolve,reject)=>{const server=(0,import_http3.createServer)(this.handleRequest.bind(this));this.server=server;server.on("error",err=>{this.log(`Server error: ${err.message}`);reject(err)});server.listen(port,()=>{const address=server.address();if(!address||typeof address==="string"){reject(new Error("Failed to get server address"));return}const actualPort=address.port;this._info={baseUrl:`http://localhost:${actualPort}`,port:actualPort};this.log(`Mock CIMD server started at ${this._info.baseUrl}`);resolve(this._info)})})}async stop(){const server=this.server;if(!server){return}return new Promise((resolve,reject)=>{server.close(err=>{if(err){reject(err)}else{this.server=null;this._info=null;this.log("Mock CIMD server stopped");resolve()}})})}get info(){if(!this._info){throw new Error("Mock CIMD server is not running")}return this._info}registerClient(options){if(!this._info){throw new Error("Mock CIMD server is not running. Call start() first.")}const path=options.path??this.nameToPath(options.name);const fullPath=`/clients/${path}/metadata.json`;const clientId=`${this._info.baseUrl}${fullPath}`;const document={client_id:clientId,client_name:options.name,redirect_uris:options.redirectUris??["http://localhost:3000/callback"],token_endpoint_auth_method:options.tokenEndpointAuthMethod??"none",grant_types:options.grantTypes??["authorization_code","refresh_token"],response_types:options.responseTypes??["code"],...options.clientUri&&{client_uri:options.clientUri},...options.logoUri&&{logo_uri:options.logoUri},...options.jwks&&{jwks:options.jwks},...options.jwksUri&&{jwks_uri:options.jwksUri},...options.scope&&{scope:options.scope},...options.contacts&&{contacts:options.contacts}};this.clients.set(fullPath,{path:fullPath,document});this.log(`Registered client: ${options.name} at ${fullPath}`);return clientId}getClientId(name){if(!this._info){throw new Error("Mock CIMD server is not running")}const path=this.nameToPath(name);const fullPath=`/clients/${path}/metadata.json`;const client=this.clients.get(fullPath);if(!client){throw new Error(`Client "${name}" not found. Register it first.`)}return client.document.client_id}registerInvalidDocument(path,document){if(!this._info){throw new Error("Mock CIMD server is not running")}const fullPath=path.startsWith("/")?path:`/${path}`;this.clients.set(fullPath,{path:fullPath,document});this.log(`Registered invalid document at ${fullPath}`)}registerFetchError(path,statusCode,body){const fullPath=path.startsWith("/")?path:`/${path}`;this.customResponses.set(fullPath,{status:statusCode,body});this.log(`Registered error response at ${fullPath}: ${statusCode}`)}removeClient(name){const path=this.nameToPath(name);const fullPath=`/clients/${path}/metadata.json`;this.clients.delete(fullPath);this.log(`Removed client: ${name}`)}clear(){this.clients.clear();this.customResponses.clear();this.log("Cleared all clients and custom responses")}handleRequest(req,res){const url=req.url??"/";const method=(req.method??"GET").toUpperCase();this.log(`${method} ${url}`);res.setHeader("Access-Control-Allow-Origin","*");res.setHeader("Access-Control-Allow-Methods","GET, OPTIONS");res.setHeader("Access-Control-Allow-Headers","Content-Type, Accept");if(method==="OPTIONS"){res.writeHead(204);res.end();return}if(method!=="GET"){res.writeHead(405,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"method_not_allowed"}));return}const customResponse=this.customResponses.get(url);if(customResponse){res.writeHead(customResponse.status,{"Content-Type":"application/json"});res.end(customResponse.body?JSON.stringify(customResponse.body):"");this.log(`Returned custom error response: ${customResponse.status}`);return}const client=this.clients.get(url);if(client){res.writeHead(200,{"Content-Type":"application/json","Cache-Control":"max-age=3600"});res.end(JSON.stringify(client.document));this.log(`Served client metadata: ${client.document.client_name}`);return}res.writeHead(404,{"Content-Type":"application/json"});res.end(JSON.stringify({error:"not_found",message:`No client at ${url}`}));this.log(`Client not found: ${url}`)}nameToPath(name){return name.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}log(message){if(this.options.debug){console.log(`[MockCimdServer] ${message}`)}}};var import_child_process=require("child_process");var import_net2=require("net");var import_utils6=require("@frontmcp/utils");var TestClientError=class extends Error{constructor(message){super(message);this.name="TestClientError";Object.setPrototypeOf(this,new.target.prototype)}};var ConnectionError=class extends TestClientError{constructor(message,cause){super(message);this.cause=cause;this.name="ConnectionError";Object.setPrototypeOf(this,new.target.prototype)}cause};var TimeoutError=class extends TestClientError{constructor(message,timeoutMs){super(message);this.timeoutMs=timeoutMs;this.name="TimeoutError";Object.setPrototypeOf(this,new.target.prototype)}timeoutMs};var McpProtocolError=class extends TestClientError{constructor(message,code,data){super(message);this.code=code;this.data=data;this.name="McpProtocolError";Object.setPrototypeOf(this,new.target.prototype)}code;data};var ServerStartError=class extends TestClientError{constructor(message,cause){super(message);this.cause=cause;this.name="ServerStartError";Object.setPrototypeOf(this,new.target.prototype)}cause};var AssertionError=class extends TestClientError{constructor(message){super(message);this.name="AssertionError";Object.setPrototypeOf(this,new.target.prototype)}};var ReplayRecordingError=class extends TestClientError{constructor(file,line,reason){super(`${file} line ${line} is not a call record: ${reason}`);this.file=file;this.line=line;this.name="ReplayRecordingError";Object.setPrototypeOf(this,new.target.prototype)}file;line};var import_net=require("net");var E2E_PORT_RANGES={"demo-e2e-public":{start:5e4,size:10},"demo-e2e-cache":{start:50010,size:10},"demo-e2e-config":{start:50020,size:10},"demo-e2e-direct":{start:50030,size:10},"demo-e2e-errors":{start:50040,size:10},"demo-e2e-hooks":{start:50050,size:10},"demo-e2e-multiapp":{start:50060,size:10},"demo-e2e-notifications":{start:50070,size:10},"demo-e2e-providers":{start:50080,size:10},"demo-e2e-standalone":{start:50090,size:10},"demo-e2e-orchestrated":{start:50100,size:10},"demo-e2e-transparent":{start:50110,size:10},"demo-e2e-cimd":{start:50120,size:10},"demo-e2e-remote-proxy":{start:50130,size:10},"demo-e2e-skills":{start:50200,size:10},"demo-e2e-remote":{start:50210,size:10},"demo-e2e-openapi":{start:50220,size:10},"demo-e2e-ui":{start:50230,size:10},"demo-e2e-codecall":{start:50240,size:10},"demo-e2e-remember":{start:50250,size:10},"demo-e2e-elicitation":{start:50260,size:10},"demo-e2e-agents":{start:50270,size:10},"demo-e2e-transport-recreation":{start:50280,size:10},"demo-e2e-jobs":{start:50290,size:10},"demo-e2e-redis":{start:50300,size:10},"demo-e2e-serverless":{start:50310,size:10},"demo-e2e-uipack":{start:50320,size:10},"demo-e2e-agent-adapters":{start:50330,size:10},"demo-e2e-guard":{start:50340,size:10},"demo-e2e-cli-exec":{start:50350,size:10},"demo-e2e-env-awareness":{start:50360,size:10},"demo-e2e-esm":{start:50370,size:10},"demo-e2e-feature-flags":{start:50380,size:10},"demo-e2e-resource-providers":{start:50390,size:10},"esm-package-server":{start:50400,size:10},"esm-package-server-hot-reload":{start:50410,size:10},"esm-package-server-cli":{start:50420,size:10},"demo-e2e-unix-socket":{start:50430,size:10},"demo-e2e-distributed":{start:50440,size:20},"demo-e2e-protocol-20260728":{start:50460,size:20},"demo-e2e-testing":{start:50480,size:20},"demo-e2e-answer-shaping":{start:50500,size:10},"mock-oauth":{start:50900,size:10},"mock-api":{start:50910,size:10},"mock-cimd":{start:50920,size:10},default:{start:51e3,size:100}};var reservedPorts=new Map;var lockedPorts=new Set;var LOCK_MAX_AGE_MS=10*60*1e3;function lockDir(){try{const os=require("node:os");const path=require("node:path");return path.join(os.tmpdir(),"frontmcp-testing-ports")}catch{return void 0}}function pidIsAlive(pid){try{process.kill(pid,0);return true}catch(error){return error.code==="EPERM"}}function acquirePortLock(port){if(lockedPorts.has(port))return false;let fs;let file;try{fs=require("node:fs");const dir=lockDir();if(!dir)return true;fs.mkdirSync(dir,{recursive:true});file=require("node:path").join(dir,`${port}.lock`)}catch{return true}const payload=JSON.stringify({pid:process.pid,at:Date.now()});for(let attempt=0;attempt<2;attempt++){try{fs.writeFileSync(file,payload,{flag:"wx"});lockedPorts.add(port);return true}catch(error){if(error.code!=="EEXIST")return true;try{const held=JSON.parse(fs.readFileSync(file,"utf8"));const fresh=typeof held.at==="number"&&Date.now()-held.at<LOCK_MAX_AGE_MS;if(fresh&&typeof held.pid==="number"&&pidIsAlive(held.pid))return false;fs.unlinkSync(file)}catch{}}}return false}function releasePortLock(port){if(!lockedPorts.delete(port))return;try{const fs=require("node:fs");const dir=lockDir();if(dir)fs.unlinkSync(require("node:path").join(dir,`${port}.lock`))}catch{}}var projectPortIndex=new Map;function getPortRange(project){const key=project;if(key in E2E_PORT_RANGES){return E2E_PORT_RANGES[key]}return E2E_PORT_RANGES.default}async function reservePort(project,preferredPort){const range=getPortRange(project);const handle=port=>({port,releaseSocket:async()=>{await releasePort(port)},release:async()=>{await releasePort(port);releasePortLock(port)}});const wantsSpecificPort=preferredPort!==void 0&&Number.isInteger(preferredPort)&&preferredPort>0;if(wantsSpecificPort){const reservation=await tryReservePort(preferredPort,project);if(reservation){return handle(preferredPort)}console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`)}let index=projectPortIndex.get(project)??0;const maxAttempts=range.size;for(let attempt=0;attempt<maxAttempts;attempt++){const port=range.start+index%range.size;index=(index+1)%range.size;if(reservedPorts.has(port)||lockedPorts.has(port)){continue}const reservation=await tryReservePort(port,project);if(reservation){projectPortIndex.set(project,index);return handle(port)}}for(let port=51e3;port<52e3;port++){if(reservedPorts.has(port)||lockedPorts.has(port))continue;if(await tryReservePort(port,project)){return handle(port)}}throw new Error(`[PortRegistry] Could not reserve a port for ${project}. Range: ${range.start}-${range.start+range.size-1}. Currently reserved: ${Array.from(reservedPorts.keys()).join(", ")}`)}async function tryReservePort(port,project){if(!acquirePortLock(port)){return false}return new Promise(resolve=>{const server=(0,import_net.createServer)();server.once("error",()=>{releasePortLock(port);resolve(false)});server.listen(port,()=>{reservedPorts.set(port,{port,project,holder:server,reservedAt:Date.now()});resolve(true)})})}async function releasePort(port){const reservation=reservedPorts.get(port);if(!reservation){return}return new Promise(resolve=>{reservation.holder.close(()=>{reservedPorts.delete(port);resolve()})})}var SERVER_EXIT_TIMEOUT_MS=12e3;var DEBUG_SERVER=process.env["DEBUG_SERVER"]==="1"||process.env["DEBUG"]==="1";var TestServer=class _TestServer{process=null;options;_info;logs=[];portRelease=null;portReleaseSocket=null;constructor(options,port,portRelease,portReleaseSocket){this.options={port,project:options.project,command:options.command??"",cwd:options.cwd??process.cwd(),env:options.env??{},startupTimeout:options.startupTimeout??3e4,healthCheckPath:options.healthCheckPath??"/health",debug:options.debug??DEBUG_SERVER};this.portRelease=portRelease??null;this.portReleaseSocket=portReleaseSocket??portRelease??null;this._info={baseUrl:`http://localhost:${port}`,port}}static async start(options){const project=options.project??"default";const maxAttempts=3;for(let attempt=1;attempt<=maxAttempts;attempt++){const{port,release,releaseSocket}=await reservePort(project,options.port);const server=new _TestServer(options,port,release,releaseSocket);try{await server.startProcess();return server}catch(error){try{await server.stop()}catch(cleanupError){if(options.debug||DEBUG_SERVER){const msg=cleanupError instanceof Error?cleanupError.message:String(cleanupError);console.warn(`[TestServer] Cleanup failed after startup error: ${msg}`)}}const isEADDRINUSE=error instanceof Error&&(error.message.includes("EADDRINUSE")||server.getLogs().some(l=>l.includes("EADDRINUSE")));if(isEADDRINUSE&&attempt<maxAttempts){const delayMs=attempt*500;if(options.debug||DEBUG_SERVER){console.warn(`[TestServer] EADDRINUSE on port ${port}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts})`)}await sleep2(delayMs);continue}throw error}}throw new Error(`[TestServer] Failed to start after ${maxAttempts} attempts`)}static async startNx(project,options={}){if(!/^[\w-]+$/.test(project)){throw new Error(`Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`)}const maxAttempts=3;for(let attempt=1;attempt<=maxAttempts;attempt++){const{port,release,releaseSocket}=await reservePort(project,options.port);const serverOptions={...options,port,project,command:`npx nx serve ${project} --port ${port}`,cwd:options.cwd??process.cwd()};const server=new _TestServer(serverOptions,port,release,releaseSocket);try{await server.startProcess();return server}catch(error){try{await server.stop()}catch(cleanupError){if(options.debug||DEBUG_SERVER){const msg=cleanupError instanceof Error?cleanupError.message:String(cleanupError);console.warn(`[TestServer] Cleanup failed after startup error: ${msg}`)}}const isEADDRINUSE=error instanceof Error&&(error.message.includes("EADDRINUSE")||server.getLogs().some(l=>l.includes("EADDRINUSE")));if(isEADDRINUSE&&attempt<maxAttempts){const delayMs=attempt*500;if(options.debug||DEBUG_SERVER){console.warn(`[TestServer] EADDRINUSE on port ${port}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts})`)}await sleep2(delayMs);continue}throw error}}throw new Error(`[TestServer] Failed to start after ${maxAttempts} attempts`)}static connect(baseUrl){const url=new URL(baseUrl);const port=parseInt(url.port,10)||(url.protocol==="https:"?443:80);const server=new _TestServer({command:"",port},port);server._info={baseUrl:baseUrl.replace(/\/$/,""),port};return server}get info(){return{...this._info}}async stop(){try{await this.stopProcess()}finally{if(this.portRelease){await this.portRelease();this.portRelease=null}this.portReleaseSocket=null}}async stopProcess(){if(this.process){this.log("Stopping server...");const serverPid=this._info.pid!==this._info.shellPid?this._info.pid:void 0;if(this.process.exitCode!==null||this.process.signalCode!==null){this.log(`Server already exited (code: ${this.process.exitCode}, signal: ${this.process.signalCode})`);this.process=null;if(serverPid!==void 0)signalIfRunning(serverPid,"SIGTERM");await this.waitForServerGone(serverPid);return}const pid=this.process.pid;try{if(pid!==void 0){process.kill(-pid,"SIGTERM")}else{this.process.kill("SIGTERM")}}catch{this.process.kill("SIGTERM")}const exitPromise=new Promise(resolve=>{if(this.process){this.process.once("exit",()=>resolve())}else{resolve()}});const killTimeout=setTimeout(()=>{if(this.process){this.log("Force killing server after timeout...");const killPid=this.process.pid;try{if(killPid!==void 0){process.kill(-killPid,"SIGKILL")}else{this.process.kill("SIGKILL")}}catch{this.process.kill("SIGKILL")}}},5e3);await exitPromise;clearTimeout(killTimeout);this.process=null;await this.waitForServerGone(serverPid);this.log("Server stopped")}}async waitForServerGone(serverPid){const deadline=Date.now()+SERVER_EXIT_TIMEOUT_MS;while(Date.now()<deadline){const serverRunning=serverPid!==void 0&&pidIsAlive(serverPid);if(!serverRunning&&!await acceptsConnections(this._info.port))return;await sleep2(100)}if(serverPid!==void 0&&pidIsAlive(serverPid)){this.log("Force killing server process after timeout...");signalIfRunning(serverPid,"SIGKILL")}}async waitForReady(timeout){const timeoutMs=timeout??this.options.startupTimeout;const deadline=Date.now()+timeoutMs;const checkInterval=100;while(Date.now()<deadline){try{const response=await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`,{method:"GET",signal:AbortSignal.timeout(1e3)});if(response.ok||response.status===404){this.log("Server is ready");return}}catch{}await sleep2(checkInterval)}throw new Error(`Server did not become ready within ${timeoutMs}ms`)}async restart(){await this.stopProcess();await this.startProcess()}getLogs(){return[...this.logs]}clearLogs(){this.logs=[]}async startProcess(){if(!this.options.command){await this.waitForReady();return}this.log(`Starting server: ${this.options.command}`);const env={...process.env,...this.options.env,PORT:String(this.options.port)};const runtimeEnv=withWorkspaceProtocolFallback(env,this.options.cwd);if(this.portReleaseSocket){await this.portReleaseSocket();this.portReleaseSocket=null;await sleep2(300)}this.process=(0,import_child_process.spawn)(this.options.command,[],{cwd:this.options.cwd,env:runtimeEnv,shell:true,detached:true,stdio:["pipe","pipe","pipe"]});this._info.pid=this.process.pid;this._info.shellPid=this.process.pid;let processExited=false;let exitCode=null;let exitError=null;this.process.stdout?.on("data",data=>{const text=data.toString();this.logs.push(text);if(this.options.debug){console.log("[SERVER]",text)}});this.process.stderr?.on("data",data=>{const text=data.toString();this.logs.push(`[ERROR] ${text}`);if(this.options.debug){console.error("[SERVER ERROR]",text)}});this.process.on("error",err=>{this.logs.push(`[SPAWN ERROR] ${err.message}`);exitError=err;if(this.options.debug){console.error("[SERVER SPAWN ERROR]",err)}});this.process.once("exit",code=>{processExited=true;exitCode=code;this.log(`Server process exited with code ${code}`)});try{await this.waitForReadyWithExitDetection(()=>{if(exitError){return{exited:true,error:exitError}}if(processExited){const allLogs=this.logs.join("\n");const errorLogs=this.logs.filter(l=>l.includes("[ERROR]")||l.toLowerCase().includes("error")).join("\n");return{exited:true,error:new ServerStartError(`Server process exited unexpectedly with code ${exitCode}.
79
79
 
80
80
  Command: ${this.options.command}
81
81
  CWD: ${this.options.cwd}
@@ -100,13 +100,13 @@ Last health check error: ${lastHealthCheckError??"none"}
100
100
  === Server Logs ===
101
101
  ${allLogs||"No logs captured"}
102
102
 
103
- TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`)}log(message){if(this.options.debug){console.log(`[TestServer] ${message}`)}}};function sleep2(ms){return new Promise(resolve=>setTimeout(resolve,ms))}function signalIfRunning(pid,signal){try{process.kill(pid,signal)}catch(error){if(error.code!=="ESRCH")throw error}}function acceptsConnections(port){return new Promise(resolve=>{const socket=(0,import_net2.connect)(port,"127.0.0.1");socket.once("connect",()=>{socket.destroy();resolve(true)});socket.once("error",()=>resolve(false))})}function findListeningPid(port){if(process.platform==="win32")return Promise.resolve(void 0);return new Promise(resolve=>{(0,import_child_process.execFile)("lsof",["-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-t"],{timeout:2e3},(error,stdout)=>{if(error){resolve(void 0);return}const pid=parseInt(String(stdout).split("\n")[0]??"",10);resolve(Number.isInteger(pid)&&pid>0?pid:void 0)})})}function withWorkspaceProtocolFallback(env,cwd){if(findInstalledProtocolPackageDir(cwd)){return env}try{const workspacePackageDir=findWorkspaceProtocolDir(cwd);if(!workspacePackageDir){return env}ensureWorkspaceProtocolLink(cwd,workspacePackageDir);if(findInstalledProtocolPackageDir(cwd)){return env}return withProtocolNodePathAlias(env,cwd,workspacePackageDir)}catch(err){if(DEBUG_SERVER){console.error(`[TestServer] Workspace protocol fallback failed: ${err instanceof Error?err.message:String(err)}`)}return env}}function ensureWorkspaceProtocolLink(cwd,workspacePackageDir){const fs=require("node:fs");const path=require("node:path");const nodeModulesDir=findWorkspaceNodeModulesDir(cwd);if(!nodeModulesDir){return}const scopeDir=path.join(nodeModulesDir,"@frontmcp");const aliasPackageDir=path.join(scopeDir,"protocol");if(fs.existsSync(aliasPackageDir)){return}fs.mkdirSync(scopeDir,{recursive:true});try{fs.symlinkSync(workspacePackageDir,aliasPackageDir,process.platform==="win32"?"junction":"dir")}catch(error){const err=error;if(err.code!=="EEXIST"){throw error}}}function withProtocolNodePathAlias(env,cwd,workspacePackageDir){const fs=require("node:fs");const os=require("node:os");const path=require("node:path");const aliasRoot=path.join(os.tmpdir(),"frontmcp-test-node-path",(0,import_utils5.sha256Hex)(cwd).slice(0,12));const scopeDir=path.join(aliasRoot,"@frontmcp");const aliasPackageDir=path.join(scopeDir,"protocol");fs.mkdirSync(scopeDir,{recursive:true});try{if(!fs.existsSync(aliasPackageDir)){fs.symlinkSync(workspacePackageDir,aliasPackageDir,process.platform==="win32"?"junction":"dir")}}catch(error){const err=error;if(err.code!=="EEXIST")throw error}const existingNodePath=env["NODE_PATH"];const nodePathEntries=[aliasRoot,...existingNodePath?existingNodePath.split(path.delimiter):[]].filter(Boolean);return{...env,NODE_PATH:[...new Set(nodePathEntries)].join(path.delimiter)}}function findUp(startDir,testFn){const path=require("node:path");let currentDir=startDir;while(true){const result=testFn(currentDir);if(result!==void 0)return result;const parentDir=path.dirname(currentDir);if(parentDir===currentDir)return void 0;currentDir=parentDir}}function findWorkspaceProtocolDir(startDir){const fs=require("node:fs");const path=require("node:path");return findUp(startDir,dir=>{const candidate=path.join(dir,"libs","protocol");return fs.existsSync(path.join(candidate,"dist","index.js"))?candidate:void 0})}function findInstalledProtocolPackageDir(startDir){const fs=require("node:fs");const path=require("node:path");return findUp(startDir,dir=>{const candidate=path.join(dir,"node_modules","@frontmcp","protocol");return fs.existsSync(path.join(candidate,"package.json"))?candidate:void 0})}function findWorkspaceNodeModulesDir(startDir){const fs=require("node:fs");const path=require("node:path");return findUp(startDir,dir=>{const candidate=path.join(dir,"node_modules");return fs.existsSync(candidate)?candidate:void 0})}var import_lazy_zod=require("@frontmcp/lazy-zod");var import_utils6=require("@frontmcp/utils");var REPLAYED_METHODS=["tools/call","resources/read","prompts/get"];var REDACTED="[REDACTED]";var MCP_ERROR_PREFIX=/^MCP error -?\d+: /;function isRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}function serverErrorCodeOf(result){const meta=isRecord(result)&&result["isError"]===true?result["_meta"]:void 0;if(!isRecord(meta)||typeof meta["errorId"]!=="string")return void 0;return typeof meta["code"]==="string"?meta["code"]:void 0}function textOf(result){const content=isRecord(result)&&Array.isArray(result["content"])?result["content"]:[];return content.map(block=>isRecord(block)?String(block["text"]??""):"").join("\n")}function replayedAnswerOf(response){if(response.error){return{error:{code:response.error.code,message:response.error.message.replace(MCP_ERROR_PREFIX,"")}}}const code=serverErrorCodeOf(response.result);return code===void 0?{answer:response.result}:{error:{code,message:textOf(response.result)}}}function compareCall(recorded,replayed,options={}){const ignore=(options.ignore??[]).map(pattern=>pattern.split("."));if(recorded.error&&"error"in replayed){const changes2=errorChanges(recorded.error,replayed.error).filter(({path})=>!ignore.some(pattern=>matchesPath(pattern,path.split("."))));return{outcome:changes2.length>0?"changed":"same",changes:changes2,error:replayed.error}}if(recorded.error)return{outcome:"gone-error",changes:[],error:recorded.error};if("error"in replayed)return{outcome:"new-error",changes:[],error:replayed.error};const changes=[];collectChanges(recorded.answer,replayed.answer,[],ignore,changes);return{outcome:changes.length>0?"changed":"same",changes}}function errorChanges(recorded,replayed){if(recorded.code!==replayed.code)return[{path:"error.code",recorded:recorded.code,replayed:replayed.code}];if(recorded.message===void 0||matchesRedacted(recorded.message,replayed.message??""))return[];return[{path:"error.message",recorded:recorded.message,replayed:replayed.message}]}function matchesRedacted(recorded,replayed){const[first,...rest]=recorded.split(REDACTED);const last=rest.pop();if(last===void 0)return recorded===replayed;const end=replayed.length-last.length;if(end<first.length||!replayed.startsWith(first)||!replayed.endsWith(last))return false;let at=first.length;for(const part of rest){const found=replayed.indexOf(part,at);if(found===-1||found+part.length>end)return false;at=found+part.length}return true}function parsedJsonText(text){const start=text.trimStart()[0];if(start!=="{"&&start!=="[")return void 0;try{return JSON.parse(text)}catch{return void 0}}function matchesPath(pattern,path){if(pattern.length===0)return path.length===0;const[segment,...rest]=pattern;if(segment==="**")return path.some((_,index)=>matchesPath(rest,path.slice(index)))||matchesPath(rest,[]);return path.length>0&&(segment==="*"||segment===path[0])&&matchesPath(rest,path.slice(1))}function collectChanges(recorded,replayed,path,ignore,changes){if(recorded===REDACTED||ignore.some(pattern=>matchesPath(pattern,path)))return;if(Array.isArray(recorded)&&Array.isArray(replayed)){const length=Math.max(recorded.length,replayed.length);for(let index=0;index<length;index++){collectChanges(recorded[index],replayed[index],[...path,String(index)],ignore,changes)}return}if(isRecord(recorded)&&isRecord(replayed)){for(const key of new Set([...Object.keys(recorded),...Object.keys(replayed)])){collectChanges(recorded[key],replayed[key],[...path,key],ignore,changes)}return}if(Object.is(recorded,replayed))return;if(typeof recorded==="string"&&typeof replayed==="string"){const[recordedJson,replayedJson]=[parsedJsonText(recorded),parsedJsonText(replayed)];if(recordedJson!==void 0&&replayedJson!==void 0){collectChanges(recordedJson,replayedJson,path,ignore,changes);return}if(matchesRedacted(recorded,replayed))return}changes.push({path:path.join("."),recorded,replayed})}var recordedCallSchema=import_lazy_zod.z.object({v:import_lazy_zod.z.literal(1,{message:"is not a recording version this replay reads"}),method:import_lazy_zod.z.enum(REPLAYED_METHODS,{message:`must be one of ${REPLAYED_METHODS.join(", ")}`}),name:import_lazy_zod.z.string(),input:import_lazy_zod.z.record(import_lazy_zod.z.string(),import_lazy_zod.z.unknown()).optional(),answer:import_lazy_zod.z.unknown().optional(),error:import_lazy_zod.z.object({code:import_lazy_zod.z.union([import_lazy_zod.z.string(),import_lazy_zod.z.number()]),message:import_lazy_zod.z.string().optional()}).optional(),session:import_lazy_zod.z.string().optional(),client:import_lazy_zod.z.object({name:import_lazy_zod.z.string(),version:import_lazy_zod.z.string()}).optional()});var OUTCOME_LABELS={same:"same",changed:"changed","new-error":"new error","gone-error":"gone error"};function parseCall(text,file,line){let value;try{value=JSON.parse(text)}catch{throw new ReplayRecordingError(file,line,"it is not JSON")}const parsed=recordedCallSchema.safeParse(value);if(parsed.success)return parsed.data;const problems=parsed.error.issues.map(issue=>`${issue.path.join(".")}: ${issue.message}`);throw new ReplayRecordingError(file,line,problems.join("; "))}async function readRecording(file){const lines=(await(0,import_utils6.readFile)(file)).split(/\r?\n/);return lines.flatMap((text,index)=>text.trim()===""?[]:[{line:index+1,call:parseCall(text,file,index+1)}])}function paramsOf(call){if(call.method==="resources/read")return{uri:call.name};return{name:call.name,...call.input&&{arguments:call.input}}}async function replaySession(file,server,options={}){const recording=await readRecording(file);const baseUrl=typeof server==="string"?server:server.info.baseUrl;const clients=new Map;const calls=[];try{for(const{line,call}of recording){const session=call.session??"";let client=clients.get(session);if(!client){const clientInfo=call.client??options.client?.clientInfo;client=await McpTestClient.create({...options.client,baseUrl,...clientInfo&&{clientInfo}}).buildAndConnect();clients.set(session,client)}const response=await client.raw.request({jsonrpc:"2.0",id:line,method:call.method,params:paramsOf(call)});calls.push({line,method:call.method,name:call.name,...compareCall(call,replayedAnswerOf(response),options)})}}finally{await Promise.allSettled([...clients.values()].map(client=>client.disconnect()))}return{file,calls,differences:calls.filter(({outcome})=>outcome!=="same")}}function preview(value){if(value===void 0)return"(none)";const json=JSON.stringify(value);return json.length>80?`${json.slice(0,77)}...`:json}function formatReplayReport({file,calls,differences}){if(differences.length===0)return`Replayed ${calls.length} calls from ${file}: every call answered as recorded`;const lines=[`Replayed ${calls.length} calls from ${file}: ${differences.length} differ`];for(const call of differences){lines.push(` line ${call.line} ${call.method} ${call.name}: ${OUTCOME_LABELS[call.outcome]}`);if(call.error){const{code,message}=call.error;lines.push(message===void 0?` ${code}`:` ${code}: ${message}`)}for(const change of call.changes){lines.push(` ${change.path}: ${preview(change.recorded)} -> ${preview(change.replayed)}`)}}return lines.join("\n")}var McpAssertions={assertSuccess(response,message){if(!response.success){const errorMsg=response.error?.message??"Unknown error";throw new Error(message??`Expected success but got error: ${errorMsg} (code: ${response.error?.code})`)}if(response.data===void 0){throw new Error(message??"Expected data but got undefined")}return response.data},assertError(response,expectedCode){if(response.success){throw new Error("Expected error but got success")}if(!response.error){throw new Error("Expected error info but got undefined")}if(expectedCode!==void 0&&response.error.code!==expectedCode){throw new Error(`Expected error code ${expectedCode} but got ${response.error.code}: ${response.error.message}`)}return response.error},assertToolSuccess(result){if("raw"in result){if(result.isError){throw new Error(`Tool call failed: ${result.error?.message??"Unknown error"}`)}}else{if(!result.success){throw new Error(`Tool call failed: ${result.error?.message??"Unknown error"}`)}if(result.data?.isError){throw new Error("Tool returned isError=true")}}},assertToolContent(result,type){let content;if("raw"in result){content=result.raw.content}else{if(!result.success||!result.data){throw new Error("Tool call was not successful")}content=result.data.content}const hasContent=content?.some(c=>c.type===type);if(!hasContent){throw new Error(`Expected tool result to have ${type} content`)}},assertTextResource(response){if("raw"in response){if(response.isError){throw new Error(`Resource read failed: ${response.error?.message??"Unknown error"}`)}const text=response.text();if(text===void 0){throw new Error("Expected text content but got undefined")}return text}else{if(!response.success||!response.data){throw new Error(`Resource read failed: ${response.error?.message??"Unknown error"}`)}const content=response.data.contents?.[0];if(!content||!("text"in content)){throw new Error("Expected text content but got undefined")}return content.text}},assertContainsTool(tools,name){const tool=tools.find(t=>t.name===name);if(!tool){const available=tools.map(t=>t.name).join(", ");throw new Error(`Expected to find tool "${name}" but got: [${available}]`)}return tool},assertContainsResource(resources,uri){const resource=resources.find(r=>r.uri===uri);if(!resource){const available=resources.map(r=>r.uri).join(", ");throw new Error(`Expected to find resource "${uri}" but got: [${available}]`)}return resource},assertContainsResourceTemplate(templates,uriTemplate){const template=templates.find(t=>t.uriTemplate===uriTemplate);if(!template){const available=templates.map(t=>t.uriTemplate).join(", ");throw new Error(`Expected to find resource template "${uriTemplate}" but got: [${available}]`)}return template},assertContainsPrompt(prompts,name){const prompt=prompts.find(p=>p.name===name);if(!prompt){const available=prompts.map(p=>p.name).join(", ");throw new Error(`Expected to find prompt "${name}" but got: [${available}]`)}return prompt}};function containsTool(tools,name){return tools.some(t=>t.name===name)}function containsResource(resources,uri){return resources.some(r=>r.uri===uri)}function containsResourceTemplate(templates,uriTemplate){return templates.some(t=>t.uriTemplate===uriTemplate)}function containsPrompt(prompts,name){return prompts.some(p=>p.name===name)}function isSuccessful(result){return result.isSuccess}function isError(result,expectedCode){if(!result.isError)return false;if(expectedCode!==void 0){return result.error?.code===expectedCode}return true}function hasTextContent(result){return result.hasTextContent()}function hasMimeType(result,mimeType){return result.hasMimeType(mimeType)}var import_utils7=require("@frontmcp/utils");function gatewayTokenBinding(serverUrl,entryPath){const base=(0,import_utils7.trimTrailing)(serverUrl,"/");const prefix=(0,import_utils7.trimSlashes)(entryPath??"");const mcpUrl=prefix?`${base}/${prefix}`:base;return{issuer:mcpUrl,audience:mcpUrl}}var jestApi;function jestGlobals(){if(jestApi)return jestApi;try{const api=require("@jest/globals");jestApi={describe:api["describe"],beforeAll:api["beforeAll"],beforeEach:api["beforeEach"],afterEach:api["afterEach"],afterAll:api["afterAll"],it:api["it"]};return jestApi}catch(error){throw new Error(`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error?error.message:String(error)}`,{cause:error})}}function newScope(parent){return{skipped:parent?.skipped??false,reason:parent?.reason,config:{},envs:[],configured:false,cleanupRegistered:false,beforeEachHooks:[],afterEachHooks:[]}}var scopes=[newScope()];function currentScope(){return scopes[scopes.length-1]}var serverEntries=new Map;function resolveConfig(chain){const merged={};for(const scope of chain){Object.assign(merged,scope.config);for(const env of scope.envs)merged.env={...merged.env,...env}}return merged}function ownerScopeOf(chain){for(let i=chain.length-1;i>0;i--){if(chain[i].configured)return chain[i]}return chain[0]}function serverKey(config){const env=Object.fromEntries(Object.entries(config.env??{}).sort(([a],[b])=>a.localeCompare(b)));return JSON.stringify({server:config.server,baseUrl:config.baseUrl,project:config.project,port:config.port,entryPath:config.entryPath,startupTimeout:config.startupTimeout,logLevel:config.logLevel,auth:config.auth,env})}function serverEnv(config){const extra={};if(config.logLevel)extra["FRONTMCP_LOG_LEVEL"]=config.logLevel;if(config.auth?.mode)extra["FRONTMCP_TEST_AUTH_MODE"]=config.auth.mode;if(config.auth?.type)extra["FRONTMCP_TEST_AUTH_TYPE"]=config.auth.type;if(Object.keys(extra).length===0&&!config.env)return void 0;return{...extra,...config.env}}async function startServerEntry(config,owner){let server;let startedByUs=false;if(config.baseUrl&&!config.server){server=TestServer.connect(config.baseUrl)}else if(config.server){const serverCommand=resolveServerCommand(config.server);const isDebug=config.logLevel==="debug"||process.env["DEBUG"]==="1"||process.env["DEBUG_SERVER"]==="1";if(isDebug){console.log(`[TestFixture] Starting server: ${serverCommand}`)}try{server=await TestServer.start({project:config.project,port:config.port,command:serverCommand,env:serverEnv(config),startupTimeout:config.startupTimeout??3e4,debug:isDebug});startedByUs=true;if(isDebug){console.log(`[TestFixture] Server started at ${server.info.baseUrl}`)}}catch(error){const errMsg=error instanceof Error?error.message:String(error);throw new Error(`Failed to start test server.
103
+ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`)}log(message){if(this.options.debug){console.log(`[TestServer] ${message}`)}}};function sleep2(ms){return new Promise(resolve=>setTimeout(resolve,ms))}function signalIfRunning(pid,signal){try{process.kill(pid,signal)}catch(error){if(error.code!=="ESRCH")throw error}}function acceptsConnections(port){return new Promise(resolve=>{const socket=(0,import_net2.connect)(port,"127.0.0.1");socket.once("connect",()=>{socket.destroy();resolve(true)});socket.once("error",()=>resolve(false))})}function findListeningPid(port){if(process.platform==="win32")return Promise.resolve(void 0);return new Promise(resolve=>{(0,import_child_process.execFile)("lsof",["-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-t"],{timeout:2e3},(error,stdout)=>{if(error){resolve(void 0);return}const pid=parseInt(String(stdout).split("\n")[0]??"",10);resolve(Number.isInteger(pid)&&pid>0?pid:void 0)})})}function withWorkspaceProtocolFallback(env,cwd){if(findInstalledProtocolPackageDir(cwd)){return env}try{const workspacePackageDir=findWorkspaceProtocolDir(cwd);if(!workspacePackageDir){return env}ensureWorkspaceProtocolLink(cwd,workspacePackageDir);if(findInstalledProtocolPackageDir(cwd)){return env}return withProtocolNodePathAlias(env,cwd,workspacePackageDir)}catch(err){if(DEBUG_SERVER){console.error(`[TestServer] Workspace protocol fallback failed: ${err instanceof Error?err.message:String(err)}`)}return env}}function ensureWorkspaceProtocolLink(cwd,workspacePackageDir){const fs=require("node:fs");const path=require("node:path");const nodeModulesDir=findWorkspaceNodeModulesDir(cwd);if(!nodeModulesDir){return}const scopeDir=path.join(nodeModulesDir,"@frontmcp");const aliasPackageDir=path.join(scopeDir,"protocol");if(fs.existsSync(aliasPackageDir)){return}fs.mkdirSync(scopeDir,{recursive:true});try{fs.symlinkSync(workspacePackageDir,aliasPackageDir,process.platform==="win32"?"junction":"dir")}catch(error){const err=error;if(err.code!=="EEXIST"){throw error}}}function withProtocolNodePathAlias(env,cwd,workspacePackageDir){const fs=require("node:fs");const os=require("node:os");const path=require("node:path");const aliasRoot=path.join(os.tmpdir(),"frontmcp-test-node-path",(0,import_utils6.sha256Hex)(cwd).slice(0,12));const scopeDir=path.join(aliasRoot,"@frontmcp");const aliasPackageDir=path.join(scopeDir,"protocol");fs.mkdirSync(scopeDir,{recursive:true});try{if(!fs.existsSync(aliasPackageDir)){fs.symlinkSync(workspacePackageDir,aliasPackageDir,process.platform==="win32"?"junction":"dir")}}catch(error){const err=error;if(err.code!=="EEXIST")throw error}const existingNodePath=env["NODE_PATH"];const nodePathEntries=[aliasRoot,...existingNodePath?existingNodePath.split(path.delimiter):[]].filter(Boolean);return{...env,NODE_PATH:[...new Set(nodePathEntries)].join(path.delimiter)}}function findUp(startDir,testFn){const path=require("node:path");let currentDir=startDir;while(true){const result=testFn(currentDir);if(result!==void 0)return result;const parentDir=path.dirname(currentDir);if(parentDir===currentDir)return void 0;currentDir=parentDir}}function findWorkspaceProtocolDir(startDir){const fs=require("node:fs");const path=require("node:path");return findUp(startDir,dir=>{const candidate=path.join(dir,"libs","protocol");return fs.existsSync(path.join(candidate,"dist","index.js"))?candidate:void 0})}function findInstalledProtocolPackageDir(startDir){const fs=require("node:fs");const path=require("node:path");return findUp(startDir,dir=>{const candidate=path.join(dir,"node_modules","@frontmcp","protocol");return fs.existsSync(path.join(candidate,"package.json"))?candidate:void 0})}function findWorkspaceNodeModulesDir(startDir){const fs=require("node:fs");const path=require("node:path");return findUp(startDir,dir=>{const candidate=path.join(dir,"node_modules");return fs.existsSync(candidate)?candidate:void 0})}var import_lazy_zod=require("@frontmcp/lazy-zod");var import_utils7=require("@frontmcp/utils");var REPLAYED_METHODS=["tools/call","resources/read","prompts/get"];var REDACTED="[REDACTED]";var MCP_ERROR_PREFIX=/^MCP error -?\d+: /;function isRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}function serverErrorCodeOf(result){const meta=isRecord(result)&&result["isError"]===true?result["_meta"]:void 0;if(!isRecord(meta)||typeof meta["errorId"]!=="string")return void 0;return typeof meta["code"]==="string"?meta["code"]:void 0}function textOf(result){const content=isRecord(result)&&Array.isArray(result["content"])?result["content"]:[];return content.map(block=>isRecord(block)?String(block["text"]??""):"").join("\n")}function replayedAnswerOf(response){if(response.error){return{error:{code:response.error.code,message:response.error.message.replace(MCP_ERROR_PREFIX,"")}}}const code=serverErrorCodeOf(response.result);return code===void 0?{answer:response.result}:{error:{code,message:textOf(response.result)}}}function compareCall(recorded,replayed,options={}){const ignore=(options.ignore??[]).map(pattern=>pattern.split("."));if(recorded.error&&"error"in replayed){const changes2=errorChanges(recorded.error,replayed.error).filter(({path})=>!ignore.some(pattern=>matchesPath(pattern,path.split("."))));return{outcome:changes2.length>0?"changed":"same",changes:changes2,error:replayed.error}}if(recorded.error)return{outcome:"gone-error",changes:[],error:recorded.error};if("error"in replayed)return{outcome:"new-error",changes:[],error:replayed.error};const changes=[];collectChanges(recorded.answer,replayed.answer,[],ignore,changes);return{outcome:changes.length>0?"changed":"same",changes}}function errorChanges(recorded,replayed){if(recorded.code!==replayed.code)return[{path:"error.code",recorded:recorded.code,replayed:replayed.code}];if(recorded.message===void 0||matchesRedacted(recorded.message,replayed.message??""))return[];return[{path:"error.message",recorded:recorded.message,replayed:replayed.message}]}function matchesRedacted(recorded,replayed){const[first,...rest]=recorded.split(REDACTED);const last=rest.pop();if(last===void 0)return recorded===replayed;const end=replayed.length-last.length;if(end<first.length||!replayed.startsWith(first)||!replayed.endsWith(last))return false;let at=first.length;for(const part of rest){const found=replayed.indexOf(part,at);if(found===-1||found+part.length>end)return false;at=found+part.length}return true}function parsedJsonText(text){const start=text.trimStart()[0];if(start!=="{"&&start!=="[")return void 0;try{return JSON.parse(text)}catch{return void 0}}function matchesPath(pattern,path){if(pattern.length===0)return path.length===0;const[segment,...rest]=pattern;if(segment==="**")return path.some((_,index)=>matchesPath(rest,path.slice(index)))||matchesPath(rest,[]);return path.length>0&&(segment==="*"||segment===path[0])&&matchesPath(rest,path.slice(1))}function collectChanges(recorded,replayed,path,ignore,changes){if(recorded===REDACTED||ignore.some(pattern=>matchesPath(pattern,path)))return;if(Array.isArray(recorded)&&Array.isArray(replayed)){const length=Math.max(recorded.length,replayed.length);for(let index=0;index<length;index++){collectChanges(recorded[index],replayed[index],[...path,String(index)],ignore,changes)}return}if(isRecord(recorded)&&isRecord(replayed)){for(const key of new Set([...Object.keys(recorded),...Object.keys(replayed)])){collectChanges(recorded[key],replayed[key],[...path,key],ignore,changes)}return}if(Object.is(recorded,replayed))return;if(typeof recorded==="string"&&typeof replayed==="string"){const[recordedJson,replayedJson]=[parsedJsonText(recorded),parsedJsonText(replayed)];if(recordedJson!==void 0&&replayedJson!==void 0){collectChanges(recordedJson,replayedJson,path,ignore,changes);return}if(matchesRedacted(recorded,replayed))return}changes.push({path:path.join("."),recorded,replayed})}var recordedCallSchema=import_lazy_zod.z.object({v:import_lazy_zod.z.literal(1,{message:"is not a recording version this replay reads"}),method:import_lazy_zod.z.enum(REPLAYED_METHODS,{message:`must be one of ${REPLAYED_METHODS.join(", ")}`}),name:import_lazy_zod.z.string(),input:import_lazy_zod.z.record(import_lazy_zod.z.string(),import_lazy_zod.z.unknown()).optional(),answer:import_lazy_zod.z.unknown().optional(),error:import_lazy_zod.z.object({code:import_lazy_zod.z.union([import_lazy_zod.z.string(),import_lazy_zod.z.number()]),message:import_lazy_zod.z.string().optional()}).optional(),session:import_lazy_zod.z.string().optional(),client:import_lazy_zod.z.object({name:import_lazy_zod.z.string(),version:import_lazy_zod.z.string()}).optional()});var OUTCOME_LABELS={same:"same",changed:"changed","new-error":"new error","gone-error":"gone error"};function parseCall(text,file,line){let value;try{value=JSON.parse(text)}catch{throw new ReplayRecordingError(file,line,"it is not JSON")}const parsed=recordedCallSchema.safeParse(value);if(parsed.success)return parsed.data;const problems=parsed.error.issues.map(issue=>`${issue.path.join(".")}: ${issue.message}`);throw new ReplayRecordingError(file,line,problems.join("; "))}async function readRecording(file){const lines=(await(0,import_utils7.readFile)(file)).split(/\r?\n/);return lines.flatMap((text,index)=>text.trim()===""?[]:[{line:index+1,call:parseCall(text,file,index+1)}])}function paramsOf(call){if(call.method==="resources/read")return{uri:call.name};return{name:call.name,...call.input&&{arguments:call.input}}}async function replaySession(file,server,options={}){const recording=await readRecording(file);const baseUrl=typeof server==="string"?server:server.info.baseUrl;const clients=new Map;const calls=[];try{for(const{line,call}of recording){const session=call.session??"";let client=clients.get(session);if(!client){const clientInfo=call.client??options.client?.clientInfo;client=await McpTestClient.create({...options.client,baseUrl,...clientInfo&&{clientInfo}}).buildAndConnect();clients.set(session,client)}const response=await client.raw.request({jsonrpc:"2.0",id:line,method:call.method,params:paramsOf(call)});calls.push({line,method:call.method,name:call.name,...compareCall(call,replayedAnswerOf(response),options)})}}finally{await Promise.allSettled([...clients.values()].map(client=>client.disconnect()))}return{file,calls,differences:calls.filter(({outcome})=>outcome!=="same")}}function preview(value){if(value===void 0)return"(none)";const json=JSON.stringify(value);return json.length>80?`${json.slice(0,77)}...`:json}function formatReplayReport({file,calls,differences}){if(differences.length===0)return`Replayed ${calls.length} calls from ${file}: every call answered as recorded`;const lines=[`Replayed ${calls.length} calls from ${file}: ${differences.length} differ`];for(const call of differences){lines.push(` line ${call.line} ${call.method} ${call.name}: ${OUTCOME_LABELS[call.outcome]}`);if(call.error){const{code,message}=call.error;lines.push(message===void 0?` ${code}`:` ${code}: ${message}`)}for(const change of call.changes){lines.push(` ${change.path}: ${preview(change.recorded)} -> ${preview(change.replayed)}`)}}return lines.join("\n")}var McpAssertions={assertSuccess(response,message){if(!response.success){const errorMsg=response.error?.message??"Unknown error";throw new Error(message??`Expected success but got error: ${errorMsg} (code: ${response.error?.code})`)}if(response.data===void 0){throw new Error(message??"Expected data but got undefined")}return response.data},assertError(response,expectedCode){if(response.success){throw new Error("Expected error but got success")}if(!response.error){throw new Error("Expected error info but got undefined")}if(expectedCode!==void 0&&response.error.code!==expectedCode){throw new Error(`Expected error code ${expectedCode} but got ${response.error.code}: ${response.error.message}`)}return response.error},assertToolSuccess(result){if("raw"in result){if(result.isError){throw new Error(`Tool call failed: ${result.error?.message??"Unknown error"}`)}}else{if(!result.success){throw new Error(`Tool call failed: ${result.error?.message??"Unknown error"}`)}if(result.data?.isError){throw new Error("Tool returned isError=true")}}},assertToolContent(result,type){let content;if("raw"in result){content=result.raw.content}else{if(!result.success||!result.data){throw new Error("Tool call was not successful")}content=result.data.content}const hasContent=content?.some(c=>c.type===type);if(!hasContent){throw new Error(`Expected tool result to have ${type} content`)}},assertTextResource(response){if("raw"in response){if(response.isError){throw new Error(`Resource read failed: ${response.error?.message??"Unknown error"}`)}const text=response.text();if(text===void 0){throw new Error("Expected text content but got undefined")}return text}else{if(!response.success||!response.data){throw new Error(`Resource read failed: ${response.error?.message??"Unknown error"}`)}const content=response.data.contents?.[0];if(!content||!("text"in content)){throw new Error("Expected text content but got undefined")}return content.text}},assertContainsTool(tools,name){const tool=tools.find(t=>t.name===name);if(!tool){const available=tools.map(t=>t.name).join(", ");throw new Error(`Expected to find tool "${name}" but got: [${available}]`)}return tool},assertContainsResource(resources,uri){const resource=resources.find(r=>r.uri===uri);if(!resource){const available=resources.map(r=>r.uri).join(", ");throw new Error(`Expected to find resource "${uri}" but got: [${available}]`)}return resource},assertContainsResourceTemplate(templates,uriTemplate){const template=templates.find(t=>t.uriTemplate===uriTemplate);if(!template){const available=templates.map(t=>t.uriTemplate).join(", ");throw new Error(`Expected to find resource template "${uriTemplate}" but got: [${available}]`)}return template},assertContainsPrompt(prompts,name){const prompt=prompts.find(p=>p.name===name);if(!prompt){const available=prompts.map(p=>p.name).join(", ");throw new Error(`Expected to find prompt "${name}" but got: [${available}]`)}return prompt}};function containsTool(tools,name){return tools.some(t=>t.name===name)}function containsResource(resources,uri){return resources.some(r=>r.uri===uri)}function containsResourceTemplate(templates,uriTemplate){return templates.some(t=>t.uriTemplate===uriTemplate)}function containsPrompt(prompts,name){return prompts.some(p=>p.name===name)}function isSuccessful(result){return result.isSuccess}function isError(result,expectedCode){if(!result.isError)return false;if(expectedCode!==void 0){return result.error?.code===expectedCode}return true}function hasTextContent(result){return result.hasTextContent()}function hasMimeType(result,mimeType){return result.hasMimeType(mimeType)}var import_utils8=require("@frontmcp/utils");function gatewayTokenBinding(serverUrl,entryPath){const base=(0,import_utils8.trimTrailing)(serverUrl,"/");const prefix=(0,import_utils8.trimSlashes)(entryPath??"");const mcpUrl=prefix?`${base}/${prefix}`:base;return{issuer:mcpUrl,audience:mcpUrl}}var jestApi;function jestGlobals(){if(jestApi)return jestApi;try{const api=require("@jest/globals");jestApi={describe:api["describe"],beforeAll:api["beforeAll"],beforeEach:api["beforeEach"],afterEach:api["afterEach"],afterAll:api["afterAll"],it:api["it"]};return jestApi}catch(error){throw new Error(`@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error?error.message:String(error)}`,{cause:error})}}function newScope(parent){return{skipped:parent?.skipped??false,reason:parent?.reason,config:{},envs:[],configured:false,cleanupRegistered:false,beforeEachHooks:[],afterEachHooks:[]}}var scopes=[newScope()];function currentScope(){return scopes[scopes.length-1]}var serverEntries=new Map;function resolveConfig(chain){const merged={};for(const scope of chain){Object.assign(merged,scope.config);for(const env of scope.envs)merged.env={...merged.env,...env}}return merged}function ownerScopeOf(chain){for(let i=chain.length-1;i>0;i--){if(chain[i].configured)return chain[i]}return chain[0]}function serverKey(config){const env=Object.fromEntries(Object.entries(config.env??{}).sort(([a],[b])=>a.localeCompare(b)));return JSON.stringify({server:config.server,baseUrl:config.baseUrl,project:config.project,port:config.port,entryPath:config.entryPath,startupTimeout:config.startupTimeout,logLevel:config.logLevel,auth:config.auth,env})}function serverEnv(config){const extra={};if(config.logLevel)extra["FRONTMCP_LOG_LEVEL"]=config.logLevel;if(config.auth?.mode)extra["FRONTMCP_TEST_AUTH_MODE"]=config.auth.mode;if(config.auth?.type)extra["FRONTMCP_TEST_AUTH_TYPE"]=config.auth.type;if(Object.keys(extra).length===0&&!config.env)return void 0;return{...extra,...config.env}}async function startServerEntry(config,owner){let server;let startedByUs=false;if(config.baseUrl&&!config.server){server=TestServer.connect(config.baseUrl)}else if(config.server){const serverCommand=resolveServerCommand(config.server);const isDebug=config.logLevel==="debug"||process.env["DEBUG"]==="1"||process.env["DEBUG_SERVER"]==="1";if(isDebug){console.log(`[TestFixture] Starting server: ${serverCommand}`)}try{server=await TestServer.start({project:config.project,port:config.port,command:serverCommand,env:serverEnv(config),startupTimeout:config.startupTimeout??3e4,debug:isDebug});startedByUs=true;if(isDebug){console.log(`[TestFixture] Server started at ${server.info.baseUrl}`)}}catch(error){const errMsg=error instanceof Error?error.message:String(error);throw new Error(`Failed to start test server.
104
104
 
105
105
  Server entry: ${config.server}
106
106
  Project: ${config.project??"default"}
107
107
  Command: ${serverCommand}
108
108
 
109
- Error: ${errMsg}`,{cause:error})}}else{throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option')}const sharedSecret=config.env?.["JWT_SECRET"];const binding=sharedSecret?gatewayTokenBinding(config.baseUrl??server.info.baseUrl,config.entryPath):void 0;const tokenFactory2=new TestTokenFactory(sharedSecret?{hmacSecret:sharedSecret,...binding}:{});return{server,startedByUs,tokenFactory:tokenFactory2,owner}}function acquireServer(chain){const config=resolveConfig(chain);const key=serverKey(config);let entry=serverEntries.get(key);if(!entry){entry=startServerEntry(config,ownerScopeOf(chain));serverEntries.set(key,entry);entry.catch(()=>serverEntries.delete(key))}return entry}async function stopServers(owner){for(const[key,pending]of Array.from(serverEntries)){let entry;try{entry=await pending}catch{serverEntries.delete(key);continue}if(owner&&entry.owner!==owner)continue;serverEntries.delete(key);if(entry.startedByUs){await entry.server.stop()}}}var fixtureContexts=new WeakMap;async function buildFixtures(chain){const config=resolveConfig(chain);const entry=await acquireServer(chain);const{server,tokenFactory:tokenFactory2}=entry;const publicMode=config.publicMode??config.auth?.mode==="public";const clientConfig={...config,publicMode};const clientInstance=McpTestClient.create({baseUrl:resolveClientBaseUrl(server,clientConfig),entryPath:clientConfig.entryPath,transport:clientConfig.transport??"streamable-http",publicMode}).build();try{await clientInstance.connect()}catch(err){const message=err instanceof Error?err.message:String(err);if(/\b401\b|unauthor/i.test(message)){console.warn(`[TestFixture] MCP client could not connect anonymously \u2014 the server requires authentication (${message}). Tests using only the 'server'/'auth' fixtures are unaffected; tests using the 'mcp' fixture must supply a token.`)}else{throw err}}const context={extraClients:[]};const fixtures={mcp:clientInstance,auth:createAuthFixture(tokenFactory2),server:createServerFixture(server,clientConfig,clientInstance,context)};fixtureContexts.set(fixtures,context);return fixtures}async function cleanupTestFixtures(fixtures,testFailed=false){if(testFailed){const logs=fixtures.server.getLogs();if(logs.length>0){console.error("\n[TestFixture] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[TestFixture] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[TestFixture] === End Server Logs ===\n")}}const extras=fixtureContexts.get(fixtures)?.extraClients??[];for(const client of extras){if(client.isConnected()){await client.disconnect().catch(()=>void 0)}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}function resolveClientBaseUrl(server,config){return config.baseUrl??server.info.baseUrl}function createAuthFixture(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture(server,config,mcp,context){return{get info(){return server.info},createClient:async opts=>{const client=await McpTestClient.create({baseUrl:resolveClientBaseUrl(server,config),entryPath:opts?.entryPath??config.entryPath,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:config.publicMode}).buildAndConnect();context.extraClients.push(client);return client},createClientBuilder:()=>{const builder=new McpTestClientBuilder({baseUrl:resolveClientBaseUrl(server,config),entryPath:config.entryPath,publicMode:config.publicMode});return builder},restart:async()=>{const reconnect=[mcp,...context.extraClients].filter(client=>client.isConnected());await server.restart();for(const client of reconnect){await client.reconnect()}},getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function runWithFixtures(fn,chain){return async(...extra)=>{const fixtures=await buildFixtures(chain);let failure;try{for(const scope of chain){for(const hook of scope.beforeEachHooks)await hook(fixtures)}await fn(fixtures,...extra)}catch(error){failure={error}}try{await runAfterEachHooks(chain,fixtures)}catch(error){failure??={error}}await cleanupTestFixtures(fixtures,failure!==void 0);if(failure)throw failure.error}}async function runAfterEachHooks(chain,fixtures){let firstError;let failed=false;for(let i=chain.length-1;i>=0;i--){for(const hook of chain[i].afterEachHooks){try{await hook(fixtures)}catch(error){if(!failed)firstError=error;failed=true}}}if(failed)throw firstError}function skipTitle(name,scope){return scope.reason?`${name} (skipped: ${scope.reason})`:name}function testWithFixtures(name,fn){const chain=scopes.slice();const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,chain));return}jestGlobals().it(name,runWithFixtures(fn,chain))}function use(config){const scope=currentScope();const{env,...rest}=config;scope.config={...scope.config,...rest};if(env)scope.envs.push(env);scope.configured=true;if(!scope.cleanupRegistered){scope.cleanupRegistered=true;jestGlobals().afterAll(async()=>{await stopServers(scope===scopes[0]?void 0:scope)})}}function skip(nameOrCondition,fnOrReason){if(typeof nameOrCondition==="boolean"){if(nameOrCondition){const scope=currentScope();scope.skipped=true;scope.reason=typeof fnOrReason==="string"?fnOrReason:void 0}return}if(typeof fnOrReason!=="function"){throw new TypeError(`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`)}jestGlobals().it.skip(nameOrCondition,runWithFixtures(fnOrReason,scopes.slice()))}function only(name,fn){const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,scopes.slice()));return}jestGlobals().it.only(name,runWithFixtures(fn,scopes.slice()))}function todo(name){jestGlobals().it.todo(name)}function each(table,...tagged){const chain=scopes.slice();const register=jestGlobals().it.each(table,...tagged);return(name,fn)=>{const scope=currentScope();if(scope.skipped){const skipRegister=jestGlobals().it.skip.each(table,...tagged);skipRegister(skipTitle(name,scope),runWithFixtures(fn,chain));return}register(name,runWithFixtures(fn,chain))}}function withScope(fn){return(...args)=>{scopes.push(newScope(currentScope()));try{fn(...args)}finally{scopes.pop()}}}function scopedRegister(pick){return(name,fn,timeout)=>{pick()(name,withScope(fn),timeout)}}function scopedEach(pick){return(...eachArgs)=>(name,fn,timeout)=>{const register=pick()(...eachArgs);register(name,withScope(fn),timeout)}}var describeWithScope=Object.assign(scopedRegister(()=>jestGlobals().describe),{only:Object.assign(scopedRegister(()=>jestGlobals().describe.only),{each:scopedEach(()=>jestGlobals().describe.only.each)}),skip:Object.assign(scopedRegister(()=>jestGlobals().describe.skip),{each:scopedEach(()=>jestGlobals().describe.skip.each)}),each:scopedEach(()=>jestGlobals().describe.each)});function eachHook(kind){return(fn,timeout)=>{if(typeof fn==="function"&&fn.length>0){const scope=currentScope();(kind==="beforeEach"?scope.beforeEachHooks:scope.afterEachHooks).push(fn);return}jestGlobals()[kind](fn,timeout)}}var test=testWithFixtures;test.use=use;test.describe=describeWithScope;test.beforeAll=((...args)=>jestGlobals().beforeAll(...args));test.beforeEach=eachHook("beforeEach");test.afterEach=eachHook("afterEach");test.afterAll=((...args)=>jestGlobals().afterAll(...args));test.skip=skip;test.only=only;test.todo=todo;test.each=each;var resolvedExpect;function resolveJestExpect(){if(resolvedExpect)return resolvedExpect;try{resolvedExpect=require("@jest/globals").expect;return resolvedExpect}catch(error){throw new Error(`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error?error.message:String(error)}`,{cause:error})}}var expect=new Proxy(function frontmcpExpect(){},{apply:(_target,thisArg,args)=>Reflect.apply(resolveJestExpect(),thisArg,args),get:(_target,prop)=>Reflect.get(resolveJestExpect(),prop),has:(_target,prop)=>Reflect.has(resolveJestExpect(),prop)});var import_lazy_zod2=require("@frontmcp/lazy-zod");var import_utils8=require("@frontmcp/utils");function getPlatformMetaNamespace(platform){return"ui"}function getPlatformMimeType(platform){return"text/html;profile=mcp-app"}function isOpenAIPlatform(platform){return platform==="openai"}function isExtAppsPlatform(platform){return platform==="ext-apps"}function isUiPlatform(platform){return true}function getToolsListMetaPrefixes(platform){return["ui/"]}function getToolCallMetaPrefixes(platform){return["ui/"]}function getForbiddenMetaPrefixes(platform){return["openai/","frontmcp/"]}function escapeRegex(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function extractUiHtml(received){if(typeof received==="string"){return received}const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){const uiHtml=meta["ui/html"];if(typeof uiHtml==="string"){return uiHtml}}return void 0}function extractMeta(received){const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){return meta}return void 0}var toHaveRenderedHtml=function(received){const html=extractUiHtml(received);const hasHtml=html!==void 0&&html.length>0;const isFallback=hasHtml&&html.includes("mdx-fallback");const pass=hasHtml&&!isFallback;return{pass,message:()=>{if(isFallback){return"Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed."}if(!hasHtml){return"Expected _meta to have ui/html property with rendered HTML"}return"Expected result not to have rendered HTML"}}};var toContainHtmlElement=function(received,tag){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected to find <${tag}> element, but no HTML content found`}}const regex=new RegExp(`<${escapeRegex(tag)}[\\s>]`,"i");const pass=regex.test(html);return{pass,message:()=>pass?`Expected HTML not to contain <${tag}> element`:`Expected HTML to contain <${tag}> element`}};var toContainBoundValue=function(received,value){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to contain bound value "${value}", but no HTML content found`}}const stringValue=String(value);const pass=html.includes(stringValue);return{pass,message:()=>pass?`Expected HTML not to contain bound value "${stringValue}"`:`Expected HTML to contain bound value "${stringValue}"`}};var toBeXssSafe=function(received){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>"Expected HTML to be XSS unsafe (no HTML found)"}}const hasScript=/<script[\s>]/i.test(html);const hasOnHandler=/\son\w+\s*=/i.test(html);const hasJavascriptUri=/javascript:/i.test(html);const issues=[];if(hasScript)issues.push("<script> tag");if(hasOnHandler)issues.push("inline event handler (onclick, etc.)");if(hasJavascriptUri)issues.push("javascript: URI");const pass=!hasScript&&!hasOnHandler&&!hasJavascriptUri;return{pass,message:()=>pass?"Expected HTML not to be XSS safe":`Expected HTML to be XSS safe, but found: ${issues.join(", ")}`}};var toHaveWidgetMetadata=function(received){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>"Expected _meta to have widget metadata, but no _meta found"}}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");const pass=hasUiHtml||hasMimeType2||hasUiObject;return{pass,message:()=>pass?"Expected result not to have widget metadata":"Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"}};var toHaveCssClass=function(received,className){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to have CSS class "${className}", but no HTML content found`}}const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`,"i");const pass=classRegex.test(html);return{pass,message:()=>pass?`Expected HTML not to have CSS class "${className}"`:`Expected HTML to have CSS class "${className}"`}};var toNotContainRawContent=function(received,content){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>`Expected HTML to contain raw content "${content}", but no HTML found`}}const pass=!html.includes(content);return{pass,message:()=>pass?`Expected HTML to contain raw content "${content}"`:`Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`}};var toHaveProperHtmlStructure=function(received){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>"Expected proper HTML structure, but no HTML content found"}}const hasEscapedTags=html.includes("&lt;")&&html.includes("&gt;");const hasHtmlTags=/<[a-z]/i.test(html);const pass=hasHtmlTags&&!hasEscapedTags;return{pass,message:()=>{if(hasEscapedTags){return"Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"}if(!hasHtmlTags){return"Expected proper HTML structure, but found no HTML tags"}return"Expected result not to have proper HTML structure"}}};var toHavePlatformMeta=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform meta for "${platform}", but no _meta found`}}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));const pass=hasExpectedPrefix&&forbiddenKeys.length===0;return{pass,message:()=>{if(!hasExpectedPrefix){return`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`}if(forbiddenKeys.length>0){return`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`}return`Expected result not to have platform meta for "${platform}"`}}};var toHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=key in meta;return{pass,message:()=>pass?`Expected _meta not to have key "${key}"`:`Expected _meta to have key "${key}"`}};var toHaveMetaValue=function(received,key,value){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`}}const actualValue=meta[key];const pass=JSON.stringify(actualValue)===JSON.stringify(value);return{pass,message:()=>pass?`Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`:`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`}};var toNotHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:true,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=!(key in meta);return{pass,message:()=>pass?`Expected _meta to have key "${key}"`:`Expected _meta not to have key "${key}"`}};var toHaveOnlyNamespacedMeta=function(received,namespace){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have keys with namespace "${namespace}", but no _meta found`}}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(namespace));const pass=wrongKeys.length===0&&metaKeys.length>0;return{pass,message:()=>{if(metaKeys.length===0){return`Expected _meta to have keys with namespace "${namespace}", but _meta is empty`}if(wrongKeys.length>0){return`Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(", ")}]`}return`Expected _meta not to have only keys with namespace "${namespace}"`}}};var toHavePlatformMimeType=function(received,platform){const meta=extractMeta(received);const expectedMimeType=getPlatformMimeType(platform);if(!meta){return{pass:false,message:()=>`Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`}}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];const pass=actualMimeType===expectedMimeType;return{pass,message:()=>pass?`Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`:`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`}};var toHavePlatformHtml=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform HTML for "${platform}", but no _meta found`}}const htmlKey="ui/html";const html=meta[htmlKey];const pass=typeof html==="string"&&html.length>0;return{pass,message:()=>pass?`Expected _meta not to have platform HTML in "${htmlKey}"`:`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`}};var uiMatchers={toHaveRenderedHtml,toContainHtmlElement,toContainBoundValue,toBeXssSafe,toHaveWidgetMetadata,toHaveCssClass,toNotContainRawContent,toHaveProperHtmlStructure,toHavePlatformMeta,toHaveMetaKey,toHaveMetaValue,toNotHaveMetaKey,toHaveOnlyNamespacedMeta,toHavePlatformMimeType,toHavePlatformHtml};var toContainTool=function(received,toolName){const tools=received;if(!Array.isArray(tools)){return{pass:false,message:()=>`Expected an array of tools, but received ${typeof received}`}}const pass=tools.some(t=>t.name===toolName);const availableTools=tools.map(t=>t.name).join(", ");return{pass,message:()=>pass?`Expected tools not to contain "${toolName}"`:`Expected tools to contain "${toolName}", but got: [${availableTools}]`}};var toBeSuccessful=function(received){const result=received;if(typeof result!=="object"||result===null||!("isSuccess"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isSuccess property`}}const pass=result.isSuccess;return{pass,message:()=>pass?"Expected result not to be successful":`Expected result to be successful, but got error: ${result.error?.message??"unknown error"}`}};var toBeError=function(received,expectedCode){const result=received;if(typeof result!=="object"||result===null||!("isError"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isError property`}}const receivedCode=typeof expectedCode==="string"?result.raw?._meta?.["code"]:result.error?.code;const codeMatches=expectedCode===void 0||receivedCode===expectedCode;const pass=result.isError&&codeMatches;return{pass,message:()=>{if(!result.isError){return"Expected result to be an error, but it was successful"}if(!codeMatches){return`Expected error code ${expectedCode}, but got ${String(receivedCode)}`}return"Expected result not to be an error"}}};var toHaveTextContent=function(received,expectedText){const result=received;if(typeof result!=="object"||result===null||!("text"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper or ResourceContentWrapper object with text method`}}const text=result.text();const hasText="hasTextContent"in result?result.hasTextContent():text!==void 0;let pass=hasText;if(pass&&expectedText!==void 0){pass=text?.includes(expectedText)??false}return{pass,message:()=>{if(!hasText){return"Expected result to have text content"}if(expectedText!==void 0&&!text?.includes(expectedText)){return`Expected text to contain "${expectedText}", but got: "${text}"`}return"Expected result not to have text content"}}};var toHaveImageContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasImageContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasImageContent method`}}const pass=result.hasImageContent();return{pass,message:()=>pass?"Expected result not to have image content":"Expected result to have image content"}};var toHaveResourceContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasResourceContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasResourceContent method`}}const pass=result.hasResourceContent();return{pass,message:()=>pass?"Expected result not to have resource content":"Expected result to have resource content"}};var toPassOutputSchemaWhole=function(received,outputSchema){const schema=outputSchema instanceof import_lazy_zod2.z.ZodType?outputSchema:import_lazy_zod2.z.object(outputSchema);if((0,import_lazy_zod2.hasTransform)(schema)){return{pass:false,message:()=>"toPassOutputSchemaWhole cannot compare keys through a transform (.transform(), z.preprocess(), .pipe()): assert on the parsed answer instead"}}const parsed=schema.safeParse(received);if(!parsed.success){const[firstIssue]=parsed.error.issues;const location=firstIssue?.path.length?` at ${firstIssue.path.join(".")}`:"";return{pass:false,message:()=>`Expected the answer to pass its output schema whole, but it does not match its output schema${location}: ${firstIssue?.message}`}}const droppedKeys=(0,import_utils8.findDroppedKeys)(received,parsed.data);const pass=droppedKeys.length===0;return{pass,message:()=>pass?"Expected the answer not to pass its output schema whole":`Expected the answer to pass its output schema whole, but parsing drops keys it does not declare: ${droppedKeys.join(", ")}`}};var toContainResource=function(received,uri){const resources=received;if(!Array.isArray(resources)){return{pass:false,message:()=>`Expected an array of resources, but received ${typeof received}`}}const pass=resources.some(r=>r.uri===uri);const availableUris=resources.map(r=>r.uri).join(", ");return{pass,message:()=>pass?`Expected resources not to contain "${uri}"`:`Expected resources to contain "${uri}", but got: [${availableUris}]`}};var toContainResourceTemplate=function(received,uriTemplate){const templates=received;if(!Array.isArray(templates)){return{pass:false,message:()=>`Expected an array of resource templates, but received ${typeof received}`}}const pass=templates.some(t=>t.uriTemplate===uriTemplate);const availableTemplates=templates.map(t=>t.uriTemplate).join(", ");return{pass,message:()=>pass?`Expected templates not to contain "${uriTemplate}"`:`Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`}};var toHaveMimeType=function(received,mimeType){const result=received;if(typeof result!=="object"||result===null||!("hasMimeType"in result)){return{pass:false,message:()=>`Expected a ResourceContentWrapper object with hasMimeType method`}}const pass=result.hasMimeType(mimeType);const actualMimeType=result.mimeType();return{pass,message:()=>pass?`Expected content not to have MIME type "${mimeType}"`:`Expected MIME type "${mimeType}", but got "${actualMimeType}"`}};var toContainPrompt=function(received,name){const prompts=received;if(!Array.isArray(prompts)){return{pass:false,message:()=>`Expected an array of prompts, but received ${typeof received}`}}const pass=prompts.some(p=>p.name===name);const availablePrompts=prompts.map(p=>p.name).join(", ");return{pass,message:()=>pass?`Expected prompts not to contain "${name}"`:`Expected prompts to contain "${name}", but got: [${availablePrompts}]`}};var toHaveMessages=function(received,count){const result=received;if(typeof result!=="object"||result===null||!("messages"in result)){return{pass:false,message:()=>`Expected a PromptResultWrapper object with messages property`}}const actualCount=result.messages?.length??0;const pass=actualCount===count;return{pass,message:()=>pass?`Expected prompt not to have ${count} messages`:`Expected prompt to have ${count} messages, but got ${actualCount}`}};var toBeValidJsonRpc=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const hasJsonRpc=response["jsonrpc"]==="2.0";const hasId="id"in response;const hasResult="result"in response;const hasError="error"in response;const hasExactlyOneResultOrError=(hasResult||hasError)&&!(hasResult&&hasError);const pass=hasJsonRpc&&hasId&&hasExactlyOneResultOrError;return{pass,message:()=>{if(pass){return"Expected response not to be valid JSON-RPC"}const issues=[];if(!hasJsonRpc)issues.push('missing or invalid "jsonrpc": "2.0"');if(!hasId)issues.push('missing "id" field');if(!hasExactlyOneResultOrError){if(!hasResult&&!hasError)issues.push('missing "result" or "error"');else issues.push('cannot have both "result" and "error"')}return`Expected valid JSON-RPC 2.0 response: ${issues.join(", ")}`}}};var toHaveResult=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="result"in response;return{pass,message:()=>pass?"Expected response not to have result":"Expected response to have result"}};var toHaveError=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="error"in response;return{pass,message:()=>pass?"Expected response not to have error":"Expected response to have error"}};var toHaveErrorCode=function(received,code){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const actualCode=response.error?.code;const pass=actualCode===code;return{pass,message:()=>pass?`Expected response not to have error code ${code}`:`Expected error code ${code}, but got ${actualCode??"no error"}`}};function isReplayReport(value){const report=value;return typeof report?.file==="string"&&Array.isArray(report.calls)&&Array.isArray(report.differences)}var toMatchRecording=function(received){if(!isReplayReport(received)){return{pass:false,message:()=>"Expected the report replaySession() returns"}}const pass=received.differences.length===0;return{pass,message:()=>formatReplayReport(received)}};var mcpMatchers={toContainTool,toBeSuccessful,toBeError,toHaveTextContent,toHaveImageContent,toHaveResourceContent,toPassOutputSchemaWhole,toContainResource,toContainResourceTemplate,toHaveMimeType,toContainPrompt,toHaveMessages,toBeValidJsonRpc,toHaveResult,toHaveError,toHaveErrorCode,toMatchRecording,...uiMatchers};var originalFetch=null;var mockingEnabled=false;var activeInterceptors=[];var HttpInterceptorImpl=class{mocks=[];_allowPassthrough=false;_isActive=true;mock(definition){const entry={definition,callCount:0,calls:[],remainingUses:definition.times??Infinity};this.mocks.push(entry);return{remove:()=>{const index=this.mocks.indexOf(entry);if(index!==-1){this.mocks.splice(index,1)}},callCount:()=>entry.callCount,calls:()=>[...entry.calls],waitForCalls:async(count,timeoutMs=5e3)=>{const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){if(entry.callCount>=count){return entry.calls.slice(0,count)}await sleep3(50)}throw new Error(`Timeout waiting for ${count} calls, got ${entry.callCount}`)}}}get(url,response,options){return this.mock({match:{url,method:"GET",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}post(url,response,options){return this.mock({match:{url,method:"POST",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}put(url,response,options){return this.mock({match:{url,method:"PUT",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}delete(url,response,options){return this.mock({match:{url,method:"DELETE",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}any(url,response,options){return this.mock({match:{url,...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}clear(){this.mocks=[]}pending(){return this.mocks.filter(m=>m.remainingUses>0).map(m=>m.definition)}isDone(){return this.mocks.every(m=>m.remainingUses<=0||m.definition.times===void 0)}assertDone(){const pending=this.pending().filter(m=>m.times!==void 0);if(pending.length>0){const descriptions=pending.map(m=>{const url=m.match.url instanceof RegExp?m.match.url.toString():m.match.url;return` - ${m.match.method??"ANY"} ${url}`});throw new Error(`Unused HTTP mocks:
109
+ Error: ${errMsg}`,{cause:error})}}else{throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option')}const sharedSecret=config.env?.["JWT_SECRET"];const binding=sharedSecret?gatewayTokenBinding(config.baseUrl??server.info.baseUrl,config.entryPath):void 0;const tokenFactory2=new TestTokenFactory(sharedSecret?{hmacSecret:sharedSecret,...binding}:{});return{server,startedByUs,tokenFactory:tokenFactory2,owner}}function acquireServer(chain){const config=resolveConfig(chain);const key=serverKey(config);let entry=serverEntries.get(key);if(!entry){entry=startServerEntry(config,ownerScopeOf(chain));serverEntries.set(key,entry);entry.catch(()=>serverEntries.delete(key))}return entry}async function stopServers(owner){for(const[key,pending]of Array.from(serverEntries)){let entry;try{entry=await pending}catch{serverEntries.delete(key);continue}if(owner&&entry.owner!==owner)continue;serverEntries.delete(key);if(entry.startedByUs){await entry.server.stop()}}}var fixtureContexts=new WeakMap;async function buildFixtures(chain){const config=resolveConfig(chain);const entry=await acquireServer(chain);const{server,tokenFactory:tokenFactory2}=entry;const publicMode=config.publicMode??config.auth?.mode==="public";const clientConfig={...config,publicMode};const clientInstance=McpTestClient.create({baseUrl:resolveClientBaseUrl(server,clientConfig),entryPath:clientConfig.entryPath,transport:clientConfig.transport??"streamable-http",publicMode,protocolVersion:config.protocolVersion,mcpLogLevel:config.mcpLogLevel}).build();try{await clientInstance.connect()}catch(err){const message=err instanceof Error?err.message:String(err);if(/\b401\b|unauthor/i.test(message)){console.warn(`[TestFixture] MCP client could not connect anonymously \u2014 the server requires authentication (${message}). Tests using only the 'server'/'auth' fixtures are unaffected; tests using the 'mcp' fixture must supply a token.`)}else{throw err}}const context={extraClients:[]};const fixtures={mcp:clientInstance,auth:createAuthFixture(tokenFactory2),server:createServerFixture(server,clientConfig,clientInstance,context)};fixtureContexts.set(fixtures,context);return fixtures}async function cleanupTestFixtures(fixtures,testFailed=false){if(testFailed){const logs=fixtures.server.getLogs();if(logs.length>0){console.error("\n[TestFixture] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[TestFixture] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[TestFixture] === End Server Logs ===\n")}}const extras=fixtureContexts.get(fixtures)?.extraClients??[];for(const client of extras){if(client.isConnected()){await client.disconnect().catch(()=>void 0)}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}function resolveClientBaseUrl(server,config){return config.baseUrl??server.info.baseUrl}function createAuthFixture(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture(server,config,mcp,context){return{get info(){return server.info},createClient:async opts=>{const client=await McpTestClient.create({baseUrl:resolveClientBaseUrl(server,config),entryPath:opts?.entryPath??config.entryPath,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:config.publicMode,protocolVersion:opts?.protocolVersion??config.protocolVersion,mcpLogLevel:config.mcpLogLevel}).buildAndConnect();context.extraClients.push(client);return client},createClientBuilder:()=>{const builder=new McpTestClientBuilder({baseUrl:resolveClientBaseUrl(server,config),entryPath:config.entryPath,publicMode:config.publicMode,protocolVersion:config.protocolVersion,mcpLogLevel:config.mcpLogLevel});return builder},restart:async()=>{const reconnect=[mcp,...context.extraClients].filter(client=>client.isConnected());await server.restart();for(const client of reconnect){await client.reconnect()}},getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function runWithFixtures(fn,chain){return async(...extra)=>{const fixtures=await buildFixtures(chain);let failure;try{for(const scope of chain){for(const hook of scope.beforeEachHooks)await hook(fixtures)}await fn(fixtures,...extra)}catch(error){failure={error}}try{await runAfterEachHooks(chain,fixtures)}catch(error){failure??={error}}await cleanupTestFixtures(fixtures,failure!==void 0);if(failure)throw failure.error}}async function runAfterEachHooks(chain,fixtures){let firstError;let failed=false;for(let i=chain.length-1;i>=0;i--){for(const hook of chain[i].afterEachHooks){try{await hook(fixtures)}catch(error){if(!failed)firstError=error;failed=true}}}if(failed)throw firstError}function skipTitle(name,scope){return scope.reason?`${name} (skipped: ${scope.reason})`:name}function testWithFixtures(name,fn){const chain=scopes.slice();const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,chain));return}jestGlobals().it(name,runWithFixtures(fn,chain))}function use(config){const scope=currentScope();const{env,...rest}=config;scope.config={...scope.config,...rest};if(env)scope.envs.push(env);scope.configured=true;if(!scope.cleanupRegistered){scope.cleanupRegistered=true;jestGlobals().afterAll(async()=>{await stopServers(scope===scopes[0]?void 0:scope)})}}function skip(nameOrCondition,fnOrReason){if(typeof nameOrCondition==="boolean"){if(nameOrCondition){const scope=currentScope();scope.skipped=true;scope.reason=typeof fnOrReason==="string"?fnOrReason:void 0}return}if(typeof fnOrReason!=="function"){throw new TypeError(`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`)}jestGlobals().it.skip(nameOrCondition,runWithFixtures(fnOrReason,scopes.slice()))}function only(name,fn){const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,scopes.slice()));return}jestGlobals().it.only(name,runWithFixtures(fn,scopes.slice()))}function todo(name){jestGlobals().it.todo(name)}function each(table,...tagged){const chain=scopes.slice();const register=jestGlobals().it.each(table,...tagged);return(name,fn)=>{const scope=currentScope();if(scope.skipped){const skipRegister=jestGlobals().it.skip.each(table,...tagged);skipRegister(skipTitle(name,scope),runWithFixtures(fn,chain));return}register(name,runWithFixtures(fn,chain))}}function withScope(fn){return(...args)=>{scopes.push(newScope(currentScope()));try{fn(...args)}finally{scopes.pop()}}}function scopedRegister(pick){return(name,fn,timeout)=>{pick()(name,withScope(fn),timeout)}}function scopedEach(pick){return(...eachArgs)=>(name,fn,timeout)=>{const register=pick()(...eachArgs);register(name,withScope(fn),timeout)}}var describeWithScope=Object.assign(scopedRegister(()=>jestGlobals().describe),{only:Object.assign(scopedRegister(()=>jestGlobals().describe.only),{each:scopedEach(()=>jestGlobals().describe.only.each)}),skip:Object.assign(scopedRegister(()=>jestGlobals().describe.skip),{each:scopedEach(()=>jestGlobals().describe.skip.each)}),each:scopedEach(()=>jestGlobals().describe.each)});function eachHook(kind){return(fn,timeout)=>{if(typeof fn==="function"&&fn.length>0){const scope=currentScope();(kind==="beforeEach"?scope.beforeEachHooks:scope.afterEachHooks).push(fn);return}jestGlobals()[kind](fn,timeout)}}var test=testWithFixtures;test.use=use;test.describe=describeWithScope;test.beforeAll=((...args)=>jestGlobals().beforeAll(...args));test.beforeEach=eachHook("beforeEach");test.afterEach=eachHook("afterEach");test.afterAll=((...args)=>jestGlobals().afterAll(...args));test.skip=skip;test.only=only;test.todo=todo;test.each=each;var resolvedExpect;function resolveJestExpect(){if(resolvedExpect)return resolvedExpect;try{resolvedExpect=require("@jest/globals").expect;return resolvedExpect}catch(error){throw new Error(`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error?error.message:String(error)}`,{cause:error})}}var expect=new Proxy(function frontmcpExpect(){},{apply:(_target,thisArg,args)=>Reflect.apply(resolveJestExpect(),thisArg,args),get:(_target,prop)=>Reflect.get(resolveJestExpect(),prop),has:(_target,prop)=>Reflect.has(resolveJestExpect(),prop)});var import_lazy_zod2=require("@frontmcp/lazy-zod");var import_utils9=require("@frontmcp/utils");function getPlatformMetaNamespace(platform){return"ui"}function getPlatformMimeType(platform){return"text/html;profile=mcp-app"}function isOpenAIPlatform(platform){return platform==="openai"}function isExtAppsPlatform(platform){return platform==="ext-apps"}function isUiPlatform(platform){return true}function getToolsListMetaPrefixes(platform){return["ui/"]}function getToolCallMetaPrefixes(platform){return["ui/"]}function getForbiddenMetaPrefixes(platform){return["openai/","frontmcp/"]}function escapeRegex(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function extractUiHtml(received){if(typeof received==="string"){return received}const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){const uiHtml=meta["ui/html"];if(typeof uiHtml==="string"){return uiHtml}}return void 0}function extractMeta(received){const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){return meta}return void 0}var toHaveRenderedHtml=function(received){const html=extractUiHtml(received);const hasHtml=html!==void 0&&html.length>0;const isFallback=hasHtml&&html.includes("mdx-fallback");const pass=hasHtml&&!isFallback;return{pass,message:()=>{if(isFallback){return"Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed."}if(!hasHtml){return"Expected _meta to have ui/html property with rendered HTML"}return"Expected result not to have rendered HTML"}}};var toContainHtmlElement=function(received,tag){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected to find <${tag}> element, but no HTML content found`}}const regex=new RegExp(`<${escapeRegex(tag)}[\\s>]`,"i");const pass=regex.test(html);return{pass,message:()=>pass?`Expected HTML not to contain <${tag}> element`:`Expected HTML to contain <${tag}> element`}};var toContainBoundValue=function(received,value){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to contain bound value "${value}", but no HTML content found`}}const stringValue=String(value);const pass=html.includes(stringValue);return{pass,message:()=>pass?`Expected HTML not to contain bound value "${stringValue}"`:`Expected HTML to contain bound value "${stringValue}"`}};var toBeXssSafe=function(received){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>"Expected HTML to be XSS unsafe (no HTML found)"}}const hasScript=/<script[\s>]/i.test(html);const hasOnHandler=/\son\w+\s*=/i.test(html);const hasJavascriptUri=/javascript:/i.test(html);const issues=[];if(hasScript)issues.push("<script> tag");if(hasOnHandler)issues.push("inline event handler (onclick, etc.)");if(hasJavascriptUri)issues.push("javascript: URI");const pass=!hasScript&&!hasOnHandler&&!hasJavascriptUri;return{pass,message:()=>pass?"Expected HTML not to be XSS safe":`Expected HTML to be XSS safe, but found: ${issues.join(", ")}`}};var toHaveWidgetMetadata=function(received){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>"Expected _meta to have widget metadata, but no _meta found"}}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");const pass=hasUiHtml||hasMimeType2||hasUiObject;return{pass,message:()=>pass?"Expected result not to have widget metadata":"Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"}};var toHaveCssClass=function(received,className){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to have CSS class "${className}", but no HTML content found`}}const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`,"i");const pass=classRegex.test(html);return{pass,message:()=>pass?`Expected HTML not to have CSS class "${className}"`:`Expected HTML to have CSS class "${className}"`}};var toNotContainRawContent=function(received,content){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>`Expected HTML to contain raw content "${content}", but no HTML found`}}const pass=!html.includes(content);return{pass,message:()=>pass?`Expected HTML to contain raw content "${content}"`:`Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`}};var toHaveProperHtmlStructure=function(received){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>"Expected proper HTML structure, but no HTML content found"}}const hasEscapedTags=html.includes("&lt;")&&html.includes("&gt;");const hasHtmlTags=/<[a-z]/i.test(html);const pass=hasHtmlTags&&!hasEscapedTags;return{pass,message:()=>{if(hasEscapedTags){return"Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"}if(!hasHtmlTags){return"Expected proper HTML structure, but found no HTML tags"}return"Expected result not to have proper HTML structure"}}};var toHavePlatformMeta=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform meta for "${platform}", but no _meta found`}}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));const pass=hasExpectedPrefix&&forbiddenKeys.length===0;return{pass,message:()=>{if(!hasExpectedPrefix){return`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`}if(forbiddenKeys.length>0){return`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`}return`Expected result not to have platform meta for "${platform}"`}}};var toHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=key in meta;return{pass,message:()=>pass?`Expected _meta not to have key "${key}"`:`Expected _meta to have key "${key}"`}};var toHaveMetaValue=function(received,key,value){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`}}const actualValue=meta[key];const pass=JSON.stringify(actualValue)===JSON.stringify(value);return{pass,message:()=>pass?`Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`:`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`}};var toNotHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:true,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=!(key in meta);return{pass,message:()=>pass?`Expected _meta to have key "${key}"`:`Expected _meta not to have key "${key}"`}};var toHaveOnlyNamespacedMeta=function(received,namespace){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have keys with namespace "${namespace}", but no _meta found`}}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(namespace));const pass=wrongKeys.length===0&&metaKeys.length>0;return{pass,message:()=>{if(metaKeys.length===0){return`Expected _meta to have keys with namespace "${namespace}", but _meta is empty`}if(wrongKeys.length>0){return`Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(", ")}]`}return`Expected _meta not to have only keys with namespace "${namespace}"`}}};var toHavePlatformMimeType=function(received,platform){const meta=extractMeta(received);const expectedMimeType=getPlatformMimeType(platform);if(!meta){return{pass:false,message:()=>`Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`}}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];const pass=actualMimeType===expectedMimeType;return{pass,message:()=>pass?`Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`:`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`}};var toHavePlatformHtml=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform HTML for "${platform}", but no _meta found`}}const htmlKey="ui/html";const html=meta[htmlKey];const pass=typeof html==="string"&&html.length>0;return{pass,message:()=>pass?`Expected _meta not to have platform HTML in "${htmlKey}"`:`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`}};var uiMatchers={toHaveRenderedHtml,toContainHtmlElement,toContainBoundValue,toBeXssSafe,toHaveWidgetMetadata,toHaveCssClass,toNotContainRawContent,toHaveProperHtmlStructure,toHavePlatformMeta,toHaveMetaKey,toHaveMetaValue,toNotHaveMetaKey,toHaveOnlyNamespacedMeta,toHavePlatformMimeType,toHavePlatformHtml};var toContainTool=function(received,toolName){const tools=received;if(!Array.isArray(tools)){return{pass:false,message:()=>`Expected an array of tools, but received ${typeof received}`}}const pass=tools.some(t=>t.name===toolName);const availableTools=tools.map(t=>t.name).join(", ");return{pass,message:()=>pass?`Expected tools not to contain "${toolName}"`:`Expected tools to contain "${toolName}", but got: [${availableTools}]`}};var toBeSuccessful=function(received){const result=received;if(typeof result!=="object"||result===null||!("isSuccess"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isSuccess property`}}const pass=result.isSuccess;return{pass,message:()=>pass?"Expected result not to be successful":`Expected result to be successful, but got error: ${result.error?.message??"unknown error"}`}};var toBeError=function(received,expectedCode){const result=received;if(typeof result!=="object"||result===null||!("isError"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isError property`}}const receivedCode=typeof expectedCode==="string"?result.raw?._meta?.["code"]:result.error?.code;const codeMatches=expectedCode===void 0||receivedCode===expectedCode;const pass=result.isError&&codeMatches;return{pass,message:()=>{if(!result.isError){return"Expected result to be an error, but it was successful"}if(!codeMatches){return`Expected error code ${expectedCode}, but got ${String(receivedCode)}`}return"Expected result not to be an error"}}};var toHaveTextContent=function(received,expectedText){const result=received;if(typeof result!=="object"||result===null||!("text"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper or ResourceContentWrapper object with text method`}}const text=result.text();const hasText="hasTextContent"in result?result.hasTextContent():text!==void 0;let pass=hasText;if(pass&&expectedText!==void 0){pass=text?.includes(expectedText)??false}return{pass,message:()=>{if(!hasText){return"Expected result to have text content"}if(expectedText!==void 0&&!text?.includes(expectedText)){return`Expected text to contain "${expectedText}", but got: "${text}"`}return"Expected result not to have text content"}}};var toHaveImageContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasImageContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasImageContent method`}}const pass=result.hasImageContent();return{pass,message:()=>pass?"Expected result not to have image content":"Expected result to have image content"}};var toHaveResourceContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasResourceContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasResourceContent method`}}const pass=result.hasResourceContent();return{pass,message:()=>pass?"Expected result not to have resource content":"Expected result to have resource content"}};var toPassOutputSchemaWhole=function(received,outputSchema){const schema=outputSchema instanceof import_lazy_zod2.z.ZodType?outputSchema:import_lazy_zod2.z.object(outputSchema);if((0,import_lazy_zod2.hasTransform)(schema)){return{pass:false,message:()=>"toPassOutputSchemaWhole cannot compare keys through a transform (.transform(), z.preprocess(), .pipe()): assert on the parsed answer instead"}}const parsed=schema.safeParse(received);if(!parsed.success){const[firstIssue]=parsed.error.issues;const location=firstIssue?.path.length?` at ${firstIssue.path.join(".")}`:"";return{pass:false,message:()=>`Expected the answer to pass its output schema whole, but it does not match its output schema${location}: ${firstIssue?.message}`}}const droppedKeys=(0,import_utils9.findDroppedKeys)(received,parsed.data);const pass=droppedKeys.length===0;return{pass,message:()=>pass?"Expected the answer not to pass its output schema whole":`Expected the answer to pass its output schema whole, but parsing drops keys it does not declare: ${droppedKeys.join(", ")}`}};var toContainResource=function(received,uri){const resources=received;if(!Array.isArray(resources)){return{pass:false,message:()=>`Expected an array of resources, but received ${typeof received}`}}const pass=resources.some(r=>r.uri===uri);const availableUris=resources.map(r=>r.uri).join(", ");return{pass,message:()=>pass?`Expected resources not to contain "${uri}"`:`Expected resources to contain "${uri}", but got: [${availableUris}]`}};var toContainResourceTemplate=function(received,uriTemplate){const templates=received;if(!Array.isArray(templates)){return{pass:false,message:()=>`Expected an array of resource templates, but received ${typeof received}`}}const pass=templates.some(t=>t.uriTemplate===uriTemplate);const availableTemplates=templates.map(t=>t.uriTemplate).join(", ");return{pass,message:()=>pass?`Expected templates not to contain "${uriTemplate}"`:`Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`}};var toHaveMimeType=function(received,mimeType){const result=received;if(typeof result!=="object"||result===null||!("hasMimeType"in result)){return{pass:false,message:()=>`Expected a ResourceContentWrapper object with hasMimeType method`}}const pass=result.hasMimeType(mimeType);const actualMimeType=result.mimeType();return{pass,message:()=>pass?`Expected content not to have MIME type "${mimeType}"`:`Expected MIME type "${mimeType}", but got "${actualMimeType}"`}};var toContainPrompt=function(received,name){const prompts=received;if(!Array.isArray(prompts)){return{pass:false,message:()=>`Expected an array of prompts, but received ${typeof received}`}}const pass=prompts.some(p=>p.name===name);const availablePrompts=prompts.map(p=>p.name).join(", ");return{pass,message:()=>pass?`Expected prompts not to contain "${name}"`:`Expected prompts to contain "${name}", but got: [${availablePrompts}]`}};var toHaveMessages=function(received,count){const result=received;if(typeof result!=="object"||result===null||!("messages"in result)){return{pass:false,message:()=>`Expected a PromptResultWrapper object with messages property`}}const actualCount=result.messages?.length??0;const pass=actualCount===count;return{pass,message:()=>pass?`Expected prompt not to have ${count} messages`:`Expected prompt to have ${count} messages, but got ${actualCount}`}};var toBeValidJsonRpc=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const hasJsonRpc=response["jsonrpc"]==="2.0";const hasId="id"in response;const hasResult="result"in response;const hasError="error"in response;const hasExactlyOneResultOrError=(hasResult||hasError)&&!(hasResult&&hasError);const pass=hasJsonRpc&&hasId&&hasExactlyOneResultOrError;return{pass,message:()=>{if(pass){return"Expected response not to be valid JSON-RPC"}const issues=[];if(!hasJsonRpc)issues.push('missing or invalid "jsonrpc": "2.0"');if(!hasId)issues.push('missing "id" field');if(!hasExactlyOneResultOrError){if(!hasResult&&!hasError)issues.push('missing "result" or "error"');else issues.push('cannot have both "result" and "error"')}return`Expected valid JSON-RPC 2.0 response: ${issues.join(", ")}`}}};var toHaveResult=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="result"in response;return{pass,message:()=>pass?"Expected response not to have result":"Expected response to have result"}};var toHaveError=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="error"in response;return{pass,message:()=>pass?"Expected response not to have error":"Expected response to have error"}};var toHaveErrorCode=function(received,code){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const actualCode=response.error?.code;const pass=actualCode===code;return{pass,message:()=>pass?`Expected response not to have error code ${code}`:`Expected error code ${code}, but got ${actualCode??"no error"}`}};function isReplayReport(value){const report=value;return typeof report?.file==="string"&&Array.isArray(report.calls)&&Array.isArray(report.differences)}var toMatchRecording=function(received){if(!isReplayReport(received)){return{pass:false,message:()=>"Expected the report replaySession() returns"}}const pass=received.differences.length===0;return{pass,message:()=>formatReplayReport(received)}};var mcpMatchers={toContainTool,toBeSuccessful,toBeError,toHaveTextContent,toHaveImageContent,toHaveResourceContent,toPassOutputSchemaWhole,toContainResource,toContainResourceTemplate,toHaveMimeType,toContainPrompt,toHaveMessages,toBeValidJsonRpc,toHaveResult,toHaveError,toHaveErrorCode,toMatchRecording,...uiMatchers};var originalFetch=null;var mockingEnabled=false;var activeInterceptors=[];var HttpInterceptorImpl=class{mocks=[];_allowPassthrough=false;_isActive=true;mock(definition){const entry={definition,callCount:0,calls:[],remainingUses:definition.times??Infinity};this.mocks.push(entry);return{remove:()=>{const index=this.mocks.indexOf(entry);if(index!==-1){this.mocks.splice(index,1)}},callCount:()=>entry.callCount,calls:()=>[...entry.calls],waitForCalls:async(count,timeoutMs=5e3)=>{const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){if(entry.callCount>=count){return entry.calls.slice(0,count)}await sleep3(50)}throw new Error(`Timeout waiting for ${count} calls, got ${entry.callCount}`)}}}get(url,response,options){return this.mock({match:{url,method:"GET",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}post(url,response,options){return this.mock({match:{url,method:"POST",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}put(url,response,options){return this.mock({match:{url,method:"PUT",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}delete(url,response,options){return this.mock({match:{url,method:"DELETE",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}any(url,response,options){return this.mock({match:{url,...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}clear(){this.mocks=[]}pending(){return this.mocks.filter(m=>m.remainingUses>0).map(m=>m.definition)}isDone(){return this.mocks.every(m=>m.remainingUses<=0||m.definition.times===void 0)}assertDone(){const pending=this.pending().filter(m=>m.times!==void 0);if(pending.length>0){const descriptions=pending.map(m=>{const url=m.match.url instanceof RegExp?m.match.url.toString():m.match.url;return` - ${m.match.method??"ANY"} ${url}`});throw new Error(`Unused HTTP mocks:
110
110
  ${descriptions.join("\n")}`)}}allowPassthrough(allow){this._allowPassthrough=allow}restore(){this._isActive=false;const index=activeInterceptors.indexOf(this);if(index!==-1){activeInterceptors.splice(index,1)}if(activeInterceptors.length===0){httpMock.disable()}}isActive(){return this._isActive}canPassthrough(){return this._allowPassthrough}async matchRequest(info){if(!this._isActive)return null;for(const entry of this.mocks){if(entry.remainingUses<=0)continue;if(this.requestMatches(info,entry.definition.match)){entry.callCount++;entry.calls.push(info);entry.remainingUses--;let mockResponse2;if(typeof entry.definition.response==="function"){mockResponse2=await entry.definition.response(info)}else{mockResponse2=entry.definition.response}if(mockResponse2.delay&&mockResponse2.delay>0){await sleep3(mockResponse2.delay)}return createMockResponse(mockResponse2)}}return null}requestMatches(info,matcher){if(!this.urlMatches(info.url,matcher.url)){return false}if(matcher.method){const methods=Array.isArray(matcher.method)?matcher.method:[matcher.method];if(!methods.includes(info.method)){return false}}if(matcher.headers){for(const[key,expected]of Object.entries(matcher.headers)){const actual=info.headers[key.toLowerCase()];if(!actual)return false;if(expected instanceof RegExp){if(!expected.test(actual))return false}else if(actual!==expected){return false}}}if(matcher.body!==void 0){if(!this.bodyMatches(info.body,matcher.body)){return false}}return true}urlMatches(url,matcher){if(typeof matcher==="string"){return url===matcher||url.includes(matcher)}else if(matcher instanceof RegExp){return matcher.test(url)}else{return matcher(url)}}bodyMatches(actual,expected){if(typeof expected==="function"){return expected(actual)}if(typeof expected==="string"){const actualStr=typeof actual==="string"?actual:JSON.stringify(actual);return actualStr===expected||actualStr.includes(expected)}if(expected instanceof RegExp){const actualStr=typeof actual==="string"?actual:JSON.stringify(actual);return expected.test(actualStr)}if(typeof actual!=="object"||actual===null){return false}for(const[key,value]of Object.entries(expected)){const actualValue=actual[key];if(typeof value==="object"&&value!==null){if(!this.bodyMatches(actualValue,value)){return false}}else if(actualValue!==value){return false}}return true}};async function interceptedFetch(input,init){const request=typeof Request!=="undefined"&&input instanceof Request?input:void 0;const url=typeof input==="string"?input:input instanceof URL?input.toString():request?.url??String(input);const method=(init?.method??request?.method??"GET").toUpperCase();const headers={};const collectHeaders=source=>{if(!source)return;if(source instanceof Headers){source.forEach((value,key)=>{headers[key.toLowerCase()]=value})}else if(Array.isArray(source)){for(const[key,value]of source){headers[key.toLowerCase()]=value}}else{for(const[key,value]of Object.entries(source)){headers[key.toLowerCase()]=value}}};collectHeaders(init?.headers??request?.headers);let body;let rawBody;let bodySource=init?.body;if((bodySource===void 0||bodySource===null)&&request&&request.body!==null){bodySource=await request.clone().text()}if(bodySource){if(typeof bodySource==="string"){rawBody=bodySource}else if(bodySource instanceof ArrayBuffer||ArrayBuffer.isView(bodySource)){rawBody=new TextDecoder().decode(bodySource)}else if(bodySource instanceof URLSearchParams){rawBody=bodySource.toString()}if(rawBody!==void 0){try{body=JSON.parse(rawBody)}catch{body=rawBody}}else{body=bodySource}}const requestInfo={url,method,headers,body,rawBody};for(let i=activeInterceptors.length-1;i>=0;i--){const interceptor=activeInterceptors[i];if(!interceptor.isActive())continue;const mockResponse2=await interceptor.matchRequest(requestInfo);if(mockResponse2){return mockResponse2}}for(const interceptor of activeInterceptors){if(interceptor.isActive()&&interceptor.canPassthrough()){if(originalFetch){return originalFetch(input,init)}}}throw new Error(`No HTTP mock found for ${method} ${url}
111
111
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`)}var RESPONSE_KEYS=new Set(["status","statusText","headers","body","delay"]);function isResponseDescriptor(value){const keys=Object.keys(value);if(keys.length===0||!keys.every(key=>RESPONSE_KEYS.has(key)))return false;if("status"in value){const status=value["status"];if(typeof status!=="number"||!Number.isInteger(status)||status<200||status>599)return false}return true}function normalizeResponse(response){const isNetworkError=!Array.isArray(response)&&response["_throwError"]===true;if(isNetworkError)return response;if(Array.isArray(response)||!isResponseDescriptor(response)){return{body:response}}return response}function createMockResponse(mock){const mockAny=mock;if(mockAny._throwError){const body2=mock.body;const message=body2&&typeof body2==="object"&&"message"in body2?String(body2["message"]):"Network error";throw new TypeError(`fetch failed: ${message}`)}const status=mock.status??200;const statusText=mock.statusText??"OK";const headers=new Headers(mock.headers??{});let body=null;if(mock.body!==void 0){if(typeof mock.body==="string"){body=mock.body;if(!headers.has("content-type")){headers.set("content-type","text/plain")}}else if(mock.body instanceof ArrayBuffer){body=mock.body}else if(Buffer.isBuffer(mock.body)){body=new Uint8Array(mock.body).buffer}else{body=JSON.stringify(mock.body);if(!headers.has("content-type")){headers.set("content-type","application/json")}}}return new Response(body,{status,statusText,headers})}function sleep3(ms){return new Promise(resolve=>setTimeout(resolve,ms))}var HttpMockManagerImpl=class{interceptor(){if(!mockingEnabled){this.enable()}const interceptor=new HttpInterceptorImpl;activeInterceptors.push(interceptor);return interceptor}enable(){if(mockingEnabled)return;if(typeof globalThis.fetch==="function"){originalFetch=globalThis.fetch;globalThis.fetch=interceptedFetch}mockingEnabled=true}disable(){if(!mockingEnabled)return;if(originalFetch){globalThis.fetch=originalFetch;originalFetch=null}activeInterceptors.length=0;mockingEnabled=false}isEnabled(){return mockingEnabled}clearAll(){for(const interceptor of activeInterceptors){interceptor.clear()}activeInterceptors.length=0}};var httpMock=new HttpMockManagerImpl;var httpResponse={json(data,status=200){return{status,headers:{"content-type":"application/json"},body:data}},text(data,status=200){return{status,headers:{"content-type":"text/plain"},body:data}},html(data,status=200){return{status,headers:{"content-type":"text/html"},body:data}},error(status,message){return{status,statusText:message??getStatusText(status),body:message?{error:message}:void 0}},notFound(message="Not Found"){return httpResponse.error(404,message)},serverError(message="Internal Server Error"){return httpResponse.error(500,message)},unauthorized(message="Unauthorized"){return httpResponse.error(401,message)},forbidden(message="Forbidden"){return httpResponse.error(403,message)},networkError(message="Network error"){return{status:0,body:{_networkError:true,message},_throwError:true}},delayed(data,delayMs,status=200){return{status,body:data,delay:delayMs}}};function getStatusText(status){const texts={200:"OK",201:"Created",204:"No Content",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error",502:"Bad Gateway",503:"Service Unavailable"};return texts[status]??"Unknown"}function escapeRegex2(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var UIAssertions={assertRenderedUI(result){const meta=result.raw._meta;if(!meta){throw new Error("Expected tool result to have _meta, but _meta is undefined")}const html=meta["ui/html"];if(!html){throw new Error("Expected tool result to have ui/html in _meta, but it is missing")}if(typeof html!=="string"){throw new Error(`Expected ui/html to be a string, but got ${typeof html}`)}if(html.includes("mdx-fallback")){throw new Error("Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. Check that @mdx-js/mdx is installed and the template syntax is valid.")}return html},assertDataBinding(html,output,keys){const missingKeys=[];for(const key of keys){const value=output[key];if(value===void 0||value===null){continue}const stringValue=String(value);if(!html.includes(stringValue)){missingKeys.push(`${key}="${stringValue}"`)}}if(missingKeys.length>0){throw new Error(`Expected HTML to contain bound values for: ${missingKeys.join(", ")}. Data binding may have failed.`)}},assertXssSafe(html){const vulnerabilities=[];if(/<script[\s>]/i.test(html)){vulnerabilities.push("<script> tag detected")}if(/\son\w+\s*=/i.test(html)){vulnerabilities.push("inline event handler detected (onclick, onerror, etc.)")}if(/javascript:/i.test(html)){vulnerabilities.push("javascript: URI detected")}if(vulnerabilities.length>0){throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join("; ")}`)}},assertProperHtmlStructure(html){if(html.includes("&lt;")&&html.includes("&gt;")){throw new Error("HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. Check that the template is being processed correctly.")}if(!/<[a-z]/i.test(html)){throw new Error("HTML contains no HTML tags - content may be plain text or rendering failed.")}},assertContainsElement(html,tag){const regex=new RegExp(`<${escapeRegex2(tag)}[\\s>]`,"i");if(!regex.test(html)){throw new Error(`Expected HTML to contain <${tag}> element`)}},assertHasCssClass(html,className){const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex2(className)}\\b[^"']*["']`,"i");if(!classRegex.test(html)){throw new Error(`Expected HTML to have CSS class "${className}"`)}},assertNotContainsRaw(html,content){if(html.includes(content)){throw new Error(`HTML contains raw content "${content}" - this component may not have been rendered. Check that all custom components are properly passed to the renderer.`)}},assertWidgetMetadata(result){const meta=result.raw._meta;if(!meta){throw new Error("Expected tool result to have _meta with widget metadata")}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");if(!hasUiHtml&&!hasMimeType2&&!hasUiObject){throw new Error("Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)")}},assertValidUI(result,boundKeys){const html=UIAssertions.assertRenderedUI(result);UIAssertions.assertProperHtmlStructure(html);UIAssertions.assertXssSafe(html);if(boundKeys&&boundKeys.length>0){try{const output=JSON.parse(result.text()||"{}");UIAssertions.assertDataBinding(html,output,boundKeys)}catch{}}return html},assertOpenAIMeta(result){UIAssertions.assertPlatformMeta(result,"openai")},assertExtAppsMeta(result){UIAssertions.assertPlatformMeta(result,"ext-apps")},assertUiMeta(result){UIAssertions.assertPlatformMeta(result,"claude")},assertPlatformMeta(result,platform){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with platform meta for "${platform}"`)}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));if(!hasExpectedPrefix){throw new Error(`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`)}const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));if(forbiddenKeys.length>0){throw new Error(`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`)}},assertNoMixedNamespaces(result,expectedNamespace){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with namespace "${expectedNamespace}"`)}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(expectedNamespace));if(wrongKeys.length>0){throw new Error(`Expected _meta to ONLY have keys with namespace "${expectedNamespace}", but found: [${wrongKeys.join(", ")}]`)}},assertPlatformMimeType(result,platform){const meta=result.raw._meta;const expectedMimeType=getPlatformMimeType(platform);if(!meta){throw new Error(`Expected tool result to have _meta with MIME type for platform "${platform}"`)}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];if(actualMimeType!==expectedMimeType){throw new Error(`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`)}},assertPlatformHtml(result,platform){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with platform HTML for "${platform}"`)}const htmlKey="ui/html";const html=meta[htmlKey];if(typeof html!=="string"||html.length===0){throw new Error(`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`)}return html},assertValidPlatformMeta(result,platform){UIAssertions.assertPlatformMeta(result,platform);UIAssertions.assertPlatformMimeType(result,platform);return UIAssertions.assertPlatformHtml(result,platform)}};var import_lazy_zod3=require("@frontmcp/lazy-zod");var basicUIToolInputSchema=import_lazy_zod3.z.object({name:import_lazy_zod3.z.string().optional().default("World")});var basicUIToolOutputSchema=import_lazy_zod3.z.object({message:import_lazy_zod3.z.string(),timestamp:import_lazy_zod3.z.number()});var BASIC_UI_TOOL_CONFIG={name:"platform-test-basic",description:"Basic UI tool for platform testing",inputSchema:basicUIToolInputSchema,outputSchema:basicUIToolOutputSchema,ui:{template:`
112
112
  <div class="platform-test-basic">
@@ -154,7 +154,7 @@ Server entry: ${currentConfig.server}
154
154
  Project: ${currentConfig.project??"default"}
155
155
  Command: ${serverCommand}
156
156
 
157
- Error: ${errMsg}`,{cause:error})}}else{throw new Error('perfTest.use() requires either "server" (entry file path) or "baseUrl" option')}}}async function createTestFixtures(testName){await initializeSharedResources();if(!serverInstance){throw new Error("Server instance not initialized")}if(!tokenFactory){throw new Error("Token factory not initialized")}const clientInstance=await McpTestClient.create({baseUrl:serverInstance.info.baseUrl,transport:currentConfig.transport??"streamable-http",publicMode:currentConfig.publicMode}).buildAndConnect();const auth=createAuthFixture2(tokenFactory);const server=createServerFixture2(serverInstance);const perfImpl=createPerfFixtures(testName,currentConfig.project??"unknown");if(currentConfig.forceGcOnBaseline!==false){await perfImpl.baseline()}return{fixtures:{mcp:clientInstance,auth,server,perf:perfImpl},perfImpl}}async function cleanupTestFixtures2(fixtures,perfImpl,testFailed=false){const measurement=perfImpl.buildMeasurement();addGlobalMeasurement(measurement);if(testFailed&&serverInstance){const logs=serverInstance.getLogs();if(logs.length>0){console.error("\n[PerfTest] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[PerfTest] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[PerfTest] === End Server Logs ===\n")}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}async function cleanupSharedResources(){if(serverInstance&&serverStartedByUs){await serverInstance.stop()}serverInstance=null;tokenFactory=null;serverStartedByUs=false}function createAuthFixture2(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture2(server){return{info:server.info,createClient:async opts=>{return McpTestClient.create({baseUrl:server.info.baseUrl,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:currentConfig.publicMode}).buildAndConnect()},createClientBuilder:()=>{return new McpTestClientBuilder({baseUrl:server.info.baseUrl,publicMode:currentConfig.publicMode})},restart:()=>server.restart(),getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand2(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function perfTestWithFixtures(name,fn){it(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function use2(config){currentConfig={...currentConfig,...config};afterAll(async()=>{await cleanupSharedResources()})}function skip2(name,fn){it.skip(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function only2(name,fn){it.only(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function todo2(name){it.todo(name)}var perfTest=perfTestWithFixtures;perfTest.use=use2;for(const hook of["describe","beforeAll","beforeEach","afterEach","afterAll"]){Object.defineProperty(perfTest,hook,{get:()=>globalThis[hook],enumerable:true,configurable:true})}perfTest.skip=skip2;perfTest.only=only2;perfTest.todo=todo2;var import_utils9=require("@frontmcp/utils");var BASELINE_START_MARKER="<!-- PERF_BASELINE_START -->";var BASELINE_END_MARKER="<!-- PERF_BASELINE_END -->";var DEFAULT_BASELINE_PATH="perf-results/baseline.json";var BaselineStore=class{baseline=null;baselinePath;constructor(baselinePath=DEFAULT_BASELINE_PATH){this.baselinePath=baselinePath}async load(){try{if(!await(0,import_utils9.fileExists)(this.baselinePath)){return null}const content=await(0,import_utils9.readFile)(this.baselinePath);this.baseline=JSON.parse(content);return this.baseline}catch{return null}}async save(baseline){const dir=this.baselinePath.substring(0,this.baselinePath.lastIndexOf("/"));await(0,import_utils9.ensureDir)(dir);await(0,import_utils9.writeFile)(this.baselinePath,JSON.stringify(baseline,null,2));this.baseline=baseline}getTestBaseline(testId){if(!this.baseline){return null}return this.baseline.tests[testId]??null}isLoaded(){return this.baseline!==null}getBaseline(){return this.baseline}static createFromMeasurements(measurements,release,commitHash){const tests={};const grouped=new Map;for(const m of measurements){const key=`${m.project}::${m.name}`;if(!grouped.has(key)){grouped.set(key,[])}const group=grouped.get(key);if(group){group.push(m)}}for(const[key,testMeasurements]of grouped){const[project]=key.split("::");const heapSamples=testMeasurements.map(m=>m.final?.memory.heapUsed??0);const durationSamples=testMeasurements.map(m=>m.timing.durationMs);const cpuSamples=testMeasurements.map(m=>m.final?.cpu.total??0);tests[key]={testId:key,project,heapUsed:calculateMetricBaseline(heapSamples),durationMs:calculateMetricBaseline(durationSamples),cpuTime:calculateMetricBaseline(cpuSamples),createdAt:new Date().toISOString(),commitHash}}return{release,timestamp:new Date().toISOString(),commitHash,tests}}};function parseBaselineFromComment(commentBody){const startIdx=commentBody.indexOf(BASELINE_START_MARKER);const endIdx=commentBody.indexOf(BASELINE_END_MARKER);if(startIdx===-1||endIdx===-1||startIdx>=endIdx){return null}const content=commentBody.substring(startIdx+BASELINE_START_MARKER.length,endIdx);const jsonMatch=content.match(/```json\s*([\s\S]*?)\s*```/);if(!jsonMatch){return null}try{return JSON.parse(jsonMatch[1])}catch{return null}}function formatBaselineAsComment(baseline){const json=JSON.stringify(baseline,null,2);return`## Performance Baseline
157
+ Error: ${errMsg}`,{cause:error})}}else{throw new Error('perfTest.use() requires either "server" (entry file path) or "baseUrl" option')}}}async function createTestFixtures(testName){await initializeSharedResources();if(!serverInstance){throw new Error("Server instance not initialized")}if(!tokenFactory){throw new Error("Token factory not initialized")}const clientInstance=await McpTestClient.create({baseUrl:serverInstance.info.baseUrl,transport:currentConfig.transport??"streamable-http",publicMode:currentConfig.publicMode}).buildAndConnect();const auth=createAuthFixture2(tokenFactory);const server=createServerFixture2(serverInstance);const perfImpl=createPerfFixtures(testName,currentConfig.project??"unknown");if(currentConfig.forceGcOnBaseline!==false){await perfImpl.baseline()}return{fixtures:{mcp:clientInstance,auth,server,perf:perfImpl},perfImpl}}async function cleanupTestFixtures2(fixtures,perfImpl,testFailed=false){const measurement=perfImpl.buildMeasurement();addGlobalMeasurement(measurement);if(testFailed&&serverInstance){const logs=serverInstance.getLogs();if(logs.length>0){console.error("\n[PerfTest] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[PerfTest] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[PerfTest] === End Server Logs ===\n")}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}async function cleanupSharedResources(){if(serverInstance&&serverStartedByUs){await serverInstance.stop()}serverInstance=null;tokenFactory=null;serverStartedByUs=false}function createAuthFixture2(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture2(server){return{info:server.info,createClient:async opts=>{return McpTestClient.create({baseUrl:server.info.baseUrl,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:currentConfig.publicMode}).buildAndConnect()},createClientBuilder:()=>{return new McpTestClientBuilder({baseUrl:server.info.baseUrl,publicMode:currentConfig.publicMode})},restart:()=>server.restart(),getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand2(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function perfTestWithFixtures(name,fn){it(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function use2(config){currentConfig={...currentConfig,...config};afterAll(async()=>{await cleanupSharedResources()})}function skip2(name,fn){it.skip(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function only2(name,fn){it.only(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function todo2(name){it.todo(name)}var perfTest=perfTestWithFixtures;perfTest.use=use2;for(const hook of["describe","beforeAll","beforeEach","afterEach","afterAll"]){Object.defineProperty(perfTest,hook,{get:()=>globalThis[hook],enumerable:true,configurable:true})}perfTest.skip=skip2;perfTest.only=only2;perfTest.todo=todo2;var import_utils10=require("@frontmcp/utils");var BASELINE_START_MARKER="<!-- PERF_BASELINE_START -->";var BASELINE_END_MARKER="<!-- PERF_BASELINE_END -->";var DEFAULT_BASELINE_PATH="perf-results/baseline.json";var BaselineStore=class{baseline=null;baselinePath;constructor(baselinePath=DEFAULT_BASELINE_PATH){this.baselinePath=baselinePath}async load(){try{if(!await(0,import_utils10.fileExists)(this.baselinePath)){return null}const content=await(0,import_utils10.readFile)(this.baselinePath);this.baseline=JSON.parse(content);return this.baseline}catch{return null}}async save(baseline){const dir=this.baselinePath.substring(0,this.baselinePath.lastIndexOf("/"));await(0,import_utils10.ensureDir)(dir);await(0,import_utils10.writeFile)(this.baselinePath,JSON.stringify(baseline,null,2));this.baseline=baseline}getTestBaseline(testId){if(!this.baseline){return null}return this.baseline.tests[testId]??null}isLoaded(){return this.baseline!==null}getBaseline(){return this.baseline}static createFromMeasurements(measurements,release,commitHash){const tests={};const grouped=new Map;for(const m of measurements){const key=`${m.project}::${m.name}`;if(!grouped.has(key)){grouped.set(key,[])}const group=grouped.get(key);if(group){group.push(m)}}for(const[key,testMeasurements]of grouped){const[project]=key.split("::");const heapSamples=testMeasurements.map(m=>m.final?.memory.heapUsed??0);const durationSamples=testMeasurements.map(m=>m.timing.durationMs);const cpuSamples=testMeasurements.map(m=>m.final?.cpu.total??0);tests[key]={testId:key,project,heapUsed:calculateMetricBaseline(heapSamples),durationMs:calculateMetricBaseline(durationSamples),cpuTime:calculateMetricBaseline(cpuSamples),createdAt:new Date().toISOString(),commitHash}}return{release,timestamp:new Date().toISOString(),commitHash,tests}}};function parseBaselineFromComment(commentBody){const startIdx=commentBody.indexOf(BASELINE_START_MARKER);const endIdx=commentBody.indexOf(BASELINE_END_MARKER);if(startIdx===-1||endIdx===-1||startIdx>=endIdx){return null}const content=commentBody.substring(startIdx+BASELINE_START_MARKER.length,endIdx);const jsonMatch=content.match(/```json\s*([\s\S]*?)\s*```/);if(!jsonMatch){return null}try{return JSON.parse(jsonMatch[1])}catch{return null}}function formatBaselineAsComment(baseline){const json=JSON.stringify(baseline,null,2);return`## Performance Baseline
158
158
 
159
159
  This comment contains the performance baseline for release ${baseline.release}.
160
160
 
@@ -166,4 +166,4 @@ ${BASELINE_END_MARKER}
166
166
 
167
167
  Generated at: ${baseline.timestamp}
168
168
  ${baseline.commitHash?`Commit: ${baseline.commitHash}`:""}
169
- `}function calculateMetricBaseline(samples){if(samples.length===0){return{mean:0,stdDev:0,min:0,max:0,p95:0,sampleCount:0}}const sorted=[...samples].sort((a,b)=>a-b);const n=sorted.length;const sum=sorted.reduce((a,b)=>a+b,0);const mean=sum/n;const squaredDiffs=sorted.map(x=>Math.pow(x-mean,2));const variance=squaredDiffs.reduce((a,b)=>a+b,0)/n;const stdDev=Math.sqrt(variance);const min=sorted[0];const max=sorted[n-1];const p95Index=Math.ceil(n*.95)-1;const p95=sorted[Math.min(p95Index,n-1)];return{mean,stdDev,min,max,p95,sampleCount:n}}var globalBaselineStore=null;function getBaselineStore(baselinePath){if(!globalBaselineStore||baselinePath){globalBaselineStore=new BaselineStore(baselinePath)}return globalBaselineStore}var DEFAULT_CONFIG={warningThresholdPercent:10,errorThresholdPercent:25,minAbsoluteChange:1024};var RegressionDetector=class{config;constructor(config){this.config={...DEFAULT_CONFIG,...config}}detectRegression(measurement,baseline){const testId=`${measurement.project}::${measurement.name}`;const metrics=[];const currentHeap=measurement.final?.memory.heapUsed??0;const heapRegression=this.checkMetric("heapUsed",baseline.heapUsed.mean,currentHeap,formatBytes);metrics.push(heapRegression);const durationRegression=this.checkMetric("durationMs",baseline.durationMs.mean,measurement.timing.durationMs,formatDuration);metrics.push(durationRegression);const currentCpu=measurement.final?.cpu.total??0;const cpuRegression=this.checkMetric("cpuTime",baseline.cpuTime.mean,currentCpu,formatMicroseconds);metrics.push(cpuRegression);const hasRegression=metrics.some(m=>m.status==="regression");const hasWarning=metrics.some(m=>m.status==="warning");const status=hasRegression?"regression":hasWarning?"warning":"ok";const message=this.buildMessage(testId,metrics,status);return{testId,status,metrics,message}}detectRegressions(measurements,baselines){const results=[];for(const measurement of measurements){const testId=`${measurement.project}::${measurement.name}`;const baseline=baselines.tests[testId];if(baseline){results.push(this.detectRegression(measurement,baseline))}}return results}checkMetric(name,baseline,current,_formatter){const absoluteChange=current-baseline;const changePercent=baseline>0?absoluteChange/baseline*100:0;let status="ok";if(Math.abs(absoluteChange)>this.config.minAbsoluteChange){if(changePercent>=this.config.errorThresholdPercent){status="regression"}else if(changePercent>=this.config.warningThresholdPercent){status="warning"}}return{metric:name,baseline,current,changePercent,absoluteChange,status}}buildMessage(testId,metrics,status){if(status==="ok"){return`${testId}: All metrics within acceptable range`}const issues=metrics.filter(m=>m.status!=="ok").map(m=>{const direction=m.absoluteChange>0?"+":"";return`${m.metric}: ${direction}${m.changePercent.toFixed(1)}%`});const statusText=status==="regression"?"REGRESSION":"WARNING";return`${testId}: ${statusText} - ${issues.join(", ")}`}};function summarizeRegressions(results){const total=results.length;const ok=results.filter(r=>r.status==="ok").length;const warnings=results.filter(r=>r.status==="warning").length;const regressions=results.filter(r=>r.status==="regression").length;let summary;if(regressions>0){summary=`${regressions} regression(s) detected out of ${total} tests`}else if(warnings>0){summary=`${warnings} warning(s) detected out of ${total} tests`}else{summary=`All ${total} tests within acceptable range`}return{total,ok,warnings,regressions,summary}}var globalDetector=null;function getRegressionDetector(config){if(!globalDetector||config){globalDetector=new RegressionDetector(config)}return globalDetector}var import_utils10=require("@frontmcp/utils");var ReportGenerator=class{detector;constructor(){this.detector=new RegressionDetector}generateReport(measurements,baseline,gitInfo){const projectGroups=new Map;for(const m of measurements){if(!projectGroups.has(m.project)){projectGroups.set(m.project,[])}const group=projectGroups.get(m.project);if(group){group.push(m)}}const projects=[];for(const[projectName,projectMeasurements]of projectGroups){const summary=this.calculateSummary(projectMeasurements);const regressions=baseline?this.detector.detectRegressions(projectMeasurements,baseline):void 0;projects.push({project:projectName,summary,measurements:projectMeasurements,regressions})}const overallSummary=this.calculateSummary(measurements);return{timestamp:new Date().toISOString(),commitHash:gitInfo?.commitHash,branch:gitInfo?.branch,summary:overallSummary,projects,baseline:baseline?{release:baseline.release,timestamp:baseline.timestamp}:void 0}}generateMarkdownReport(report){const lines=[];lines.push("## Performance Test Results");lines.push("");const statusEmoji=this.getStatusEmoji(report.summary);lines.push(`**Status:** ${statusEmoji} ${this.getSummaryText(report.summary)}`);lines.push("");lines.push("### Summary");lines.push("");lines.push("| Metric | Value |");lines.push("|--------|-------|");lines.push(`| Total Tests | ${report.summary.totalTests} |`);lines.push(`| Passed | ${report.summary.passedTests} |`);lines.push(`| Warnings | ${report.summary.warningTests} |`);lines.push(`| Failed | ${report.summary.failedTests} |`);lines.push(`| Memory Leaks | ${report.summary.leakTests} |`);lines.push("");if(report.baseline){lines.push(`**Baseline:** ${report.baseline.release} (${report.baseline.timestamp})`);lines.push("")}lines.push("### Project Breakdown");lines.push("");for(const project of report.projects){lines.push(`#### ${project.project}`);lines.push("");lines.push(this.generateProjectTable(project));lines.push("");const leakTests=project.measurements.filter(m=>m.leakDetectionResults&&m.leakDetectionResults.length>0);if(leakTests.length>0){const parallelTests=leakTests.filter(m=>m.leakDetectionResults?.some(r=>this.isParallelResult(r)));if(parallelTests.length>0){lines.push("**Parallel Stress Test Results:**");lines.push("");lines.push("| Test | Workers | Iterations | Duration | Total req/s |");lines.push("|------|---------|------------|----------|-------------|");for(const m of parallelTests){for(const result of m.leakDetectionResults||[]){if(this.isParallelResult(result)){const parallelResult=result;const durationStr=parallelResult.durationMs?`${(parallelResult.durationMs/1e3).toFixed(2)}s`:"N/A";lines.push(`| ${m.name} | ${parallelResult.workersUsed} | ${parallelResult.totalIterations} | ${durationStr} | ${parallelResult.totalRequestsPerSecond.toFixed(1)} |`)}}}lines.push("")}lines.push("**Memory Interval Analysis:**");lines.push("");for(const m of leakTests){for(const result of m.leakDetectionResults||[]){if(result.intervals&&result.intervals.length>0){lines.push(`*${m.name}:*`);lines.push("");if(this.isParallelResult(result)){const parallelResult=result;lines.push("| Worker | req/s | Iterations |");lines.push("|--------|-------|------------|");for(const worker of parallelResult.perWorkerStats){lines.push(`| ${worker.workerId} | ${worker.requestsPerSecond.toFixed(1)} | ${worker.iterationsCompleted} |`)}lines.push("")}lines.push("| Interval | Heap Start | Heap End | Delta | Rate/iter |");lines.push("|----------|------------|----------|-------|-----------|");for(const interval of result.intervals){lines.push(`| ${interval.startIteration}-${interval.endIteration} | ${formatBytes(interval.heapAtStart)} | ${formatBytes(interval.heapAtEnd)} | ${interval.deltaFormatted} | ${formatBytes(interval.growthRatePerIteration)}/iter |`)}lines.push("");const durationStr=result.durationMs?`${(result.durationMs/1e3).toFixed(2)}s`:"N/A";const rpsStr=result.requestsPerSecond?`${result.requestsPerSecond.toFixed(1)} req/s`:"N/A";lines.push(`Total: ${formatBytes(result.totalGrowth)}, R\xB2=${result.rSquared.toFixed(3)} | ${result.samples.length} iterations in ${durationStr} (${rpsStr})`);lines.push("")}}}}if(project.regressions&&project.regressions.length>0){const regressionsWithIssues=project.regressions.filter(r=>r.status!=="ok");if(regressionsWithIssues.length>0){lines.push("**Regressions:**");for(const r of regressionsWithIssues){const emoji=r.status==="regression"?"\u274C":"\u26A0\uFE0F";lines.push(`- ${emoji} ${r.message}`)}lines.push("")}}}lines.push("---");lines.push(`Generated at: ${report.timestamp}`);if(report.commitHash){lines.push(`Commit: \`${report.commitHash.substring(0,8)}\``)}if(report.branch){lines.push(`Branch: \`${report.branch}\``)}return lines.join("\n")}generateJsonReport(report){return JSON.stringify(report,null,2)}calculateSummary(measurements){let passedTests=0;let warningTests=0;let failedTests=0;let leakTests=0;for(const m of measurements){const hasError=m.issues.some(i=>i.severity==="error");const hasWarning=m.issues.some(i=>i.severity==="warning");const hasLeak=m.issues.some(i=>i.type==="memory-leak");if(hasLeak){leakTests++}if(hasError){failedTests++}else if(hasWarning){warningTests++}else{passedTests++}}return{totalTests:measurements.length,passedTests,warningTests,failedTests,leakTests}}getStatusEmoji(summary){if(summary.failedTests>0||summary.leakTests>0){return"X"}if(summary.warningTests>0){return"!"}return"OK"}getSummaryText(summary){if(summary.failedTests>0){return`${summary.failedTests} test(s) failed`}if(summary.leakTests>0){return`${summary.leakTests} memory leak(s) detected`}if(summary.warningTests>0){return`${summary.warningTests} warning(s)`}return"All tests passed"}generateProjectTable(project){const lines=[];lines.push("| Test | Duration | Heap Delta | CPU Time | Status |");lines.push("|------|----------|------------|----------|--------|");for(const m of project.measurements){const status=this.getTestStatus(m);const heapDelta=m.memoryDelta?formatBytes(m.memoryDelta.heapUsed):"N/A";const cpuTime=m.final?.cpu.total?formatMicroseconds(m.final.cpu.total):"N/A";lines.push(`| ${m.name} | ${formatDuration(m.timing.durationMs)} | ${heapDelta} | ${cpuTime} | ${status} |`)}return lines.join("\n")}getTestStatus(m){const hasError=m.issues.some(i=>i.severity==="error");const hasLeak=m.issues.some(i=>i.type==="memory-leak");const hasWarning=m.issues.some(i=>i.severity==="warning");if(hasLeak){return"LEAK"}if(hasError){return"FAIL"}if(hasWarning){return"WARN"}return"OK"}isParallelResult(result){return typeof result==="object"&&result!==null&&"workersUsed"in result&&"perWorkerStats"in result&&"totalRequestsPerSecond"in result}};async function saveReports(measurements,outputDir,baseline,gitInfo){await(0,import_utils10.ensureDir)(outputDir);const generator=new ReportGenerator;const report=generator.generateReport(measurements,baseline,gitInfo);const jsonPath=`${outputDir}/report.json`;const markdownPath=`${outputDir}/report.md`;await(0,import_utils10.writeFile)(jsonPath,generator.generateJsonReport(report));await(0,import_utils10.writeFile)(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}var import_client=require("@frontmcp/protocol/client");var import_client2=require("@frontmcp/protocol/client");var import_node_net=__toESM(require("node:net"));function isTcpPortListening(port,host="127.0.0.1",timeoutMs=1500){return new Promise(resolve=>{const socket=import_node_net.default.connect({port,host});socket.unref();socket.setTimeout(timeoutMs);let settled=false;const finish=listening=>{if(settled)return;settled=true;socket.destroy();resolve(listening)};socket.once("connect",()=>finish(true));socket.once("error",()=>finish(false));socket.once("timeout",()=>finish(false))})}0&&(module.exports={AssertionError,AuthHeaders,BASIC_UI_TOOL_CONFIG,BaselineStore,ConnectionError,DefaultInterceptorChain,DefaultMockRegistry,EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,EXPECTED_GENERIC_TOOL_CALL_META_KEYS,EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,EXPECTED_OPENAI_TOOL_CALL_META_KEYS,FULL_UI_TOOL_CONFIG,LeakDetector,McpAssertions,McpClient,McpProtocolError,McpStdioClientTransport,McpTestClient,McpTestClientBuilder,MetricsCollector,MockAPIServer,MockCimdServer,MockOAuthServer,PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl,RegressionDetector,ReplayRecordingError,ReportGenerator,ServerStartError,SseTransport,StreamableHttpTransport,TestClientError,TestServer,TestTokenFactory,TestUsers,TimeoutError,UIAssertions,assertNoLeak,basicUIToolInputSchema,basicUIToolOutputSchema,buildUserAgent,clearGlobalMeasurements,containsPrompt,containsResource,containsResourceTemplate,containsTool,createLeakDetector,createPerfFixtures,createReportGenerator,createTestUser,expect,forceFullGc,forceGc,formatBaselineAsComment,formatBytes,formatDuration,formatMicroseconds,formatReplayReport,fullUIToolInputSchema,fullUIToolOutputSchema,generateBasicUIToolOutput,generateFullUIToolOutput,getBaselineStore,getForbiddenMetaPrefixes,getGlobalMeasurements,getPlatformClientInfo,getPlatformMetaNamespace,getPlatformMimeType,getPlatformUserAgent,getRegressionDetector,getToolCallMetaPrefixes,getToolsListMetaPrefixes,hasMimeType,hasTextContent,httpMock,httpResponse,interceptors,isError,isExtAppsPlatform,isGcAvailable,isOpenAIPlatform,isSuccessful,isTcpPortListening,isUiPlatform,mcpMatchers,mockResponse,parseBaselineFromComment,perfTest,replaySession,saveReports,summarizeRegressions,test,uiMatchers});
169
+ `}function calculateMetricBaseline(samples){if(samples.length===0){return{mean:0,stdDev:0,min:0,max:0,p95:0,sampleCount:0}}const sorted=[...samples].sort((a,b)=>a-b);const n=sorted.length;const sum=sorted.reduce((a,b)=>a+b,0);const mean=sum/n;const squaredDiffs=sorted.map(x=>Math.pow(x-mean,2));const variance=squaredDiffs.reduce((a,b)=>a+b,0)/n;const stdDev=Math.sqrt(variance);const min=sorted[0];const max=sorted[n-1];const p95Index=Math.ceil(n*.95)-1;const p95=sorted[Math.min(p95Index,n-1)];return{mean,stdDev,min,max,p95,sampleCount:n}}var globalBaselineStore=null;function getBaselineStore(baselinePath){if(!globalBaselineStore||baselinePath){globalBaselineStore=new BaselineStore(baselinePath)}return globalBaselineStore}var DEFAULT_CONFIG={warningThresholdPercent:10,errorThresholdPercent:25,minAbsoluteChange:1024};var RegressionDetector=class{config;constructor(config){this.config={...DEFAULT_CONFIG,...config}}detectRegression(measurement,baseline){const testId=`${measurement.project}::${measurement.name}`;const metrics=[];const currentHeap=measurement.final?.memory.heapUsed??0;const heapRegression=this.checkMetric("heapUsed",baseline.heapUsed.mean,currentHeap,formatBytes);metrics.push(heapRegression);const durationRegression=this.checkMetric("durationMs",baseline.durationMs.mean,measurement.timing.durationMs,formatDuration);metrics.push(durationRegression);const currentCpu=measurement.final?.cpu.total??0;const cpuRegression=this.checkMetric("cpuTime",baseline.cpuTime.mean,currentCpu,formatMicroseconds);metrics.push(cpuRegression);const hasRegression=metrics.some(m=>m.status==="regression");const hasWarning=metrics.some(m=>m.status==="warning");const status=hasRegression?"regression":hasWarning?"warning":"ok";const message=this.buildMessage(testId,metrics,status);return{testId,status,metrics,message}}detectRegressions(measurements,baselines){const results=[];for(const measurement of measurements){const testId=`${measurement.project}::${measurement.name}`;const baseline=baselines.tests[testId];if(baseline){results.push(this.detectRegression(measurement,baseline))}}return results}checkMetric(name,baseline,current,_formatter){const absoluteChange=current-baseline;const changePercent=baseline>0?absoluteChange/baseline*100:0;let status="ok";if(Math.abs(absoluteChange)>this.config.minAbsoluteChange){if(changePercent>=this.config.errorThresholdPercent){status="regression"}else if(changePercent>=this.config.warningThresholdPercent){status="warning"}}return{metric:name,baseline,current,changePercent,absoluteChange,status}}buildMessage(testId,metrics,status){if(status==="ok"){return`${testId}: All metrics within acceptable range`}const issues=metrics.filter(m=>m.status!=="ok").map(m=>{const direction=m.absoluteChange>0?"+":"";return`${m.metric}: ${direction}${m.changePercent.toFixed(1)}%`});const statusText=status==="regression"?"REGRESSION":"WARNING";return`${testId}: ${statusText} - ${issues.join(", ")}`}};function summarizeRegressions(results){const total=results.length;const ok=results.filter(r=>r.status==="ok").length;const warnings=results.filter(r=>r.status==="warning").length;const regressions=results.filter(r=>r.status==="regression").length;let summary;if(regressions>0){summary=`${regressions} regression(s) detected out of ${total} tests`}else if(warnings>0){summary=`${warnings} warning(s) detected out of ${total} tests`}else{summary=`All ${total} tests within acceptable range`}return{total,ok,warnings,regressions,summary}}var globalDetector=null;function getRegressionDetector(config){if(!globalDetector||config){globalDetector=new RegressionDetector(config)}return globalDetector}var import_utils11=require("@frontmcp/utils");var ReportGenerator=class{detector;constructor(){this.detector=new RegressionDetector}generateReport(measurements,baseline,gitInfo){const projectGroups=new Map;for(const m of measurements){if(!projectGroups.has(m.project)){projectGroups.set(m.project,[])}const group=projectGroups.get(m.project);if(group){group.push(m)}}const projects=[];for(const[projectName,projectMeasurements]of projectGroups){const summary=this.calculateSummary(projectMeasurements);const regressions=baseline?this.detector.detectRegressions(projectMeasurements,baseline):void 0;projects.push({project:projectName,summary,measurements:projectMeasurements,regressions})}const overallSummary=this.calculateSummary(measurements);return{timestamp:new Date().toISOString(),commitHash:gitInfo?.commitHash,branch:gitInfo?.branch,summary:overallSummary,projects,baseline:baseline?{release:baseline.release,timestamp:baseline.timestamp}:void 0}}generateMarkdownReport(report){const lines=[];lines.push("## Performance Test Results");lines.push("");const statusEmoji=this.getStatusEmoji(report.summary);lines.push(`**Status:** ${statusEmoji} ${this.getSummaryText(report.summary)}`);lines.push("");lines.push("### Summary");lines.push("");lines.push("| Metric | Value |");lines.push("|--------|-------|");lines.push(`| Total Tests | ${report.summary.totalTests} |`);lines.push(`| Passed | ${report.summary.passedTests} |`);lines.push(`| Warnings | ${report.summary.warningTests} |`);lines.push(`| Failed | ${report.summary.failedTests} |`);lines.push(`| Memory Leaks | ${report.summary.leakTests} |`);lines.push("");if(report.baseline){lines.push(`**Baseline:** ${report.baseline.release} (${report.baseline.timestamp})`);lines.push("")}lines.push("### Project Breakdown");lines.push("");for(const project of report.projects){lines.push(`#### ${project.project}`);lines.push("");lines.push(this.generateProjectTable(project));lines.push("");const leakTests=project.measurements.filter(m=>m.leakDetectionResults&&m.leakDetectionResults.length>0);if(leakTests.length>0){const parallelTests=leakTests.filter(m=>m.leakDetectionResults?.some(r=>this.isParallelResult(r)));if(parallelTests.length>0){lines.push("**Parallel Stress Test Results:**");lines.push("");lines.push("| Test | Workers | Iterations | Duration | Total req/s |");lines.push("|------|---------|------------|----------|-------------|");for(const m of parallelTests){for(const result of m.leakDetectionResults||[]){if(this.isParallelResult(result)){const parallelResult=result;const durationStr=parallelResult.durationMs?`${(parallelResult.durationMs/1e3).toFixed(2)}s`:"N/A";lines.push(`| ${m.name} | ${parallelResult.workersUsed} | ${parallelResult.totalIterations} | ${durationStr} | ${parallelResult.totalRequestsPerSecond.toFixed(1)} |`)}}}lines.push("")}lines.push("**Memory Interval Analysis:**");lines.push("");for(const m of leakTests){for(const result of m.leakDetectionResults||[]){if(result.intervals&&result.intervals.length>0){lines.push(`*${m.name}:*`);lines.push("");if(this.isParallelResult(result)){const parallelResult=result;lines.push("| Worker | req/s | Iterations |");lines.push("|--------|-------|------------|");for(const worker of parallelResult.perWorkerStats){lines.push(`| ${worker.workerId} | ${worker.requestsPerSecond.toFixed(1)} | ${worker.iterationsCompleted} |`)}lines.push("")}lines.push("| Interval | Heap Start | Heap End | Delta | Rate/iter |");lines.push("|----------|------------|----------|-------|-----------|");for(const interval of result.intervals){lines.push(`| ${interval.startIteration}-${interval.endIteration} | ${formatBytes(interval.heapAtStart)} | ${formatBytes(interval.heapAtEnd)} | ${interval.deltaFormatted} | ${formatBytes(interval.growthRatePerIteration)}/iter |`)}lines.push("");const durationStr=result.durationMs?`${(result.durationMs/1e3).toFixed(2)}s`:"N/A";const rpsStr=result.requestsPerSecond?`${result.requestsPerSecond.toFixed(1)} req/s`:"N/A";lines.push(`Total: ${formatBytes(result.totalGrowth)}, R\xB2=${result.rSquared.toFixed(3)} | ${result.samples.length} iterations in ${durationStr} (${rpsStr})`);lines.push("")}}}}if(project.regressions&&project.regressions.length>0){const regressionsWithIssues=project.regressions.filter(r=>r.status!=="ok");if(regressionsWithIssues.length>0){lines.push("**Regressions:**");for(const r of regressionsWithIssues){const emoji=r.status==="regression"?"\u274C":"\u26A0\uFE0F";lines.push(`- ${emoji} ${r.message}`)}lines.push("")}}}lines.push("---");lines.push(`Generated at: ${report.timestamp}`);if(report.commitHash){lines.push(`Commit: \`${report.commitHash.substring(0,8)}\``)}if(report.branch){lines.push(`Branch: \`${report.branch}\``)}return lines.join("\n")}generateJsonReport(report){return JSON.stringify(report,null,2)}calculateSummary(measurements){let passedTests=0;let warningTests=0;let failedTests=0;let leakTests=0;for(const m of measurements){const hasError=m.issues.some(i=>i.severity==="error");const hasWarning=m.issues.some(i=>i.severity==="warning");const hasLeak=m.issues.some(i=>i.type==="memory-leak");if(hasLeak){leakTests++}if(hasError){failedTests++}else if(hasWarning){warningTests++}else{passedTests++}}return{totalTests:measurements.length,passedTests,warningTests,failedTests,leakTests}}getStatusEmoji(summary){if(summary.failedTests>0||summary.leakTests>0){return"X"}if(summary.warningTests>0){return"!"}return"OK"}getSummaryText(summary){if(summary.failedTests>0){return`${summary.failedTests} test(s) failed`}if(summary.leakTests>0){return`${summary.leakTests} memory leak(s) detected`}if(summary.warningTests>0){return`${summary.warningTests} warning(s)`}return"All tests passed"}generateProjectTable(project){const lines=[];lines.push("| Test | Duration | Heap Delta | CPU Time | Status |");lines.push("|------|----------|------------|----------|--------|");for(const m of project.measurements){const status=this.getTestStatus(m);const heapDelta=m.memoryDelta?formatBytes(m.memoryDelta.heapUsed):"N/A";const cpuTime=m.final?.cpu.total?formatMicroseconds(m.final.cpu.total):"N/A";lines.push(`| ${m.name} | ${formatDuration(m.timing.durationMs)} | ${heapDelta} | ${cpuTime} | ${status} |`)}return lines.join("\n")}getTestStatus(m){const hasError=m.issues.some(i=>i.severity==="error");const hasLeak=m.issues.some(i=>i.type==="memory-leak");const hasWarning=m.issues.some(i=>i.severity==="warning");if(hasLeak){return"LEAK"}if(hasError){return"FAIL"}if(hasWarning){return"WARN"}return"OK"}isParallelResult(result){return typeof result==="object"&&result!==null&&"workersUsed"in result&&"perWorkerStats"in result&&"totalRequestsPerSecond"in result}};async function saveReports(measurements,outputDir,baseline,gitInfo){await(0,import_utils11.ensureDir)(outputDir);const generator=new ReportGenerator;const report=generator.generateReport(measurements,baseline,gitInfo);const jsonPath=`${outputDir}/report.json`;const markdownPath=`${outputDir}/report.md`;await(0,import_utils11.writeFile)(jsonPath,generator.generateJsonReport(report));await(0,import_utils11.writeFile)(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}var import_client=require("@frontmcp/protocol/client");var import_client2=require("@frontmcp/protocol/client");var import_node_net=__toESM(require("node:net"));function isTcpPortListening(port,host="127.0.0.1",timeoutMs=1500){return new Promise(resolve=>{const socket=import_node_net.default.connect({port,host});socket.unref();socket.setTimeout(timeoutMs);let settled=false;const finish=listening=>{if(settled)return;settled=true;socket.destroy();resolve(listening)};socket.once("connect",()=>finish(true));socket.once("error",()=>finish(false));socket.once("timeout",()=>finish(false))})}0&&(module.exports={AssertionError,AuthHeaders,BASIC_UI_TOOL_CONFIG,BaselineStore,ConnectionError,DefaultInterceptorChain,DefaultMockRegistry,EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,EXPECTED_GENERIC_TOOL_CALL_META_KEYS,EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,EXPECTED_OPENAI_TOOL_CALL_META_KEYS,FULL_UI_TOOL_CONFIG,LeakDetector,McpAssertions,McpClient,McpProtocolError,McpStdioClientTransport,McpTestClient,McpTestClientBuilder,MetricsCollector,MockAPIServer,MockCimdServer,MockOAuthServer,PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl,RegressionDetector,ReplayRecordingError,ReportGenerator,ServerStartError,SseTransport,StreamableHttpTransport,TestClientError,TestServer,TestTokenFactory,TestUsers,TimeoutError,UIAssertions,assertNoLeak,basicUIToolInputSchema,basicUIToolOutputSchema,buildUserAgent,clearGlobalMeasurements,containsPrompt,containsResource,containsResourceTemplate,containsTool,createLeakDetector,createPerfFixtures,createReportGenerator,createTestUser,expect,forceFullGc,forceGc,formatBaselineAsComment,formatBytes,formatDuration,formatMicroseconds,formatReplayReport,fullUIToolInputSchema,fullUIToolOutputSchema,generateBasicUIToolOutput,generateFullUIToolOutput,getBaselineStore,getForbiddenMetaPrefixes,getGlobalMeasurements,getPlatformClientInfo,getPlatformMetaNamespace,getPlatformMimeType,getPlatformUserAgent,getRegressionDetector,getToolCallMetaPrefixes,getToolsListMetaPrefixes,hasMimeType,hasTextContent,httpMock,httpResponse,interceptors,isError,isExtAppsPlatform,isGcAvailable,isOpenAIPlatform,isSuccessful,isTcpPortListening,isUiPlatform,mcpMatchers,mockResponse,parseBaselineFromComment,perfTest,replaySession,saveReports,summarizeRegressions,test,uiMatchers});