@skyramp/mcp 0.2.8 → 0.2.150-rc.ldw-

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 (90) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +17 -14
  3. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  4. package/build/commands/localDevTestChangesCommand.js +188 -0
  5. package/build/index.js +74 -13
  6. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  7. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  8. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +7 -6
  9. package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
  10. package/build/prompts/local-dev/local-dev-plan.js +413 -0
  11. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  12. package/build/prompts/local-dev/local-dev-prompts.js +155 -0
  13. package/build/prompts/prompt-utils.d.ts +8 -0
  14. package/build/prompts/prompt-utils.js +33 -0
  15. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  16. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  17. package/build/prompts/sut-setup/shared.d.ts +20 -0
  18. package/build/prompts/sut-setup/shared.js +69 -7
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  20. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  22. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
  23. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  24. package/build/prompts/testbot/testbot-prompts.js +4 -34
  25. package/build/resources/testbotResource.js +2 -1
  26. package/build/services/TestExecutionService.d.ts +2 -1
  27. package/build/services/TestExecutionService.js +9 -9
  28. package/build/services/TestExecutionService.test.js +42 -1
  29. package/build/services/TestGenerationService.d.ts +2 -2
  30. package/build/tool-phases.js +4 -0
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +42 -18
  32. package/build/tools/enrichTestWithMocksTool.d.ts +15 -0
  33. package/build/tools/enrichTestWithMocksTool.js +413 -0
  34. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  35. package/build/tools/enrichTestWithMocksTool.test.js +60 -0
  36. package/build/tools/executeSkyrampTestTool.d.ts +4 -0
  37. package/build/tools/executeSkyrampTestTool.js +39 -18
  38. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  39. package/build/tools/executeSkyrampTestTool.test.js +16 -0
  40. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +79 -0
  41. package/build/tools/generate-tests/batchMockGenerationTool.js +395 -0
  42. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  43. package/build/tools/generate-tests/generateMockRestTool.d.ts +56 -6
  44. package/build/tools/generate-tests/generateMockRestTool.js +193 -21
  45. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
  46. package/build/tools/generateEnrichedIntegrationTestTool.js +209 -0
  47. package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
  48. package/build/tools/localDevWorkflowFixes.test.js +144 -0
  49. package/build/tools/one-click/oneClickTool.d.ts +10 -0
  50. package/build/tools/one-click/oneClickTool.js +177 -21
  51. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  52. package/build/tools/one-click/oneClickTool.test.js +172 -0
  53. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  54. package/build/tools/preflightMockCheckTool.js +91 -0
  55. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  56. package/build/tools/test-management/analyzeChangesTool.js +59 -38
  57. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  58. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  59. package/build/types/OneClickCommands.d.ts +1 -1
  60. package/build/types/RepositoryAnalysis.d.ts +117 -0
  61. package/build/types/RepositoryAnalysis.js +16 -2
  62. package/build/types/TestTypes.d.ts +11 -2
  63. package/build/types/TestTypes.js +11 -1
  64. package/build/utils/analyze-openapi.js +18 -1
  65. package/build/utils/analyze-openapi.test.d.ts +1 -0
  66. package/build/utils/analyze-openapi.test.js +19 -0
  67. package/build/utils/branchDiff.d.ts +17 -1
  68. package/build/utils/branchDiff.js +96 -14
  69. package/build/utils/branchDiff.test.d.ts +1 -0
  70. package/build/utils/branchDiff.test.js +107 -0
  71. package/build/utils/docker.test.js +1 -1
  72. package/build/utils/featureFlags.d.ts +15 -1
  73. package/build/utils/featureFlags.js +19 -2
  74. package/build/utils/featureFlags.test.js +57 -2
  75. package/build/utils/grpcMockValidation.d.ts +1 -0
  76. package/build/utils/grpcMockValidation.js +49 -0
  77. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  78. package/build/utils/grpcMockValidation.test.js +41 -0
  79. package/build/utils/httpMethodValidation.d.ts +4 -0
  80. package/build/utils/httpMethodValidation.js +13 -0
  81. package/build/utils/mockCompatibility.d.ts +49 -0
  82. package/build/utils/mockCompatibility.js +80 -0
  83. package/build/utils/mockCompatibility.test.d.ts +1 -0
  84. package/build/utils/mockCompatibility.test.js +88 -0
  85. package/build/utils/versions.d.ts +3 -3
  86. package/build/utils/versions.js +1 -1
  87. package/build/workspace/workspace.d.ts +35 -21
  88. package/build/workspace/workspace.js +8 -4
  89. package/build/workspace/workspace.test.js +65 -6
  90. package/package.json +2 -2
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { injectMockImports } from "./enrichTestWithMocksTool.js";
6
+ describe("injectMockImports", () => {
7
+ let tmpDir;
8
+ let testFile;
9
+ let mockFile;
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-enrich-"));
12
+ testFile = path.join(tmpDir, "account_integration_test.py");
13
+ mockFile = path.join(tmpDir, "billing_preferences_PUT_mock.py");
14
+ fs.writeFileSync(mockFile, [
15
+ "# Command: skyramp generate mock rest http://billing-service:4000/v1/preferences --method PUT",
16
+ "def get_all_mocks():",
17
+ " return []",
18
+ "",
19
+ ].join("\n"));
20
+ });
21
+ afterEach(() => {
22
+ fs.rmSync(tmpDir, { recursive: true, force: true });
23
+ });
24
+ it("applies mocks in every Python test function", () => {
25
+ const source = [
26
+ "import skyramp",
27
+ "",
28
+ "def test_success():",
29
+ " client = skyramp.Client(port=35142)",
30
+ " assert True",
31
+ "",
32
+ "def test_error():",
33
+ " client = skyramp.Client(",
34
+ " port=35142,",
35
+ " )",
36
+ " assert True",
37
+ "",
38
+ ].join("\n");
39
+ const enriched = injectMockImports(source, testFile, "billing", [mockFile]);
40
+ expect(enriched.match(/^ apply_all_mocks\(client\)$/gm)).toHaveLength(2);
41
+ expect(enriched).toContain("MOCK_SERVICES");
42
+ });
43
+ it("does not duplicate apply calls when rerun", () => {
44
+ const source = [
45
+ "import skyramp",
46
+ "",
47
+ "def test_success():",
48
+ " client = skyramp.Client(port=35142)",
49
+ " assert True",
50
+ "",
51
+ "def test_error():",
52
+ " client = skyramp.Client(port=35142)",
53
+ " assert True",
54
+ "",
55
+ ].join("\n");
56
+ const once = injectMockImports(source, testFile, "billing", [mockFile]);
57
+ const twice = injectMockImports(once, testFile, "billing", [mockFile]);
58
+ expect(twice.match(/^ apply_all_mocks\(client\)$/gm)).toHaveLength(2);
59
+ });
60
+ });
@@ -1,2 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { TestType } from "../types/TestTypes.js";
3
+ export declare const CONTRACT_EXECUTION_MODES: readonly ["provider", "consumer"];
4
+ export type ContractExecutionMode = (typeof CONTRACT_EXECUTION_MODES)[number];
5
+ export declare function shouldInjectSkyrampBaseUrl(testType: TestType, contractMode?: ContractExecutionMode): boolean;
2
6
  export declare function registerExecuteSkyrampTestTool(server: McpServer): void;
