@yasserkhanorg/e2e-agents 1.8.1 → 1.8.2

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.
@@ -0,0 +1,143 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { mkdirSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { CrossImpactAgent } from '../../agents/cross-impact.js';
6
+ import { ImpactAnalystAgent } from '../../agents/impact-analyst.js';
7
+ import { RegressionAdvisorAgent } from '../../agents/regression-advisor.js';
8
+ import { StrategistAgent } from '../../agents/strategist.js';
9
+ import { TestDesignerAgent } from '../../agents/test-designer.js';
10
+ import { CrewOrchestrator } from '../../crew/orchestrator.js';
11
+ const VALID_WORKFLOWS = new Set(['full-qa', 'quick-check', 'design-only']);
12
+ function uniqueStrings(values) {
13
+ return Array.from(new Set(values.filter(Boolean)));
14
+ }
15
+ function singleLine(value) {
16
+ return value.replace(/\s+/g, ' ').trim();
17
+ }
18
+ function chooseCrewWorkflow(explicitWorkflow, plan) {
19
+ if (explicitWorkflow && VALID_WORKFLOWS.has(explicitWorkflow)) {
20
+ return explicitWorkflow;
21
+ }
22
+ if (plan.decision.action === 'must-add-tests' || plan.metrics.uncoveredP0P1Flows > 0) {
23
+ return 'design-only';
24
+ }
25
+ return 'quick-check';
26
+ }
27
+ function registerCrewAgents(orchestrator) {
28
+ orchestrator.registerAgent(new ImpactAnalystAgent());
29
+ orchestrator.registerAgent(new StrategistAgent());
30
+ orchestrator.registerAgent(new TestDesignerAgent());
31
+ orchestrator.registerAgent(new CrossImpactAgent());
32
+ orchestrator.registerAgent(new RegressionAdvisorAgent());
33
+ }
34
+ export async function runPlanCrewAnalysis(plan, config, args) {
35
+ const reportRoot = config.testsRoot || config.path;
36
+ const workflow = chooseCrewWorkflow(args.crewWorkflow, plan);
37
+ const normalizedProvider = config.llm.provider?.trim().toLowerCase();
38
+ const providerOverride = normalizedProvider && normalizedProvider !== 'auto' ? normalizedProvider : 'auto';
39
+ const orchestrator = new CrewOrchestrator();
40
+ registerCrewAgents(orchestrator);
41
+ const result = await orchestrator.run({
42
+ appPath: config.path,
43
+ testsRoot: reportRoot,
44
+ gitSince: args.gitSince || config.git.since,
45
+ routeFamilies: config.routeFamilies,
46
+ apiSurface: config.apiSurface,
47
+ workflow,
48
+ providerOverride: providerOverride === 'auto' ? undefined : providerOverride,
49
+ budgetUSD: args.budgetUSD,
50
+ dryRun: args.dryRun,
51
+ });
52
+ const ctx = result.context;
53
+ const highRiskCrossImpacts = ctx.crossImpacts.filter((entry) => entry.riskLevel === 'high');
54
+ const manualReviewEntries = ctx.strategyEntries.filter((entry) => entry.approach === 'manual-review');
55
+ return {
56
+ workflow,
57
+ providerOverride,
58
+ summary: {
59
+ impactedFlows: ctx.impactedFlows.length,
60
+ strategyEntries: ctx.strategyEntries.length,
61
+ testDesigns: ctx.testDesigns.length,
62
+ crossImpacts: ctx.crossImpacts.length,
63
+ highRiskCrossImpacts: highRiskCrossImpacts.length,
64
+ regressionRisks: ctx.regressionRisks.length,
65
+ findings: ctx.findings.length,
66
+ generatedSpecs: ctx.generatedSpecs.length,
67
+ manualReviewEntries: manualReviewEntries.length,
68
+ totalCostUSD: Number(ctx.usage.totalCost.toFixed(4)),
69
+ totalTokens: ctx.usage.totalTokens,
70
+ },
71
+ impactedFlows: ctx.impactedFlows,
72
+ strategyEntries: ctx.strategyEntries,
73
+ testDesigns: ctx.testDesigns,
74
+ crossImpacts: ctx.crossImpacts,
75
+ regressionRisks: ctx.regressionRisks,
76
+ findings: ctx.findings,
77
+ warnings: uniqueStrings([...ctx.warnings, ...result.warnings]),
78
+ timings: result.timings,
79
+ };
80
+ }
81
+ export function buildCrewMarkdown(crew) {
82
+ const lines = [
83
+ '### Crew Insights',
84
+ '',
85
+ `Workflow: \`${crew.workflow}\``,
86
+ `Provider override: \`${crew.providerOverride}\``,
87
+ `Impacted flows: **${crew.summary.impactedFlows}**`,
88
+ `Strategy entries: **${crew.summary.strategyEntries}**`,
89
+ `Structured test designs: **${crew.summary.testDesigns}**`,
90
+ `Cross-impacts: **${crew.summary.crossImpacts}** (${crew.summary.highRiskCrossImpacts} high risk)`,
91
+ `Findings: **${crew.summary.findings}**`,
92
+ `Estimated AI cost: **$${crew.summary.totalCostUSD.toFixed(4)}**`,
93
+ ];
94
+ if (crew.strategyEntries.length > 0) {
95
+ lines.push('');
96
+ lines.push('Top Strategy Recommendations:');
97
+ for (const entry of crew.strategyEntries.slice(0, 5)) {
98
+ lines.push(`- ${entry.priority} ${entry.flowName} -> ${entry.approach} (${entry.crossImpactRisk} cross-impact risk)`);
99
+ }
100
+ }
101
+ if (crew.testDesigns.length > 0) {
102
+ lines.push('');
103
+ lines.push('Structured Test Designs:');
104
+ for (const design of crew.testDesigns.slice(0, 3)) {
105
+ lines.push(`- ${design.flowName}: ${design.testCases.length} designed test case(s)`);
106
+ }
107
+ }
108
+ const riskyCrossImpacts = crew.crossImpacts.filter((entry) => entry.riskLevel === 'high');
109
+ if (riskyCrossImpacts.length > 0) {
110
+ lines.push('');
111
+ lines.push('High-Risk Cross-Impacts:');
112
+ for (const entry of riskyCrossImpacts.slice(0, 5)) {
113
+ lines.push(`- ${entry.sourceFamily} -> ${entry.affectedFamily}: ${entry.sharedDependency}`);
114
+ }
115
+ }
116
+ if (crew.findings.length > 0) {
117
+ lines.push('');
118
+ lines.push('Crew Findings:');
119
+ for (const finding of crew.findings.slice(0, 5)) {
120
+ lines.push(`- ${finding.severity} ${finding.type}: ${finding.summary}`);
121
+ }
122
+ }
123
+ if (crew.warnings.length > 0) {
124
+ lines.push('');
125
+ lines.push('Crew Warnings:');
126
+ for (const warning of crew.warnings.slice(0, 5)) {
127
+ lines.push(`- ${singleLine(warning)}`);
128
+ }
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+ export function appendCrewToSummary(baseMarkdown, crew) {
133
+ return `${baseMarkdown}\n\n---\n\n${buildCrewMarkdown(crew)}`;
134
+ }
135
+ export function writeCrewArtifacts(reportRoot, crew) {
136
+ const outputDir = join(reportRoot, '.e2e-ai-agents');
137
+ mkdirSync(outputDir, { recursive: true });
138
+ const crewSummaryPath = join(outputDir, 'crew-summary.json');
139
+ const crewMarkdownPath = join(outputDir, 'crew-summary.md');
140
+ writeFileSync(crewSummaryPath, JSON.stringify(crew, null, 2), 'utf-8');
141
+ writeFileSync(crewMarkdownPath, buildCrewMarkdown(crew), 'utf-8');
142
+ return { crewSummaryPath, crewMarkdownPath };
143
+ }
@@ -56,6 +56,7 @@ const FLAGS = {
56
56
  '--pipeline-parallel': { key: 'pipelineParallel', type: 'boolean' },
57
57
  '--pipeline-dry-run': { key: 'pipelineDryRun', type: 'boolean' },
58
58
  '--fail-on-must-add-tests': { key: 'failOnMustAddTests', type: 'boolean' },
59
+ '--crew': { key: 'crew', type: 'boolean' },
59
60
  '--create-pr': { key: 'createPr', type: 'boolean' },
60
61
  '--dry-run': { key: 'dryRun', type: 'boolean' },
61
62
  '--generate': { key: 'analyzeGenerate', type: 'boolean' },
@@ -99,6 +100,7 @@ const FLAGS = {
99
100
  '--output': { key: 'trainOutput', type: 'string' },
100
101
  '--server-path': { key: 'serverPath', type: 'string' },
101
102
  '--workflow': { key: 'crewWorkflow', type: 'string' },
103
+ '--crew-workflow': { key: 'crewWorkflow', type: 'string' },
102
104
  // -- number flags (with isFinite guard) --
103
105
  '--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
104
106
  '--time': { key: 'timeLimitMinutes', type: 'number' },
@@ -56,6 +56,8 @@ export function printUsage() {
56
56
  ' --policy-risky-patterns <globs> Comma-separated risky file globs',
57
57
  ' --policy-enforcement-mode <mode> advisory | warn | block',
58
58
  ' --policy-block-actions <actions> Comma-separated CI actions to block/warn',
59
+ ' --crew Run Crew enrichment and attach insights to plan output',
60
+ ' --crew-workflow <name> full-qa | quick-check | design-only',
59
61
  ' --ci-comment-path <path> Write CI markdown summary',
60
62
  ' --github-output <path> Write GitHub Actions outputs',
61
63
  ' --fail-on-must-add-tests Exit non-zero on must-add-tests decision',
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
3
  // See LICENSE.txt for license information.
3
4
  /**
@@ -6,7 +7,7 @@
6
7
  */
7
8
  import { spawnSync } from 'child_process';
8
9
  import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
9
- import { resolve } from 'path';
10
+ import { join, resolve, dirname } from 'path';
10
11
  import { globSync } from 'glob';
11
12
  /**
12
13
  * SECURITY: Path validation helper
@@ -471,12 +472,152 @@ export class E2EAgentsMCPServer {
471
472
  }
472
473
  }
473
474
  /**
474
- * Start MCP server
475
- * Usage: node dist/mcp-server.js
475
+ * Read the package version at runtime so the MCP initialize response
476
+ * always reflects the installed version.
476
477
  */
478
+ function getPackageVersion() {
479
+ try {
480
+ const pkgPath = join(dirname(__dirname), 'package.json');
481
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
482
+ return pkg.version || '0.0.0';
483
+ }
484
+ catch {
485
+ return '0.0.0';
486
+ }
487
+ }
488
+ /**
489
+ * Encode a JSON-RPC message with Content-Length framing.
490
+ * Exported for testability.
491
+ */
492
+ export function encodeJsonRpcMessage(message) {
493
+ const body = JSON.stringify(message);
494
+ return `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
495
+ }
496
+ /**
497
+ * Parse Content-Length framed JSON-RPC messages from a buffer.
498
+ * Returns parsed messages and the remaining (unconsumed) buffer.
499
+ * Exported for testability.
500
+ */
501
+ export function parseJsonRpcFrames(input) {
502
+ const messages = [];
503
+ let buffer = Buffer.from(input);
504
+ while (true) {
505
+ const headerEnd = buffer.indexOf('\r\n\r\n');
506
+ if (headerEnd === -1)
507
+ break;
508
+ const headerText = buffer.slice(0, headerEnd).toString('utf8');
509
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
510
+ if (!match) {
511
+ buffer = Buffer.alloc(0);
512
+ break;
513
+ }
514
+ const contentLength = Number(match[1]);
515
+ const messageEnd = headerEnd + 4 + contentLength;
516
+ if (buffer.length < messageEnd)
517
+ break;
518
+ const body = buffer.slice(headerEnd + 4, messageEnd).toString('utf8');
519
+ buffer = buffer.slice(messageEnd);
520
+ messages.push(JSON.parse(body));
521
+ }
522
+ return { messages, remainder: buffer };
523
+ }
524
+ /**
525
+ * Handle a single JSON-RPC message against the server.
526
+ * Returns the response message (or null for notifications).
527
+ * Exported for testability.
528
+ */
529
+ export async function handleJsonRpcMessage(server, message) {
530
+ const { id, method, params } = message;
531
+ const version = getPackageVersion();
532
+ if (method === 'initialize') {
533
+ return {
534
+ jsonrpc: '2.0',
535
+ id,
536
+ result: {
537
+ protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',
538
+ capabilities: { tools: {}, resources: {}, prompts: {} },
539
+ serverInfo: { name: 'e2e-agents-mcp', version },
540
+ },
541
+ };
542
+ }
543
+ if (method === 'notifications/initialized' || method === 'initialized') {
544
+ return null;
545
+ }
546
+ if (method === 'tools/list') {
547
+ return {
548
+ jsonrpc: '2.0',
549
+ id,
550
+ result: {
551
+ tools: server.getTools().map((tool) => ({
552
+ name: tool.name,
553
+ description: tool.description,
554
+ inputSchema: tool.inputSchema,
555
+ })),
556
+ },
557
+ };
558
+ }
559
+ if (method === 'tools/call') {
560
+ const resultText = await server.callTool(typeof params?.name === 'string' ? params.name : '', typeof params?.arguments === 'object' && params.arguments !== null ? params.arguments : {});
561
+ let isError = false;
562
+ try {
563
+ const parsed = JSON.parse(resultText);
564
+ isError = Boolean(parsed.error);
565
+ }
566
+ catch {
567
+ isError = false;
568
+ }
569
+ return {
570
+ jsonrpc: '2.0',
571
+ id,
572
+ result: { content: [{ type: 'text', text: resultText }], isError },
573
+ };
574
+ }
575
+ if (method === 'resources/list') {
576
+ return { jsonrpc: '2.0', id, result: { resources: [] } };
577
+ }
578
+ if (method === 'prompts/list') {
579
+ return { jsonrpc: '2.0', id, result: { prompts: [] } };
580
+ }
581
+ if (method === 'ping') {
582
+ return { jsonrpc: '2.0', id, result: {} };
583
+ }
584
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
585
+ }
586
+ /**
587
+ * Start MCP server over stdio using Content-Length framed JSON-RPC messages.
588
+ */
589
+ export function startStdioServer(repoRoot = process.cwd()) {
590
+ const server = new E2EAgentsMCPServer(repoRoot);
591
+ let buffer = Buffer.alloc(0);
592
+ const sendMessage = (message) => {
593
+ process.stdout.write(encodeJsonRpcMessage(message));
594
+ };
595
+ const sendError = (id, code, msg) => {
596
+ sendMessage({ jsonrpc: '2.0', id, error: { code, message: msg } });
597
+ };
598
+ const processBuffer = () => {
599
+ const { messages, remainder } = parseJsonRpcFrames(buffer);
600
+ buffer = remainder;
601
+ for (const parsed of messages) {
602
+ void handleJsonRpcMessage(server, parsed)
603
+ .then((response) => {
604
+ if (response)
605
+ sendMessage(response);
606
+ })
607
+ .catch((error) => {
608
+ sendError(parsed.id ?? null, -32603, error instanceof Error ? error.message : String(error));
609
+ });
610
+ }
611
+ };
612
+ process.stdin.on('data', (chunk) => {
613
+ buffer = Buffer.concat([buffer, chunk]);
614
+ processBuffer();
615
+ });
616
+ process.stdin.on('end', () => {
617
+ process.exit(0);
618
+ });
619
+ }
477
620
  if (require.main === module) {
478
- const server = new E2EAgentsMCPServer();
479
- console.log('E2E Agents MCP Server started');
480
- console.log('Tools:', server.getTools().map((t) => t.name).join(', '));
621
+ startStdioServer();
481
622
  }
482
623
  export default E2EAgentsMCPServer;
@@ -141,6 +141,17 @@ export class LLMProviderFactory {
141
141
  '3. Set OPENAI_API_KEY environment variable\n' +
142
142
  '4. Set LLM_PROVIDER environment variable');
143
143
  }
144
+ /**
145
+ * Create provider from an explicit preference when supplied, otherwise
146
+ * fall back to environment auto-detection.
147
+ */
148
+ static async createFromPreference(providerPreference) {
149
+ const normalized = providerPreference?.trim().toLowerCase();
150
+ if (!normalized || normalized === 'auto') {
151
+ return this.createFromEnv();
152
+ }
153
+ return this.createFromString(normalized);
154
+ }
144
155
  /**
145
156
  * Create provider from simple string format
146
157
  *
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  interface Tool {
2
3
  name: string;
3
4
  description: string;
@@ -31,5 +32,37 @@ export declare class E2EAgentsMCPServer {
31
32
  */
32
33
  getTools(): Tool[];
33
34
  }
35
+ /**
36
+ * Encode a JSON-RPC message with Content-Length framing.
37
+ * Exported for testability.
38
+ */
39
+ export declare function encodeJsonRpcMessage(message: Record<string, unknown>): string;
40
+ /**
41
+ * Parse Content-Length framed JSON-RPC messages from a buffer.
42
+ * Returns parsed messages and the remaining (unconsumed) buffer.
43
+ * Exported for testability.
44
+ */
45
+ export declare function parseJsonRpcFrames(input: Buffer): {
46
+ messages: Array<{
47
+ id?: unknown;
48
+ method?: string;
49
+ params?: Record<string, unknown>;
50
+ }>;
51
+ remainder: Buffer<ArrayBuffer>;
52
+ };
53
+ /**
54
+ * Handle a single JSON-RPC message against the server.
55
+ * Returns the response message (or null for notifications).
56
+ * Exported for testability.
57
+ */
58
+ export declare function handleJsonRpcMessage(server: E2EAgentsMCPServer, message: {
59
+ id?: unknown;
60
+ method?: string;
61
+ params?: Record<string, unknown>;
62
+ }): Promise<Record<string, unknown> | null>;
63
+ /**
64
+ * Start MCP server over stdio using Content-Length framed JSON-RPC messages.
65
+ */
66
+ export declare function startStdioServer(repoRoot?: string): void;
34
67
  export default E2EAgentsMCPServer;
35
68
  //# sourceMappingURL=mcp-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAaA,UAAU,IAAI;IACV,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AA4GD;;;GAGG;AACH,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,WAAW,CAAc;gBAErB,QAAQ,GAAE,MAAsB;IAM5C,OAAO,CAAC,WAAW;IAmGnB;;;OAGG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB5E,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,QAAQ;IAyDhB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,QAAQ,IAAI,IAAI,EAAE;CAGrB;AAYD,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AAeA,UAAU,IAAI;IACV,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AA4GD;;;GAGG;AACH,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,WAAW,CAAc;gBAErB,QAAQ,GAAE,MAAsB;IAM5C,OAAO,CAAC,WAAW;IAmGnB;;;OAGG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB5E,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,QAAQ;IAyDhB,OAAO,CAAC,aAAa;IA2BrB,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,QAAQ,IAAI,IAAI,EAAE;CAGrB;AAgBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAG7E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAC,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;CAAC,CA0BtK;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACtC,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAC,GAC3E,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAmEzC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,GAAE,MAAsB,GAAG,IAAI,CAmCvE;AAMD,eAAe,kBAAkB,CAAC"}
@@ -1,8 +1,13 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
4
  // See LICENSE.txt for license information.
4
5
  Object.defineProperty(exports, "__esModule", { value: true });
5
6
  exports.E2EAgentsMCPServer = void 0;
7
+ exports.encodeJsonRpcMessage = encodeJsonRpcMessage;
8
+ exports.parseJsonRpcFrames = parseJsonRpcFrames;
9
+ exports.handleJsonRpcMessage = handleJsonRpcMessage;
10
+ exports.startStdioServer = startStdioServer;
6
11
  /**
7
12
  * MCP Server for E2E Agents - SECURITY HARDENED
8
13
  * Exposes tools for Claude and Playwright agents to discover, generate, and heal tests
@@ -475,12 +480,152 @@ class E2EAgentsMCPServer {
475
480
  }
476
481
  exports.E2EAgentsMCPServer = E2EAgentsMCPServer;
477
482
  /**
478
- * Start MCP server
479
- * Usage: node dist/mcp-server.js
483
+ * Read the package version at runtime so the MCP initialize response
484
+ * always reflects the installed version.
480
485
  */
486
+ function getPackageVersion() {
487
+ try {
488
+ const pkgPath = (0, path_1.join)((0, path_1.dirname)(__dirname), 'package.json');
489
+ const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
490
+ return pkg.version || '0.0.0';
491
+ }
492
+ catch {
493
+ return '0.0.0';
494
+ }
495
+ }
496
+ /**
497
+ * Encode a JSON-RPC message with Content-Length framing.
498
+ * Exported for testability.
499
+ */
500
+ function encodeJsonRpcMessage(message) {
501
+ const body = JSON.stringify(message);
502
+ return `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
503
+ }
504
+ /**
505
+ * Parse Content-Length framed JSON-RPC messages from a buffer.
506
+ * Returns parsed messages and the remaining (unconsumed) buffer.
507
+ * Exported for testability.
508
+ */
509
+ function parseJsonRpcFrames(input) {
510
+ const messages = [];
511
+ let buffer = Buffer.from(input);
512
+ while (true) {
513
+ const headerEnd = buffer.indexOf('\r\n\r\n');
514
+ if (headerEnd === -1)
515
+ break;
516
+ const headerText = buffer.slice(0, headerEnd).toString('utf8');
517
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
518
+ if (!match) {
519
+ buffer = Buffer.alloc(0);
520
+ break;
521
+ }
522
+ const contentLength = Number(match[1]);
523
+ const messageEnd = headerEnd + 4 + contentLength;
524
+ if (buffer.length < messageEnd)
525
+ break;
526
+ const body = buffer.slice(headerEnd + 4, messageEnd).toString('utf8');
527
+ buffer = buffer.slice(messageEnd);
528
+ messages.push(JSON.parse(body));
529
+ }
530
+ return { messages, remainder: buffer };
531
+ }
532
+ /**
533
+ * Handle a single JSON-RPC message against the server.
534
+ * Returns the response message (or null for notifications).
535
+ * Exported for testability.
536
+ */
537
+ async function handleJsonRpcMessage(server, message) {
538
+ const { id, method, params } = message;
539
+ const version = getPackageVersion();
540
+ if (method === 'initialize') {
541
+ return {
542
+ jsonrpc: '2.0',
543
+ id,
544
+ result: {
545
+ protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',
546
+ capabilities: { tools: {}, resources: {}, prompts: {} },
547
+ serverInfo: { name: 'e2e-agents-mcp', version },
548
+ },
549
+ };
550
+ }
551
+ if (method === 'notifications/initialized' || method === 'initialized') {
552
+ return null;
553
+ }
554
+ if (method === 'tools/list') {
555
+ return {
556
+ jsonrpc: '2.0',
557
+ id,
558
+ result: {
559
+ tools: server.getTools().map((tool) => ({
560
+ name: tool.name,
561
+ description: tool.description,
562
+ inputSchema: tool.inputSchema,
563
+ })),
564
+ },
565
+ };
566
+ }
567
+ if (method === 'tools/call') {
568
+ const resultText = await server.callTool(typeof params?.name === 'string' ? params.name : '', typeof params?.arguments === 'object' && params.arguments !== null ? params.arguments : {});
569
+ let isError = false;
570
+ try {
571
+ const parsed = JSON.parse(resultText);
572
+ isError = Boolean(parsed.error);
573
+ }
574
+ catch {
575
+ isError = false;
576
+ }
577
+ return {
578
+ jsonrpc: '2.0',
579
+ id,
580
+ result: { content: [{ type: 'text', text: resultText }], isError },
581
+ };
582
+ }
583
+ if (method === 'resources/list') {
584
+ return { jsonrpc: '2.0', id, result: { resources: [] } };
585
+ }
586
+ if (method === 'prompts/list') {
587
+ return { jsonrpc: '2.0', id, result: { prompts: [] } };
588
+ }
589
+ if (method === 'ping') {
590
+ return { jsonrpc: '2.0', id, result: {} };
591
+ }
592
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
593
+ }
594
+ /**
595
+ * Start MCP server over stdio using Content-Length framed JSON-RPC messages.
596
+ */
597
+ function startStdioServer(repoRoot = process.cwd()) {
598
+ const server = new E2EAgentsMCPServer(repoRoot);
599
+ let buffer = Buffer.alloc(0);
600
+ const sendMessage = (message) => {
601
+ process.stdout.write(encodeJsonRpcMessage(message));
602
+ };
603
+ const sendError = (id, code, msg) => {
604
+ sendMessage({ jsonrpc: '2.0', id, error: { code, message: msg } });
605
+ };
606
+ const processBuffer = () => {
607
+ const { messages, remainder } = parseJsonRpcFrames(buffer);
608
+ buffer = remainder;
609
+ for (const parsed of messages) {
610
+ void handleJsonRpcMessage(server, parsed)
611
+ .then((response) => {
612
+ if (response)
613
+ sendMessage(response);
614
+ })
615
+ .catch((error) => {
616
+ sendError(parsed.id ?? null, -32603, error instanceof Error ? error.message : String(error));
617
+ });
618
+ }
619
+ };
620
+ process.stdin.on('data', (chunk) => {
621
+ buffer = Buffer.concat([buffer, chunk]);
622
+ processBuffer();
623
+ });
624
+ process.stdin.on('end', () => {
625
+ process.exit(0);
626
+ });
627
+ }
481
628
  if (require.main === module) {
482
- const server = new E2EAgentsMCPServer();
483
- console.log('E2E Agents MCP Server started');
484
- console.log('Tools:', server.getTools().map((t) => t.name).join(', '));
629
+ startStdioServer();
485
630
  }
486
631
  exports.default = E2EAgentsMCPServer;
@@ -55,6 +55,11 @@ export declare class LLMProviderFactory {
55
55
  * 5. Error (no provider available)
56
56
  */
57
57
  static createFromEnv(): Promise<LLMProvider>;
58
+ /**
59
+ * Create provider from an explicit preference when supplied, otherwise
60
+ * fall back to environment auto-detection.
61
+ */
62
+ static createFromPreference(providerPreference?: string): Promise<LLMProvider>;
58
63
  /**
59
64
  * Create provider from simple string format
60
65
  *
@@ -1 +1 @@
1
- {"version":3,"file":"provider_factory.d.ts","sourceRoot":"","sources":["../src/provider_factory.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAKR,WAAW,EAIX,cAAc,EAEjB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,kBAAkB;IAC3B;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW;IAmBlD;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAWtD;;;;;;;;;OASG;WACU,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC;IAyElD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,WAAW;CAkC/D;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB;;;OAGG;IACH,OAAO,EAAE,cAAc,CAAC;IAExB;;;OAGG;IACH,QAAQ,EAAE,cAAc,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,mBAAmB,GAAG,wBAAwB,CAAC,CAAC;CACrF;AA+ID;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC;IACxE,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC,CAsCD"}
1
+ {"version":3,"file":"provider_factory.d.ts","sourceRoot":"","sources":["../src/provider_factory.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAKR,WAAW,EAIX,cAAc,EAEjB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,kBAAkB;IAC3B;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW;IAmBlD;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAWtD;;;;;;;;;OASG;WACU,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC;IAyElD;;;OAGG;WACU,oBAAoB,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IASpF;;;;;;;;;;OAUG;IACH,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,WAAW;CAkC/D;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IACzB;;;OAGG;IACH,OAAO,EAAE,cAAc,CAAC;IAExB;;;OAGG;IACH,QAAQ,EAAE,cAAc,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,mBAAmB,GAAG,wBAAwB,CAAC,CAAC;CACrF;AA+ID;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC;IACxE,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC,CAsCD"}
@@ -145,6 +145,17 @@ class LLMProviderFactory {
145
145
  '3. Set OPENAI_API_KEY environment variable\n' +
146
146
  '4. Set LLM_PROVIDER environment variable');
147
147
  }
148
+ /**
149
+ * Create provider from an explicit preference when supplied, otherwise
150
+ * fall back to environment auto-detection.
151
+ */
152
+ static async createFromPreference(providerPreference) {
153
+ const normalized = providerPreference?.trim().toLowerCase();
154
+ if (!normalized || normalized === 'auto') {
155
+ return this.createFromEnv();
156
+ }
157
+ return this.createFromString(normalized);
158
+ }
148
159
  /**
149
160
  * Create provider from simple string format
150
161
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "AI-powered E2E test impact analysis, generation, and healing. Analyzes code changes to identify affected Playwright tests, detects coverage gaps, and generates or repairs specs using pluggable LLM providers (Claude, OpenAI, Ollama). Includes MCP server, traceability, and CI/CD integration.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -28,7 +28,8 @@
28
28
  },
29
29
  "bin": {
30
30
  "e2e-ai-agents": "./dist/cli.js",
31
- "e2e-qa-agent": "./dist/qa-agent/cli.js"
31
+ "e2e-qa-agent": "./dist/qa-agent/cli.js",
32
+ "e2e-agents-mcp": "./dist/mcp-server.js"
32
33
  },
33
34
  "files": [
34
35
  "dist",
@@ -38,7 +39,7 @@
38
39
  ],
39
40
  "scripts": {
40
41
  "build": "npm run build:cjs && npm run build:esm",
41
- "postbuild": "chmod +x dist/cli.js dist/qa-agent/cli.js",
42
+ "postbuild": "chmod +x dist/cli.js dist/qa-agent/cli.js dist/mcp-server.js",
42
43
  "build:cjs": "tsc -p tsconfig.json",
43
44
  "build:esm": "tsc -p tsconfig.esm.json && node scripts/write-esm-package.js",
44
45
  "clean": "rm -rf dist",