@skyramp/mcp 0.0.63-rc.5 → 0.0.63-rc.6

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.
@@ -14,9 +14,14 @@ Read \`package.json\` / \`requirements.txt\`, \`docker-compose.yml\`, route/cont
14
14
  and model/schema files (Zod schemas, Pydantic models, TypeScript interfaces, DTOs)
15
15
  to understand the tech stack, endpoint shapes, auth mechanisms, and request/response schemas.
16
16
 
17
- ### Step 2: Identify resource relationships
17
+ ### Step 2: Identify resource relationships and parameter locations
18
18
  Map how endpoints relate to each other — which POST creates resources consumed by other endpoints?
19
19
  **Resolve nested/sub-router paths** from the Router Mounting section above.
20
+ **CRITICAL — Distinguish query params vs request body:** For each endpoint, determine whether
21
+ parameters are sent as URL query params (typical for GET search/filter/list) or request body
22
+ (typical for POST/PUT/PATCH). Look at FastAPI \`Query()\` annotations, Express \`req.query\` usage,
23
+ Spring \`@RequestParam\`, Flask \`request.args\`, etc. Populate \`queryParams\` in interactions
24
+ for GET endpoints that accept search/filter/pagination parameters.
20
25
 
21
26
  ${nextStep}`;
22
27
  }
@@ -28,7 +33,11 @@ ${nextStep}`;
28
33
  Read handler code for the changed endpoints and their model/schema files (Zod schemas,
29
34
  Pydantic models, DTOs) to understand request/response shapes. Find related endpoints via
30
35
  imports, shared models, adjacent route files. Resolve nested/sub-router paths from Router
31
- Mounting context.`
36
+ Mounting context.
37
+ **CRITICAL — Query params vs body:** For GET endpoints (especially search/filter/list),
38
+ identify which parameters are URL query params vs request body. Look at framework-specific
39
+ annotations (FastAPI \`Query()\`, Express \`req.query\`, Spring \`@RequestParam\`, etc.).
40
+ Pass these as \`queryParams\` (not \`requestBody\`) when generating scenarios.`
32
41
  : isUIOnly
33
42
  ? `### Step 2: Identify consumed API endpoints
34
43
  UI-only PR — read changed components to find API calls (fetch, axios, hooks).`
@@ -50,11 +59,13 @@ Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessi
50
59
  : `### Step 3: Draft integration scenarios
51
60
  Draft multi-step scenarios simulating realistic user workflows:
52
61
  - **Cross-resource data flow**: Foreign key relationships, parent→child creation, verification
53
- - **Search/filter verification**: Create data, search, verify results
62
+ - **Search/filter verification**: Create data, search for it using \`queryParams\`, verify results
54
63
  - **Negative/error paths**: Invalid references → appropriate errors
55
64
  - **UI user journeys**: Concrete browser steps for frontend changes
56
65
 
57
- **Quality:** Realistic request bodies, response data verification, actual field names for chaining.
66
+ **Quality:** Realistic request bodies for POST/PUT/PATCH, \`queryParams\` for GET search/filter,
67
+ response data verification, actual field names for chaining.
68
+ **Parameter placement:** GET search/filter endpoints MUST use \`queryParams\`, not \`requestBody\`.
58
69
 
59
70
  ### Step 4: Call recommend tests
60
71
  Call \`skyramp_recommend_tests\` with \`sessionId: "${p.sessionId}"\``;
@@ -20,6 +20,13 @@ export function getAuthSnippets(authHeaderValue) {
20
20
  }
21
21
  return { authSchemeSnippet: "", authTokenSnippet: "" };
22
22
  }
