@robotyxx/robotyx-mcp 0.1.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 +76 -0
- package/dist/gateway-bridge.js +92 -0
- package/dist/index.js +1274 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# robotyx-mcp
|
|
2
|
+
|
|
3
|
+
Stdio MCP server that wraps Robotyx's REST API. Lets Claude Code / Claude
|
|
4
|
+
Desktop browse recorded workflows, refine their TestSteps into grouped
|
|
5
|
+
functional steps ("clicks 1-7 → Log in as admin"), and push the result back
|
|
6
|
+
— without leaving the chat.
|
|
7
|
+
|
|
8
|
+
No new business logic lives here. Every tool is a thin wrapper around an
|
|
9
|
+
existing endpoint the Robotyx web UI also uses.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | Wraps | Notes |
|
|
14
|
+
|------|-------|-------|
|
|
15
|
+
| `list_workflows` | `GET /api/workflows` | Summary rows. |
|
|
16
|
+
| `get_workflow` | `GET /api/workflows/:id` | Screenshots stripped by default. Pass `include_screenshots: "all"` or `"indices"` + `screenshot_step_indices: [1,3,5]` when you need visual context. |
|
|
17
|
+
| `list_testsuites` | `GET /api/testsuites` | |
|
|
18
|
+
| `get_testsuite` | `GET /api/testsuites/:id` | Includes cases + their steps. |
|
|
19
|
+
| `create_testsuite` | `POST /api/testsuites` | |
|
|
20
|
+
| `add_test_case` | `POST /api/testsuites/:id/cases` | Optionally links a Workflow id. |
|
|
21
|
+
| `set_test_steps` | `POST /api/testsuites/:id/cases/:caseId/import-mapping` | The refine path. Body shape mirrors the UI's Excel import — pass `workflow_step_ids` (full ordered list from `get_workflow`) + `mappings: [{description, expected_result, workflow_step_indices: [1,2,3]}]`. |
|
|
22
|
+
| `merge_step` | `POST .../steps/:stepId/merge` | |
|
|
23
|
+
| `split_step` | `POST .../steps/:stepId/split` | |
|
|
24
|
+
| `create_defect` | `POST /api/defects` | |
|
|
25
|
+
|
|
26
|
+
## Wire-up — Claude Code
|
|
27
|
+
|
|
28
|
+
Drop a `.mcp.json` in your workspace root (already done for
|
|
29
|
+
`ai-automation-platform/` and `footprint-platform/`):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"robotyx": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "tsx", "C:\\Users\\…\\ai-automation-platform\\robotyx-mcp\\src\\index.ts"],
|
|
37
|
+
"env": {
|
|
38
|
+
"ROBOTYX_URL": "http://localhost:3001",
|
|
39
|
+
"ROBOTYX_API_KEY": "${ROBOTYX_API_KEY}"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then in PowerShell (once per machine):
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
[Environment]::SetEnvironmentVariable("ROBOTYX_API_KEY", "rx_…", "User")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Restart Claude Code so it picks up the new MCP. Verify with `/mcp` — you
|
|
53
|
+
should see `robotyx: ✓ connected (10 tools)`.
|
|
54
|
+
|
|
55
|
+
## Wire-up — Claude Desktop
|
|
56
|
+
|
|
57
|
+
Add the same `mcpServers` block to
|
|
58
|
+
`%APPDATA%\Claude\claude_desktop_config.json` and restart the app.
|
|
59
|
+
|
|
60
|
+
## Typical refine loop
|
|
61
|
+
|
|
62
|
+
1. `list_workflows` → pick a workflow id.
|
|
63
|
+
2. `get_workflow` (no screenshots) → AI proposes a grouping based on
|
|
64
|
+
step descriptions + action types.
|
|
65
|
+
3. `get_workflow` with `include_screenshots: "indices"` and the ambiguous
|
|
66
|
+
step indices → AI disambiguates visually.
|
|
67
|
+
4. Either `add_test_case` (linking the workflow) or pick an existing one
|
|
68
|
+
from `get_testsuite`.
|
|
69
|
+
5. `set_test_steps` with the proposed mapping → done.
|
|
70
|
+
|
|
71
|
+
## Token budget
|
|
72
|
+
|
|
73
|
+
A 30-step browser recording with full-page PNGs is ~200-400k tokens with
|
|
74
|
+
`include_screenshots: "all"`. Always start with `"none"`, then use
|
|
75
|
+
`"indices"` to fetch only the screenshots you need. The server strips
|
|
76
|
+
`footprint_*` bookkeeping keys (raw a11y dumps, console logs) unconditionally.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway tool bridge — exposes ALL of the gateway's aggregated MCP tools
|
|
3
|
+
* (autopilot / selenium / electron / playwright / java / api-agent …) to an
|
|
4
|
+
* external MCP client (Claude Code) through a single server, and forwards every
|
|
5
|
+
* call to the gateway's POST /api/tools/call.
|
|
6
|
+
*
|
|
7
|
+
* Why a bridge instead of wiring each MCP into the client directly: tool calls
|
|
8
|
+
* must flow through the gateway so the gateway's RecordingManager captures them
|
|
9
|
+
* into the active workflow (see /api/tools/call). Calling the underlying MCP
|
|
10
|
+
* servers directly would bypass recording — and we'd have to solve recording
|
|
11
|
+
* per-client. One bridge → all tools, all recorded, all replayable by the
|
|
12
|
+
* PlaybackEngine.
|
|
13
|
+
*
|
|
14
|
+
* Env:
|
|
15
|
+
* ROBOTYX_URL — gateway base URL (default http://localhost:3001)
|
|
16
|
+
* ROBOTYX_API_KEY — long-lived `rx_…` Bearer token (required)
|
|
17
|
+
*
|
|
18
|
+
* .mcp.json:
|
|
19
|
+
* "gateway-tools": {
|
|
20
|
+
* "command": "npx",
|
|
21
|
+
* "args": ["-y", "tsx", "…/robotyx-mcp/src/gateway-bridge.ts"],
|
|
22
|
+
* "env": { "ROBOTYX_URL": "http://localhost:3001", "ROBOTYX_API_KEY": "rx_…" }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
26
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
27
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
28
|
+
const ROBOTYX_URL = (process.env.ROBOTYX_URL || "http://localhost:3001").replace(/\/+$/, "");
|
|
29
|
+
const ROBOTYX_API_KEY = process.env.ROBOTYX_API_KEY || "";
|
|
30
|
+
if (!ROBOTYX_API_KEY) {
|
|
31
|
+
process.stderr.write("[gateway-bridge] ROBOTYX_API_KEY is not set. Mint a key in the Robotyx UI " +
|
|
32
|
+
"(Settings → API keys) and set it in the MCP env. Exiting.\n");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
async function fetchTools() {
|
|
36
|
+
const res = await fetch(`${ROBOTYX_URL}/api/tools`, {
|
|
37
|
+
headers: { Authorization: `Bearer ${ROBOTYX_API_KEY}` },
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`GET /api/tools → ${res.status} ${await res.text().catch(() => "")}`);
|
|
41
|
+
}
|
|
42
|
+
const data = (await res.json());
|
|
43
|
+
return data.tools ?? [];
|
|
44
|
+
}
|
|
45
|
+
/** Forward a tool call to the gateway; it routes + records + returns the MCP result. */
|
|
46
|
+
async function callTool(name, args) {
|
|
47
|
+
const res = await fetch(`${ROBOTYX_URL}/api/tools/call`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${ROBOTYX_API_KEY}`,
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ name, args }),
|
|
54
|
+
});
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
return { isError: true, content: [{ type: "text", text: `${res.status}: ${text}` }] };
|
|
58
|
+
}
|
|
59
|
+
// The gateway returns the underlying MCP result ({ content, isError }) verbatim.
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(text);
|
|
62
|
+
if (parsed && Array.isArray(parsed.content))
|
|
63
|
+
return parsed;
|
|
64
|
+
return { content: [{ type: "text", text }] };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { content: [{ type: "text", text }] };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const server = new Server({ name: "gateway-tools", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
71
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
72
|
+
const tools = await fetchTools();
|
|
73
|
+
return {
|
|
74
|
+
tools: tools.map((t) => ({
|
|
75
|
+
name: t.name,
|
|
76
|
+
description: `[${t.server}] ${t.description || ""}`.trim(),
|
|
77
|
+
inputSchema: t.inputSchema || { type: "object" },
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
82
|
+
const { name, arguments: args } = req.params;
|
|
83
|
+
try {
|
|
84
|
+
return await callTool(name, (args ?? {}));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return { isError: true, content: [{ type: "text", text: err?.message ?? String(err) }] };
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
const transport = new StdioServerTransport();
|
|
91
|
+
await server.connect(transport);
|
|
92
|
+
process.stderr.write(`[gateway-bridge] connected, target ${ROBOTYX_URL}\n`);
|