@frontmcp/testing 1.10.9-rc.4 → 1.10.9-rc.5

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.
package/esm/index.mjs CHANGED
@@ -169,4 +169,4 @@ ${BASELINE_END_MARKER}
169
169
 
170
170
  Generated at: ${baseline.timestamp}
171
171
  ${baseline.commitHash?`Commit: ${baseline.commitHash}`:""}
172
- `}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}import{ensureDir as ensureDir2,writeFile as writeFile2}from"@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 ensureDir2(outputDir);const generator=new ReportGenerator;const report=generator.generateReport(measurements,baseline,gitInfo);const jsonPath=`${outputDir}/report.json`;const markdownPath=`${outputDir}/report.md`;await writeFile2(jsonPath,generator.generateJsonReport(report));await writeFile2(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}import{Client}from"@frontmcp/protocol";import{StdioClientTransport}from"@frontmcp/protocol";import net from"node:net";function isTcpPortListening(port,host="127.0.0.1",timeoutMs=1500){return new Promise(resolve=>{const socket=net.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))})}export{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,Client as McpClient,McpProtocolError,StdioClientTransport as McpStdioClientTransport,McpTestClient,McpTestClientBuilder,MetricsCollector,MockAPIServer,MockCimdServer,MockOAuthServer,PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl,RegressionDetector,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,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,saveReports,summarizeRegressions,test,uiMatchers};
172
+ `}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}import{ensureDir as ensureDir2,writeFile as writeFile2}from"@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 ensureDir2(outputDir);const generator=new ReportGenerator;const report=generator.generateReport(measurements,baseline,gitInfo);const jsonPath=`${outputDir}/report.json`;const markdownPath=`${outputDir}/report.md`;await writeFile2(jsonPath,generator.generateJsonReport(report));await writeFile2(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}import{Client}from"@frontmcp/protocol/client";import{StdioClientTransport}from"@frontmcp/protocol/client";import net from"node:net";function isTcpPortListening(port,host="127.0.0.1",timeoutMs=1500){return new Promise(resolve=>{const socket=net.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))})}export{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,Client as McpClient,McpProtocolError,StdioClientTransport as McpStdioClientTransport,McpTestClient,McpTestClientBuilder,MetricsCollector,MockAPIServer,MockCimdServer,MockOAuthServer,PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl,RegressionDetector,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,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,saveReports,summarizeRegressions,test,uiMatchers};
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/testing",
3
- "version": "1.10.9-rc.4",
3
+ "version": "1.10.9-rc.5",
4
4
  "description": "End-to-end testing for FrontMCP servers: Jest fixtures with a connected MCP client, MCP matchers, JWT and OAuth mocks, and HTTP mocking",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://frontmcp.dev/reference/testing",
@@ -87,9 +87,9 @@
87
87
  "./esm": null
88
88
  },
89
89
  "peerDependencies": {
90
- "@frontmcp/lazy-zod": "1.10.9-rc.4",
91
- "@frontmcp/sdk": "1.10.9-rc.4",
92
- "@frontmcp/ui": "1.10.9-rc.4",
90
+ "@frontmcp/lazy-zod": "1.10.9-rc.5",
91
+ "@frontmcp/sdk": "1.10.9-rc.5",
92
+ "@frontmcp/ui": "1.10.9-rc.5",
93
93
  "@jest/globals": "^29.0.0 || ^30.0.0",
94
94
  "@playwright/test": "^1.40.0",
95
95
  "jest": "^29.0.0 || ^30.0.0"
@@ -112,8 +112,8 @@
112
112
  "node": ">=24.0.0"
113
113
  },
