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.
@@ -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
+ };