@skyramp/mcp 0.3.0 → 0.3.1-ldw-beta

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 (112) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +19 -13
  3. package/build/commands/commandLibrary.test.d.ts +1 -0
  4. package/build/commands/commandLibrary.test.js +59 -0
  5. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  6. package/build/commands/localDevTestChangesCommand.js +201 -0
  7. package/build/index.js +80 -6
  8. package/build/playwright/traceRecordingPrompt.js +2 -0
  9. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  10. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  11. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
  12. package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
  13. package/build/prompts/local-dev/local-dev-plan.js +466 -0
  14. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  15. package/build/prompts/local-dev/local-dev-prompts.js +190 -0
  16. package/build/prompts/prompt-utils.d.ts +8 -0
  17. package/build/prompts/prompt-utils.js +33 -0
  18. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
  22. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  23. package/build/prompts/testbot/testbot-prompts.js +5 -34
  24. package/build/resources/testbotResource.js +2 -1
  25. package/build/services/AnalyticsService.d.ts +1 -1
  26. package/build/services/TestExecutionService.d.ts +2 -1
  27. package/build/services/TestExecutionService.js +8 -3
  28. package/build/services/TestExecutionService.test.js +83 -3
  29. package/build/services/TestGenerationService.d.ts +2 -2
  30. package/build/services/TestGenerationService.js +7 -21
  31. package/build/services/containerEnv.js +3 -1
  32. package/build/tool-phases.js +6 -0
  33. package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
  34. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  35. package/build/tools/enrichTestWithMocksTool.js +726 -0
  36. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  37. package/build/tools/enrichTestWithMocksTool.test.js +266 -0
  38. package/build/tools/executeSkyrampTestTool.d.ts +11 -0
  39. package/build/tools/executeSkyrampTestTool.js +63 -22
  40. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  41. package/build/tools/executeSkyrampTestTool.test.js +36 -0
  42. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
  43. package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
  44. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -21
  45. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +1 -0
  46. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  47. package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
  48. package/build/tools/generate-tests/generateMockRestTool.js +234 -22
  49. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  50. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
  51. package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
  52. package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
  53. package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
  54. package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
  55. package/build/tools/localDevWorkerComposeTool.js +224 -0
  56. package/build/tools/localDevWorkerComposeTool.test.d.ts +1 -0
  57. package/build/tools/localDevWorkerComposeTool.test.js +129 -0
  58. package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
  59. package/build/tools/localDevWorkflowFixes.test.js +255 -0
  60. package/build/tools/one-click/oneClickTool.d.ts +13 -0
  61. package/build/tools/one-click/oneClickTool.js +195 -24
  62. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  63. package/build/tools/one-click/oneClickTool.test.js +172 -0
  64. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  65. package/build/tools/preflightMockCheckTool.js +96 -0
  66. package/build/tools/queryProxyMocksTool.d.ts +70 -0
  67. package/build/tools/queryProxyMocksTool.js +522 -0
  68. package/build/tools/queryProxyMocksTool.test.d.ts +1 -0
  69. package/build/tools/queryProxyMocksTool.test.js +164 -0
  70. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  71. package/build/tools/test-management/analyzeChangesTool.js +61 -38
  72. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  73. package/build/types/OneClickCommands.d.ts +1 -1
  74. package/build/types/RepositoryAnalysis.d.ts +117 -0
  75. package/build/types/RepositoryAnalysis.js +16 -2
  76. package/build/types/TestExecution.d.ts +1 -0
  77. package/build/types/TestTypes.d.ts +25 -7
  78. package/build/types/TestTypes.js +19 -5
  79. package/build/utils/analyze-openapi.js +18 -1
  80. package/build/utils/analyze-openapi.test.d.ts +1 -0
  81. package/build/utils/analyze-openapi.test.js +19 -0
  82. package/build/utils/branchDiff.d.ts +17 -1
  83. package/build/utils/branchDiff.js +99 -14
  84. package/build/utils/branchDiff.test.d.ts +1 -0
  85. package/build/utils/branchDiff.test.js +109 -0
  86. package/build/utils/featureFlags.d.ts +16 -0
  87. package/build/utils/featureFlags.js +20 -0
  88. package/build/utils/featureFlags.test.js +57 -2
  89. package/build/utils/grpcMockValidation.d.ts +1 -0
  90. package/build/utils/grpcMockValidation.js +49 -0
  91. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  92. package/build/utils/grpcMockValidation.test.js +41 -0
  93. package/build/utils/httpMethodValidation.d.ts +4 -0
  94. package/build/utils/httpMethodValidation.js +15 -0
  95. package/build/utils/mockCompatibility.d.ts +49 -0
  96. package/build/utils/mockCompatibility.js +82 -0
  97. package/build/utils/mockCompatibility.test.d.ts +1 -0
  98. package/build/utils/mockCompatibility.test.js +79 -0
  99. package/build/utils/progress.js +10 -5
  100. package/build/utils/versions.d.ts +1 -0
  101. package/build/utils/versions.js +2 -0
  102. package/build/utils/workspaceAuth.d.ts +9 -0
  103. package/build/utils/workspaceAuth.js +51 -1
  104. package/build/workspace/workspace.d.ts +28 -14
  105. package/build/workspace/workspace.js +3 -1
  106. package/node_modules/playwright/lib/mcp/browser/tools/screenshot.js +1 -1
  107. package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +45 -0
  108. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +6 -22
  109. package/node_modules/playwright/lib/mcp/test/resultCode.test.js +48 -0
  110. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  111. package/node_modules/playwright/package.json +1 -1
  112. package/package.json +2 -2