23
+ export const PATH_PARAM_UUID_GUIDANCE = `**Path parameters in contract/fuzz tests:** for endpoints with path params (e.g. \`/coupons/{coupon_id}\`), ` +
24
+ `keep the placeholder in \`endpointURL\` — do NOT substitute it. ` +
25
+ `Pass the concrete value separately via \`pathParams\` (e.g. \`coupon_id=3fa85f64-5717-4562-b3fc-2c963f66afa6\`). ` +
26
+ `This lets the CLI derive a clean filename from the resource name (\`coupons_GET_...\`) instead of the UUID. ` +
27
+ `Prefer example values from the OpenAPI schema (\`example\`, \`x-example\`, or enum values). ` +
28
+ `If no example exists, use a realistic UUID with varied hex digits such as \`3fa85f64-5717-4562-b3fc-2c963f66afa6\`. ` +
29
+ `Never use all-same-digit patterns like \`00000000-0000-0000-0000-000000000000\` or \`22222222-2222-2222-2222-222222222222\` — these look like placeholders and are rarely valid resource IDs.`;
23
30
  export function buildPrioritizationDimensions() {
24
31
  return `## Prioritization Dimensions (evaluate each candidate test)
25
32
 
@@ -145,7 +152,27 @@ replace them with accurate ones.
145
152
 
146
153
  **Available test types:**
147
154
  - **Integration** — multi-endpoint workflows that chain data across resources
148
- - **Contract** — response schema validation for new/changed endpoints
155
+ - **Contract** — validates API contracts between services. Two distinct modes — choose based on role:
156
+ - **Provider contract test** (\`providerMode: true\`): Recommend when this codebase IS the API provider/owner.
157
+ Use when: new endpoints are added, existing responses are modified, an OpenAPI spec exists to validate against,
158
+ or you need to verify the implementation still honors its contracts after a code change.
159
+ The test calls the real provider and asserts the response conforms to the spec.
160
+ - **Consumer contract test** (\`consumerMode: true\`): Recommend when this codebase CALLS another service's API.
161
+ Use when: the service makes outbound HTTP calls to downstream APIs, you need to verify outbound requests
162
+ conform to the downstream contract, or you want to catch consumer-side drift before it reaches the provider.
163
+ The test uses a request-aware mock as the provider — no live downstream service needed.
164
+ - **Both modes** (\`providerMode: true, consumerMode: true\`): Recommend for service boundaries where this
165
+ codebase is simultaneously an API owner (upstream) AND a client of another service (downstream).
166
+ - **Default (neither set)**: generates a standard contract test equivalent to both modes. Use only when
167
+ the role is unclear or as a fallback when no spec is available.
168
+
169
+ **Signal for consumer contract test:** Look for outbound HTTP client code (fetch, axios, httpx, requests, http.Client),
170
+ service client classes, or calls to external base URLs in the codebase. If an endpoint's implementation
171
+ makes downstream calls, that downstream boundary is a consumer contract test candidate.
172
+
173
+ **Signal for provider contract test:** Look for new or modified endpoint handlers, route changes, response
174
+ shape modifications, or the presence of an OpenAPI spec. If the diff adds/changes an endpoint this service owns,
175
+ that is a provider contract test candidate.
149
176
 
150
177
  **Do NOT recommend Fuzz tests.** Fuzz testing is available as a manual tool but must not appear in automated recommendations.
151
178
  - **E2E** — user journeys spanning frontend to backend (needs Playwright traces)
@@ -170,7 +197,11 @@ export function buildToolWorkflows(authHeaderValue, authTypeValue = "") {
170
197
  let authGuidance;
171
198
  let authParams;
172
199
  if (noAuth) {
173
- authGuidance = "";
200
+ authGuidance = `**Auth Verification Required:** The workspace config indicates no authentication, but you MUST verify this independently before omitting auth:
201
+ 1. **OpenAPI spec** \u2192 check \`securitySchemes\` / \`securityDefinitions\` for \`type: http\`, \`type: apiKey\`, or \`type: oauth2\`
202
+ 2. **Source code** \u2192 look for auth middleware (\`passport\`, \`jwt.verify\`, \`authMiddleware\`, \`@requires_auth\`, \`Depends(get_current_user)\`, \`@UseGuards\`), route guards, or token extraction logic
203
+ 3. **Route definitions** \u2192 check if routes have auth decorators or middleware applied
204
+ If you find auth requirements, pass the appropriate \`authHeader\` (e.g., "Authorization") and \`authScheme\` (e.g., "Bearer") to EVERY tool call. Only pass \`authHeader: ""\` if you confirm the API is truly unauthenticated.`;
174
205
  authParams = { authHeader: "" };
175
206
  }
176
207
  else if (isAuthorizationHeader) {
@@ -203,7 +234,7 @@ To skip auth for unauthenticated endpoints, pass \`authHeader: ""\`.`;
203
234
  }
204
235
  const authCallParams = serializeAuthCallParams(authParams);
205
236
  const authHeaderLine = noAuth
206
- ? `**No Auth:** This API does not require authentication. Pass \`authHeader: ""\` or omit auth params.`
237
+ ? `**No Auth (from workspace config):** Workspace indicates no authentication. **Verify independently** if you find auth in the OpenAPI spec or source code, override with the correct \`authHeader\` and \`authScheme\`.`
207
238
  : `**Auth params:** \`${authCallParams}\` — pass to EVERY tool call below.`;
