wopee-mcp 1.20.0 → 1.23.0

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.
package/README.md CHANGED
@@ -298,7 +298,7 @@ Dispatches an autonomous testing agent to execute test cases for a selected suit
298
298
  - `testCases` - Array of test case objects to execute, each containing:
299
299
  - `testCaseId` - The ID of the test case
300
300
  - `userStoryId` - The ID of the associated user story
301
- - **Returns:** Success message indicating the agent has been dispatched
301
+ - **Returns:** Array of executed test case objects with their initial execution state (uuid, executionStatus, agentReportStatus, codeReportStatus, etc.)
302
302
 
303
303
  **Example Usage:**
304
304
 
@@ -306,6 +306,27 @@ Dispatches an autonomous testing agent to execute test cases for a selected suit
306
306
  Dispatch agent for my latest suite's user story US001 and test case TC003
307
307
  ```
308
308
 
309
+ ### Test Results
310
+
311
+ #### `wopee_fetch_executed_test_cases`
312
+
313
+ Fetches executed test cases and their results for a given analysis suite. Use this to check the status and reports of dispatched agent runs.
314
+
315
+ - **Parameters:**
316
+ - `suiteUuid` - The UUID of the analysis suite to fetch results for
317
+ - `analysisIdentifier` _(optional)_ - Analysis identifier to narrow results (e.g. `A068`)
318
+ - **Returns:** Array of results grouped by user story, each containing executed test cases with execution status, agent report, agent report status, code report, and code report status
319
+
320
+ **Example Usage:**
321
+
322
+ ```
323
+ Fetch test results for suite <suiteUuid>
324
+ ```
325
+
326
+ ```
327
+ Show me the executed test cases for my latest analysis suite
328
+ ```
329
+
309
330
  ## Typical Workflow
310
331
 
311
332
  1. **Start with a suite:**
@@ -326,6 +347,20 @@ Dispatch agent for my latest suite's user story US001 and test case TC003
326
347
  4. **Run tests:**
327
348
  - Use `wopee_dispatch_agent` to execute test cases with the autonomous testing agent
328
349
 
350
+ 5. **Check results:**
351
+ - Use `wopee_fetch_executed_test_cases` to check the status and reports of dispatched agent runs
352
+ - Or use the `fetch-test-results` prompt for a formatted summary of all test results
353
+
354
+ ## Available Prompts
355
+
356
+ ### `fetch-project-summary`
357
+
358
+ Fetches analysis suites and their user stories/test cases, then displays a formatted summary with two markdown tables: a suite overview and a detailed test case breakdown.
359
+
360
+ ### `fetch-test-results`
361
+
362
+ Fetches analysis suites and their executed test case results, then displays formatted markdown tables showing execution status, agent report status, and code report status for each test case. Also surfaces failed report details.
363
+
329
364
  ## Notes
330
365
 
331
366
  - Most tools require a `suiteUuid`. Always start by fetching or creating a suite.
@@ -0,0 +1,65 @@
1
+ import { PromptName } from "../shared/types.js";
2
+ export const fetchTestResults = {
3
+ name: PromptName.FETCH_TEST_RESULTS,
4
+ config: {
5
+ title: "Fetch test results",
6
+ description: "Fetch and display test execution results for analysis suites",
7
+ },
8
+ handler: () => ({
9
+ messages: [
10
+ {
11
+ role: "user",
12
+ content: {
13
+ type: "text",
14
+ text: `Please, fetch my analysis suites using 'wopee_fetch_analysis_suites' tool. If there are more than 5 suites, ask the user to select which suite(s) they want to see results for.
15
+
16
+ Then, for each selected suite, fetch the executed test cases using 'wopee_fetch_executed_test_cases' tool with the suite's uuid and analysisIdentifier.
17
+
18
+ After fetching all of the necessary data, summarize and display readable markdown tables as an output in the same chat conversation. Do NOT create, write, or save any files to disk - only display the formatted tables in your response.
19
+
20
+ Display the following tables:
21
+
22
+ 1. **Suite Overview Table** - one row per suite showing:
23
+ - Suite name
24
+ - Analysis identifier
25
+ - Execution status
26
+ - Suite running status
27
+
28
+ ### Example:
29
+ | Suite Name | Identifier | Execution Status | Running Status |
30
+ |------------|------------|------------------|----------------|
31
+ | Analysis - A068 | A068 | FINISHED | IDLE |
32
+ | Analysis - A067 | A067 | IN_PROGRESS | IN_PROGRESS |
33
+
34
+ ___
35
+
36
+ 2. **Test Case Results Table** - for each suite that has executed test cases:
37
+ - Suite name
38
+ - User story ID
39
+ - Test case ID
40
+ - Execution status
41
+ - Agent report status (PASSED/FAILED or N/A)
42
+ - Code report status (PASSED/FAILED or N/A)
43
+
44
+ ### Example:
45
+ | Suite Name | User Story | Test Case | Execution | Agent Status | Code Status |
46
+ |------------|------------|-----------|-----------|--------------|-------------|
47
+ | Analysis - A068 | US001 | TC001 | FINISHED | PASSED | PASSED |
48
+ | | US001 | TC002 | FINISHED | PASSED | FAILED |
49
+ | | US002 | TC001 | FINISHED | FAILED | N/A |
50
+ | Analysis - A067 | US001 | TC001 | IN_PROGRESS | N/A | N/A |
51
+
52
+ ___
53
+
54
+ 3. If any test case has a FAILED agent or code report status, display the report content below the tables under a "Failed Reports" section, showing the suite name, test case ID, and the report text.
55
+
56
+ ## Important Notes:
57
+ - If a test case's executionStatus is IN_PROGRESS, note that results are not yet available.
58
+ - Use "N/A" for report statuses that are null (not yet available).
59
+ - Group test cases by user story within each suite.
60
+ `,
61
+ },
62
+ },
63
+ ],
64
+ }),
65
+ };
@@ -1,2 +1,3 @@
1
1
  import { fetchProjectSummary } from "./fetch_project_summary/index.js";
2
- export const PROMPTS = [fetchProjectSummary];
2
+ import { fetchTestResults } from "./fetch_test_results/index.js";
3
+ export const PROMPTS = [fetchProjectSummary, fetchTestResults];
@@ -1,4 +1,5 @@
1
1
  export var PromptName;
2
2
  (function (PromptName) {
3
3
  PromptName["FETCH_PROJECT_SUMMARY"] = "fetch-project-summary";
4
+ PromptName["FETCH_TEST_RESULTS"] = "fetch-test-results";
4
5
  })(PromptName || (PromptName = {}));
@@ -5,9 +5,11 @@ import { wopeeDispatchAgent } from "./wopee_dispatch_agent/index.js";
5
5
  import { wopeeDispatchAnalysis } from "./wopee_dispatch_analysis/index.js";
6
6
  import { wopeeCreateBlankSuite } from "./wopee_create_blank_suite/index.js";
7
7
  import { wopeeFetchAnalysisSuites } from "./wopee_fetch_analysis_suites/index.js";
8
+ import { wopeeFetchExecutedTestCases } from "./wopee_fetch_executed_test_cases/index.js";
8
9
  export const TOOLS = [
9
10
  wopeeCreateBlankSuite,
10
11
  wopeeFetchAnalysisSuites,
12
+ wopeeFetchExecutedTestCases,
11
13
  wopeeDispatchAnalysis,
12
14
  wopeeDispatchAgent,
13
15
  wopeeFetchArtifact,
@@ -155,6 +155,28 @@ export const GenerateReusableTestCaseSteps = `
155
155
  generateReusableTestCaseSteps(input: $input)
