wopee-mcp 1.20.0 → 1.21.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.
@@ -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
  {
@@ -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
  };
@@ -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
  };
@@ -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.21.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",