@skyramp/mcp 0.0.59 → 0.0.60-rc.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.
Files changed (35) hide show
  1. package/build/index.js +32 -5
  2. package/build/prompts/test-recommendation/analysisOutputPrompt.js +98 -0
  3. package/build/prompts/test-recommendation/recommendationSections.js +226 -0
  4. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +71 -0
  5. package/build/prompts/test-recommendation/test-recommendation-prompt.js +166 -104
  6. package/build/prompts/testGenerationPrompt.js +2 -3
  7. package/build/prompts/testbot/testbot-prompts.js +96 -93
  8. package/build/resources/analysisResources.js +254 -0
  9. package/build/services/ScenarioGenerationService.js +70 -26
  10. package/build/tools/generate-tests/generateIntegrationRestTool.js +54 -1
  11. package/build/tools/generate-tests/generateScenarioRestTool.js +8 -5
  12. package/build/tools/submitReportTool.js +28 -0
  13. package/build/tools/test-maintenance/stateCleanupTool.js +8 -0
  14. package/build/tools/test-recommendation/analyzeRepositoryTool.js +349 -217
  15. package/build/tools/test-recommendation/recommendTestsTool.js +163 -159
  16. package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
  17. package/build/types/RepositoryAnalysis.js +99 -12
  18. package/build/utils/AnalysisStateManager.js +40 -23
  19. package/build/utils/branchDiff.js +47 -0
  20. package/build/utils/pr-comment-parser.js +124 -0
  21. package/build/utils/projectMetadata.js +188 -0
  22. package/build/utils/projectMetadata.test.js +81 -0
  23. package/build/utils/repoScanner.js +378 -0
  24. package/build/utils/routeParsers.js +213 -0
  25. package/build/utils/routeParsers.test.js +87 -0
  26. package/build/utils/scenarioDrafting.js +119 -0
  27. package/build/utils/scenarioDrafting.test.js +66 -0
  28. package/build/utils/trace-parser.js +166 -0
  29. package/build/utils/workspaceAuth.js +16 -0
  30. package/package.json +1 -1
  31. package/build/prompts/test-recommendation/repository-analysis-prompt.js +0 -326
  32. package/build/prompts/test-recommendation/test-mapping-prompt.js +0 -266
  33. package/build/tools/test-recommendation/mapTestsTool.js +0 -243
  34. package/build/types/TestMapping.js +0 -173
  35. package/build/utils/scoring-engine.js +0 -380
@@ -1,207 +1,211 @@
1
+ import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
1
2
  import { z } from "zod";
2
- import { StateManager, } from "../../utils/AnalysisStateManager.js";
3
- import { TestType } from "../../types/TestTypes.js";
3
+ import { simpleGit } from "simple-git";
4
+ import { StateManager, getSessionFilePath, getRegisteredSessions, getSessionData, hasSessionData, } from "../../utils/AnalysisStateManager.js";
4
5
  import { logger } from "../../utils/logger.js";
5
6
  import { AnalyticsService } from "../../services/AnalyticsService.js";
6
- /** Maps each test type to the Skyramp MCP tool that generates it */
7
- const TEST_TYPE_TO_TOOL = {
8
- [TestType.CONTRACT]: "skyramp_contract_test_generation",
9
- [TestType.SMOKE]: "skyramp_smoke_test_generation",
10
- [TestType.INTEGRATION]: "skyramp_integration_test_generation",
11
- [TestType.FUZZ]: "skyramp_fuzz_test_generation",
12
- [TestType.E2E]: "skyramp_e2e_test_generation",
13
- [TestType.LOAD]: "skyramp_load_test_generation",
14
- [TestType.UI]: "skyramp_ui_test_generation",
15
- };
7
+ import { buildRecommendationPrompt } from "../../prompts/test-recommendation/test-recommendation-prompt.js";
8
+ import { repositoryAnalysisSchema } from "../../types/RepositoryAnalysis.js";
9
+ import { parsePRComments } from "../../utils/pr-comment-parser.js";
10
+ import { getWorkspaceAuthHeader } from "../../utils/workspaceAuth.js";
16
11
  /**
17
- * Recommend Tests Tool
18
- * MCP tool for generating actionable test recommendations
12
+ * Extract GitHub owner/repo from the origin remote URL.
13
+ * Handles both SSH (git@github.com:owner/repo.git) and
14
+ * HTTPS (https://github.com/owner/repo.git) formats.
19
15
  */