156
156
  }
157
157
  `;
158
+ export const FetchExecutedTestCases = `
159
+ query FetchExecutedTestCases($input: FetchExecutedTestCasesInput!) {
160
+ fetchExecutedTestCases(input: $input) {
161
+ userStoryId
162
+ executedTestCases {
163
+ uuid
164
+ suiteUuid
165
+ analysisSuiteUuid
166
+ analysisIdentifier
167
+ userStoryId
168
+ testCaseId
169
+ executionStatus
170
+ agentReport
171
+ agentReportStatus
172
+ codeReport
173
+ codeReportStatus
174
+ createdAt
175
+ updatedAt
176
+ }
177
+ }
178
+ }
179
+ `;
158
180
  export const CreateBlankAnalysisSuite = `
159
181
  mutation CreateBlankAnalysisSuite($projectUuid: ID!) {
160
182
  createBlankAnalysisSuite(projectUuid: $projectUuid) {
@@ -15,12 +15,12 @@ export async function fetchArtifact(input) {
15
15
  const result = await requestClient(FetchArtifact, {
16
16
  input: parsedInput,
17
17
  });
18
- if (!result || !result?.fetchArtifact || !result?.fetchArtifact?.content)
18
+ if (!result?.fetchArtifact?.content)
19
19
  return {
20
20
  content: [
21
21
  {
22
22
  type: "text",
23
- text: "Failed to fetch file",
23
+ text: "Failed to fetch file: no content returned",
24
24
  },
25
25
  ],
26
26
  };
@@ -54,12 +54,12 @@ export async function generateAIDataFile(input) {
54
54
  const generationResult = await requestClient(query, {
55
55
  input: parsedInput,
56
56
  });
57
- if (!generationResult || !generationResult[dataKey])
57
+ if (!generationResult?.[dataKey])
58
58
  return {
59
59
  content: [
60
60
  {
61
61
  type: "text",
62
- text: `Failed to generate ${description}`,
62
+ text: `Failed to generate ${description}: no result returned`,
63
63
  },
64
64
  ],
65
65
  };
@@ -86,12 +86,12 @@ export async function updateArtifact(input) {
86
86
  const updateFileResult = await requestClient(UpdateArtifact, {
87
87
  input: parsedInput,
88
88
  });
89
- if (!updateFileResult || !updateFileResult.updateArtifact)
89
+ if (!updateFileResult?.updateArtifact)
90
90
  return {
91
91
  content: [
92
92
  {
93
93
  type: "text",
94
- text: "Failed to update file",
94
+ text: "Failed to update file: operation returned false",
95
95
  },
96
96
  ],
97
97
  };
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { GenerateAppContext, GenerateGeneralUserStories, GenerateReusableTestCases, GenerateReusableTestCaseSteps, GenerateTestCases, GenerateTestCaseSteps, GenerateUserStoriesWithTestCases, } from "./gql-queries.js";
3
3
  import { ArtifactType, GenerateArtifactType } from "./types.js";
4
+ import { RequestError } from "../../utils/requestClient.js";
4
5
  export function _convertToArtifactType(type) {
5
6
  switch (type) {
6
7
  case GenerateArtifactType.APP_CONTEXT:
@@ -69,6 +70,16 @@ export function _parseGenerateArtifactType(type) {
69
70
  }
70
71
  export function _parseError(error) {
71
72
  console.error(error instanceof z.ZodError ? error.issues : error);
73
+ if (error instanceof RequestError) {
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: `[${error.code}] ${error.message}${error.retryable ? " (retryable)" : ""}`,
79
+ },
80
+ ],
81
+ };
82
+ }
72
83
  return {
73
84
  content: [
74
85
  {
@@ -2,6 +2,7 @@ export var ToolName;
2
2
  (function (ToolName) {
3
3
  ToolName["WOPEE_CREATE_BLANK_SUITE"] = "wopee_create_blank_suite";
4
4
  ToolName["WOPEE_FETCH_ANALYSIS_SUITES"] = "wopee_fetch_analysis_suites";
5
+ ToolName["WOPEE_FETCH_EXECUTED_TEST_CASES"] = "wopee_fetch_executed_test_cases";
5
6
  ToolName["WOPEE_DISPATCH_ANALYSIS"] = "wopee_dispatch_analysis";
6
7
  ToolName["WOPEE_DISPATCH_AGENT"] = "wopee_dispatch_agent";
7
8
  ToolName["WOPEE_FETCH_ARTIFACT"] = "wopee_fetch_artifact";
@@ -56,3 +57,8 @@ export var GenerationStatus;
56
57
  GenerationStatus["FINISHED"] = "FINISHED";
57
58
  GenerationStatus["FAILED"] = "FAILED";
58
59
  })(GenerationStatus || (GenerationStatus = {}));
60
+ export var ReportStatus;
61
+ (function (ReportStatus) {
62
+ ReportStatus["PASSED"] = "PASSED";
63
+ ReportStatus["FAILED"] = "FAILED";
64
+ })(ReportStatus || (ReportStatus = {}));
@@ -1,5 +1,6 @@
1
1
  import { getConfig } from "../../utils/getConfig.js";
2
2
  import { requestClient } from "../../utils/requestClient.js";
3
+ import { _parseError } from "../shared/helpers.js";
3
4
  import { ToolName } from "../shared/types.js";
4
5
  import { CreateBlankAnalysisSuite } from "../shared/gql-queries.js";
5
6
  export const wopeeCreateBlankSuite = {
@@ -9,35 +10,40 @@ export const wopeeCreateBlankSuite = {
9
10
  description: "Create a blank analysis suite for a project",
10
11
  },
11
12
  handler: async () => {
12
- const { WOPEE_PROJECT_UUID } = getConfig();
13
- if (!WOPEE_PROJECT_UUID)
13
+ try {
14
+ const { WOPEE_PROJECT_UUID } = getConfig();
15
+ if (!WOPEE_PROJECT_UUID)
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: "WOPEE_PROJECT_UUID is not set",
21
+ },
22
+ ],
23
+ };
24
+ const result = await requestClient(CreateBlankAnalysisSuite, {
25
+ projectUuid: WOPEE_PROJECT_UUID,
26
+ });
27
+ if (!result?.createBlankAnalysisSuite)
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: "Failed to create blank suite: no data returned",
33
+ },
34
+ ],
35
+ };
14
36
  return {
15
37
  content: [
16
38
  {
17
39
  type: "text",
18
- text: "WOPEE_PROJECT_UUID is not set",
40
+ text: JSON.stringify(result.createBlankAnalysisSuite, null, 2),
19
41
  },
20
42
  ],
21
43
  };
22
- const result = await requestClient(CreateBlankAnalysisSuite, {
23
- projectUuid: WOPEE_PROJECT_UUID,
24
- });
25
- if (!result || !result.createBlankAnalysisSuite)
26
- return {
27
- content: [
28
- {
29
- type: "text",
30
- text: "Failed to fetch analysis suites",
31
- },
32
- ],
33
- };
34
- return {
35
- content: [
36
- {
37
- type: "text",
38
- text: JSON.stringify(result.createBlankAnalysisSuite, null, 2),
39
- },
40
- ],
41
- };
44
+ }
45
+ catch (error) {
46
+ return _parseError(error);
47
+ }
42
48
  },
43
49
  };
@@ -4,26 +4,27 @@ import { _parseError } from "../shared/helpers.js";
4
4
  import { createDispatchAgentInput } from "./factory.js";
5
5
  import { DispatchAgent } from "../shared/gql-queries.js";
6
6
  import { requestClient } from "../../utils/requestClient.js";
7
+ import { withRetry } from "../../utils/withRetry.js";
7
8
  export const wopeeDispatchAgent = {
8
9
  name: ToolName.WOPEE_DISPATCH_AGENT,
9
10
  config: {
10
11
  title: "Dispatch agent",
11
- description: "Dispatch agent testing for selected suite's test cases",
12
+ description: "Dispatch agent testing for selected suite's test cases. Note: there is a 10-second per-project rate limit between dispatches; concurrent calls will auto-retry with backoff.",
12
13
  inputSchema: WopeeDispatchAgentInputSchema.shape,
13
14
  },
14
15
  handler: async (input) => {
15
16
  try {
16
17
  const dispatchAgentInput = createDispatchAgentInput(input);
17
18
  const parsedInput = DispatchAgentInputSchema.parse(dispatchAgentInput);
18
- const result = await requestClient(DispatchAgent, {
19
+ const result = await withRetry(() => requestClient(DispatchAgent, {
19
20
  input: parsedInput,
20
- });
21
- if (!result || result?.dispatchAgent?.length === 0)
21
+ }));
22
+ if (!result?.dispatchAgent?.length)
22
23
  return {
23
24
  content: [
24
25
  {
25
26
  type: "text",
26
- text: "Failed to dispatch agent",
27
+ text: "Failed to dispatch agent: no dispatch result returned",
27
28
  },
28
29
  ],
29
30
  };
@@ -31,7 +32,7 @@ export const wopeeDispatchAgent = {
31
32
  content: [
32
33
  {
33
34
  type: "text",
34
- text: "Agent dispatched successfully",
35
+ text: JSON.stringify(result.dispatchAgent, null, 2),
35
36
  },
36
37
  ],
37
38
  };
@@ -3,27 +3,28 @@ import { DispatchAnalysisInputSchema, WopeeDispatchAnalysisInputSchema, } from "
3
3
  import { createDispatchAnalysisInput } from "./factory.js";
4
4
  import { DispatchAnalysis } from "../shared/gql-queries.js";
5
5
  import { requestClient } from "../../utils/requestClient.js";
6
+ import { withRetry } from "../../utils/withRetry.js";
6
7
  import { ToolName } from "../shared/types.js";
7
8
  export const wopeeDispatchAnalysis = {
8
9
  name: ToolName.WOPEE_DISPATCH_ANALYSIS,
9
10
  config: {
10
11
  title: "Dispatch analysis",
11
- description: "Create and dispatch analysis/crawling suite for a project",
12
+ description: "Create and dispatch analysis/crawling suite for a project. Note: there is a 10-second per-project rate limit between dispatches; concurrent calls will auto-retry with backoff.",
12
13
  inputSchema: WopeeDispatchAnalysisInputSchema.shape,
13
14
  },
14
15
  handler: async (input) => {
15
16
  try {
16
17
  const rawInput = createDispatchAnalysisInput(input);
17
18
  const parsedInput = DispatchAnalysisInputSchema.parse(rawInput);
18
- const result = await requestClient(DispatchAnalysis, {
19
+ const result = await withRetry(() => requestClient(DispatchAnalysis, {
19
20
  input: parsedInput,
20
- });
21
- if (!result || !result.dispatchAnalysis)
21
+ }));
22
+ if (!result?.dispatchAnalysis)
22
23
  return {
23
24
  content: [
24
25
  {
25
26
  type: "text",
26
- text: "Failed to dispatch agent",
27
+ text: "Failed to dispatch agent: no analysis suite returned",
27
28
  },
28
29
  ],
29
30
  };
@@ -1,5 +1,6 @@
1
1
  import { getConfig } from "../../utils/getConfig.js";
2
2
  import { requestClient } from "../../utils/requestClient.js";
3
+ import { _parseError } from "../shared/helpers.js";
3
4
  import { ToolName } from "../shared/types.js";
4
5
  import { FetchAnalysisSuites } from "../shared/gql-queries.js";
5
6
  export const wopeeFetchAnalysisSuites = {
@@ -9,35 +10,40 @@ export const wopeeFetchAnalysisSuites = {
9
10
  description: "Fetch project's analysis suites from Woopee",
10
11
  },
11
12
  handler: async () => {
12
- const { WOPEE_PROJECT_UUID } = getConfig();
13
- if (!WOPEE_PROJECT_UUID)
13
+ try {
14
+ const { WOPEE_PROJECT_UUID } = getConfig();
15
+ if (!WOPEE_PROJECT_UUID)
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text",
20
+ text: "WOPEE_PROJECT_UUID is not set",
21
+ },
22
+ ],
23
+ };
24
+ const result = await requestClient(FetchAnalysisSuites, {
25
+ projectUuid: WOPEE_PROJECT_UUID,
26
+ });
27
+ if (!result?.fetchAnalysisSuites)
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: "Failed to fetch analysis suites: no data returned",
33
+ },
34
+ ],
35
+ };
14
36
  return {
15
37
  content: [
16
38
  {
17
39
  type: "text",
18
- text: "WOPEE_PROJECT_UUID is not set",
40
+ text: JSON.stringify(result.fetchAnalysisSuites, null, 2),
19
41
  },
20
42
  ],
21
43
  };
22
- const result = await requestClient(FetchAnalysisSuites, {
23
- projectUuid: WOPEE_PROJECT_UUID,
24
- });
25
- if (!result || !result.fetchAnalysisSuites)
26
- return {
27
- content: [
28
- {
29
- type: "text",
30
- text: "Failed to fetch analysis suites",
31
- },
32
- ],
33
- };
34
- return {
35
- content: [
36
- {
37
- type: "text",
38
- text: JSON.stringify(result.fetchAnalysisSuites, null, 2),
39
- },
40
- ],
41
- };
44
+ }
45
+ catch (error) {
46
+ return _parseError(error);
47
+ }
42
48
  },
43
49
  };
@@ -0,0 +1,13 @@
1
+ import { getConfig } from "../../utils/getConfig.js";
2
+ export const createFetchExecutedTestCasesInput = (input) => {
3
+ const { WOPEE_PROJECT_UUID } = getConfig();
4
+ if (!WOPEE_PROJECT_UUID)
5
+ throw new Error("WOPEE_PROJECT_UUID is not set");
6
+ return {
7
+ projectUuid: WOPEE_PROJECT_UUID,
8
+ analysisSuiteUuid: input.suiteUuid,
9
+ ...(input.analysisIdentifier
10
+ ? { analysisIdentifier: input.analysisIdentifier }
11
+ : {}),
12
+ };
13
+ };
@@ -0,0 +1,43 @@
1
+ import { WopeeFetchExecutedTestCasesInputSchema, FetchExecutedTestCasesInputSchema, } from "./schema.js";
2
+ import { ToolName } from "../shared/types.js";
3
+ import { _parseError } from "../shared/helpers.js";
4
+ import { createFetchExecutedTestCasesInput } from "./factory.js";
5
+ import { FetchExecutedTestCases } from "../shared/gql-queries.js";
6
+ import { requestClient } from "../../utils/requestClient.js";
7
+ export const wopeeFetchExecutedTestCases = {
8
+ name: ToolName.WOPEE_FETCH_EXECUTED_TEST_CASES,
9
+ config: {
10
+ title: "Fetch executed test cases",
11
+ description: "Fetch executed test cases and their results (agent report, code report, execution status) for a given analysis suite.",
12
+ inputSchema: WopeeFetchExecutedTestCasesInputSchema.shape,
13
+ },
14
+ handler: async (input) => {
15
+ try {
16
+ const factoryInput = createFetchExecutedTestCasesInput(input);
17
+ const parsedInput = FetchExecutedTestCasesInputSchema.parse(factoryInput);
18
+ const result = await requestClient(FetchExecutedTestCases, {
19
+ input: parsedInput,
20
+ });
21
+ if (!result?.fetchExecutedTestCases)
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: "Failed to fetch executed test cases: no data returned",
27
+ },
28
+ ],
29
+ };
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: JSON.stringify(result.fetchExecutedTestCases, null, 2),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (error) {
40
+ return _parseError(error);
41
+ }
42
+ },
43
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ export const WopeeFetchExecutedTestCasesInputSchema = z.object({
3
+ suiteUuid: z
4
+ .string({
5
+ description: "UUID of the analysis suite to fetch executed test cases for",
6
+ })
7
+ .min(1, "Suite UUID is required"),
8
+ analysisIdentifier: z
9
+ .string({
10
+ description: "Analysis identifier of the suite (ex. A068). Can be found in the analysis suite data.",
11
+ })
12
+ .optional(),
13
+ });
14
+ export const FetchExecutedTestCasesInputSchema = z.object({
15
+ projectUuid: z.string().min(1, "Project UUID is required"),
16
+ analysisSuiteUuid: z.string().min(1, "Suite UUID is required"),
17
+ analysisIdentifier: z.string().optional(),
18
+ });
@@ -1,37 +1,57 @@
1
1
  import { getConfig } from "./getConfig.js";
2
+ export class RequestError extends Error {
3
+ code;
4
+ status;
5
+ retryable;
6
+ constructor(message, code, options) {
7
+ super(message);
8
+ this.name = "RequestError";
9
+ this.code = code;
10
+ this.status = options?.status ?? null;
11
+ this.retryable = options?.retryable ?? false;
12
+ }
13
+ }
14
+ function classifyGraphQLErrors(errors) {
15
+ const combined = errors.map((e) => e.message).join("; ");
16
+ const lower = combined.toLowerCase();
17
+ if (lower.includes("rate limit")) {
18
+ return new RequestError(combined, "RATE_LIMITED", { retryable: true });
19
+ }
20
+ if (lower.includes("usage limit") || lower.includes("usage_limit")) {
21
+ return new RequestError(combined, "USAGE_LIMIT_EXCEEDED");
22
+ }
23
+ return new RequestError(combined, "GRAPHQL_ERROR");
24
+ }
2
25
  export const requestClient = async (query, variables) => {
3
26
  const { WOPEE_API_URL, WOPEE_API_KEY } = getConfig();
27
+ if (!WOPEE_API_KEY) {
28
+ throw new RequestError("WOPEE_API_KEY is not set", "AUTH_ERROR");
29
+ }
30
+ const headers = {
31
+ api_key: WOPEE_API_KEY,
32
+ "Content-Type": "application/json",
33
+ };
34
+ let response;
4
35
  try {
5
- if (!WOPEE_API_KEY) {
6
- console.error("[REQUEST_CLIENT_ERROR]: WOPEE_API_KEY is not set");
7
- return null;
8
- }
9
- const headers = {
10
- api_key: WOPEE_API_KEY,
11
- "Content-Type": "application/json",
12
- };
13
- const response = await fetch(`${WOPEE_API_URL}`, {
36
+ response = await fetch(`${WOPEE_API_URL}`, {
14
37
  method: "POST",
15
38
  headers,
16
- body: JSON.stringify({
17
- query,
18
- variables,
19
- }),
39
+ body: JSON.stringify({ query, variables }),
20
40
  });
21
- if (!response.ok) {
22
- const errorText = await response.text();
23
- console.error("[REQUEST_CLIENT_ERROR]:", errorText);
24
- return null;
25
- }
26
- const result = (await response.json());
27
- if (result.errors && result.errors.length > 0) {
28
- console.error("GraphQL errors:", result.errors);
29
- return null;
30
- }
31
- return result.data || null;
32
41
  }
33
42
  catch (error) {
34
- console.error("[REQUEST_CLIENT_ERROR]:", error);
35
- return null;
43
+ throw new RequestError(`Network error: ${error instanceof Error ? error.message : String(error)}`, "NETWORK_ERROR", { retryable: true });
44
+ }
45
+ if (!response.ok) {
46
+ const errorText = await response.text().catch(() => "");
47
+ const status = response.status;
48
+ const isRateLimit = status === 429 || errorText.toLowerCase().includes("rate limit");
49
+ const isRetryable = isRateLimit || status >= 500;
50
+ throw new RequestError(`HTTP ${status}: ${errorText || response.statusText}`, isRateLimit ? "RATE_LIMITED" : "HTTP_ERROR", { status, retryable: isRetryable });
51
+ }
52
+ const result = (await response.json());
53
+ if (result.errors && result.errors.length > 0) {
54
+ throw classifyGraphQLErrors(result.errors);
36
55
  }
56
+ return result.data;
37
57
  };
@@ -0,0 +1,24 @@
1
+ import { RequestError } from "./requestClient.js";
2
+ export async function withRetry(fn, options) {
3
+ const maxRetries = options?.maxRetries ?? 3;
4
+ const baseDelayMs = options?.baseDelayMs ?? 2000;
5
+ let lastError;
6
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
7
+ try {
8
+ return await fn();
9
+ }
10
+ catch (error) {
11
+ if (error instanceof RequestError &&
12
+ error.retryable &&
13
+ attempt < maxRetries) {
14
+ lastError = error;
15
+ const delay = baseDelayMs * Math.pow(2, attempt);
16
+ console.error(`[RETRY] Attempt ${attempt + 1}/${maxRetries} failed (${error.code}): ${error.message}. Retrying in ${delay}ms...`);
17
+ await new Promise((resolve) => setTimeout(resolve, delay));
18
+ continue;
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+ throw lastError;
24
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "bin": {
5
5
  "wopee-mcp": "./build/index.js"
6
6
  },
7
- "version": "1.20.0",
7
+ "version": "1.23.0",
8
8
  "mcpName": "io.github.Wopee-io/wopee-mcp",
9
9
  "description": "Wopee.io MCP server for autonomous testing platform",
10
10
  "main": "./build/index.js",