wopee-mcp 1.18.2 → 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.
- package/LICENSE +21 -0
- package/README.md +21 -3
- package/build/tools/shared/handlers.js +6 -6
- package/build/tools/shared/helpers.js +11 -0
- package/build/tools/wopee_create_blank_suite/index.js +29 -23
- package/build/tools/wopee_dispatch_agent/index.js +6 -5
- package/build/tools/wopee_dispatch_analysis/factory.js +1 -1
- package/build/tools/wopee_dispatch_analysis/index.js +6 -5
- package/build/tools/wopee_dispatch_analysis/schema.js +16 -0
- package/build/tools/wopee_fetch_analysis_suites/index.js +29 -23
- package/build/utils/requestClient.js +46 -26
- package/build/utils/withRetry.js +24 -0
- package/package.json +20 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wopee.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Wopee MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI-powered autonomous testing for your apps -- connect Claude, Cursor, or any MCP-compatible AI agent to [Wopee.io](https://wopee.io) and generate test cases, user stories, and run autonomous tests in seconds.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx wopee-mcp
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
> **[Documentation](https://docs.wopee.io/guides/wopee-mcp/)** | **[Landing Page](https://wopee.io/mcp/)** | **[Dashboard](https://cmd.wopee.io)**
|
|
4
10
|
|
|
5
11
|
## Setup
|
|
6
12
|
|
|
@@ -173,14 +179,18 @@ Fetch all existing analysis suites for my project
|
|
|
173
179
|
|
|
174
180
|
#### `wopee_dispatch_analysis`
|
|
175
181
|
|
|
176
|
-
Creates and dispatches a new analysis/crawling suite for your project. Use this to start a fresh analysis session.
|
|
182
|
+
Creates and dispatches a new analysis/crawling suite for your project, or reruns an existing one. Use this to start a fresh analysis session or to re-trigger a previous analysis.
|
|
177
183
|
|
|
178
184
|
- **Parameters:**
|
|
179
185
|
- `additionalInstructions` _(optional)_ - Additional instructions to guide the agent during the analysis/crawling phase (e.g. focus areas, things to ignore, login steps, etc.)
|
|
180
186
|
- `additionalVariables` _(optional)_ - Additional environment variables to pass to the analysis. Array of objects, each with:
|
|
181
187
|
- `key` - Variable name, must be uppercase with underscores only (e.g. `MY_VAR`, `BASE_URL`)
|
|
182
188
|
- `value` - Variable value (non-empty string)
|
|
183
|
-
-
|
|
189
|
+
- `rerun` _(optional)_ - If provided, reruns an existing analysis suite instead of creating a new one. Object with:
|
|
190
|
+
- `suiteUuid` - UUID of the existing suite to rerun
|
|
191
|
+
- `analysisIdentifier` - Analysis identifier of the existing suite
|
|
192
|
+
- `mode` - Rerun mode: `FULL` (reruns the entire analysis including crawling and generation) or `CRAWLING` (reruns only the crawling phase)
|
|
193
|
+
- **Returns:** Success message with the created/rerun suite information
|
|
184
194
|
|
|
185
195
|
**Example Usage:**
|
|
186
196
|
|
|
@@ -196,6 +206,14 @@ Dispatch a new analysis suite and focus on the checkout flow
|
|
|
196
206
|
Dispatch a new analysis suite with additional variables CARD_FILAMENT=123321123 and AUTH_TOKEN=abc123
|
|
197
207
|
```
|
|
198
208
|
|
|
209
|
+
```
|
|
210
|
+
Rerun the full analysis for suite <suiteUuid> with analysis identifier <analysisIdentifier>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
Rerun only the crawling phase for suite <suiteUuid> with analysis identifier <analysisIdentifier>
|
|
215
|
+
```
|
|
216
|
+
|
|
199
217
|
#### `wopee_create_blank_suite`
|
|
200
218
|
|
|
201
219
|
Creates a blank analysis suite for your project. Use this when you want to manually configure and populate a suite rather than having it automatically analyzed.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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:
|
|
40
|
+
text: JSON.stringify(result.createBlankAnalysisSuite, null, 2),
|
|
19
41
|
},
|
|
20
42
|
],
|
|
21
43
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
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
|
};
|
|
@@ -25,6 +25,21 @@ const AdditionalVariableSchema = z.object({
|
|
|
25
25
|
.string({ description: "Variable value. Must be a non-empty string." })
|
|
26
26
|
.min(1, "Value must be a non-empty string"),
|
|
27
27
|
});
|
|
28
|
+
const WopeeRerunOptionsSchema = z.object({
|
|
29
|
+
suiteUuid: z
|
|
30
|
+
.string({
|
|
31
|
+
description: "UUID of the existing suite to rerun.",
|
|
32
|
+
})
|
|
33
|
+
.min(1, "Suite UUID is required"),
|
|
34
|
+
analysisIdentifier: z
|
|
35
|
+
.string({
|
|
36
|
+
description: "Analysis identifier of the existing suite to rerun.",
|
|
37
|
+
})
|
|
38
|
+
.min(1, "Analysis identifier is required"),
|
|
39
|
+
mode: z.nativeEnum(RerunMode, {
|
|
40
|
+
description: "Rerun mode: FULL reruns the entire analysis (crawling + analysis), CRAWLING reruns only the crawling phase.",
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
28
43
|
export const WopeeDispatchAnalysisInputSchema = z.object({
|
|
29
44
|
additionalInstructions: z
|
|
30
45
|
.string({ description: "Additional instructions for the agent" })
|
|
@@ -34,4 +49,5 @@ export const WopeeDispatchAnalysisInputSchema = z.object({
|
|
|
34
49
|
description: "Additional environment variables for the analysis. Each variable needs a key (uppercase, e.g. BASE_URL) and a non-empty value.",
|
|
35
50
|
})
|
|
36
51
|
.nullish(),
|
|
52
|
+
rerun: WopeeRerunOptionsSchema.nullish().describe("If provided, reruns an existing analysis suite instead of creating a new one. Requires suiteUuid, analysisIdentifier, and mode."),
|
|
37
53
|
});
|
|
@@ -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
|
-
|
|
13
|
-
|
|
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:
|
|
40
|
+
text: JSON.stringify(result.fetchAnalysisSuites, null, 2),
|
|
19
41
|
},
|
|
20
42
|
],
|
|
21
43
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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.
|
|
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",
|
|
@@ -16,19 +16,30 @@
|
|
|
16
16
|
"build",
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
|
+
"homepage": "https://wopee.io/mcp/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/Wopee-io/wopee-mcp"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Wopee-io/wopee-mcp/issues"
|
|
26
|
+
},
|
|
19
27
|
"keywords": [
|
|
20
28
|
"wopee",
|
|
21
|
-
"
|
|
29
|
+
"wopee.io",
|
|
22
30
|
"mcp",
|
|
23
|
-
"
|
|
24
|
-
"testing",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
31
|
+
"model context protocol",
|
|
32
|
+
"ai testing",
|
|
33
|
+
"autonomous testing",
|
|
34
|
+
"test automation",
|
|
35
|
+
"claude",
|
|
36
|
+
"cursor",
|
|
37
|
+
"ai agent",
|
|
38
|
+
"visual testing",
|
|
39
|
+
"playwright"
|
|
29
40
|
],
|
|
30
41
|
"author": "Sota Giorgadze <sota.giorgadze@wopee.io>",
|
|
31
|
-
"license": "
|
|
42
|
+
"license": "MIT",
|
|
32
43
|
"dependencies": {
|
|
33
44
|
"@modelcontextprotocol/sdk": "^1.21.0",
|
|
34
45
|
"undici": "^7.18.2",
|