@@ -5,9 +5,13 @@ import { AnalyticsService } from "../services/AnalyticsService.js";
5
5
  import { TestExecutionStatus } from "../types/TestExecution.js";
6
6
  import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
7
7
  import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
8
- import { StateManager } from "../utils/AnalysisStateManager.js";
8
+ import { StateManager, } from "../utils/AnalysisStateManager.js";
9
9
  import { logger } from "../utils/logger.js";
10
10
  const TOOL_NAME = "skyramp_execute_test";
11
+ export const CONTRACT_EXECUTION_MODES = ["provider", "consumer"];
12
+ export function shouldInjectSkyrampBaseUrl(testType, contractMode) {
13
+ return testType !== TestType.CONTRACT || contractMode !== "consumer";
14
+ }
11
15
  export function registerExecuteSkyrampTestTool(server) {
12
16
  server.registerTool(TOOL_NAME, {
13
17
  description: `Execute a Skyramp-generated test in isolated containerized environments for reliable, deterministic testing.
@@ -53,10 +57,14 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
53
57
  .describe("Programming language of the test file to execute (e.g., python, javascript, typescript, java)"),
54
58
  testType: z
55
59
  .nativeEnum(TestType)
56
- .describe("Type of the test to execute."),
60
+ .describe("Type of the test to execute. Note: 'mock' is NOT a valid test type — mock files are deployed via their apply_mock() function, not executed as tests."),
57
61
  testFile: z
58
62
  .string()
59
63
  .describe("ALWAYS USE ABSOLUTE PATH to the test file to execute"),
64
+ contractMode: z
65
+ .enum(CONTRACT_EXECUTION_MODES)
66
+ .optional()
67
+ .describe("Only applies when testType is 'contract'. Use 'provider' for provider contract tests that hit the real service under test and need SKYRAMP_TEST_BASE_URL. Use 'consumer' only for consumer contract tests with inline mocks that do not hit the real service. Defaults to provider behavior when omitted."),
60
68
  token: z
61
69
  .string()
62
70
  .describe("Skyramp authentication token for test execution. USE EMPTY STRING WHEN USER CONFIRMS 'No token required'"),
@@ -81,23 +89,33 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
81
89
  keywords: ["run test", "execute test"],
82
90
  },
83
91
  }, async (params, extra) => {
84
- let errorResult;
85
- // Helper to send progress notifications to the MCP client
92
+ // Helper to send progress notifications to the MCP client.
93
+ // Wrapped in try-catch so a broken/disconnected transport never crashes
94
+ // the tool handler or causes an unhandled rejection.
86
95
  const sendProgress = async (progress, total, message) => {
87
96
  const progressToken = extra._meta?.progressToken;
88
97
  if (progressToken !== undefined) {
89
- const notification = {
90
- method: "notifications/progress",
91
- params: {
92
- progressToken,
93
- progress,
94
- total,
95
- message,
96
- },
97
- };
98
- await extra.sendNotification(notification);
98
+ try {
99
+ const notification = {
100
+ method: "notifications/progress",
101
+ params: {
102
+ progressToken,
103
+ progress,
104
+ total,
105
+ message,
106
+ },
107
+ };
108
+ await extra.sendNotification(notification);
109
+ }
110
+ catch {
111
+ // Progress notifications are best-effort — swallow failures so
112
+ // a flaky or disconnected transport doesn't kill the execution.
113
+ }
99
114
  }
100
115
  };
116
+ // Send immediate acknowledgment
117
+ await sendProgress(0, 100, "Starting execution...");
118
+ let errorResult;
101
119
  // Progress callback adapter for TestExecutionService
102
120
  const onExecutionProgress = async (progress) => {
103
121
  await sendProgress(progress.percent, 100, progress.message);
@@ -107,20 +125,22 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
107
125
  let useHostNetwork = false;
108
126
  try {
109
127
  // Send initial progress
110
- await sendProgress(0, 100, "Starting test execution...");
128
+ await sendProgress(5, 100, "Starting test execution...");
111
129
  // Always resolve workspace config for dockerNetwork (host networking)
112
130
  // and optionally inject SKYRAMP_TEST_BASE_URL if not already set.
113
131
  if (params.workspacePath) {
114
132
  const { baseUrl, dockerNetwork, candidates } = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language);
115
133
  useHostNetwork = !!dockerNetwork;
116
- if (!process.env.SKYRAMP_TEST_BASE_URL) {
134
+ const shouldInjectBaseUrl = shouldInjectSkyrampBaseUrl(params.testType, params.contractMode);
135
+ if (shouldInjectBaseUrl && !process.env.SKYRAMP_TEST_BASE_URL) {
117
136
  if (baseUrl) {
118
137
  process.env.SKYRAMP_TEST_BASE_URL = baseUrl;
119
138
  didSetSkyrampBaseUrl = true;
120
139
  }
121
140
  else if (candidates.length > 0) {
122
141
  return {
123
- content: [{
142
+ content: [
143
+ {
124
144
  type: "text",
125
145
  text: [
126
146
  `Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
@@ -128,7 +148,8 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
128
148
  ``,
129
149
  `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
130
150
  ].join("\n"),
131
- }],
151
+ },
152
+ ],
132
153
  isError: true,
133
154
  };
134
155
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { TestType } from "../types/TestTypes.js";
3
+ import { shouldInjectSkyrampBaseUrl } from "./executeSkyrampTestTool.js";
4
+ describe("shouldInjectSkyrampBaseUrl", () => {
5
+ it("injects for provider contract tests by default", () => {
6
+ expect(shouldInjectSkyrampBaseUrl(TestType.CONTRACT)).toBe(true);
7
+ expect(shouldInjectSkyrampBaseUrl(TestType.CONTRACT, "provider")).toBe(true);
8
+ });
9
+ it("skips injection only for explicit consumer contract tests", () => {
10
+ expect(shouldInjectSkyrampBaseUrl(TestType.CONTRACT, "consumer")).toBe(false);
11
+ });
12
+ it("injects for non-contract tests", () => {
13
+ expect(shouldInjectSkyrampBaseUrl(TestType.INTEGRATION, "consumer")).toBe(true);
14
+ expect(shouldInjectSkyrampBaseUrl(TestType.UI)).toBe(true);
15
+ });
16
+ });
@@ -0,0 +1,79 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ import { MockProtocol } from "../../types/TestTypes.js";
5
+ export { MockProtocol } from "../../types/TestTypes.js";
6
+ export declare const BATCH_MOCK_PROTOCOL_EXAMPLES = "\n<example protocol=\"rest\">\nBatch REST item. Required fields: protocol=\"rest\", endpointURL, method unless apiSchema is provided.\n{\n \"protocol\": \"rest\",\n \"endpointURL\": \"http://identity-service:4000/api/internal/v1/users/validate\",\n \"method\": \"POST\",\n \"requestAware\": true,\n \"responseData\": \"{\"valid\":true}\"\n}\n</example>\n\n<example protocol=\"grpc\">\nBatch gRPC item. Required fields: protocol=\"grpc\", endpointURL as host:port, protoPath, grpcServiceName. method is the RPC name; empty means all service methods.\n{\n \"protocol\": \"grpc\",\n \"endpointURL\": \"profile-service:50051\",\n \"protoPath\": \"/repo/proto/profiles.proto\",\n \"grpcServiceName\": \"ProfileService\",\n \"method\": \"GetProfile\",\n \"responseData\": \"{\"id\":\"profile_123\"}\"\n}\n</example>\n\n<example protocol=\"kafka-producer\">\nBatch Kafka producer validation item. Required fields: protocol=\"kafka\", endpointURL as broker host:port, kafkaTopic, and requestData. Do not pass method or responseData.\n{\n \"protocol\": \"kafka\",\n \"endpointURL\": \"broker:9092\",\n \"kafkaTopic\": \"orders\",\n \"requestData\": \"{\"order_id\":\"ord_123\"}\"\n}\n</example>\n\n<example protocol=\"kafka-consumer\">\nBatch Kafka consumer message item. Required fields: protocol=\"kafka\", endpointURL as broker host:port, kafkaTopic, and responseData. Do not pass method or requestData.\n{\n \"protocol\": \"kafka\",\n \"endpointURL\": \"broker:9092\",\n \"kafkaTopic\": \"orders\",\n \"responseData\": \"{\"accepted\":true}\"\n}\n</example>";
7
+ declare const mockSpecSchema: z.ZodObject<{
8
+ protocol: z.ZodDefault<z.ZodNativeEnum<typeof MockProtocol>>;
9
+ endpointURL: z.ZodString;
10
+ method: z.ZodDefault<z.ZodString>;
11
+ responseData: z.ZodOptional<z.ZodString>;
12
+ responseStatusCode: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
13
+ requestData: z.ZodDefault<z.ZodString>;
14
+ requestAware: z.ZodDefault<z.ZodBoolean>;
15
+ apiSchema: z.ZodDefault<z.ZodString>;
16
+ protoPath: z.ZodOptional<z.ZodString>;
17
+ grpcServiceName: z.ZodOptional<z.ZodString>;
18
+ protobufExtraFiles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
+ protobufExtraImportPath: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
+ kafkaTopic: z.ZodOptional<z.ZodString>;
21
+ output: z.ZodOptional<z.ZodString>;
22
+ outputDir: z.ZodOptional<z.ZodString>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ method: string;
25
+ endpointURL: string;
26
+ apiSchema: string;
27
+ requestData: string;
28
+ responseStatusCode: string;
29
+ protocol: MockProtocol;
30
+ requestAware: boolean;
31
+ output?: string | undefined;
32
+ outputDir?: string | undefined;
33
+ responseData?: string | undefined;
34
+ protoPath?: string | undefined;
35
+ grpcServiceName?: string | undefined;
36
+ protobufExtraFiles?: string[] | undefined;
37
+ protobufExtraImportPath?: string[] | undefined;
38
+ kafkaTopic?: string | undefined;
39
+ }, {
40
+ endpointURL: string;
41
+ output?: string | undefined;
42
+ outputDir?: string | undefined;
43
+ method?: string | undefined;
44
+ apiSchema?: string | undefined;
45
+ requestData?: string | undefined;
46
+ responseStatusCode?: string | undefined;
47
+ responseData?: string | undefined;
48
+ protocol?: MockProtocol | undefined;
49
+ protoPath?: string | undefined;
50
+ grpcServiceName?: string | undefined;
51
+ protobufExtraFiles?: string[] | undefined;
52
+ protobufExtraImportPath?: string[] | undefined;
53
+ kafkaTopic?: string | undefined;
54
+ requestAware?: boolean | undefined;
55
+ }>;
56
+ export type MockSpec = z.infer<typeof mockSpecSchema>;
57
+ export type MockSpecInput = z.input<typeof mockSpecSchema>;
58
+ export interface BatchMockResult {
59
+ index: number;
60
+ protocol: string;
61
+ endpointURL: string;
62
+ method: string;
63
+ topic?: string;
64
+ success: boolean;
65
+ outputDir?: string;
66
+ output?: string;
67
+ error?: string;
68
+ detail?: string;
69
+ }
70
+ export interface BatchMockParams {
71
+ mocks: MockSpecInput[];
72
+ outputDir: string;
73
+ runtime?: string;
74
+ dockerNetwork?: string;
75
+ language?: string;
76
+ framework?: string;
77
+ }
78
+ export declare function executeBatchMockGeneration(params: BatchMockParams): Promise<CallToolResult>;
79
+ export declare function registerBatchMockTool(server: McpServer): void;
@@ -0,0 +1,395 @@
1
+ import { z } from "zod";
2
+ import { SkyrampClient } from "@skyramp/skyramp";
3
+ import { MockProtocol } from "../../types/TestTypes.js";
4
+ export { MockProtocol } from "../../types/TestTypes.js";
5
+ import { AnalyticsService } from "../../services/AnalyticsService.js";
6
+ import { logger } from "../../utils/logger.js";
7
+ import { getEntryPoint } from "../../utils/telemetry.js";
8
+ import fs from "fs";
9
+ import { getRestMockMethodValidationError, normalizeRestMockMethod, } from "../../utils/httpMethodValidation.js";
10
+ import { getGrpcMockResponseValidationError } from "../../utils/grpcMockValidation.js";
11
+ import { validateMockCompatibility } from "../../utils/mockCompatibility.js";
12
+ export const BATCH_MOCK_PROTOCOL_EXAMPLES = `
13
+ <example protocol="rest">
14
+ Batch REST item. Required fields: protocol="rest", endpointURL, method unless apiSchema is provided.
15
+ {
16
+ "protocol": "rest",
17
+ "endpointURL": "http://identity-service:4000/api/internal/v1/users/validate",
18
+ "method": "POST",
19
+ "requestAware": true,
20
+ "responseData": "{\"valid\":true}"
21
+ }
22
+ </example>
23
+
24
+ <example protocol="grpc">
25
+ Batch gRPC item. Required fields: protocol="grpc", endpointURL as host:port, protoPath, grpcServiceName. method is the RPC name; empty means all service methods.
26
+ {
27
+ "protocol": "grpc",
28
+ "endpointURL": "profile-service:50051",
29
+ "protoPath": "/repo/proto/profiles.proto",
30
+ "grpcServiceName": "ProfileService",
31
+ "method": "GetProfile",
32
+ "responseData": "{\"id\":\"profile_123\"}"
33
+ }
34
+ </example>
35
+
36
+ <example protocol="kafka-producer">
37
+ Batch Kafka producer validation item. Required fields: protocol="kafka", endpointURL as broker host:port, kafkaTopic, and requestData. Do not pass method or responseData.
38
+ {
39
+ "protocol": "kafka",
40
+ "endpointURL": "broker:9092",
41
+ "kafkaTopic": "orders",
42
+ "requestData": "{\"order_id\":\"ord_123\"}"
43
+ }
44
+ </example>
45
+
46
+ <example protocol="kafka-consumer">
47
+ Batch Kafka consumer message item. Required fields: protocol="kafka", endpointURL as broker host:port, kafkaTopic, and responseData. Do not pass method or requestData.
48
+ {
49
+ "protocol": "kafka",
50
+ "endpointURL": "broker:9092",
51
+ "kafkaTopic": "orders",
52
+ "responseData": "{\"accepted\":true}"
53
+ }
54
+ </example>`;
55
+ const mockSpecSchema = z.object({
56
+ protocol: z
57
+ .nativeEnum(MockProtocol)
58
+ .default(MockProtocol.Rest)
59
+ .describe("Protocol for this mock: 'rest', 'grpc', or 'kafka'. See <example> blocks in the tool description for required fields."),
60
+ endpointURL: z
61
+ .string()
62
+ .describe("For REST: the base URL or endpoint URL. " +
63
+ "For gRPC: the target service address as host:port. " +
64
+ "For Kafka: the broker address as host:port."),
65
+ method: z
66
+ .string()
67
+ .default("")
68
+ .describe("For REST: HTTP method, required unless apiSchema is provided. For gRPC: RPC method name, empty generates all service methods. For Kafka: MUST be empty; use kafkaTopic."),
69
+ responseData: z
70
+ .string()
71
+ .optional()
72
+ .describe("REST/gRPC response body data; Kafka consumer messages. Inline JSON/YAML or @/path/to/file."),
73
+ responseStatusCode: z
74
+ .string()
75
+ .default("")
76
+ .refine((val) => !val || /^\d{3}$/.test(val), {
77
+ message: "Must be a 3-digit HTTP status code (e.g., '200', '201') or empty string",
78
+ })
79
+ .describe("HTTP status code the mock should return (e.g., '200', '201')."),
80
+ requestData: z
81
+ .string()
82
+ .default("")
83
+ .describe("REST request body sample for request-aware mocks; Kafka producer validation payload/schema."),
84
+ requestAware: z
85
+ .boolean()
86
+ .default(false)
87
+ .describe("When true, generates a dynamic mock that varies by request."),
88
+ apiSchema: z
89
+ .string()
90
+ .default("")
91
+ .describe("Path or URL to OpenAPI/Swagger schema file."),
92
+ protoPath: z
93
+ .string()
94
+ .optional()
95
+ .describe("Required for protocol='grpc'. Path to the .proto file."),
96
+ grpcServiceName: z
97
+ .string()
98
+ .optional()
99
+ .describe("Required for protocol='grpc'. gRPC service name."),
100
+ protobufExtraFiles: z
101
+ .array(z.string())
102
+ .optional()
103
+ .describe("Optional extra .proto files for gRPC compilation."),
104
+ protobufExtraImportPath: z
105
+ .array(z.string())
106
+ .optional()
107
+ .describe("Optional extra import directories for protobuf compilation."),
108
+ kafkaTopic: z
109
+ .string()
110
+ .optional()
111
+ .describe("Required for protocol='kafka'. Kafka topic name."),
112
+ output: z
113
+ .string()
114
+ .optional()
115
+ .describe("Optional output file name for this mock. Use when multiple mocks share the same endpoint and method so generated files do not overwrite each other."),
116
+ outputDir: z
117
+ .string()
118
+ .optional()
119
+ .describe("Optional output directory for this mock. Defaults to the batch-level outputDir."),
120
+ });
121
+ function validateMockSpec(spec) {
122
+ const protocol = spec.protocol ?? MockProtocol.Rest;
123
+ if (protocol === MockProtocol.Rest) {
124
+ const methodError = getRestMockMethodValidationError(spec.method);
125
+ if (methodError)
126
+ return methodError;
127
+ if (!spec.apiSchema && !spec.method) {
128
+ return "REST mock method is required unless apiSchema is provided for schema-wide generation";
129
+ }
130
+ }
131
+ if (protocol === MockProtocol.Grpc) {
132
+ const missing = [];
133
+ if (!spec.protoPath)
134
+ missing.push("protoPath");
135
+ if (!spec.grpcServiceName)
136
+ missing.push("grpcServiceName");
137
+ if (missing.length > 0) {
138
+ return `gRPC mock requires: ${missing.join(", ")}`;
139
+ }
140
+ const grpcResponseError = getGrpcMockResponseValidationError(spec.responseData);
141
+ if (grpcResponseError)
142
+ return grpcResponseError;
143
+ }
144
+ if (protocol === MockProtocol.Kafka) {
145
+ if (!spec.kafkaTopic)
146
+ return "Kafka mock requires: kafkaTopic";
147
+ if (spec.method) {
148
+ return "Kafka mock method must be empty; use kafkaTopic for the topic name. Produce validation uses requestData and consumer messages use responseData";
149
+ }
150
+ const hasRequestData = Boolean(spec.requestData && spec.requestData.trim());
151
+ const hasResponseData = Boolean(spec.responseData && spec.responseData.trim());
152
+ if (hasRequestData === hasResponseData) {
153
+ return "Kafka mock requires exactly one of requestData (producer validation) or responseData (consumer messages)";
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ function buildGenerateOptions(spec, shared) {
159
+ const protocol = spec.protocol ?? MockProtocol.Rest;
160
+ const language = shared.language ?? "python";
161
+ const framework = shared.framework ??
162
+ (language === "typescript" || language === "javascript"
163
+ ? "playwright"
164
+ : "");
165
+ const outputDir = spec.outputDir ?? shared.outputDir;
166
+ const common = {
167
+ protocol,
168
+ language,
169
+ framework,
170
+ outputDir,
171
+ output: spec.output,
172
+ force: true,
173
+ overwrite: true,
174
+ runtime: shared.runtime,
175
+ dockerNetwork: shared.dockerNetwork,
176
+ responseData: spec.responseData,
177
+ entrypoint: getEntryPoint(),
178
+ };
179
+ if (protocol === MockProtocol.Grpc) {
180
+ return {
181
+ ...common,
182
+ method: spec.method ?? "",
183
+ url: spec.endpointURL,
184
+ protoPath: spec.protoPath,
185
+ service: spec.grpcServiceName,
186
+ extraFiles: spec.protobufExtraFiles ?? [],
187
+ extraImports: spec.protobufExtraImportPath ?? [],
188
+ };
189
+ }
190
+ if (protocol === MockProtocol.Kafka) {
191
+ return {
192
+ ...common,
193
+ url: spec.endpointURL,
194
+ topic: spec.kafkaTopic,
195
+ requestData: spec.requestData?.trim() ? spec.requestData : undefined,
196
+ };
197
+ }
198
+ // REST
199
+ return {
200
+ ...common,
201
+ method: normalizeRestMockMethod(spec.method),
202
+ uri: spec.endpointURL,
203
+ apiSchema: spec.apiSchema ? [spec.apiSchema] : [],
204
+ requestData: spec.requestData ?? "",
205
+ responseStatusCode: spec.responseStatusCode ?? "",
206
+ requestAware: spec.requestAware ?? false,
207
+ mockPort: 0,
208
+ optionalFields: false,
209
+ };
210
+ }
211
+ export async function executeBatchMockGeneration(params) {
212
+ const { mocks, outputDir } = params;
213
+ fs.mkdirSync(outputDir, { recursive: true });
214
+ const client = new SkyrampClient();
215
+ const results = [];
216
+ for (let i = 0; i < mocks.length; i++) {
217
+ const spec = mocks[i];
218
+ const protocol = spec.protocol ?? MockProtocol.Rest;
219
+ const method = spec.method ?? "";
220
+ // Validate spec
221
+ const validationError = validateMockSpec(spec);
222
+ if (validationError) {
223
+ results.push({
224
+ index: i,
225
+ protocol,
226
+ endpointURL: spec.endpointURL,
227
+ method,
228
+ topic: spec.kafkaTopic,
229
+ success: false,
230
+ error: validationError,
231
+ });
232
+ continue;
233
+ }
234
+ const dockerLocalDev = params.runtime === "docker" || Boolean(params.dockerNetwork?.trim());
235
+ const compatibility = validateMockCompatibility([{ ...spec, protocol }], {
236
+ dockerLocalDev,
237
+ });
238
+ const blockingWarnings = compatibility.warnings.filter((warning) => warning.blocking);
239
+ if (blockingWarnings.length > 0) {
240
+ results.push({
241
+ index: i,
242
+ protocol,
243
+ endpointURL: spec.endpointURL,
244
+ method,
245
+ topic: spec.kafkaTopic,
246
+ success: false,
247
+ error: blockingWarnings
248
+ .map((warning) => `${warning.code}: ${warning.message} Suggestion: ${warning.suggestion}`)
249
+ .join("\n"),
250
+ });
251
+ continue;
252
+ }
253
+ const resolvedOutputDir = spec.outputDir ?? outputDir;
254
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
255
+ // Build options and generate
256
+ const options = buildGenerateOptions(spec, {
257
+ outputDir,
258
+ runtime: params.runtime,
259
+ dockerNetwork: params.dockerNetwork,
260
+ language: params.language,
261
+ framework: params.framework,
262
+ });
263
+ try {
264
+ let result;
265
+ if (protocol === MockProtocol.Grpc) {
266
+ result = await client.generateGrpcMock(options);
267
+ }
268
+ else if (protocol === MockProtocol.Kafka) {
269
+ result = await client.generateKafkaMock(options);
270
+ }
271
+ else {
272
+ result = await client.generateRestMock(options);
273
+ }
274
+ if (!result || !result.toLowerCase().includes("success")) {
275
+ results.push({
276
+ index: i,
277
+ protocol,
278
+ endpointURL: spec.endpointURL,
279
+ method,
280
+ topic: spec.kafkaTopic,
281
+ success: false,
282
+ error: `Mock generation failed: ${result || "no output from backend"}`,
283
+ });
284
+ continue;
285
+ }
286
+ results.push({
287
+ index: i,
288
+ protocol,
289
+ endpointURL: spec.endpointURL,
290
+ method,
291
+ topic: spec.kafkaTopic,
292
+ success: true,
293
+ outputDir: resolvedOutputDir,
294
+ output: spec.output,
295
+ detail: result,
296
+ });
297
+ }
298
+ catch (error) {
299
+ const errorMessage = error instanceof Error ? error.message : String(error);
300
+ logger.error("Batch mock generation failed for spec", {
301
+ index: i,
302
+ protocol,
303
+ endpointURL: spec.endpointURL,
304
+ error: errorMessage,
305
+ });
306
+ results.push({
307
+ index: i,
308
+ protocol,
309
+ endpointURL: spec.endpointURL,
310
+ method,
311
+ topic: spec.kafkaTopic,
312
+ success: false,
313
+ error: errorMessage,
314
+ });
315
+ }
316
+ }
317
+ const succeeded = results.filter((r) => r.success);
318
+ const failed = results.filter((r) => !r.success);
319
+ const summaryLines = [
320
+ `## Batch Mock Generation Summary`,
321
+ ``,
322
+ `- **Total:** ${mocks.length}`,
323
+ `- **Succeeded:** ${succeeded.length}`,
324
+ `- **Failed:** ${failed.length}`,
325
+ `- **Default Output Directory:** ${outputDir}`,
326
+ ``,
327
+ ];
328
+ if (succeeded.length > 0) {
329
+ summaryLines.push(`### Successful Mocks`);
330
+ for (const r of succeeded) {
331
+ summaryLines.push(`- [${r.index}] ${r.protocol.toUpperCase()} ${r.endpointURL} ${r.method || (r.protocol === MockProtocol.Kafka ? `topic:${r.topic ?? "?"}` : "(all methods)")}${r.output ? ` -> ${r.outputDir ?? outputDir}/${r.output}` : ""}`);
332
+ }
333
+ summaryLines.push(``);
334
+ }
335
+ if (failed.length > 0) {
336
+ summaryLines.push(`### Failed Mocks`);
337
+ for (const r of failed) {
338
+ summaryLines.push(`- [${r.index}] ${r.protocol.toUpperCase()} ${r.endpointURL} ${r.method || (r.protocol === MockProtocol.Kafka ? `topic:${r.topic ?? "?"}` : "(all methods)")}: ${r.error}`);
339
+ }
340
+ summaryLines.push(``);
341
+ }
342
+ return {
343
+ content: [{ type: "text", text: summaryLines.join("\n") }],
344
+ isError: failed.length > 0,
345
+ };
346
+ }
347
+ const TOOL_NAME = "skyramp_batch_mock_generation";
348
+ const batchMockSchema = {
349
+ mocks: z
350
+ .array(mockSpecSchema)
351
+ .min(1)
352
+ .describe("Array of mock specifications. Each element describes one mock to generate."),
353
+ outputDir: z
354
+ .string()
355
+ .describe("Directory where generated mock files will be written."),
356
+ runtime: z
357
+ .string()
358
+ .default("local")
359
+ .describe("Runtime environment: 'local', 'docker', or 'kubernetes'."),
360
+ dockerNetwork: z
361
+ .string()
362
+ .default("")
363
+ .describe("Docker network name for containerized testing."),
364
+ language: z
365
+ .string()
366
+ .default("python")
367
+ .describe("Programming language for generated mocks (python, typescript, javascript, java)."),
368
+ framework: z
369
+ .string()
370
+ .default("")
371
+ .describe("Testing framework (e.g., pytest, playwright)."),
372
+ };
373
+ export function registerBatchMockTool(server) {
374
+ server.registerTool(TOOL_NAME, {
375
+ description: `Generate multiple mock server implementations in a single call using Skyramp.
376
+
377
+ This tool accepts an array of mock specifications and generates all mocks in one operation,
378
+ avoiding the need for N separate tool calls when mocking multiple downstream services.
379
+
380
+ Each mock spec can target REST, gRPC, or Kafka protocols. Partial failures are handled
381
+ gracefully — if some mocks fail, the others still succeed and are reported.
382
+
383
+ Use this tool when you need to mock 2+ downstream services simultaneously (e.g., for a
384
+ local-dev workflow with multiple dependencies).
385
+
386
+ ${BATCH_MOCK_PROTOCOL_EXAMPLES}`,
387
+ inputSchema: batchMockSchema,
388
+ }, async (params) => {
389
+ const result = await executeBatchMockGeneration(params);
390
+ AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
391
+ // Silently ignore analytics errors
392
+ });
393
+ return result;
394
+ });
395
+ }