@@ -1,8 +1,16 @@
1
1
  import { z } from "zod";
2
- import { lookupCommand, getAllCommands } from "../../commands/commandLibrary.js";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { LOCAL_DEV_SKILL_CONTENT } from "../../commands/localDevTestChangesCommand.js";
5
+ import { lookupCommand, getAllCommands, } from "../../commands/commandLibrary.js";
3
6
  import { logger } from "../../utils/logger.js";
4
7
  import { AnalyticsService } from "../../services/AnalyticsService.js";
5
8
  const TOOL_NAME = "skyramp_one_click_tool";
9
+ export var CallingAgentIde;
10
+ (function (CallingAgentIde) {
11
+ CallingAgentIde["Cursor"] = "cursor";
12
+ CallingAgentIde["Claude"] = "claude";
13
+ })(CallingAgentIde || (CallingAgentIde = {}));
6
14
  /**
7
15
  * Generates the `workflow` parameter description from the live command library.
8
16
  * The LLM reads this when filling the tool call, matches the user's intent against
@@ -10,6 +18,92 @@ const TOOL_NAME = "skyramp_one_click_tool";
10
18
  * matching workflow ID. No hardcoded enum — adding a new workflow to the library
11
19
  * automatically updates this description.
12
20
  */
21
+ async function pathExists(filePath) {
22
+ try {
23
+ await fs.access(filePath);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ async function assertRepositoryPathIsSafe(repositoryPath) {
31
+ const stat = await fs.stat(repositoryPath).catch(() => undefined);
32
+ if (!stat?.isDirectory()) {
33
+ throw new Error(`repositoryPath must be an existing directory: ${repositoryPath}`);
34
+ }
35
+ if (!(await pathExists(path.join(repositoryPath, ".git")))) {
36
+ throw new Error(`repositoryPath must point to a git repository: ${repositoryPath}`);
37
+ }
38
+ }
39
+ function normalizeCallingAgentIde(callingAgent) {
40
+ switch (callingAgent?.trim().toLowerCase()) {
41
+ case CallingAgentIde.Cursor:
42
+ return CallingAgentIde.Cursor;
43
+ case CallingAgentIde.Claude:
44
+ return CallingAgentIde.Claude;
45
+ default:
46
+ return undefined;
47
+ }
48
+ }
49
+ function resolveLocalDevSkillTarget(repositoryPath, callingAgent) {
50
+ switch (callingAgent) {
51
+ case CallingAgentIde.Cursor:
52
+ return {
53
+ agent: CallingAgentIde.Cursor,
54
+ skillPath: path.join(repositoryPath, ".cursor", "skills", "local-dev", "SKILL.md"),
55
+ };
56
+ case CallingAgentIde.Claude:
57
+ return {
58
+ agent: CallingAgentIde.Claude,
59
+ skillPath: path.join(repositoryPath, ".claude", "skills", "local-dev", "SKILL.md"),
60
+ };
61
+ default:
62
+ return undefined;
63
+ }
64
+ }
65
+ async function ensureLocalDevSkillFile(repositoryPath, callingAgent) {
66
+ await assertRepositoryPathIsSafe(repositoryPath);
67
+ const target = resolveLocalDevSkillTarget(repositoryPath, callingAgent);
68
+ if (!target)
69
+ return {};
70
+ if (await pathExists(target.skillPath))
71
+ return { target };
72
+ await fs.mkdir(path.dirname(target.skillPath), { recursive: true });
73
+ await fs.writeFile(target.skillPath, LOCAL_DEV_SKILL_CONTENT, "utf8");
74
+ return { target, installed: target.skillPath };
75
+ }
76
+ async function buildLocalDevBootstrapStatus(params) {
77
+ if (params.workflow !== "local_dev_test_changes")
78
+ return "";
79
+ const skillResult = await ensureLocalDevSkillFile(params.repositoryPath, params.callingAgent);
80
+ const workspacePath = path.join(params.repositoryPath, ".skyramp", "workspace.yml");
81
+ const workspaceExists = await pathExists(workspacePath);
82
+ const hasUnsupportedCallingAgent = params.rawCallingAgent !== undefined && params.callingAgent === undefined;
83
+ const skillStatus = skillResult.installed
84
+ ? `Installed local-dev skill file for ${skillResult.target?.agent}:
85
+ - ${skillResult.installed}`
86
+ : skillResult.target
87
+ ? `Local-dev skill file already installed for ${skillResult.target.agent}: ${skillResult.target.skillPath}`
88
+ : hasUnsupportedCallingAgent
89
+ ? "Local-dev skill file not installed: this agent does not support repo-local skills, so skill creation was skipped."
90
+ : "Local-dev skill file not installed: pass `callingAgent` as `cursor` or `claude` to install a repo-local skill. Agents without repo-local skill support can ignore this step.";
91
+ return `
92
+ ## Bootstrap Status
93
+
94
+ ${skillStatus}
95
+
96
+ Base branch: ${params.baseBranch ?? "main"}
97
+ skyramp_local_dev baseCommit: ${params.baseBranch ?? "main"}
98
+ Services to mock: ${params.servicesToMock ?? "not specified"}
99
+ Real services: ${params.realServices ?? "not specified"}
100
+ Mock protocols: ${params.mockProtocols ?? "not specified"}
101
+ Calling agent: ${params.rawCallingAgent ?? params.callingAgent ?? "not specified"}
102
+
103
+ Workspace config: ${workspaceExists ? `found at ${workspacePath}` : `missing at ${workspacePath}`}
104
+
105
+ `;
106
+ }
13
107
  function buildWorkflowParamDescription() {
14
108
  const commands = getAllCommands();
15
109
  const sections = commands.map((cmd) => {
@@ -29,7 +123,31 @@ function buildWorkflowParamDescription() {
29
123
  return (`The workflow to execute. Match the user's intent against the available workflows below and pass the matching workflow ID.\n\n` +
30
124
  `Available workflows:\n${sections.join("\n\n")}`);
31
125
  }
32
- const oneClickSchema = z.object({
126
+ function isLocalDevWorkflowAvailable() {
127
+ return getAllCommands().some((cmd) => cmd.id === "local_dev_test_changes");
128
+ }
129
+ function buildToolDescription() {
130
+ const localDevInstructions = isLocalDevWorkflowAvailable()
131
+ ? `- When the user asks to run Skyramp local-dev, test local changes, validate a feature branch, test a branch against a base/prod/main branch, or mock downstream services for local testing: MUST call this tool with workflow \`local_dev_test_changes\`, \`callingAgent\` set to the current MCP client (\`cursor\` or \`claude\` for skill installation; VS Code does not support repo-local Agent Skills), \`repositoryPath\`, and \`baseBranch\`. Do NOT self-orchestrate — call this tool first and follow the returned instructions.\n`
132
+ : "";
133
+ return `Execute predefined, multi-step testing workflows with a single tool call.
134
+
135
+ **Purpose**: The One-Click Commands tool provides a mechanism to define and execute complex, multi-step testing workflows by orchestrating multiple MCP tools in sequence. Commands enable users to accomplish high-level testing goals (e.g., "comprehensively test this endpoint") through a single invocation, without requiring new MCP tool implementations for each workflow.
136
+
137
+ **When to use**:
138
+ ${localDevInstructions}- When the user names a specific endpoint and explicitly asks for comprehensive, thorough, or deep testing (e.g. "comprehensively test this endpoint", "thoroughly test the products endpoint"): MUST call this tool with workflow \`test_given_endpoint_comprehensively\`. Do NOT self-orchestrate — call this tool first and follow the returned instructions.
139
+ - When the user wants to scan the entire repo (no specific endpoint, no PR diff) to get ranked recommendations, generate top N types, and execute: MUST call this tool with workflow \`full_repo_scan_recommend_generate_and_execute_top_n_tests\`.
140
+ - Do NOT invoke these workflows for simple single-tool requests such as "generate a smoke test" or "run this test" — call the relevant tool directly instead.
141
+
142
+ **How it works**: Pass the \`workflow\` ID that matches the user's intent (see the \`workflow\` parameter description for available workflows and their trigger phrases). The tool returns a **workflow specification** (step-by-step instructions). You must then **execute that workflow** by calling the listed MCP tools in order, passing outputs (e.g. state file paths) from one step as inputs to the next. The tool does not execute other tools itself; it provides the declarative workflow for you to run.
143
+
144
+ **Design**:
145
+ - **MCP-only**: Workflows orchestrate only MCP tools; no CLI execution.
146
+ - **Declarative**: Workflows are sequences of tool calls with dependencies.
147
+ - **State-efficient**: State files are created only by recommendation/map tools (in system temp, e.g. /tmp); do not create any state files in the user repository.
148
+ - **Composable**: Complex workflows are built from existing Skyramp tools.`;
149
+ }
150
+ const baseOneClickSchema = z.object({
33
151
  workflow: z.string().describe(buildWorkflowParamDescription()),
34
152
  repositoryPath: z
35
153
  .string()
@@ -51,10 +169,64 @@ const oneClickSchema = z.object({
51
169
  .optional()
52
170
  .describe("Authentication/session token for test execution. Always pass this to skyramp_execute_test to avoid auth errors; use empty string '' if no auth required."),
53
171
  });
172
+ function buildOneClickSchema() {
173
+ if (!isLocalDevWorkflowAvailable()) {
174
+ return baseOneClickSchema;
175
+ }
176
+ return baseOneClickSchema.extend({
177
+ baseBranch: z
178
+ .string()
179
+ .optional()
180
+ .default("main")
181
+ .describe("Base branch, commit SHA, or tag to diff against. Used by local_dev_test_changes workflow. Default: 'main'. If the user names a branch such as 'prod branch', pass that exact branch only when it exists or ask for clarification; do not silently substitute main."),
182
+ servicesToMock: z
183
+ .string()
184
+ .optional()
185
+ .describe("Comma-separated downstream service names the user explicitly wants mocked in the local_dev_test_changes workflow. When the user says 'mock all', 'mock everything', 'mock all downstream', or 'mock all except X', pass the sentinel value \"all\" (not an empty string and not omitted). Omit only when the user did not express any intent to mock services."),
186
+ realServices: z
187
+ .string()
188
+ .optional()
189
+ .describe("Comma-separated service names that should receive real traffic in the local_dev_test_changes workflow, such as infrastructure, the service under test, or first-party local dependencies."),
190
+ mockProtocols: z
191
+ .string()
192
+ .optional()
193
+ .describe("Comma-separated protocols the user explicitly wants mocked in local_dev_test_changes, such as 'rest', 'grpc', or 'kafka'. Omit to use the workflow default of mocking only true third-party downstream services."),
194
+ proxyUrl: z
195
+ .string()
196
+ .optional()
197
+ .describe("Direct URL (host:port or full URL, including an ingress route) of a Skyramp worker running as a proxy. When provided, local_dev_test_changes queries it via skyramp_query_proxy_mocks for real recorded downstream traffic. Mutually exclusive with proxyKubeconfig/proxyNamespace."),
198
+ proxyKubeconfig: z
199
+ .string()
200
+ .optional()
201
+ .describe("Absolute path to a kubeconfig for reaching a Skyramp worker proxy in-cluster (used with proxyNamespace). Mutually exclusive with proxyUrl."),
202
+ proxyNamespace: z
203
+ .string()
204
+ .optional()
205
+ .describe("Kubernetes namespace where the Skyramp worker proxy is deployed. Required together with proxyKubeconfig."),
206
+ callingAgent: z
207
+ .string()
208
+ .optional()
209
+ .describe("Calling MCP client for local_dev_test_changes. Pass 'cursor' or 'claude' to install only that agent's local-dev skill file under .cursor or .claude. Pass 'vscode' if calling from VS Code; skill creation will be skipped."),
210
+ });
211
+ }
212
+ function renderStepGuidance(step, cmd, params) {
213
+ let guidance = step.conditionalGuidance ?? "";
214
+ if (!guidance || cmd.id !== "local_dev_test_changes")
215
+ return guidance;
216
+ const baseCommit = params.baseBranch ?? "main";
217
+ guidance = guidance
218
+ .replaceAll("<BASE_BRANCH from one-click baseBranch, default main>", baseCommit)
219
+ .replaceAll("<BASE_BRANCH>", baseCommit)
220
+ .replaceAll("<servicesToMock from one-click params, if provided; use all when the user requested mock all>", params.servicesToMock ?? "not specified")
221
+ .replaceAll("<realServices from one-click params, if provided>", params.realServices ?? "not specified")
222
+ .replaceAll("<mockProtocols from one-click params, if provided>", params.mockProtocols ?? "not specified");
223
+ return guidance;
224
+ }
54
225
  /**
55
226
  * Build workflow instruction text from any command definition and params.
227
+ * Optional `stepsPreamble` is inserted between the intro header and Step 0.
56
228
  */
57
- function buildWorkflowFromCommand(cmd, params) {
229
+ function buildWorkflowFromCommand(cmd, params, stepsPreamble = "") {
58
230
  const getParamDisplay = (key) => {
59
231
  const v = params[key];
60
232
  if (v === undefined || v === null)
@@ -90,8 +262,9 @@ function buildWorkflowFromCommand(cmd, params) {
90
262
  block += `\n\nCapture for later steps: ${tc.outputs.join(", ")}.`;
91
263
  }
92
264
  }
93
- if (step.conditionalGuidance) {
94
- block += `\n\n**Note**: ${step.conditionalGuidance}`;
265
+ const conditionalGuidance = renderStepGuidance(step, cmd, params);
266
+ if (conditionalGuidance) {
267
+ block += `\n\n**Note**: ${conditionalGuidance}`;
95
268
  }
96
269
  stepLines.push(block);
97
270
  }
@@ -108,6 +281,7 @@ Execute the following steps in order. Pass outputs (e.g. state file path) from o
108
281
 
109
282
  `;
110
283
  return (intro +
284
+ stepsPreamble +
111
285
  stepLines.join("\n\n---\n\n") +
112
286
  `
113
287
 
@@ -122,23 +296,9 @@ Execute the following steps in order. Pass outputs (e.g. state file path) from o
122
296
  `);
123
297
  }
124
298
  export function registerOneClickTool(server) {
299
+ const oneClickSchema = buildOneClickSchema();
125
300
  server.registerTool(TOOL_NAME, {
126
- description: `Execute predefined, multi-step testing workflows with a single tool call.
127
-
128
- **Purpose**: The One-Click Commands tool provides a mechanism to define and execute complex, multi-step testing workflows by orchestrating multiple MCP tools in sequence. Commands enable users to accomplish high-level testing goals (e.g., "comprehensively test this endpoint") through a single invocation, without requiring new MCP tool implementations for each workflow.
129
-
130
- **When to use**:
131
- - When the user names a specific endpoint and explicitly asks for comprehensive, thorough, or deep testing (e.g. "comprehensively test this endpoint", "thoroughly test the products endpoint"): MUST call this tool with workflow \`test_given_endpoint_comprehensively\`. Do NOT self-orchestrate — call this tool first and follow the returned instructions.
132
- - When the user wants to scan the entire repo (no specific endpoint, no PR diff) to get ranked recommendations, generate top N types, and execute: MUST call this tool with workflow \`full_repo_scan_recommend_generate_and_execute_top_n_tests\`.
133
- - Do NOT invoke these workflows for simple single-tool requests such as "generate a smoke test" or "run this test" — call the relevant tool directly instead.
134
-
135
- **How it works**: Pass the \`workflow\` ID that matches the user's intent (see the \`workflow\` parameter description for available workflows and their trigger phrases). The tool returns a **workflow specification** (step-by-step instructions). You must then **execute that workflow** by calling the listed MCP tools in order, passing outputs (e.g. state file paths) from one step as inputs to the next. The tool does not execute other tools itself; it provides the declarative workflow for you to run.
136
-
137
- **Design**:
138
- - **MCP-only**: Workflows orchestrate only MCP tools; no CLI execution.
139
- - **Declarative**: Workflows are sequences of tool calls with dependencies.
140
- - **State-efficient**: State files are created only by recommendation/map tools (in system temp, e.g. /tmp); do not create any state files in the user repository.
141
- - **Composable**: Complex workflows are built from existing Skyramp tools.`,
301
+ description: buildToolDescription(),
142
302
  inputSchema: oneClickSchema.shape,
143
303
  }, async (params) => {
144
304
  let errorResult;
@@ -148,6 +308,7 @@ export function registerOneClickTool(server) {
148
308
  repositoryPath: params.repositoryPath,
149
309
  });
150
310
  const cmd = lookupCommand(params.workflow);
311
+ const normalizedCallingAgent = normalizeCallingAgentIde(params.callingAgent);
151
312
  const oneClickParams = {
152
313
  workflow: params.workflow,
153
314
  repositoryPath: params.repositoryPath,
@@ -155,8 +316,18 @@ export function registerOneClickTool(server) {
155
316
  apiSchema: params.apiSchema,
156
317
  topN: params.topN ?? 3,
157
318
  token: params.token ?? "",
319
+ baseBranch: params.baseBranch,
320
+ servicesToMock: params.servicesToMock,
321
+ realServices: params.realServices,
322
+ mockProtocols: params.mockProtocols,
323
+ proxyUrl: params.proxyUrl,
324
+ proxyKubeconfig: params.proxyKubeconfig,
325
+ proxyNamespace: params.proxyNamespace,
326
+ callingAgent: normalizedCallingAgent,
327
+ rawCallingAgent: params.callingAgent,
158
328
  };
159
- const workflowText = buildWorkflowFromCommand(cmd, oneClickParams);
329
+ const bootstrapStatus = await buildLocalDevBootstrapStatus(oneClickParams);
330
+ const workflowText = buildWorkflowFromCommand(cmd, oneClickParams, bootstrapStatus);
160
331
  return {
161
332
  content: [
162
333
  {
@@ -182,9 +353,9 @@ export function registerOneClickTool(server) {
182
353
  return errorResult;
183
354
  }
184
355
  finally {
185
- AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
356
+ void Promise.resolve(AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
186
357
  workflow: params.workflow,
187
- }).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
358
+ })).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
188
359
  }
189
360
  });
190
361
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,172 @@
1
+ import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import { execFileSync } from "child_process";
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
7
+ AnalyticsService: { pushMCPToolEvent: jest.fn() },
8
+ }));
9
+ const { registerOneClickTool } = await import("./oneClickTool.js");
10
+ function registerToolForTest() {
11
+ let registered;
12
+ const server = {
13
+ registerTool: jest.fn((name, config, handler) => {
14
+ registered = { name, config, handler };
15
+ }),
16
+ };
17
+ registerOneClickTool(server);
18
+ if (!registered)
19
+ throw new Error("tool was not registered");
20
+ return registered;
21
+ }
22
+ describe("skyramp_one_click_tool", () => {
23
+ let tmpDir;
24
+ let originalLocalDevEnv;
25
+ beforeEach(() => {
26
+ originalLocalDevEnv = process.env.SKYRAMP_FEATURE_LOCAL_DEV;
27
+ process.env.SKYRAMP_FEATURE_LOCAL_DEV = "1";
28
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-"));
29
+ execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" });
30
+ });
31
+ afterEach(() => {
32
+ if (originalLocalDevEnv === undefined) {
33
+ delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
34
+ }
35
+ else {
36
+ process.env.SKYRAMP_FEATURE_LOCAL_DEV = originalLocalDevEnv;
37
+ }
38
+ fs.rmSync(tmpDir, { recursive: true, force: true });
39
+ });
40
+ it("routes local-dev requests and installs local-dev skill files", async () => {
41
+ const tool = registerToolForTest();
42
+ const result = await tool.handler({
43
+ workflow: "local_dev_test_changes",
44
+ repositoryPath: tmpDir,
45
+ callingAgent: "cursor",
46
+ baseBranch: "checkr-prod-flow",
47
+ });
48
+ const text = result.content[0].text;
49
+ expect(result.isError).toBe(false);
50
+ expect(text).toContain('skyramp_local_dev baseCommit: checkr-prod-flow');
51
+ const cursorSkill = fs.readFileSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"), "utf-8");
52
+ expect(cursorSkill).toContain("mock only true third-party downstream services");
53
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(true);
54
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
55
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
56
+ });
57
+ it("installs only the Claude local-dev skill file", async () => {
58
+ const tool = registerToolForTest();
59
+ const result = await tool.handler({
60
+ workflow: "local_dev_test_changes",
61
+ repositoryPath: tmpDir,
62
+ callingAgent: "claude",
63
+ });
64
+ expect(result.isError).toBe(false);
65
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(true);
66
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
67
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
68
+ });
69
+ it("skips skill creation for agents without repo-local skill support", async () => {
70
+ const tool = registerToolForTest();
71
+ const result = await tool.handler({
72
+ workflow: "local_dev_test_changes",
73
+ repositoryPath: tmpDir,
74
+ callingAgent: "vscode",
75
+ });
76
+ expect(result.isError).toBe(false);
77
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
78
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
79
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
80
+ });
81
+ it("does not install a skill file without a recognized calling agent", async () => {
82
+ const tool = registerToolForTest();
83
+ const result = await tool.handler({
84
+ workflow: "local_dev_test_changes",
85
+ repositoryPath: tmpDir,
86
+ callingAgent: "unknown",
87
+ });
88
+ expect(result.isError).toBe(false);
89
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
90
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
91
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
92
+ });
93
+ it("surfaces local-dev mock routing parameters in workflow text", async () => {
94
+ const tool = registerToolForTest();
95
+ const result = await tool.handler({
96
+ workflow: "local_dev_test_changes",
97
+ repositoryPath: tmpDir,
98
+ callingAgent: "cursor",
99
+ baseBranch: "checkr-prod-flow",
100
+ servicesToMock: "stripe",
101
+ realServices: "partner-accounts,redpanda",
102
+ mockProtocols: "rest",
103
+ });
104
+ const text = result.content[0].text;
105
+ expect(result.isError).toBe(false);
106
+ expect(text).toContain("Services to mock: stripe");
107
+ expect(text).toContain("Real services: partner-accounts,redpanda");
108
+ expect(text).toContain("Mock protocols: rest");
109
+ expect(text).toContain("<SERVICE_ROUTING>");
110
+ expect(text).toContain("<MOCK_SERVICES>stripe</MOCK_SERVICES>");
111
+ expect(text).toContain("<REAL_SERVICES>partner-accounts,redpanda</REAL_SERVICES>");
112
+ expect(text).toContain("<MOCK_PROTOCOLS>rest</MOCK_PROTOCOLS>");
113
+ });
114
+ it("does not install local-dev skill files outside an existing git repository", async () => {
115
+ const tool = registerToolForTest();
116
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-nonrepo-"));
117
+ try {
118
+ const result = await tool.handler({
119
+ workflow: "local_dev_test_changes",
120
+ repositoryPath: nonRepo,
121
+ callingAgent: "cursor",
122
+ });
123
+ expect(result.isError).toBe(true);
124
+ expect(result.content[0].text).toContain("repositoryPath must point to a git repository");
125
+ expect(fs.existsSync(path.join(nonRepo, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
126
+ expect(fs.existsSync(path.join(nonRepo, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
127
+ expect(fs.existsSync(path.join(nonRepo, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
128
+ }
129
+ finally {
130
+ fs.rmSync(nonRepo, { recursive: true, force: true });
131
+ }
132
+ });
133
+ it("validates repositoryPath even when skill creation is skipped", async () => {
134
+ const tool = registerToolForTest();
135
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-nonrepo-"));
136
+ try {
137
+ const result = await tool.handler({
138
+ workflow: "local_dev_test_changes",
139
+ repositoryPath: nonRepo,
140
+ callingAgent: "vscode",
141
+ });
142
+ expect(result.isError).toBe(true);
143
+ expect(result.content[0].text).toContain("repositoryPath must point to a git repository");
144
+ expect(fs.existsSync(path.join(nonRepo, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
145
+ expect(fs.existsSync(path.join(nonRepo, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
146
+ expect(fs.existsSync(path.join(nonRepo, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
147
+ }
148
+ finally {
149
+ fs.rmSync(nonRepo, { recursive: true, force: true });
150
+ }
151
+ });
152
+ it("hides local-dev workflow when the feature flag is disabled", async () => {
153
+ delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
154
+ const tool = registerToolForTest();
155
+ expect(String(tool.config.description)).not.toContain("local_dev_test_changes");
156
+ expect(String(tool.config.inputSchema)).not.toContain("local_dev_test_changes");
157
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("baseBranch");
158
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("servicesToMock");
159
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("realServices");
160
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("mockProtocols");
161
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("callingAgent");
162
+ const result = await tool.handler({
163
+ workflow: "local_dev_test_changes",
164
+ repositoryPath: tmpDir,
165
+ });
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content[0].text).toContain('Unknown workflow: "local_dev_test_changes"');
168
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
169
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
170
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
171
+ });
172
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPreflightMockCheckTool(server: McpServer): void;
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import { validateMockCompatibility, MockProtocol, } from "../utils/mockCompatibility.js";
3
+ const warningSchema = z.object({
4
+ code: z.string().describe("Warning code identifier (e.g. REST_LOOPBACK_URL, GRPC_PERSISTENT_CONN)."),
5
+ message: z.string().describe("Human-readable description of the issue."),
6
+ suggestion: z.string().describe("Recommended action to resolve or work around the issue."),
7
+ affectedMocks: z.array(z.string()).describe("Mock identifiers (service name or endpoint URL) affected by this warning."),
8
+ blocking: z.boolean().optional().describe("When true, the affected mocks cannot intercept traffic and must not be generated or deployed."),
9
+ });
10
+ const preflightOutputSchema = {
11
+ hasBlockers: z.boolean().describe("True when at least one warning is blocking. Blocking mocks must be removed from the generation list before proceeding."),
12
+ warnings: z.array(warningSchema).describe("All compatibility warnings. Inspect the blocking field on each entry; blocking ones must be skipped, advisory ones may proceed with caution."),
13
+ };
14
+ const TOOL_NAME = "skyramp_preflight_mock_check";
15
+ const mockSpecSchema = z.object({
16
+ protocol: z.nativeEnum(MockProtocol).describe("The protocol of the mock"),
17
+ endpointURL: z.string().describe("The endpoint URL for the mock"),
18
+ method: z.string().optional().describe("HTTP method (for REST mocks)"),
19
+ grpcServiceName: z.string().optional().describe("gRPC service name (for gRPC mocks)"),
20
+ kafkaTopic: z.string().optional().describe("Kafka topic (for Kafka mocks)"),
21
+ });
22
+ const inputSchema = {
23
+ mocks: z
24
+ .array(mockSpecSchema)
25
+ .min(1)
26
+ .describe("Array of mock specifications to validate for compatibility"),
27
+ dockerLocalDev: z
28
+ .boolean()
29
+ .default(true)
30
+ .describe("When true, apply Docker local-dev checks such as REST loopback URL blockers. Set false for non-Docker/local runtimes."),
31
+ };
32
+ function formatWarning(warning) {
33
+ const icon = warning.blocking ? "🚫" : "⚠️ ";
34
+ return [
35
+ `${icon} [${warning.code}]${warning.blocking ? " BLOCKING — do not generate or deploy these mocks" : ""}`,
36
+ ` ${warning.message}`,
37
+ ` Suggestion: ${warning.suggestion}`,
38
+ ` Affected: ${warning.affectedMocks.join(", ")}`,
39
+ ].join("\n");
40
+ }
41
+ export function registerPreflightMockCheckTool(server) {
42
+ server.registerTool(TOOL_NAME, {
43
+ description: "Pre-flight compatibility check for mock deployments. Validates that a set of mocks " +
44
+ "can be deployed together without known protocol conflicts (e.g., REST loopback URLs in Docker local-dev " +
45
+ "and gRPC persistent connection caveats). Returns two classes of " +
46
+ "issues: advisory warnings (mocks are deployable but may exhibit runtime issues) and " +
47
+ "blocking warnings (mocks cannot intercept traffic and must not be generated or deployed). " +
48
+ "Check hasBlockers in the result and the blocking flag on each warning before proceeding.",
49
+ inputSchema,
50
+ outputSchema: preflightOutputSchema,
51
+ }, async (params) => {
52
+ const mocks = params.mocks;
53
+ const dockerLocalDev = params.dockerLocalDev !== false;
54
+ const result = validateMockCompatibility(mocks, { dockerLocalDev });
55
+ let text;
56
+ if (result.warnings.length === 0) {
57
+ text = [
58
+ "✅ Pre-flight check passed — all clear.",
59
+ "",
60
+ `${mocks.length} mock(s) validated. No known compatibility issues detected.`,
61
+ "Safe to deploy in a single apply_mock() call.",
62
+ ].join("\n");
63
+ }
64
+ else {
65
+ const blockingCount = result.warnings.filter((w) => w.blocking).length;
66
+ const advisoryCount = result.warnings.length - blockingCount;
67
+ const headerParts = [];
68
+ if (blockingCount > 0)
69
+ headerParts.push(`${blockingCount} blocking`);
70
+ if (advisoryCount > 0)
71
+ headerParts.push(`${advisoryCount} advisory`);
72
+ text = [
73
+ `⚠️ Pre-flight check: ${result.warnings.length} issue(s) detected (${headerParts.join(", ")}).`,
74
+ "",
75
+ ...result.warnings.map(formatWarning),
76
+ "",
77
+ result.hasBlockers
78
+ ? "🚫 Blocking mocks must be removed from the generation list — they cannot intercept traffic. Advisory mocks may still be deployed with the caveats noted above."
79
+ : "Advisory warnings only — mocks are deployable, but consider the suggestions above to avoid runtime issues.",
80
+ ].join("\n");
81
+ }
82
+ return {
83
+ structuredContent: {
84
+ hasBlockers: result.hasBlockers,
85
+ warnings: result.warnings.map((w) => ({
86
+ code: w.code,
87
+ message: w.message,
88
+ suggestion: w.suggestion,
89
+ affectedMocks: w.affectedMocks,
90
+ ...(w.blocking !== undefined ? { blocking: w.blocking } : {}),
91
+ })),
92
+ },
93
+ content: [{ type: "text", text }],
94
+ };
95
+ });
96
+ }
@@ -0,0 +1,70 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { MockProtocol } from "../types/TestTypes.js";
3
+ interface RestOrGrpcSampleMetadata {
4
+ destination?: string;
5
+ path?: string;
6
+ method?: string;
7
+ scheme?: string;
8
+ service?: string;
9
+ topic?: string;
10
+ port?: number;
11
+ }
12
+ interface ProxySample {
13
+ timestamp?: string;
14
+ request?: {
15
+ body?: string;
16
+ data?: string;
17
+ };
18
+ response?: {
19
+ body?: string;
20
+ data?: string;
21
+ statusCode?: number;
22
+ };
23
+ metadata?: RestOrGrpcSampleMetadata;
24
+ }
25
+ export interface NormalizedMockSpec {
26
+ protocol: MockProtocol;
27
+ endpointURL: string;
28
+ method?: string;
29
+ responseData?: string;
30
+ responseStatusCode?: string;
31
+ requestData?: string;
32
+ requestAware?: boolean;
33
+ grpcServiceName?: string;
34
+ kafkaTopic?: string;
35
+ }
36
+ declare const PROTOCOLS: readonly ["rest", "grpc", "kafka"];
37
+ type Protocol = (typeof PROTOCOLS)[number];
38
+ /**
39
+ * Combine a bare destination hostname with the separately-reported port
40
+ * (`metadata.port`) into `host:port`, which is what mock generation expects for
41
+ * the endpoint origin. Leaves the destination untouched if it already carries a
42
+ * port or if no usable port was reported.
43
+ */
44
+ export declare function withPort(destination: string, port?: number): string;
45
+ /**
46
+ * Convert raw proxy samples for a single protocol into deduplicated mock specs
47
+ * whose fields map 1:1 onto skyramp_batch_mock_generation. Deduplication keeps
48
+ * one representative per distinct (destination, method/path/topic, status).
49
+ */
50
+ export declare function normalizeSamplesToMockSpecs(protocol: Protocol, samples: ProxySample[]): NormalizedMockSpec[];
51
+ /** Normalize a user-supplied proxy address into a scheme://host:port base. */
52
+ export declare function normalizeProxyBaseUrl(proxyUrl: string): string;
53
+ /**
54
+ * Validate that exactly one access mode was provided and return a normalized
55
+ * descriptor. Throws a user-facing error otherwise.
56
+ */
57
+ export declare function resolveAccessMode(params: {
58
+ proxyUrl?: string;
59
+ kubeconfig?: string;
60
+ namespace?: string;
61
+ }): {
62
+ mode: "url";
63
+ proxyUrl: string;
64
+ } | {
65
+ mode: "kube";
66
+ kubeconfig: string;
67
+ namespace: string;
68
+ };
69
+ export declare function registerQueryProxyMocksTool(server: McpServer): void;
70
+ export {};