208
239
  return `## How to Generate Tests — Tool Workflows
209
240
 
@@ -212,10 +243,14 @@ ${authGuidance}
212
243
 
213
244
  **For multi-endpoint workflows (integration tests) — Scenario → Integration pipeline:**
214
245
  1. Call \`skyramp_scenario_test_generation\` once per step: \`scenarioName\`, \`destination\`,
215
- \`baseURL\`, \`method\`, \`path\`, \`requestBody\`, \`responseBody\`, \`${authCallParams}\`.
246
+ \`baseURL\`, \`method\`, \`path\`, \`requestBody\` OR \`queryParams\`, \`responseBody\`, \`${authCallParams}\`.
216
247
  \`statusCode\` is optional — defaults: POST→201, DELETE→204, GET/PUT/PATCH→200. Only override for non-standard codes.
217
248
  **OpenAPI spec is NOT required.** \`apiSchema\` is OPTIONAL — omit it if no spec exists.
218
- \`requestBody\` should use realistic field values from source code schemas (Zod, Pydantic, DTOs).
249
+ **CRITICAL Query params vs request body:**
250
+ - For **POST/PUT/PATCH**: use \`requestBody\` with realistic field values from source code schemas.
251
+ - For **GET/DELETE with search/filter/pagination**: use \`queryParams\` (JSON string, e.g., \`{"q": "bear", "limit": 10}\`).
252
+ NEVER put query parameters in \`requestBody\` for GET requests — GET request bodies are non-standard and may be ignored or rejected.
253
+ - For **GET by ID**: no \`requestBody\` or \`queryParams\` needed — the ID is in the path.
219
254
  \`responseBody\` should match the actual API response shape from source code (including all fields
220
255
  returned by the controller — e.g., \`id\`, \`ownerId\`, \`createdAt\`, included relations like \`collection\`, \`tags\`).
221
256
  Wrap in \`{"response": ...}\` if the API uses an envelope pattern. If omitted, a synthetic response is generated.
@@ -233,6 +268,16 @@ ${authGuidance}
233
268
  If an OpenAPI spec exists, ALSO pass \`apiSchema\` — it enables schema-aware validation
234
269
  (contract tests verify response structure against the spec).
235
270
  Without a spec, \`endpointURL\` alone is sufficient.
271
+ ${PATH_PARAM_UUID_GUIDANCE}
272
+
273
+ **Contract test mode selection — set based on this service's role at the boundary:**
274
+ - \`providerMode: true\` — this service IS the API; validates the implementation matches the spec.
275
+ Use for new or modified endpoints this codebase owns, especially when an OpenAPI spec is present.
276
+ - \`consumerMode: true\` — this service CALLS another API; validates outbound requests conform to the downstream contract.
277
+ Use when the endpoint's implementation makes HTTP calls to external services (look for fetch/axios/httpx/http.Client/service clients).
278
+ A request-aware mock stands in for the real downstream service — no live dependency needed.
279
+ - Both — use when the service boundary is both a provider (owns an API) and a consumer (calls a downstream API).
280
+ - Neither (default) — use only when the role is ambiguous or no spec is available.
236
281
 
237
282
  **For UI tests (no Playwright recording):**
238
283
  1. \`skyramp_start_trace_collection\` (playwright: true)
@@ -108,9 +108,10 @@ Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
108
108
  const detailBlocks = detailEndpoints
109
109
  .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => {
110
110
  const reqBody = i.request.body ? `\n requestBody: ${JSON.stringify(i.request.body)}` : "";
111
+ const qParams = i.request.queryParams ? `\n queryParams: ${JSON.stringify(i.request.queryParams)}` : "";
111
112
  const resBody = i.response.body ? `\n responseBody: ${JSON.stringify(i.response.body)}` : "";
112
113
  const headers = i.request.headers ? `\n headers: ${JSON.stringify(i.request.headers)}` : "";
113
- return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type}): ${i.description}${reqBody}${resBody}${headers}`;
114
+ return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type}): ${i.description}${reqBody}${qParams}${resBody}${headers}`;
114
115
  })))
115
116
  .join("\n");
116
117
  interactionSection = `
@@ -118,7 +119,7 @@ Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
118
119
  ${summaryLines}
119
120
 
120
121
  ### Detailed (request/response bodies)
121
- ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies.
122
+ ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies and query parameters.
122
123
  ${detailBlocks}
123
124
  `;
124
125
  }
@@ -131,24 +132,51 @@ ${detailBlocks}
131
132
  const scenarioBlocks = scenarios
132
133
  .map((s) => {
133
134
  const stepLines = s.steps.map((st) => ` ${st.order ?? ""}. **${st.method} ${st.path}** → ${st.expectedStatusCode ?? 200}: ${st.description || ""}`).join("\n");
134
- const toolCalls = s.steps.map((st) => ` skyramp_scenario_test_generation({ scenarioName: "${s.scenarioName}", destination: "${s.scenarioName}", baseURL: "${baseUrl}", method: "${st.method}", path: "${st.path}", statusCode: ${st.expectedStatusCode ?? 200}, authHeader: "${authHeaderValue}"${authSchemeSnippet}${authTokenSnippet}, requestBody: <from source schemas for ${st.method} ${st.path}> })`).join("\n");
135
+ const toolCalls = s.steps.map((st) => {
136
+ const isBodyMethod = ["POST", "PUT", "PATCH"].includes(st.method);
137
+ const hasQueryParams = st.queryParams && Object.keys(st.queryParams).length > 0;
138
+ const isIdPath = /\{\w+\}$/.test(st.path);
139
+ let dataParam;
140
+ if (isBodyMethod) {
141
+ dataParam = `requestBody: <from source schemas for ${st.method} ${st.path}>`;
142
+ }
143
+ else if (hasQueryParams) {
144
+ dataParam = `queryParams: '${JSON.stringify(st.queryParams)}'`;
145
+ }
146
+ else if (isIdPath) {
147
+ dataParam = "";
148
+ }
149
+ else {
150
+ dataParam = `queryParams: <derive from source code for ${st.method} ${st.path} as a JSON string of URL query params>`;
151
+ }
152
+ const dataSnippet = dataParam ? `, ${dataParam}` : "";
153
+ const authSnippet = authHeaderValue
154
+ ? `, authHeader: "${authHeaderValue}"${authSchemeSnippet}${authTokenSnippet}`
155
+ : `, authHeader: <determine from OpenAPI spec securitySchemes or source code auth middleware — use "" ONLY if confirmed unauthenticated>`;
156
+ return ` skyramp_scenario_test_generation({ scenarioName: "${s.scenarioName}", destination: "${s.scenarioName}", baseURL: "${baseUrl}", method: "${st.method}", path: "${st.path}", statusCode: ${st.expectedStatusCode ?? 200}${authSnippet}${dataSnippet} })`;
157
+ }).join("\n");
135
158
  return (` ### ${s.scenarioName} (${s.category}, ${s.priority})\n` +
136
159
  ` ${s.description}\n` +
