moltbrowser-mcp 0.0.1
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 +201 -0
- package/README.md +958 -0
- package/cli.js +24 -0
- package/config.d.ts +241 -0
- package/hub-cli.js +72 -0
- package/index.d.ts +23 -0
- package/index.js +19 -0
- package/package.json +42 -0
- package/src/README.md +3 -0
- package/src/execution-translator.js +590 -0
- package/src/hub-client.js +246 -0
- package/src/hub-tools.js +968 -0
- package/src/proxy-server.js +643 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the WebMCP Hub REST API.
|
|
3
|
+
*
|
|
4
|
+
* Handles config lookup (with caching), upload, update, and voting.
|
|
5
|
+
* Graceful degradation: if the hub is unreachable, returns empty results
|
|
6
|
+
* so the proxy can fall back to vanilla Playwright MCP behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const HUB_BASE = process.env.HUB_URL || 'https://www.webmcp-hub.com';
|
|
10
|
+
|
|
11
|
+
// In-memory cache: key = "domain|url", value = { configs, timestamp }
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} path
|
|
17
|
+
* @param {RequestInit} [init]
|
|
18
|
+
* @returns {Promise<Response>}
|
|
19
|
+
*/
|
|
20
|
+
async function hubFetch(path, init) {
|
|
21
|
+
const HUB_API_KEY = process.env.HUB_API_KEY || '';
|
|
22
|
+
const headers = { ...(init?.headers || {}) };
|
|
23
|
+
if (init?.body) {
|
|
24
|
+
headers['Content-Type'] = 'application/json';
|
|
25
|
+
}
|
|
26
|
+
if (HUB_API_KEY) {
|
|
27
|
+
headers['Authorization'] = `Bearer ${HUB_API_KEY}`;
|
|
28
|
+
}
|
|
29
|
+
return fetch(`${HUB_BASE}${path}`, { ...init, headers });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Verify the configured API key against the hub.
|
|
34
|
+
*
|
|
35
|
+
* @returns {Promise<{ valid: boolean, username?: string, error?: string, unreachable?: boolean }>}
|
|
36
|
+
*/
|
|
37
|
+
async function verifyApiKey() {
|
|
38
|
+
try {
|
|
39
|
+
const res = await hubFetch('/api/me');
|
|
40
|
+
if (res.status === 401) {
|
|
41
|
+
const body = await res.json().catch(() => ({}));
|
|
42
|
+
return { valid: false, error: body.error || 'Invalid API key' };
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
return { valid: false, error: `Hub returned ${res.status}` };
|
|
46
|
+
}
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
return { valid: true, username: data.username };
|
|
49
|
+
} catch (_err) {
|
|
50
|
+
return { valid: false, error: _err.message, unreachable: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Look up configs for a domain/URL. Returns executable configs with tools.
|
|
56
|
+
* Results are cached for CACHE_TTL_MS.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} domain
|
|
59
|
+
* @param {string} [url]
|
|
60
|
+
* @returns {Promise<{ configs: Array<object> }>}
|
|
61
|
+
*/
|
|
62
|
+
async function lookupConfig(domain, url) {
|
|
63
|
+
const cacheKey = `${domain}|${url || ''}`;
|
|
64
|
+
|
|
65
|
+
const cached = cache.get(cacheKey);
|
|
66
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
67
|
+
return { configs: cached.configs };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const params = new URLSearchParams({ domain, executable: 'true' });
|
|
72
|
+
if (url) params.set('url', url);
|
|
73
|
+
|
|
74
|
+
const res = await hubFetch(`/api/configs/lookup?${params}`);
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
return { configs: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const configs = data.configs || [];
|
|
81
|
+
|
|
82
|
+
// Only cache non-empty results. Empty results may be caused by transient
|
|
83
|
+
// hub issues or auth failures — caching them makes the failure sticky for
|
|
84
|
+
// the full TTL (5 min), preventing recovery on the next lookup.
|
|
85
|
+
if (configs.length > 0) {
|
|
86
|
+
cache.set(cacheKey, { configs, timestamp: Date.now() });
|
|
87
|
+
}
|
|
88
|
+
return { configs };
|
|
89
|
+
} catch (_err) {
|
|
90
|
+
// Hub unreachable — graceful degradation
|
|
91
|
+
return { configs: [] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Upload a new config to the hub.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} data - Config data
|
|
99
|
+
* @returns {Promise<{ config?: object, error?: string, existingId?: string, status: number }>}
|
|
100
|
+
*/
|
|
101
|
+
async function uploadConfig(data) {
|
|
102
|
+
const res = await hubFetch('/api/configs', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
body: JSON.stringify(data),
|
|
105
|
+
});
|
|
106
|
+
const body = await res.json();
|
|
107
|
+
if (res.status === 409) {
|
|
108
|
+
return { error: body.error, existingId: body.existingId, status: 409 };
|
|
109
|
+
}
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
return { error: JSON.stringify(body.error), status: res.status };
|
|
112
|
+
}
|
|
113
|
+
return { config: body, status: 201 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Update an existing config by ID.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} id
|
|
120
|
+
* @param {object} data
|
|
121
|
+
* @returns {Promise<{ config?: object, error?: string, status: number }>}
|
|
122
|
+
*/
|
|
123
|
+
async function updateConfig(id, data) {
|
|
124
|
+
const res = await hubFetch(`/api/configs/${id}`, {
|
|
125
|
+
method: 'PATCH',
|
|
126
|
+
body: JSON.stringify(data),
|
|
127
|
+
});
|
|
128
|
+
const body = await res.json();
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
return { error: JSON.stringify(body.error), status: res.status };
|
|
131
|
+
}
|
|
132
|
+
return { config: body, status: 200 };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Vote on a tool within a config.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} configId
|
|
139
|
+
* @param {string} toolName
|
|
140
|
+
* @param {number} vote - 1 for upvote, -1 for downvote
|
|
141
|
+
* @returns {Promise<{ result?: object, error?: string, status: number }>}
|
|
142
|
+
*/
|
|
143
|
+
async function voteOnTool(configId, toolName, vote) {
|
|
144
|
+
const res = await hubFetch(`/api/configs/${configId}/vote`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
body: JSON.stringify({ toolName, vote }),
|
|
147
|
+
});
|
|
148
|
+
const body = await res.json();
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
return { error: body.error || JSON.stringify(body), status: res.status };
|
|
151
|
+
}
|
|
152
|
+
return { result: body, status: 200 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fetch a single config by ID.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} id
|
|
159
|
+
* @returns {Promise<{ config?: object, error?: string, status: number }>}
|
|
160
|
+
*/
|
|
161
|
+
async function getConfig(id) {
|
|
162
|
+
const res = await hubFetch(`/api/configs/${id}`);
|
|
163
|
+
const body = await res.json();
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
return { error: body.error || JSON.stringify(body), status: res.status };
|
|
166
|
+
}
|
|
167
|
+
return { config: body, status: 200 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add a single tool to an existing config.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} configId
|
|
174
|
+
* @param {object} tool - { name, description, inputSchema, annotations?, execution? }
|
|
175
|
+
* @returns {Promise<{ tool?: object, error?: string, status: number }>}
|
|
176
|
+
*/
|
|
177
|
+
async function addTool(configId, tool) {
|
|
178
|
+
const res = await hubFetch(`/api/configs/${configId}/tools`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
body: JSON.stringify(tool),
|
|
181
|
+
});
|
|
182
|
+
const body = await res.json();
|
|
183
|
+
if (res.status === 409) {
|
|
184
|
+
return { error: body.error, status: 409 };
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
return { error: JSON.stringify(body.error), status: res.status };
|
|
188
|
+
}
|
|
189
|
+
return { tool: body, status: 201 };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Update a specific tool within a config in-place.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} configId
|
|
196
|
+
* @param {string} toolName
|
|
197
|
+
* @param {object} updates - Partial tool fields (description, inputSchema, annotations, execution)
|
|
198
|
+
* @returns {Promise<{ tool?: object, error?: string, status: number }>}
|
|
199
|
+
*/
|
|
200
|
+
async function updateTool(configId, toolName, updates) {
|
|
201
|
+
const res = await hubFetch(`/api/configs/${configId}/tools/${encodeURIComponent(toolName)}`, {
|
|
202
|
+
method: 'PATCH',
|
|
203
|
+
body: JSON.stringify(updates),
|
|
204
|
+
});
|
|
205
|
+
const body = await res.json();
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
return { error: body.error || JSON.stringify(body), status: res.status };
|
|
208
|
+
}
|
|
209
|
+
return { tool: body, status: 200 };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Delete a specific tool from a config by name.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} configId
|
|
216
|
+
* @param {string} toolName
|
|
217
|
+
* @returns {Promise<{ config?: object, error?: string, status: number }>}
|
|
218
|
+
*/
|
|
219
|
+
async function deleteTool(configId, toolName) {
|
|
220
|
+
const res = await hubFetch(`/api/configs/${configId}/tools/${encodeURIComponent(toolName)}`, {
|
|
221
|
+
method: 'DELETE',
|
|
222
|
+
});
|
|
223
|
+
const body = await res.json();
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
return { error: body.error || JSON.stringify(body), status: res.status };
|
|
226
|
+
}
|
|
227
|
+
return { config: body, status: 200 };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Clear the lookup cache (useful for testing). */
|
|
231
|
+
function clearCache() {
|
|
232
|
+
cache.clear();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
lookupConfig,
|
|
237
|
+
uploadConfig,
|
|
238
|
+
updateConfig,
|
|
239
|
+
addTool,
|
|
240
|
+
updateTool,
|
|
241
|
+
getConfig,
|
|
242
|
+
voteOnTool,
|
|
243
|
+
deleteTool,
|
|
244
|
+
clearCache,
|
|
245
|
+
verifyApiKey,
|
|
246
|
+
};
|