114
114
  "dependencies": {
115
- "@frontmcp/protocol": "1.10.9-rc.4",
116
- "@frontmcp/utils": "1.10.9-rc.4",
115
+ "@frontmcp/protocol": "1.10.9-rc.5",
116
+ "@frontmcp/utils": "1.10.9-rc.5",
117
117
  "@swc/core": "~1.15.41",
118
118
  "@swc/jest": "~0.2.38",
119
119
  "jose": "^6.0.11",
package/index.js CHANGED
@@ -1,4 +1,4 @@
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_protocol.Client,McpProtocolError:()=>McpProtocolError,McpStdioClientTransport:()=>import_protocol2.StdioClientTransport,McpTestClient:()=>McpTestClient,McpTestClientBuilder:()=>McpTestClientBuilder,MetricsCollector:()=>MetricsCollector,MockAPIServer:()=>MockAPIServer,MockCimdServer:()=>MockCimdServer,MockOAuthServer:()=>MockOAuthServer,PLATFORM_DETECTION_PATTERNS:()=>PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl:()=>PerfFixturesImpl,RegressionDetector:()=>RegressionDetector,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,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,saveReports:()=>saveReports,summarizeRegressions:()=>summarizeRegressions,test:()=>test,uiMatchers:()=>uiMatchers});module.exports=__toCommonJS(index_exports);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 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=`${url.pathname.replace(/\/+$/,"")}/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=(this.entryPath??"").replace(/^\/+|\/+$/g,"");const basePath=url.pathname.replace(/\/+$/,"");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 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=(this.config.entryPath??"").replace(/^\/+|\/+$/g,"");const basePath=url.pathname.replace(/\/+$/,"");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"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,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=>{this.log("warn","Resource subscription not yet implemented")},unsubscribe:async _uri=>{this.log("warn","Resource unsubscription not yet implemented")}};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=(this.config.entryPath??"").replace(/^\/+|\/+$/g,"");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_utils=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}
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,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,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,saveReports:()=>saveReports,summarizeRegressions:()=>summarizeRegressions,test:()=>test,uiMatchers:()=>uiMatchers});module.exports=__toCommonJS(index_exports);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 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=`${url.pathname.replace(/\/+$/,"")}/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=(this.entryPath??"").replace(/^\/+|\/+$/g,"");const basePath=url.pathname.replace(/\/+$/,"");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 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=(this.config.entryPath??"").replace(/^\/+|\/+$/g,"");const basePath=url.pathname.replace(/\/+$/,"");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"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,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=>{this.log("warn","Resource subscription not yet implemented")},unsubscribe:async _uri=>{this.log("warn","Resource unsubscription not yet implemented")}};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=(this.config.entryPath??"").replace(/^\/+|\/+$/g,"");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_utils=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
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_utils.base64urlEncode)((0,import_utils.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_utils.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>
@@ -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_utils5=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_utils5.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_utils5.writeFile)(jsonPath,generator.generateJsonReport(report));await(0,import_utils5.writeFile)(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}var import_protocol=require("@frontmcp/protocol");var import_protocol2=require("@frontmcp/protocol");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,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,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,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_utils5=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_utils5.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_utils5.writeFile)(jsonPath,generator.generateJsonReport(report));await(0,import_utils5.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,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,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,saveReports,summarizeRegressions,test,uiMatchers});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/testing",
3
- "version": "1.10.9-rc.4",
3
+ "version": "1.10.9-rc.5",
4
4
  "description": "End-to-end testing for FrontMCP servers: Jest fixtures with a connected MCP client, MCP matchers, JWT and OAuth mocks, and HTTP mocking",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://frontmcp.dev/reference/testing",
@@ -87,9 +87,9 @@
87
87
  "./esm": null
88
88
  },
89
89
  "peerDependencies": {
90
- "@frontmcp/lazy-zod": "1.10.9-rc.4",
91
- "@frontmcp/sdk": "1.10.9-rc.4",
92
- "@frontmcp/ui": "1.10.9-rc.4",
90
+ "@frontmcp/lazy-zod": "1.10.9-rc.5",
91
+ "@frontmcp/sdk": "1.10.9-rc.5",
92
+ "@frontmcp/ui": "1.10.9-rc.5",
93
93
  "@jest/globals": "^29.0.0 || ^30.0.0",
94
94
  "@playwright/test": "^1.40.0",
95
95
  "jest": "^29.0.0 || ^30.0.0"
@@ -112,8 +112,8 @@
112
112
  "node": ">=24.0.0"
113
113
  },
114
114
  "dependencies": {
115
- "@frontmcp/protocol": "1.10.9-rc.4",
116
- "@frontmcp/utils": "1.10.9-rc.4",
115
+ "@frontmcp/protocol": "1.10.9-rc.5",
116
+ "@frontmcp/utils": "1.10.9-rc.5",
117
117
  "@swc/core": "~1.15.41",
118
118
  "@swc/jest": "~0.2.38",
119
119
  "jose": "^6.0.11",
@@ -2,7 +2,7 @@
2
2
  * Raw MCP protocol client re-exports for low-level e2e testing.
3
3
  * For programmatic access, prefer connect()/connectOpenAI() from @frontmcp/sdk.
4
4
  */
5
- export { Client as McpClient } from '@frontmcp/protocol';
6
- export { StdioClientTransport as McpStdioClientTransport } from '@frontmcp/protocol';
5
+ export { Client as McpClient } from '@frontmcp/protocol/client';
6
+ export { StdioClientTransport as McpStdioClientTransport } from '@frontmcp/protocol/client';
7
7
  export { isTcpPortListening } from './port-probe';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/raw-client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,oBAAoB,IAAI,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/raw-client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,oBAAoB,IAAI,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC"}