137
160
  ` **Steps:**\n${stepLines}\n` +
138
161
  ` **Chaining keys:** ${s.chainingKeys.join(", ") || "none"}\n` +
139
162
  ` **Tool calls:**\n${toolCalls}\n` +
140
- ` Then: skyramp_integration_test_generation({ scenarioFile: "scenario_${s.scenarioName}.json", authHeader: "${authHeaderValue}"${authSchemeSnippet}${authTokenSnippet} })`);
163
+ ` Then: skyramp_integration_test_generation({ scenarioFile: "scenario_${s.scenarioName}.json"${authHeaderValue ? `, authHeader: "${authHeaderValue}"${authSchemeSnippet}${authTokenSnippet}` : `, authHeader: <same auth as scenario calls above>`} })`);
141
164
  })
142
165
  .join("\n\n");
166
+ const authWarning = !authHeaderValue
167
+ ? `\n**WARNING — No auth configured.** Before generating scenarios without auth, verify the API does not require authentication by checking the OpenAPI spec \`securitySchemes\` and source code auth middleware. If auth is needed, include \`authHeader\` (and \`authScheme\` if applicable) in every \`skyramp_scenario_test_generation\` call.\n`
168
+ : "";
143
169
  scenarioSection = `
144
170
  ## Drafted Scenarios
145
- **Base URL:** \`${baseUrl}\` | **Auth:** \`${authHeaderValue}\`${authTypeValue ? ` | **Auth type:** \`${authTypeValue}\`` : ""}
171
+ **Base URL:** \`${baseUrl}\` | **Auth:** \`${authHeaderValue || "none (verify independently)"}\`${authTypeValue ? ` | **Auth type:** \`${authTypeValue}\`` : ""}${authWarning}
146
172
 
147
173
  Only use scenarios where resources are ACTUALLY related in the codebase. Replace any
148
174
  scenario that pairs unrelated resources with one reflecting real foreign key relationships.
149
175
 
150
176
  **Quality bar:** Realistic request bodies, actual foreign keys for chaining, response data
151
177
  verification (not just status codes), realistic test data (not "test product").
178
+ **Query vs Body:** For GET/DELETE requests, use \`queryParams\` (not \`requestBody\`) for search terms,
179
+ filters, pagination. Only POST/PUT/PATCH use \`requestBody\`.
152
180
  **Path verification:** Cross-reference paths against Router Mounting context — use correct
153
181
  nested paths. **Request bodies:** Replace placeholders with actual schemas from source code.
154
182
 
@@ -164,7 +192,8 @@ Draft at least 2-3 MEANINGFUL scenarios based on your codebase analysis:
164
192
  2. **Search/filter + verify** — create data, search, verify results
165
193
  3. **Error handling** — invalid cross-resource references → appropriate errors
166
194
 
167
- Use base URL: \`${analysis.apiEndpoints.baseUrl}\` and auth: \`${authHeaderValue}\`${authTypeValue ? ` (auth type: \`${authTypeValue}\`)` : ""}.
195
+ Use base URL: \`${analysis.apiEndpoints.baseUrl}\` and auth: \`${authHeaderValue || "none — verify independently from OpenAPI spec and source code"}\`${authTypeValue ? ` (auth type: \`${authTypeValue}\`)` : ""}.
196
+ ${!authHeaderValue ? "**Verify auth requirements** from OpenAPI spec and source code before omitting auth headers." : ""}
168
197
  `;
169
198
  }
170
199
  }
@@ -242,6 +242,7 @@ describe("buildRecommendationPrompt — PR History section", () => {
242
242
  expect(getOccurrences).toBe(1);
243
243
  expect(recSection).toContain("contract — POST /api/items");
244
244
  });
245
+ // Ensure de-duplicates across different scenarios independently
245
246
  it("de-duplicates across different scenarios independently", () => {
246
247
  const ctx = makePRContext({
247
248
  previousRecommendations: [
@@ -2,7 +2,7 @@ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { logger } from "../../utils/logger.js";
4
4
  import { AnalyticsService } from "../../services/AnalyticsService.js";
5
- import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS } from "../test-recommendation/recommendationSections.js";
5
+ import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, PATH_PARAM_UUID_GUIDANCE } from "../test-recommendation/recommendationSections.js";
6
6
  function getTestbotPrompt(prTitle, prDescription, diffFile, testDirectory, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, maxCritical = MAX_CRITICAL_TESTS, prNumber, userPrompt) {
7
7
  const promptSection = userPrompt ? `## Follow-up Request via @skyramp-testbot
8
8
 
