@roveapi/mcp 1.0.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/dist/index.mjs +219 -0
- package/package.json +32 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
function loadConfig(env) {
|
|
10
|
+
return {
|
|
11
|
+
apiBaseUrl: env.ROVE_API_BASE_URL ?? "http://127.0.0.1:3001",
|
|
12
|
+
apiKey: env.ROVE_API_KEY ?? "rvp_live_demo"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/rest-client.ts
|
|
17
|
+
async function parseJson(response) {
|
|
18
|
+
return await response.json();
|
|
19
|
+
}
|
|
20
|
+
function createRestClient(apiBaseUrl, apiKey) {
|
|
21
|
+
async function call(path, init) {
|
|
22
|
+
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
23
|
+
...init,
|
|
24
|
+
headers: {
|
|
25
|
+
"content-type": "application/json",
|
|
26
|
+
authorization: `Bearer ${apiKey}`,
|
|
27
|
+
...init?.headers ?? {}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const body = await parseJson(response).catch(() => ({}));
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`API ${path} failed (${response.status}): ${JSON.stringify(body)}`);
|
|
33
|
+
}
|
|
34
|
+
return body;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
createSession: (input) => call("/v1/browser/session", { method: "POST", body: JSON.stringify(input ?? {}) }),
|
|
38
|
+
runAction: (input) => call("/v1/browser/action", { method: "POST", body: JSON.stringify(input) }),
|
|
39
|
+
standaloneScreenshot: (input) => call("/v1/browser/screenshot", { method: "POST", body: JSON.stringify(input) }),
|
|
40
|
+
extract: (input) => call("/v1/browser/extract", { method: "POST", body: JSON.stringify(input) }),
|
|
41
|
+
getArtifacts: (sessionId) => call(`/v1/browser/artifacts/${sessionId}`, { method: "GET" })
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/session-store.ts
|
|
46
|
+
var SessionStore = class {
|
|
47
|
+
sessions = /* @__PURE__ */ new Map();
|
|
48
|
+
get(clientId) {
|
|
49
|
+
return this.sessions.get(clientId);
|
|
50
|
+
}
|
|
51
|
+
set(clientId, sessionId) {
|
|
52
|
+
this.sessions.set(clientId, sessionId);
|
|
53
|
+
}
|
|
54
|
+
clear(clientId) {
|
|
55
|
+
this.sessions.delete(clientId);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/tools.ts
|
|
60
|
+
var TOOL_DEFS = [
|
|
61
|
+
{
|
|
62
|
+
name: "navigate",
|
|
63
|
+
description: "Navigate within a browser session, creating one if needed. Explicit session_id becomes the active session for subsequent calls.",
|
|
64
|
+
inputSchema: { type: "object", properties: { url: { type: "string" }, session_id: { type: "string" } }, required: ["url"] },
|
|
65
|
+
mappedEndpoint: "POST /v1/browser/action (navigate)"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "interact",
|
|
69
|
+
description: "Perform click/fill style actions in active session.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: { session_id: { type: "string" }, action: { type: "string", enum: ["click", "fill"] }, params: { type: "object" } },
|
|
73
|
+
required: ["session_id", "action"]
|
|
74
|
+
},
|
|
75
|
+
mappedEndpoint: "POST /v1/browser/action (click/fill)"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "extract_schema",
|
|
79
|
+
description: "Reserved mapping for extraction path.",
|
|
80
|
+
inputSchema: { type: "object", properties: { url: { type: "string" }, schema: { type: "object" } }, required: ["url", "schema"] },
|
|
81
|
+
mappedEndpoint: "POST /v1/browser/extract"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "screenshot",
|
|
85
|
+
description: "Take in-session or standalone screenshot. Explicit session_id is persisted as active session context.",
|
|
86
|
+
inputSchema: { type: "object", properties: { session_id: { type: "string" }, url: { type: "string" } } },
|
|
87
|
+
mappedEndpoint: "POST /v1/browser/action (screenshot) or POST /v1/browser/screenshot"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "get_a11y_tree",
|
|
91
|
+
description: "Get accessibility tree snapshot for active session.",
|
|
92
|
+
inputSchema: { type: "object", properties: { session_id: { type: "string" } }, required: ["session_id"] },
|
|
93
|
+
mappedEndpoint: "POST /v1/browser/action (get_a11y_tree)"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "close_session",
|
|
97
|
+
description: "Close current session and clear MCP state only when closing the currently stored session.",
|
|
98
|
+
inputSchema: { type: "object", properties: { session_id: { type: "string" } }, required: ["session_id"] },
|
|
99
|
+
mappedEndpoint: "POST /v1/browser/action (close_session)"
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
function resolveSessionId(input, sessionStore, clientId) {
|
|
103
|
+
const explicit = input.session_id;
|
|
104
|
+
if (typeof explicit === "string" && explicit.length > 0) {
|
|
105
|
+
sessionStore.set(clientId, explicit);
|
|
106
|
+
return explicit;
|
|
107
|
+
}
|
|
108
|
+
const fromStore = sessionStore.get(clientId);
|
|
109
|
+
if (!fromStore) throw new Error("session_id required and no active session in store");
|
|
110
|
+
return fromStore;
|
|
111
|
+
}
|
|
112
|
+
async function runTool(toolName, input, deps) {
|
|
113
|
+
const { rest: rest2, sessions: sessions2, clientId } = deps;
|
|
114
|
+
switch (toolName) {
|
|
115
|
+
case "navigate": {
|
|
116
|
+
const url = input.url;
|
|
117
|
+
if (typeof url !== "string" || !url) throw new Error("url is required");
|
|
118
|
+
let sessionId = typeof input.session_id === "string" ? input.session_id : sessions2.get(clientId);
|
|
119
|
+
if (!sessionId) {
|
|
120
|
+
const created = await rest2.createSession({});
|
|
121
|
+
sessionId = created.session_id;
|
|
122
|
+
}
|
|
123
|
+
sessions2.set(clientId, sessionId);
|
|
124
|
+
const result = await rest2.runAction({ session_id: sessionId, action: "navigate", params: { url } });
|
|
125
|
+
return { session_id: sessionId, ...result };
|
|
126
|
+
}
|
|
127
|
+
case "interact": {
|
|
128
|
+
const action = input.action;
|
|
129
|
+
if (action !== "click" && action !== "fill") throw new Error("action must be click|fill");
|
|
130
|
+
const sessionId = resolveSessionId(input, sessions2, clientId);
|
|
131
|
+
const result = await rest2.runAction({
|
|
132
|
+
session_id: sessionId,
|
|
133
|
+
action,
|
|
134
|
+
params: input.params ?? {}
|
|
135
|
+
});
|
|
136
|
+
return { session_id: sessionId, ...result };
|
|
137
|
+
}
|
|
138
|
+
case "extract_schema": {
|
|
139
|
+
const url = input.url;
|
|
140
|
+
if (typeof url !== "string" || !url) throw new Error("url is required");
|
|
141
|
+
const schema = input.schema;
|
|
142
|
+
if (!schema || typeof schema !== "object") throw new Error("schema is required");
|
|
143
|
+
return rest2.extract({
|
|
144
|
+
url,
|
|
145
|
+
schema,
|
|
146
|
+
wait_for_selector: input.wait_for_selector
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
case "screenshot": {
|
|
150
|
+
const explicit = typeof input.session_id === "string" ? input.session_id : void 0;
|
|
151
|
+
const stored = sessions2.get(clientId);
|
|
152
|
+
const sessionId = explicit ?? stored;
|
|
153
|
+
if (sessionId) {
|
|
154
|
+
sessions2.set(clientId, sessionId);
|
|
155
|
+
const result = await rest2.runAction({ session_id: sessionId, action: "screenshot", params: {} });
|
|
156
|
+
return { session_id: sessionId, ...result };
|
|
157
|
+
}
|
|
158
|
+
const url = input.url;
|
|
159
|
+
if (typeof url !== "string" || !url) throw new Error("url is required for standalone screenshot");
|
|
160
|
+
return rest2.standaloneScreenshot({ url });
|
|
161
|
+
}
|
|
162
|
+
case "get_a11y_tree": {
|
|
163
|
+
const sessionId = resolveSessionId(input, sessions2, clientId);
|
|
164
|
+
const result = await rest2.runAction({ session_id: sessionId, action: "get_a11y_tree", params: {} });
|
|
165
|
+
return { session_id: sessionId, ...result };
|
|
166
|
+
}
|
|
167
|
+
case "close_session": {
|
|
168
|
+
const explicit = input.session_id;
|
|
169
|
+
const sessionId = typeof explicit === "string" && explicit.length > 0 ? explicit : resolveSessionId(input, sessions2, clientId);
|
|
170
|
+
const result = await rest2.runAction({ session_id: sessionId, action: "close_session", params: {} });
|
|
171
|
+
const currentlyStored = sessions2.get(clientId);
|
|
172
|
+
const cleared = currentlyStored === sessionId;
|
|
173
|
+
if (cleared) sessions2.clear(clientId);
|
|
174
|
+
return { session_id: sessionId, ...result, cleanup: { session_store_cleared: cleared } };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/index.ts
|
|
180
|
+
var config = loadConfig(process.env);
|
|
181
|
+
var rest = createRestClient(config.apiBaseUrl, config.apiKey);
|
|
182
|
+
var sessions = new SessionStore();
|
|
183
|
+
var server = new Server(
|
|
184
|
+
{ name: "rove-browser", version: "1.0.0" },
|
|
185
|
+
{ capabilities: { tools: {} } }
|
|
186
|
+
);
|
|
187
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
188
|
+
tools: TOOL_DEFS.map((t) => ({
|
|
189
|
+
name: t.name,
|
|
190
|
+
description: t.description,
|
|
191
|
+
inputSchema: t.inputSchema
|
|
192
|
+
}))
|
|
193
|
+
}));
|
|
194
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
195
|
+
const { name, arguments: args } = request.params;
|
|
196
|
+
try {
|
|
197
|
+
const result = await runTool(
|
|
198
|
+
name,
|
|
199
|
+
args ?? {},
|
|
200
|
+
{ rest, sessions, clientId: "mcp" }
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
204
|
+
};
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
208
|
+
isError: true
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
async function main() {
|
|
213
|
+
const transport = new StdioServerTransport();
|
|
214
|
+
await server.connect(transport);
|
|
215
|
+
}
|
|
216
|
+
main().catch((err) => {
|
|
217
|
+
console.error("MCP server failed to start:", err);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@roveapi/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Rove browser automation API — use from Claude, Cursor, VS Code",
|
|
5
|
+
"bin": {
|
|
6
|
+
"rove-mcp": "./dist/index.mjs"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.mjs",
|
|
9
|
+
"files": ["dist", "README.md"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.mjs --packages=external --external:@modelcontextprotocol/sdk",
|
|
13
|
+
"dev": "tsx watch src/index.ts",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "tsx --test test/*.test.ts"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["mcp", "browser", "automation", "playwright", "a11y", "claude", "cursor"],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@rove/dsl": "workspace:*",
|
|
27
|
+
"esbuild": "^0.27",
|
|
28
|
+
"typescript": "^5",
|
|
29
|
+
"tsx": "^4",
|
|
30
|
+
"@types/node": "^22"
|
|
31
|
+
}
|
|
32
|
+
}
|