20
- const recommendTestsSchema = z.object({
21
- stateFile: z
22
- .string()
23
- .describe("Path to state file from skyramp_map_tests (contains both analysis and mapping results)"),
16
+ async function getGitHubSlug(repoPath) {
17
+ try {
18
+ const git = simpleGit(repoPath);
19
+ const remotes = await git.getRemotes(true);
20
+ const origin = remotes.find((r) => r.name === "origin");
21
+ const url = origin?.refs?.fetch;
22
+ if (!url)
23
+ return null;
24
+ const sshMatch = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
25
+ if (sshMatch)
26
+ return { owner: sshMatch[1], repo: sshMatch[2] };
27
+ const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
28
+ if (httpsMatch)
29
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
30
+ return null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ const recommendTestsSchema = {
37
+ sessionId: completable(z.string().describe("Session ID from skyramp_analyze_repository"), async (value) => {
38
+ return Array.from(getRegisteredSessions().keys()).filter((id) => id.startsWith(value.toString()));
39
+ }),
24
40
  topN: z
25
41
  .number()
26
- .default(7)
27
- .describe("Number of top test types to recommend (default: 5)"),
28
- minScore: z
42
+ .optional()
43
+ .describe("Number of recommendations. Defaults to 5."),
44
+ prNumber: z
29
45
  .number()
30
- .default(60)
31
- .describe("Minimum score threshold for recommendations (default: 60)"),
32
- });
46
+ .optional()
47
+ .describe("GitHub PR number. When provided in diff scope, fetches previous TestBot comments to avoid re-recommendations."),
48
+ };
33
49
  const TOOL_NAME = "skyramp_recommend_tests";
34
50
  export function registerRecommendTestsTool(server) {
35
51
  server.registerTool(TOOL_NAME, {
36
- description: `Generate actionable test recommendations with ready-to-use generation prompts.
37
-
38
- **PREREQUISITE**: Call skyramp_map_tests first to get the stateFile path.
52
+ description: `Generate actionable test recommendations based on enriched repository analysis.
39
53
 
40
- This tool reads the saved test mapping results and generates:
41
- - Ordered list of recommended test types (high/medium/low priority)
42
- - Specific test scenarios based on actual repository endpoints and flows
43
- - Artifact availability status and guidance for missing artifacts
44
- - Step-by-step next actions
54
+ **PREREQUISITE**: Call skyramp_analyze_repository first to get the sessionId.
45
55
 
46
- SAMPLE OUTPUT:
47
- \`\`\`
48
- Recommended Tests (Prioritized)
49
- Priority: <PRIORITY HIGH, MEDIUM, OR LOW>
50
- 1. <TEST TYPE> Tests [**CRITICAL: NEVER MENTION THE SCORE HERE IN THE OUTPUT**]
51
- Rationale:
52
- Specific Tests to Create:
53
- Test 1: <TEST NAME>
54
- Description: <TEST DESCRIPTION>
55
- Target Flow: <TARGET FLOW>
56
- \`\`\`
56
+ This tool reads the enriched analysis (endpoints with interactions, drafted scenarios)
57
+ and generates prioritized test recommendations using LLM reasoning over the actual data.
57
58
 
58
- For each recommended test type, you'll get:
59
- - 2-3 specific tests you can create right now
60
- - Required vs. available artifacts. THIS SHOULD NOT CHANGE PRIORITIZATION OF THE TESTS.
61
- - Guidance for creating missing artifacts WITHOUT PROVIDING ANY CLI COMMANDS.
59
+ For each recommended test, you'll get:
60
+ - Priority (high/medium/low) with reasoning based on actual endpoint data
61
+ - Specific test referencing concrete interactions (request→response pairs)
62
+ - For integration/E2E: references to draftedScenarios by scenarioName
63
+ - Which Skyramp tool to call for generation
62
64
 
63
65
  **CRITICAL RULES**:
64
66
  - THE PRIORITY SHOULD ONLY BE DEFINED AS HIGH, MEDIUM, OR LOW NOTHING ELSE.
65
67
  - DO NOT SHOW ANY PRIORITY BREAKDOWN IN THE OUTPUT.
66
68
  - DON'T MARK ANY TEST BLOCKED EVEN IF REQUIRED ARTIFACTS ARE MISSING.
69
+ - Reference specific interactions by description when recommending smoke/contract/fuzz tests.
70
+ - Reference specific draftedScenarios by scenarioName when recommending integration/E2E tests.
67
71
 
68
- Output: TestRecommendation with prioritized, actionable test recommendations.
69
- **CRITICAL:** At the end of the tool execution, MUST display the below message:
70
72
  ** This tool is currently in Early Preview stage. Please verify the results. **`,
71
- inputSchema: recommendTestsSchema.shape,
73
+ inputSchema: recommendTestsSchema,
72
74
  }, async (params) => {
73
75
  let errorResult;
74
76
  try {
75
77
  logger.info("Recommend tests tool invoked", {
76
- stateFile: params.stateFile,
78
+ sessionId: params.sessionId,
77
79
  topN: params.topN,
78
- minScore: params.minScore,
79
80
  });
80
- // Read state file using StateManager
81
- if (!params.stateFile) {
82
- throw new Error("stateFile is required");
81
+ if (!params.sessionId) {
82
+ throw new Error("sessionId is required");
83
83
  }
84
- const stateManager = StateManager.fromStatePath(params.stateFile);
85
- if (!stateManager.exists()) {
86
- throw new Error(`State file not found: ${params.stateFile}. Run skyramp_map_tests first.`);
84
+ // Load session data: try process memory first, then fall back to state file
85
+ let stateData = null;
86
+ if (hasSessionData(params.sessionId)) {
87
+ stateData = getSessionData(params.sessionId);
88
+ logger.info("Loaded analysis from process memory", { sessionId: params.sessionId });
89
+ }
90
+ else {
91
+ // Fall back to state file for backward compatibility
92
+ const registeredPath = getSessionFilePath(params.sessionId);
93
+ const stateManager = registeredPath
94
+ ? StateManager.fromStatePath(registeredPath)
95
+ : StateManager.fromSessionId(params.sessionId);
96
+ if (!stateManager.exists()) {
97
+ throw new Error(`Analysis session not found: ${params.sessionId}. Run skyramp_analyze_repository first.`);
98
+ }
99
+ stateData = await stateManager.readData();
100
+ logger.info("Loaded analysis from state file (legacy)", { sessionId: params.sessionId });
87
101
  }
88
- const stateData = await stateManager.readData();
89
102
  if (!stateData) {
90
- throw new Error(`Failed to read state file: ${params.stateFile}. Run skyramp_map_tests first.`);
103
+ throw new Error(`Failed to read analysis session: ${params.sessionId}. Run skyramp_analyze_repository first.`);
104
+ }
105
+ const { analysisScope } = stateData;
106
+ let { repositoryPath, analysis } = stateData;
107
+ // Fallback: if the LLM saved RepositoryAnalysis at the root level
108
+ // (without the wrapper), detect it and use the data directly.
109
+ if (!analysis && stateData.apiEndpoints) {
110
+ logger.info("Detected unwrapped RepositoryAnalysis — using root data");
111
+ analysis = stateData;
112
+ repositoryPath = repositoryPath || analysis?.metadata?.repositoryName || "unknown";
91
113
  }
92
- const { mapping, repositoryPath, analysisScope, analysis } = stateData;
93
- if (!mapping) {
94
- throw new Error("State file is missing required data (mapping). Run skyramp_map_tests first.");
114
+ if (!analysis) {
115
+ throw new Error("Session is missing analysis data. Run skyramp_analyze_repository first.");
116
+ }
117
+ // Validate analysis against the Zod schema to catch malformed LLM output early
118
+ const parseResult = repositoryAnalysisSchema.safeParse(analysis);
119
+ if (!parseResult.success) {
120
+ const issues = parseResult.error.issues.slice(0, 5).map((i) => `${i.path.join(".")}: ${i.message}`);
121
+ logger.warning("Analysis data has schema issues (proceeding with best-effort)", {
122
+ issueCount: parseResult.error.issues.length,
123
+ sample: issues,
124
+ });
125
+ }
126
+ // Guard critical nested fields the prompt builder accesses
127
+ if (!analysis.apiEndpoints?.endpoints) {
128
+ analysis.apiEndpoints = { totalCount: 0, baseUrl: "", endpoints: [] };
129
+ }
130
+ if (!analysis.businessContext?.draftedScenarios) {
131
+ analysis.businessContext = {
132
+ ...(analysis.businessContext || { mainPurpose: "", userFlows: [], dataFlows: [], integrationPatterns: [] }),
133
+ draftedScenarios: analysis.businessContext?.draftedScenarios || [],
134
+ };
135
+ }
136
+ if (!analysis.authentication) {
137
+ analysis.authentication = { method: "none", configLocation: "", envVarsRequired: [], setupExample: "" };
138
+ }
139
+ if (!analysis.existingTests) {
140
+ analysis.existingTests = {
141
+ frameworks: [], coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 },
142
+ testLocations: {}, hasCoverageReports: false,
143
+ };
95
144
  }
96
145
  const scope = analysisScope || "full_repo";
97
- const isDiffScope = scope === "current_branch_diff";
98
- // Build a diff-context section that gives the LLM explicit targets to test
99
- const apiBaseUrl = (analysis?.apiEndpoints?.baseUrl || "").replace(/\/+$/, "");
100
- let diffContextSection = "";
101
- if (isDiffScope) {
102
- const diff = analysis?.branchDiffContext;
103
- const hasEndpoints = diff &&
104
- (diff.newEndpoints.length > 0 || diff.modifiedEndpoints.length > 0);
105
- const hasChangedFiles = diff && diff.changedFiles.length > 0;
106
- if (hasEndpoints) {
107
- const lines = [];
108
- for (const ep of diff.newEndpoints) {
109
- lines.push(`- **${ep.method} ${apiBaseUrl}${ep.path}** (new) — \`${ep.sourceFile}\``);
146
+ const sessionId = params.sessionId;
147
+ const effectiveTopN = params.topN ?? (scope === "current_branch_diff" ? 7 : 10);
148
+ // Fetch PR context when prNumber is provided and in diff scope
149
+ let prContext = stateData.prContext;
150
+ if (!prContext && params.prNumber && scope === "current_branch_diff") {
151
+ const slug = await getGitHubSlug(repositoryPath);
152
+ if (slug) {
153
+ try {
154
+ prContext = await parsePRComments(slug.owner, slug.repo, params.prNumber);
110
155
  }
111
- for (const ep of diff.modifiedEndpoints) {
112
- lines.push(`- **${ep.method} ${apiBaseUrl}${ep.path}** (modified) \`${ep.sourceFile}\``);
156
+ catch (err) {
157
+ logger.warning("Failed to fetch PR context", {
158
+ error: err instanceof Error ? err.message : String(err),
159
+ });
113
160
  }
114
- diffContextSection = `
115
- ## Changed Endpoints in This PR
116
- ${lines.join("\n")}
117
-
118
- `;
119
161
  }
120
- else if (hasChangedFiles) {
121
- // No route decorators parsed, but files did change show them so the
122
- // LLM can infer which endpoints are affected
123
- diffContextSection = `
124
- ## Changed Files in This PR
125
- ${diff.changedFiles.map((f) => `- \`${f}\``).join("\n")}
126
-
127
- `;
162
+ else {
163
+ logger.debug("Could not determine GitHub owner/repo from git remotesskipping PR context");
128
164
  }
129
165
  }
130
- // Build an explicit action list so the agent knows exactly which
131
- // generate tools to call — ordered by priority.
132
- const buildActionList = (types) => types
133
- .map((t) => {
134
- const tool = TEST_TYPE_TO_TOOL[t];
135
- return tool ? `- **${t}** call \`${tool}\`` : `- **${t}**`;
136
- })
137
- .join("\n");
138
- const highActions = buildActionList(mapping.summary.highPriority);
139
- const mediumActions = buildActionList(mapping.summary.mediumPriority);
140
- // Check if E2E or UI tests are in the priority lists
141
- const allPriority = [
142
- ...mapping.summary.highPriority,
143
- ...mapping.summary.mediumPriority,
144
- ];
145
- const hasE2EOrUI = allPriority.some((t) => t === TestType.E2E || t === TestType.UI);
146
- const traceGuidance = hasE2EOrUI
147
- ? `
148
- ### Trace Files for E2E/UI Tests
149
-
150
- E2E and UI test generation requires pre-recorded trace files. Search the repository for:
151
- - Backend traces: \`**/skyramp*trace*.json\`, \`**/skyramp-traces.json\`
152
- - Playwright traces: \`**/skyramp*playwright*.zip\`, \`**/*playwright*.zip\`
153
-
154
- Look in the test directory, repository root, and \`.skyramp/\` directories.
155
-
156
- **IMPORTANT — Verify trace relevance before using it:**
157
- Before passing a trace file to a test generation tool, inspect its contents to confirm it actually exercises the UI components or pages affected by the PR. A trace recorded before the current changes will not cover new UI elements. If the trace does NOT cover the changed UI:
158
- - Do NOT use it for generating tests for the new changes.
159
- - Report in \`issuesFound\`: "A Playwright trace file was found (<filename>) but it does not cover the new UI changes in this PR. To generate UI tests for the new functionality, record a new trace that exercises the changed pages/components and commit it, then re-run the Testbot."
166
+ const workspaceAuthHeader = repositoryPath
167
+ ? await getWorkspaceAuthHeader(repositoryPath)
168
+ : undefined;
169
+ const prompt = buildRecommendationPrompt(analysis, scope, "all", effectiveTopN, prContext, workspaceAuthHeader);
170
+ // H3: Mode-specific output header
171
+ const modeLabel = scope === "current_branch_diff" ? "PR Mode" : "Repo Mode";
172
+ const modeDesc = scope === "current_branch_diff"
173
+ ? `Focused on branch changes. Top ${effectiveTopN} recommendations — top 4 will be generated, remaining reported.`
174
+ : `Comprehensive test strategy. Top ${effectiveTopN} tests across the entire application.`;
175
+ // J4: PR awareness messaging
176
+ let prAwareness = "";
177
+ if (prContext && prContext.previousRecommendations.length > 0) {
178
+ const implemented = prContext.previousRecommendations.filter(r => r.status === "implemented").length;
179
+ const pending = prContext.previousRecommendations.filter(r => r.status === "recommended").length;
180
+ prAwareness = `\n**PR History**: ${implemented} tests already implemented, ${pending} previously recommended.`;
181
+ if (implemented > 0) {
182
+ prAwareness += ` Building on existing coverage — new recommendations complement what\'s already been added.`;
183
+ }
184
+ }
185
+ // G4: Include totalConsidered instruction in output
186
+ const totalEndpointMethods = analysis.apiEndpoints.endpoints.reduce((acc, ep) => acc + (ep.methods?.length || 0), 0);
187
+ const totalInteractions = analysis.apiEndpoints.endpoints.reduce((acc, ep) => acc + (ep.methods || []).reduce((a2, m) => a2 + (m.interactions?.length || 0), 0), 0);
188
+ const totalScenarios = analysis.businessContext.draftedScenarios.length;
189
+ const output = `# Test Recommendations (${modeLabel})
160
190
 
161
- - **Both found and relevant** → call \`skyramp_e2e_test_generation\` with both trace files
162
- - **Only Playwright ZIP found and relevant** → call \`skyramp_ui_test_generation\` with the Playwright file
163
- - **No traces found** → do NOT silently skip. Include in \`issuesFound\` when submitting your report: "E2E/UI tests were recommended but could not be generated because no Playwright trace file (.zip) was found in the repository. To enable E2E/UI test generation, record a Playwright trace and commit the .zip file, then re-run the Testbot."
164
- `
165
- : "";
166
- const nextActionsSection = mapping.summary.highPriority.length > 0 ||
167
- mapping.summary.mediumPriority.length > 0
168
- ? `
169
- ## Required Next Actions
191
+ **Session**: \`${sessionId}\`
192
+ **Repository**: \`${repositoryPath}\`
193
+ **Mode**: ${modeLabel} ${modeDesc}${prAwareness}
194
+ **Catalog**: ${totalEndpointMethods} endpoint methods, ${totalInteractions} interactions, ${totalScenarios} scenarios
195
+ **Target**: Top ${effectiveTopN} recommendations ranked by value. Top 4 = generate & execute. #5-#${effectiveTopN} = report as additional recommendations.
170
196
 
171
- You MUST call the following Skyramp MCP generate tools, in order.
172
- Do NOT skip any. Do NOT just run existing tests — generate new ones.
197
+ ## Available Resources
198
+ - Summary: \`skyramp://analysis/${sessionId}/summary\`
199
+ - Endpoints: \`skyramp://analysis/${sessionId}/endpoints\`
200
+ - Scenarios: \`skyramp://analysis/${sessionId}/scenarios\`
173
201
 
174
- ### High Priority (call these first)
175
- ${highActions || "none"}
202
+ ---
176
203
 
177
- ${mediumActions ? `### Medium Priority (call after high)\n${mediumActions}\n` : ""}${isDiffScope && ((analysis?.branchDiffContext?.newEndpoints?.length ?? 0) + (analysis?.branchDiffContext?.modifiedEndpoints?.length ?? 0)) > 0 ? `\nTarget the changed endpoint(s) listed above for each generated test. Use the full URL (including base URL) as the \`endpointURL\` parameter when calling generate tools.` : ""}${traceGuidance}
178
- `
179
- : "";
180
- const output = `# Test Recommendations
181
-
182
- **State File**: \`${params.stateFile}\`
183
- **Repository**: \`${repositoryPath}\`
184
- **Scope**: ${isDiffScope ? "current branch diff" : "full repository"}
185
- ${diffContextSection}
186
- ## Priority Summary
187
- - High: ${mapping.summary.highPriority.join(", ") || "none"}
188
- - Medium: ${mapping.summary.mediumPriority.join(", ") || "none"}
189
- - Low: ${mapping.summary.lowPriority.join(", ") || "none"}
190
- ${nextActionsSection}
191
- **MANDATORY RULES**:
192
- 1. Use ONLY "high", "medium", "low" for priority — NO numeric scores.
193
- 2. DO NOT mark any test "blocked" even if artifacts are missing.
194
- 3. DO NOT create or save any files — output everything inline.
195
- 4. For each generated test, pass the proper endpoint URL as the \`endpointURL\` parameter to the generate tool and DO NOT pass base URL as the \`endpointURL\` parameter.
204
+ ${prompt}
196
205
 
197
206
  ** This tool is currently in Early Preview stage. Please verify the results. **`;
198
207
  return {
199
- content: [
200
- {
201
- type: "text",
202
- text: output,
203
- },
204
- ],
208
+ content: [{ type: "text", text: output }],
205
209
  isError: false,
206
210
  };
207
211
  }
@@ -29,7 +29,7 @@ Fields per service:
29
29
  python → pytest or robot
30
30
  typescript / javascript → playwright
31
31
  java → junit
32
- outputDir — service root relative to repo root, e.g. "services/api-gateway"
32
+ outputDir — service root relative to repo root where the test files will be generated, e.g. "services/api-gateway"
33
33
  api.schemaPath — path or URL to OpenAPI/Protobuf/GraphQL schema
34
34
  Search for: openapi.json, swagger.yaml, *.proto, *.graphql
35
35
  Framework defaults: FastAPI → /openapi.json, Express → /api-docs, Spring → /v3/api-docs
@@ -1,19 +1,111 @@
1
1
  import { z } from "zod";
2
- // Zod schemas for validation
2
+ // ── Zod schemas ──
3
3
  export const analysisScopeSchema = z.enum(["full_repo", "current_branch_diff"]);
4
+ export const paramInfoSchema = z.object({
5
+ name: z.string(),
6
+ type: z.string(),
7
+ required: z.boolean(),
8
+ description: z.string().optional(),
9
+ constraints: z.string().optional(),
10
+ });
11
+ export const cookieInfoSchema = z.object({
12
+ name: z.string(),
13
+ sampleValue: z.string(),
14
+ purpose: z.string(),
15
+ httpOnly: z.boolean(),
16
+ secure: z.boolean(),
17
+ });
18
+ export const endpointInteractionSchema = z.object({
19
+ description: z.string(),
20
+ type: z.enum(["success", "error", "edge-case"]),
21
+ request: z.object({
22
+ contentType: z.string().optional(),
23
+ body: z.record(z.any()).optional(),
24
+ headers: z.record(z.string()).optional(),
25
+ queryParams: z.record(z.any()).optional(),
26
+ }),
27
+ response: z.object({
28
+ statusCode: z.number(),
29
+ description: z.string(),
30
+ body: z.record(z.any()).optional(),
31
+ headers: z.record(z.string()).optional(),
32
+ cookies: z.array(cookieInfoSchema).optional(),
33
+ }),
34
+ });
35
+ export const endpointMethodSchema = z.object({
36
+ method: z.string(),
37
+ description: z.string(),
38
+ queryParams: z.array(paramInfoSchema),
39
+ authRequired: z.boolean(),
40
+ authType: z.string().optional(),
41
+ scopes: z.array(z.string()).optional(),
42
+ sourceFile: z.string(),
43
+ interactions: z.array(endpointInteractionSchema),
44
+ dependsOn: z.array(z.string()).optional(),
45
+ createsResource: z.string().optional(),
46
+ });
47
+ export const enrichedEndpointSchema = z.object({
48
+ path: z.string(),
49
+ resourceGroup: z.string(),
50
+ pathParams: z.array(paramInfoSchema),
51
+ methods: z.array(endpointMethodSchema),
52
+ });
53
+ export const chainingRefSchema = z.object({
54
+ sourceStep: z.number(),
55
+ sourceField: z.string(),
56
+ sourceLocation: z.enum(["body", "header", "cookie"]),
57
+ targetParam: z.string(),
58
+ targetLocation: z.enum(["path", "body", "query", "header", "cookie"]),
59
+ });
60
+ export const scenarioStepSchema = z.object({
61
+ order: z.number(),
62
+ method: z.string(),
63
+ path: z.string(),
64
+ description: z.string(),
65
+ interactionType: z.enum(["success", "error", "edge-case"]),
66
+ requestBody: z.record(z.any()).optional(),
67
+ expectedStatusCode: z.number(),
68
+ expectedResponseFields: z.array(z.string()).optional(),
69
+ chainsFrom: z
70
+ .union([chainingRefSchema, z.array(chainingRefSchema)])
71
+ .optional(),
72
+ });
73
+ export const draftedScenarioSchema = z.object({
74
+ scenarioName: z.string(),
75
+ description: z.string(),
76
+ category: z.enum([
77
+ "crud",
78
+ "workflow",
79
+ "auth",
80
+ "error-handling",
81
+ "data-validation",
82
+ ]),
83
+ priority: z.enum(["high", "medium", "low"]),
84
+ steps: z.array(scenarioStepSchema),
85
+ chainingKeys: z.array(z.string()),
86
+ requiresAuth: z.boolean(),
87
+ estimatedComplexity: z.enum(["simple", "moderate", "complex"]),
88
+ source: z.enum(["code-inferred", "trace", "documentation"]).optional(),
89
+ });
4
90
  export const branchDiffContextSchema = z.object({
5
91
  currentBranch: z.string(),
6
92
  baseBranch: z.string(),
7
93
  changedFiles: z.array(z.string()),
8
94
  newEndpoints: z.array(z.object({
9
95
  path: z.string(),
10
- method: z.string(),
11
- sourceFile: z.string(),
96
+ methods: z.array(z.object({
97
+ method: z.string(),
98
+ sourceFile: z.string(),
99
+ interactionCount: z.number(),
100
+ })),
12
101
  })),
13
102
  modifiedEndpoints: z.array(z.object({
14
103
  path: z.string(),
15
- method: z.string(),
16
- sourceFile: z.string(),
104
+ methods: z.array(z.object({
105
+ method: z.string(),
106
+ sourceFile: z.string(),
107
+ changeType: z.enum(["added", "modified", "removed"]),
108
+ })),
17
109
  })),
18
110
  affectedServices: z.array(z.string()),
19
111
  summary: z.string().optional(),
@@ -62,6 +154,7 @@ export const repositoryAnalysisSchema = z.object({
62
154
  userFlows: z.array(z.string()),
63
155
  dataFlows: z.array(z.string()),
64
156
  integrationPatterns: z.array(z.string()),
157
+ draftedScenarios: z.array(draftedScenarioSchema),
65
158
  }),
66
159
  artifacts: z.object({
67
160
  openApiSpecs: z.array(z.object({
@@ -86,13 +179,7 @@ export const repositoryAnalysisSchema = z.object({
86
179
  apiEndpoints: z.object({
87
180
  totalCount: z.number(),
88
181
  baseUrl: z.string(),
89
- endpoints: z.array(z.object({
90
- path: z.string(),
91
- method: z.string(),
92
- resourceGroup: z.string(),
93
- authRequired: z.boolean(),
94
- sourceFile: z.string().optional(),
95
- })),
182
+ endpoints: z.array(enrichedEndpointSchema),
96
183
  }),
97
184
  authentication: z.object({
98
185
  method: z.enum(["bearer", "api-key", "oauth2", "basic", "jwt", "none"]),
@@ -1,7 +1,39 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as os from "os";
4
+ import * as crypto from "crypto";
4
5
  import { logger } from "./logger.js";
6
+ /**
7
+ * Per-process registry mapping sessionId → stateFilePath.
8
+ * Resource list callbacks iterate this map so each stdio process only
9
+ * advertises its own sessions, eliminating cross-process leakage when
10
+ * multiple MCP clients share the same filesystem (e.g. /tmp).
11
+ */
12
+ const processSessionRegistry = new Map();
13
+ /**
14
+ * In-memory session store: sessionId → analysis data.
15
+ * Eliminates the need for the LLM to read/write state files on disk.
16
+ * The analyze tool stores a skeleton here; the recommend tool reads it.
17
+ */
18
+ const inMemorySessionStore = new Map();
19
+ export function registerSession(sessionId, stateFilePath) {
20
+ processSessionRegistry.set(sessionId, stateFilePath);
21
+ }
22
+ export function getSessionFilePath(sessionId) {
23
+ return processSessionRegistry.get(sessionId);
24
+ }
25
+ export function getRegisteredSessions() {
26
+ return processSessionRegistry;
27
+ }
28
+ export function storeSessionData(sessionId, data) {
29
+ inMemorySessionStore.set(sessionId, data);
30
+ }
31
+ export function getSessionData(sessionId) {
32
+ return inMemorySessionStore.get(sessionId);
33
+ }
34
+ export function hasSessionData(sessionId) {
35
+ return inMemorySessionStore.has(sessionId);
36
+ }
5
37
  /**
6
38
  * File prefix mapping for each state type
7
39
  */
@@ -24,16 +56,22 @@ export class StateManager {
24
56
  /**
25
57
  * Create a new state manager
26
58
  * @param stateType Type of state (analysis, recommendation)
27
- * @param sessionId Unique session identifier (defaults to timestamp)
59
+ * @param sessionId Unique session identifier (defaults to UUID)
28
60
  * @param stateDir Directory to store state files (defaults to /tmp)
29
61
  */
30
62
  constructor(stateType = "analysis", sessionId, stateDir) {
31
63
  this.stateType = stateType;
32
- this.sessionId = sessionId || Date.now().toString();
64
+ this.sessionId = sessionId || crypto.randomUUID();
33
65
  const baseDir = stateDir || os.tmpdir();
34
66
  const prefix = STATE_FILE_PREFIXES[stateType];
35
67
  this.stateFile = path.join(baseDir, `${prefix}-${this.sessionId}.json`);
36
68
  }
69
+ /**
70
+ * Create state manager from a sessionId (resolves the state file path internally)
71
+ */
72
+ static fromSessionId(sessionId, stateType = "recommendation", stateDir) {
73
+ return new StateManager(stateType, sessionId, stateDir);
74
+ }
37
75
  /**
38
76
  * Create state manager from existing state file path
39
77
  */
@@ -118,42 +156,24 @@ export class StateManager {
118
156
  throw new Error(`Failed to write state file: ${error.message}`);
119
157
  }
120
158
  }
121
- /**
122
- * Get the state file path
123
- */
124
159
  getStatePath() {
125
160
  return this.stateFile;
126
161
  }
127
- /**
128
- * Get the session ID
129
- */
130
162
  getSessionId() {
131
163
  return this.sessionId;
132
164
  }
133
- /**
134
- * Get the state type
135
- */
136
165
  getStateType() {
137
166
  return this.stateType;
138
167
  }
139
- /**
140
- * Check if state file exists
141
- */
142
168
  exists() {
143
169
  return fs.existsSync(this.stateFile);
144
170
  }
145
- /**
146
- * Delete the state file
147
- */
148
171
  async delete() {
149
172
  if (this.exists()) {
150
173
  await fs.promises.unlink(this.stateFile);
151
174
  logger.debug(`Deleted state file: ${this.stateFile}`);
152
175
  }
153
176
  }
154
- /**
155
- * Get state file size in bytes
156
- */
157
177
  async getSize() {
158
178
  if (!this.exists()) {
159
179
  return 0;
@@ -161,9 +181,6 @@ export class StateManager {
161
181
  const stats = await fs.promises.stat(this.stateFile);
162
182
  return stats.size;
163
183
  }
164
- /**
165
- * Get human-readable state file size
166
- */
167
184
  async getSizeFormatted() {
168
185
  const bytes = await this.getSize();
169
186
  if (bytes === 0)