@@ -119,6 +119,7 @@ Generate a net-new test. Use a unique descriptive filename to avoid overwriting
119
119
  For internal/microservice APIs: add \`providerMode: true\` to verify implementation matches the contract.
120
120
  For client-facing APIs consumed by frontend: add \`consumerMode: true\`.
121
121
  For critical service boundaries: pass both \`providerMode\` and \`consumerMode\`.
122
+ - ${PATH_PARAM_UUID_GUIDANCE}
122
123
  - **E2E/UI**: only if relevant Playwright traces exist in \`${testDirectory}\`, repo root, or \`.skyramp/\`.
123
124
  Without traces, move to \`additionalRecommendations\`. Do NOT suggest calling \`skyramp_ui_test_generation\`
124
125
  or \`skyramp_e2e_test_generation\` — those require traces. Instead, tell the user to record Playwright
@@ -134,6 +134,29 @@ ${JSON.stringify(traceRequest, null, 2)}
134
134
  const requestHeaders = {
135
135
  "Content-Type": ["application/json"],
136
136
  };
137
+ let queryParams = {};
138
+ if (params.queryParams) {
139
+ try {
140
+ const parsed = JSON.parse(params.queryParams);
141
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
142
+ for (const [k, v] of Object.entries(parsed)) {
143
+ queryParams[k] = Array.isArray(v)
144
+ ? v.map(String)
145
+ : [String(v)];
146
+ }
147
+ }
148
+ else {
149
+ logger.warning("queryParams JSON is not a plain object, ignoring", {
150
+ queryParams: params.queryParams,
151
+ });
152
+ }
153
+ }
154
+ catch {
155
+ logger.warning("Could not parse queryParams, ignoring", {
156
+ queryParams: params.queryParams,
157
+ });
158
+ }
159
+ }
137
160
  const authHeaderName = params.authHeader ?? "";
138
161
  if (authHeaderName) {
139
162
  if (params.authToken) {
@@ -158,7 +181,7 @@ ${JSON.stringify(traceRequest, null, 2)}
158
181
  ResponseHeaders: responseHeaders,
159
182
  Method: method,
160
183
  Path: basePath ? basePath + params.path : params.path,
161
- QueryParams: {},
184
+ QueryParams: queryParams,
162
185
  StatusCode: statusCode,
163
186
  Port: port,
164
187
  Timestamp: timestamp,
@@ -8,7 +8,7 @@ import { logger } from "../utils/logger.js";
8
8
  import { buildContainerEnv } from "./containerEnv.js";
9
9
  const DEFAULT_TIMEOUT = 300000; // 5 minutes
10
10
  const MAX_CONCURRENT_EXECUTIONS = 5;
11
- export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.13";
11
+ export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.14";
12
12
  const DOCKER_PLATFORM = "linux/amd64";
13
13
  const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
14
14
  // Temp file with valid empty JSON — used instead of /dev/null for .json config files
@@ -99,7 +99,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
99
99
  `Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
100
100
  ...candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`),
101
101
  ``,
102
- `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's outputDir unique in .skyramp/workspace.yml.`,
102
+ `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
103
103
  ].join("\n"),
104
104
  }],
105
105
  isError: true,
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
  import { ScenarioGenerationService } from "../../services/ScenarioGenerationService.js";
3
3
  import { baseSchema, AUTH_PLACEHOLDER_TOKEN } from "../../types/TestTypes.js";
4
4
  import { AnalyticsService } from "../../services/AnalyticsService.js";
5
+ import { getWorkspaceAuthConfig, WorkspaceAuthType } from "../../utils/workspaceAuth.js";
6
+ import { logger } from "../../utils/logger.js";
5
7
  const scenarioTestSchema = {
6
8
  scenarioName: z
7
9
  .string()
@@ -29,7 +31,16 @@ const scenarioTestSchema = {
29
31
  requestBody: z
30
32
  .string()
31
33
  .optional()
32
- .describe("JSON string of the request body parsed by AI from the scenario"),
34
+ .describe("JSON string of the request body parsed by AI from the scenario. "
35
+ + "IMPORTANT: For GET and DELETE requests, do NOT put query/filter/search parameters here — use queryParams instead. "
36
+ + "requestBody should only contain data sent in the HTTP body (typically for POST, PUT, PATCH)."),
37
+ queryParams: z
38
+ .string()
39
+ .optional()
40
+ .describe("JSON string of URL query parameters (e.g., '{\"q\": \"bear\", \"limit\": 10}'). "
41
+ + "Use this for GET request filters, search terms, pagination, sorting — any parameter that belongs in the URL query string. "
42
+ + "CRITICAL: For search/filter/list endpoints (e.g., GET /products/search?q=bear&limit=10), parameters MUST go here, NOT in requestBody. "
43
+ + "GET request bodies are non-standard and may be ignored or rejected by servers and frameworks, so always encode these parameters in the URL query string instead of the request body."),
33
44
  responseBody: z
34
45
  .string()
35
46
  .optional()
@@ -91,7 +102,8 @@ Returns a single TraceRequest object with:
91
102
  The AI should parse the natural language scenario and provide:
92
103
  - HTTP method (POST, GET, PUT, DELETE)
93
104
  - API path with CONCRETE ID values, not templates (e.g., /api/v1/products/70885, NOT /api/v1/products/{product_id})
94
- - Request body (JSON string, if applicable)
105
+ - Request body (JSON string) for POST/PUT/PATCH requests
106
+ - Query parameters (JSON string) for GET search/filter/list requests — NEVER put query params in requestBody
95
107
  - Response body (JSON string, if applicable)
96
108
  - Status code (optional, defaults based on method)
97
109
  - Entity details (name, price, quantity, ID as needed)
@@ -112,6 +124,25 @@ When generating an integration test using the scenario file created by this tool
112
124
  Passing both scenarioFile and apiSchema/endpointURL will cause the test generation to fail.`,
113
125
  inputSchema: scenarioTestSchema,
