marble-headed-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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
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
+ const DEFAULT_BASE_URL = 'http://localhost:5055';
8
+ function resolveBaseUrl() {
9
+ const base = process.env.HEADED_SERVER_BASE_URL || DEFAULT_BASE_URL;
10
+ return base.replace(/\/+$/, '');
11
+ }
12
+ function buildUrl(pathname) {
13
+ return `${resolveBaseUrl()}${pathname}`;
14
+ }
15
+ async function postJson(pathname, payload) {
16
+ const response = await fetch(buildUrl(pathname), {
17
+ method: 'POST',
18
+ headers: { 'Content-Type': 'application/json' },
19
+ body: JSON.stringify(payload ?? {}),
20
+ });
21
+ const text = await response.text();
22
+ let json = null;
23
+ try {
24
+ json = JSON.parse(text);
25
+ }
26
+ catch (error) {
27
+ json = null;
28
+ }
29
+ return { status: response.status, ok: response.ok, text, json };
30
+ }
31
+ async function getText(pathname) {
32
+ const response = await fetch(buildUrl(pathname));
33
+ const text = await response.text();
34
+ return { status: response.status, ok: response.ok, text };
35
+ }
36
+ function inferExtensionFromMime(mimeType) {
37
+ if (!mimeType)
38
+ return null;
39
+ if (mimeType.includes('jpeg') || mimeType.includes('jpg'))
40
+ return 'jpg';
41
+ if (mimeType.includes('png'))
42
+ return 'png';
43
+ if (mimeType.includes('webp'))
44
+ return 'webp';
45
+ return null;
46
+ }
47
+ function inferExtensionFromUrl(inputUrl) {
48
+ try {
49
+ const parsed = new URL(inputUrl);
50
+ const ext = path.extname(parsed.pathname || '');
51
+ return ext ? ext.replace('.', '') : null;
52
+ }
53
+ catch (error) {
54
+ return null;
55
+ }
56
+ }
57
+ async function saveImageFromUrl({ url, filename }) {
58
+ if (!url) {
59
+ return { ok: false, error: 'url is required.' };
60
+ }
61
+ const normalizedUrl = String(url).trim();
62
+ if (!normalizedUrl) {
63
+ return { ok: false, error: 'url must be non-empty.' };
64
+ }
65
+ if (typeof fetch !== 'function') {
66
+ return { ok: false, error: 'Global fetch is not available in this Node runtime.' };
67
+ }
68
+ const response = await fetch(normalizedUrl);
69
+ if (!response.ok) {
70
+ return { ok: false, error: `Failed to fetch image (${response.status}).` };
71
+ }
72
+ const arrayBuffer = await response.arrayBuffer();
73
+ const buffer = Buffer.from(arrayBuffer);
74
+ const contentType = response.headers.get('content-type') || '';
75
+ const extensionFromMime = inferExtensionFromMime(contentType);
76
+ const extensionFromUrl = inferExtensionFromUrl(normalizedUrl);
77
+ const safeName = filename ? path.basename(filename) : '';
78
+ const extension = extensionFromMime || (safeName.includes('.') ? safeName.split('.').pop() : null) || extensionFromUrl || 'jpg';
79
+ const baseName = safeName ? safeName.replace(/\.[^.]+$/, '') : `headed-${Date.now()}`;
80
+ const fileName = `${baseName}.${extension}`;
81
+ const targetPath = path.join('/tmp', fileName);
82
+ await fs.writeFile(targetPath, buffer);
83
+ return { ok: true, path: targetPath, bytes: buffer.length };
84
+ }
85
+ const TOOLS = [
86
+ {
87
+ name: 'headed_start_session',
88
+ description: 'Start a headed browser session (optionally with email + password for login).',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ email: { type: 'string' },
93
+ password: { type: 'string' },
94
+ },
95
+ additionalProperties: false,
96
+ },
97
+ },
98
+ {
99
+ name: 'headed_end_session',
100
+ description: 'End a headed browser session.',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ browserSessionId: { type: 'number' },
105
+ },
106
+ required: ['browserSessionId'],
107
+ additionalProperties: false,
108
+ },
109
+ },
110
+ {
111
+ name: 'headed_navigate_to_project',
112
+ description: 'Navigate a headed session to /sims/{projectId}.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ browserSessionId: { type: 'number' },
117
+ projectId: { type: ['number', 'string'] },
118
+ },
119
+ required: ['browserSessionId', 'projectId'],
120
+ additionalProperties: false,
121
+ },
122
+ },
123
+ {
124
+ name: 'headed_send_msg',
125
+ description: 'Send a chat message in the headed session and capture screenshots (returns links).',
126
+ inputSchema: {
127
+ type: 'object',
128
+ properties: {
129
+ browserSessionId: { type: 'number' },
130
+ waitDuration: { type: 'number' },
131
+ screenshotInterval: { type: 'number' },
132
+ msg: { type: 'string' },
133
+ },
134
+ required: ['browserSessionId', 'waitDuration', 'screenshotInterval', 'msg'],
135
+ additionalProperties: false,
136
+ },
137
+ },
138
+ {
139
+ name: 'screenshots_preview',
140
+ description: 'Render screenshots HTML (accepts screenshots/screenshotUrls array or full response body).',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ screenshots: { type: 'array', items: { type: 'string' } },
145
+ screenshotUrls: { type: 'array', items: { type: 'string' } },
146
+ response: { type: 'object' },
147
+ },
148
+ additionalProperties: false,
149
+ },
150
+ },
151
+ {
152
+ name: 'screenshots_get_preview',
153
+ description: 'Fetch the preview HTML page for a stored screenshot set.',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {
157
+ previewId: { type: 'string' },
158
+ },
159
+ required: ['previewId'],
160
+ additionalProperties: false,
161
+ },
162
+ },
163
+ {
164
+ name: 'workflow_end_to_end_project_generation',
165
+ description: 'Run the server-side end-to-end project workflow (features → provision → boilerplate).',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ title: { type: 'string' },
170
+ description: { type: 'string' },
171
+ technologies: { type: 'array', items: { type: 'string' } },
172
+ language: { type: 'string' },
173
+ planId: { type: ['string', 'number'] },
174
+ personalizationNotes: { type: 'string' },
175
+ email: { type: 'string' },
176
+ user_id: { type: 'string' },
177
+ show_scaffold: { type: 'boolean' },
178
+ show_checkpoints: { type: 'boolean' },
179
+ show_walkthrough: { type: 'boolean' },
180
+ onboarding_completed: { type: 'boolean' },
181
+ target_skills: { type: 'array', items: { type: 'string' } },
182
+ project_setup_complete: { type: 'boolean' },
183
+ boilerplate_setup_complete: { type: 'boolean' },
184
+ plan_id: { type: ['string', 'number'] },
185
+ plan_project_id: { type: ['string', 'number'] },
186
+ posthogDistinctId: { type: 'string' },
187
+ workflowRunName: { type: 'string' },
188
+ },
189
+ required: ['title', 'description', 'technologies'],
190
+ additionalProperties: true,
191
+ },
192
+ },
193
+ {
194
+ name: 'complete_project',
195
+ description: 'Run the server-side complete_project workflow (overwrites project files).',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {
199
+ projectId: { type: 'number' },
200
+ },
201
+ required: ['projectId'],
202
+ additionalProperties: false,
203
+ },
204
+ },
205
+ {
206
+ name: 'save_image_base64',
207
+ description: 'Download an image URL to /tmp and return the file path.',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {
211
+ url: { type: 'string' },
212
+ filename: { type: 'string' },
213
+ },
214
+ required: ['url'],
215
+ additionalProperties: false,
216
+ },
217
+ },
218
+ ];
219
+ const server = new Server({
220
+ name: 'marble-headed-mcp',
221
+ version: '0.1.0',
222
+ }, {
223
+ capabilities: {
224
+ tools: {},
225
+ },
226
+ });
227
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
228
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
229
+ const { name, arguments: args } = request.params;
230
+ try {
231
+ switch (name) {
232
+ case 'headed_start_session': {
233
+ const result = await postJson('/start_session', args);
234
+ return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
235
+ }
236
+ case 'headed_end_session': {
237
+ const result = await postJson('/end_session', args);
238
+ return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
239
+ }
240
+ case 'headed_navigate_to_project': {
241
+ const result = await postJson('/navigate_to_project', args);
242
+ return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
243
+ }
244
+ case 'headed_send_msg': {
245
+ const result = await postJson('/send_msg_headed', args);
246
+ return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
247
+ }
248
+ case 'screenshots_preview': {
249
+ const payload = args?.screenshots
250
+ ? { screenshots: args.screenshots }
251
+ : args?.screenshotUrls
252
+ ? { screenshotUrls: args.screenshotUrls }
253
+ : args?.response || args;
254
+ const result = await postJson('/preview_screenshots', payload);
255
+ return { content: [{ type: 'text', text: JSON.stringify({ status: result.status, ok: result.ok, html: result.text }, null, 2) }] };
256
+ }
257
+ case 'screenshots_get_preview': {
258
+ const previewId = args?.previewId;
259
+ const result = await getText(`/preview_screenshots/${encodeURIComponent(previewId)}`);
260
+ return { content: [{ type: 'text', text: JSON.stringify({ status: result.status, ok: result.ok, html: result.text }, null, 2) }] };
261
+ }
262
+ case 'workflow_end_to_end_project_generation': {
263
+ const result = await postJson('/end_to_end_project_generation', args);
264
+ return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
265
+ }
266
+ case 'complete_project': {
267
+ const result = await postJson('/complete_project', args);
268
+ return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
269
+ }
270
+ case 'save_image_base64': {
271
+ const saveResult = await saveImageFromUrl({
272
+ url: args?.url,
273
+ filename: args?.filename,
274
+ });
275
+ return { content: [{ type: 'text', text: JSON.stringify(saveResult, null, 2) }] };
276
+ }
277
+ default:
278
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
279
+ }
280
+ }
281
+ catch (error) {
282
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: error?.message || String(error) }, null, 2) }] };
283
+ }
284
+ });
285
+ const transport = new StdioServerTransport();
286
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "marble-headed-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Marble headed automation endpoints",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "marble-headed-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepare": "npm run build",
18
+ "watch": "tsc --watch",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "marble",
24
+ "headed",
25
+ "playwright",
26
+ "workflow",
27
+ "model-context-protocol"
28
+ ],
29
+ "author": "Marble Team",
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.25.3"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.10.1",
39
+ "typescript": "^5.7.2"
40
+ }
41
+ }