114
126
  }, async (params) => {
127
+ if (!params.authHeader) {
128
+ try {
129
+ const repoPath = params.outputDir || process.cwd();
130
+ const wsAuth = await getWorkspaceAuthConfig(repoPath);
131
+ if (wsAuth.authHeader && wsAuth.authType !== WorkspaceAuthType.None) {
132
+ logger.info("Auth header was empty — resolved from workspace config", {
133
+ authHeader: wsAuth.authHeader,
134
+ authType: wsAuth.authType,
135
+ });
136
+ params.authHeader = wsAuth.authHeader;
137
+ if (/^authorization$/i.test(wsAuth.authHeader) && wsAuth.authType) {
138
+ params.authScheme = params.authScheme || wsAuth.authType;
139
+ }
140
+ }
141
+ }
142
+ catch {
143
+ logger.warning("Could not resolve auth from workspace config");
144
+ }
145
+ }
115
146
  const service = new ScenarioGenerationService();
116
147
  const result = await service.parseScenario(params);
117
148
  AnalyticsService.pushMCPToolEvent(TOOL_NAME, result, params).catch(() => {
@@ -463,10 +463,15 @@ to produce a unified state file for the test health workflow.
463
463
  if (methodObj) {
464
464
  const alreadyHasStatus = methodObj.interactions.some((i) => i.response.statusCode === entry.statusCode);
465
465
  if (!alreadyHasStatus) {
466
+ const traceRequest = {};
467
+ if (entry.requestBody)
468
+ traceRequest.body = entry.requestBody;
469
+ if (entry.queryParams)
470
+ traceRequest.queryParams = entry.queryParams;
466
471
  methodObj.interactions.push({
467
472
  description: `${entry.method} ${entry.path} \u2192 ${entry.statusCode} (from trace)`,
468
473
  type: "success",
469
- request: entry.requestBody ? { body: entry.requestBody } : {},
474
+ request: Object.keys(traceRequest).length > 0 ? traceRequest : {},
470
475
  response: {
471
476
  statusCode: entry.statusCode,
472
477
  description: `Observed in trace (${traceResult.format})`,
@@ -140,7 +140,7 @@ back into the state file for use by \`skyramp_actions\`.
140
140
  `Cannot determine SKYRAMP_TEST_BASE_URL — one or more test files match multiple services:`,
141
141
  ...lines,
142
142
  ``,
143
- `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's outputDir unique in .skyramp/workspace.yml.`,
143
+ `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
144
144
  ].join("\n"),
145
145
  }],
146
146
  isError: true,
@@ -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 where the test files will be generated, e.g. "services/api-gateway"
32
+ testDirectory — 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
@@ -188,8 +188,8 @@ Create one service entry per deployable unit. You MUST include:
188
188
  parts.push(svc.language);
189
189
  if (svc.framework)
190
190
  parts.push(svc.framework);
191
- if (svc.outputDir)
192
- parts.push(`@ ${svc.outputDir}`);
191
+ if (svc.testDirectory)
192
+ parts.push(`@ ${svc.testDirectory}`);
193
193
  lines.push(` - ${parts.join(" | ")}`);
194
194
  if (svc.api?.schemaPath)
195
195
  lines.push(` API: ${svc.api.schemaPath}`);
@@ -64,6 +64,7 @@ export const scenarioStepSchema = z.object({
64
64
  description: z.string(),
65
65
  interactionType: z.enum(["success", "error", "edge-case"]),
66
66
  requestBody: z.record(z.any()).optional(),
67
+ queryParams: z.record(z.any()).optional(),
67
68
  responseBody: z.record(z.any()).optional(),
68
69
  expectedStatusCode: z.number(),
69
70
  expectedResponseFields: z.array(z.string()).optional(),
@@ -38,16 +38,27 @@ function sanitizeHeaders(headers) {
38
38
  return result;
39
39
  }
40
40
  function parseSkyrampTrace(data) {
41
- return data.map((entry) => ({
42
- method: (entry.method || entry.request?.method || "GET").toUpperCase(),
43
- path: entry.path || entry.url || entry.request?.url || "/",
44
- statusCode: entry.statusCode || entry.status || entry.response?.status || 0,
45
- requestHeaders: entry.request?.headers ? sanitizeHeaders(entry.request.headers) : undefined,
46
- requestBody: entry.request?.body || entry.requestBody,
47
- responseHeaders: entry.response?.headers ? sanitizeHeaders(entry.response.headers) : undefined,
48
- responseBody: summarizeBody(entry.response?.body || entry.responseBody),
49
- timestamp: entry.timestamp || entry.request?.timestamp || "",
50
- }));
41
+ return data.map((entry) => {
42
+ const rawQp = entry.QueryParams || entry.queryParams;
43
+ let queryParams;
44
+ if (rawQp && typeof rawQp === "object" && !Array.isArray(rawQp) && Object.keys(rawQp).length > 0) {
45
+ queryParams = {};
46
+ for (const [k, v] of Object.entries(rawQp)) {
47
+ queryParams[k] = Array.isArray(v) ? v.map(String) : [String(v)];
48
+ }
49
+ }
50
+ return {
51
+ method: (entry.method || entry.Method || entry.request?.method || "GET").toUpperCase(),
52
+ path: entry.path || entry.Path || entry.url || entry.request?.url || "/",
53
+ statusCode: entry.statusCode || entry.StatusCode || entry.status || entry.response?.status || 0,
54
+ requestHeaders: entry.request?.headers ? sanitizeHeaders(entry.request.headers) : undefined,
55
+ requestBody: entry.request?.body || entry.requestBody || entry.RequestBody,
56
+ queryParams,
57
+ responseHeaders: entry.response?.headers ? sanitizeHeaders(entry.response.headers) : undefined,
58
+ responseBody: summarizeBody(entry.response?.body || entry.responseBody || entry.ResponseBody),
59
+ timestamp: entry.timestamp || entry.Timestamp || entry.request?.timestamp || "",
60
+ };
61
+ });
51
62
  }
52
63
  function parseHarTrace(harData) {
53
64
  const entries = harData?.log?.entries || [];
@@ -79,12 +90,22 @@ function parseHarTrace(harData) {
79
90
  resBody = res.content.text;
80
91
  }
81
92
  }
93
+ const queryParams = {};
94
+ url.searchParams.forEach((v, k) => {
95
+ if (queryParams[k]) {
96
+ queryParams[k].push(v);
97
+ }
98
+ else {
99
+ queryParams[k] = [v];
100
+ }
101
+ });
82
102
  return {
83
103
  method: (req.method || "GET").toUpperCase(),
84
- path: url.pathname + (url.search || ""),
104
+ path: url.pathname,
85
105
  statusCode: res.status || 0,
86
106
  requestHeaders: sanitizeHeaders(reqHeaders),
87
107
  requestBody: reqBody,
108
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
88
109
  responseHeaders: sanitizeHeaders(resHeaders),
89
110
  responseBody: summarizeBody(resBody),
90
111
  timestamp: entry.startedDateTime || "",
@@ -20,12 +20,12 @@ export async function getWorkspaceAuthHeader(repositoryPath) {
20
20
  * Returns scheme+host+port for the service that owns the given test file.
21
21
  *
22
22
  * Matching strategy (in order):
23
- * 1. Filter services whose `outputDir` is a path prefix of `testFile`.
24
- * 2. If multiple services match (shared outputDir), narrow by `language`.
23
+ * 1. Filter services whose `testDirectory` is a path prefix of `testFile`.
24
+ * 2. If multiple services match (shared testDirectory), narrow by `language`.
25
25
  * 3. If still ambiguous, return all candidates so the LLM can resolve it —
26
26
  * never guess when multiple services are plausible owners.
27
27
  *
28
- * e.g. repositoryPath=/repo, outputDir=backend/tests, testFile=/repo/backend/tests/test_api.py
28
+ * e.g. repositoryPath=/repo, testDirectory=backend/tests, testFile=/repo/backend/tests/test_api.py
29
29
  * → { baseUrl: "http://localhost:8000", candidates: [] }
30
30
  */
31
31
  export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
@@ -42,12 +42,12 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
42
42
  const absTestFile = path.isAbsolute(testFile)
43
43
  ? testFile
44
44
  : path.resolve(repositoryPath, testFile);
45
- // Step 1: filter by outputDir prefix
45
+ // Step 1: filter by testDirectory prefix
46
46
  let matches = services.filter((s) => {
47
- if (!s.outputDir || !s.api?.baseUrl)
47
+ if (!s.testDirectory || !s.api?.baseUrl)
48
48
  return false;
49
- const absOutputDir = path.resolve(repositoryPath, s.outputDir);
50
- return absTestFile.startsWith(absOutputDir + path.sep) || absTestFile === absOutputDir;
49
+ const absTestDirectory = path.resolve(repositoryPath, s.testDirectory);
50
+ return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
51
51
  });
52
52
  // Step 2: narrow by language if still ambiguous
53
53
  if (matches.length > 1 && language) {
@@ -64,7 +64,7 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
64
64
  return {
65
65
  baseUrl: undefined,
66
66
  candidates: matches.map((s) => ({
67
- serviceName: s.serviceName ?? s.outputDir,
67
+ serviceName: s.serviceName ?? s.testDirectory,
68
68
  baseUrl: s.api.baseUrl,
69
69
  })),
70
70
  };
@@ -6,13 +6,13 @@ jest.mock("@skyramp/skyramp", () => {
6
6
  {
7
7
  serviceName: "backend",
8
8
  language: "python",
9
- outputDir: "backend/tests",
9
+ testDirectory: "backend/tests",
10
10
  api: { baseUrl: "http://localhost:8000" },
11
11
  },
12
12
  {
13
13
  serviceName: "frontend",
14
14
  language: "typescript",
15
- outputDir: "frontend/tests",
15
+ testDirectory: "frontend/tests",
16
16
  api: { baseUrl: "http://localhost:5173" },
17
17
  },
18
18
  ],
@@ -30,17 +30,17 @@ describe("getWorkspaceBaseUrl", () => {
30
30
  expect(result.baseUrl).toBeUndefined();
31
31
  expect(result.candidates).toHaveLength(0);
32
32
  });
33
- it("should match backend service by outputDir prefix", async () => {
33
+ it("should match backend service by testDirectory prefix", async () => {
34
34
  const result = await getWorkspaceBaseUrl(REPO, `${REPO}/backend/tests/test_api.py`);
35
35
  expect(result.baseUrl).toBe("http://localhost:8000");
36
36
  expect(result.candidates).toHaveLength(0);
37
37
  });
38
- it("should match frontend service by outputDir prefix", async () => {
38
+ it("should match frontend service by testDirectory prefix", async () => {
39
39
  const result = await getWorkspaceBaseUrl(REPO, `${REPO}/frontend/tests/auth.spec.ts`);
40
40
  expect(result.baseUrl).toBe("http://localhost:5173");
41
41
  expect(result.candidates).toHaveLength(0);
42
42
  });
43
- it("should return no match when testFile does not match any service outputDir", async () => {
43
+ it("should return no match when testFile does not match any service testDirectory", async () => {
44
44
  const result = await getWorkspaceBaseUrl(REPO, `${REPO}/other/tests/test_something.py`);
45
45
  expect(result.baseUrl).toBeUndefined();
46
46
  expect(result.candidates).toHaveLength(0);
@@ -53,7 +53,7 @@ describe("getWorkspaceBaseUrl", () => {
53
53
  services: [
54
54
  {
55
55
  serviceName: "api",
56
- outputDir: "tests",
56
+ testDirectory: "tests",
57
57
  api: { baseUrl: "http://localhost:8000/api/v2" },
58
58
  },
59
59
  ],
@@ -73,7 +73,7 @@ describe("getWorkspaceBaseUrl", () => {
73
73
  expect(result.baseUrl).toBeUndefined();
74
74
  expect(result.candidates).toHaveLength(0);
75
75
  });
76
- it("should disambiguate by language when multiple services share the same outputDir", async () => {
76
+ it("should disambiguate by language when multiple services share the same testDirectory", async () => {
77
77
  const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
78
78
  WorkspaceConfigManager.mockImplementationOnce(() => ({
79
79
  exists: jest.fn().mockResolvedValue(true),
@@ -82,13 +82,13 @@ describe("getWorkspaceBaseUrl", () => {
82
82
  {
83
83
  serviceName: "svc-python",
84
84
  language: "python",
85
- outputDir: "shared/tests",
85
+ testDirectory: "shared/tests",
86
86
  api: { baseUrl: "http://localhost:8000" },
87
87
  },
88
88
  {
89
89
  serviceName: "svc-ts",
90
90
  language: "typescript",
91
- outputDir: "shared/tests",
91
+ testDirectory: "shared/tests",
92
92
  api: { baseUrl: "http://localhost:9000" },
93
93
  },
94
94
  ],
@@ -98,7 +98,7 @@ describe("getWorkspaceBaseUrl", () => {
98
98
  expect(result.baseUrl).toBe("http://localhost:8000");
99
99
  expect(result.candidates).toHaveLength(0);
100
100
  });
101
- it("should return candidates when outputDir and language are both shared across services", async () => {
101
+ it("should return candidates when testDirectory and language are both shared across services", async () => {
102
102
  const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
103
103
  WorkspaceConfigManager.mockImplementationOnce(() => ({
104
104
  exists: jest.fn().mockResolvedValue(true),
@@ -107,13 +107,13 @@ describe("getWorkspaceBaseUrl", () => {
107
107
  {
108
108
  serviceName: "svc-a",
109
109
  language: "python",
110
- outputDir: "shared/tests",
110
+ testDirectory: "shared/tests",
111
111
  api: { baseUrl: "http://localhost:8000" },
112
112
  },
113
113
  {
114
114
  serviceName: "svc-b",
115
115
  language: "python",
116
- outputDir: "shared/tests",
116
+ testDirectory: "shared/tests",
117
117
  api: { baseUrl: "http://localhost:9000" },
118
118
  },
119
119
  ],
@@ -128,12 +128,30 @@ describe("getWorkspaceBaseUrl", () => {
128
128
  "http://localhost:9000",
129
129
  ]);
130
130
  });
131
+ it("should match service using testDirectory as the workspace field for test file lookup", async () => {
132
+ const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
133
+ WorkspaceConfigManager.mockImplementationOnce(() => ({
134
+ exists: jest.fn().mockResolvedValue(true),
135
+ read: jest.fn().mockResolvedValue({
136
+ services: [
137
+ {
138
+ serviceName: "api",
139
+ testDirectory: "tests",
140
+ api: { baseUrl: "http://localhost:9000" },
141
+ },
142
+ ],
143
+ }),
144
+ }));
145
+ const result = await getWorkspaceBaseUrl(REPO, `${REPO}/tests/test_api.py`);
146
+ expect(result.baseUrl).toBe("http://localhost:9000");
147
+ expect(result.candidates).toHaveLength(0);
148
+ });
131
149
  it("should return no match when matched service baseUrl is not a valid URL", async () => {
132
150
  const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
133
151
  WorkspaceConfigManager.mockImplementationOnce(() => ({
134
152
  exists: jest.fn().mockResolvedValue(true),
135
153
  read: jest.fn().mockResolvedValue({
136
- services: [{ api: { baseUrl: "not-a-valid-url" }, outputDir: "tests" }],
154
+ services: [{ api: { baseUrl: "not-a-valid-url" }, testDirectory: "tests" }],
137
155
  }),
138
156
  }));
139
157
  const result = await getWorkspaceBaseUrl(REPO, `${REPO}/tests/test_api.py`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.0.63-rc.5",
3
+ "version": "0.0.63-rc.6",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.24.3",
48
48
  "@playwright/test": "^1.55.0",
49
- "@skyramp/skyramp": "1.3.13",
49
+ "@skyramp/skyramp": "1.3.14",
50
50
  "dockerode": "^4.0.6",
51
51
  "fast-glob": "^3.3.3",
52
52
  "simple-git": "^3.30.0",