marble-headed-mcp 0.1.43 → 0.1.44
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 +49 -0
- package/dist/index.js +97 -43
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1 +1,50 @@
|
|
|
1
1
|
# marble-headed-mcp
|
|
2
|
+
|
|
3
|
+
Local-first MCP development setup (no npm publish needed to test).
|
|
4
|
+
|
|
5
|
+
## Config files
|
|
6
|
+
|
|
7
|
+
- `mcp.local.json`: runs local build (`node dist/index.js`)
|
|
8
|
+
- `mcp.prod.json`: runs published npm package (`npx marble-headed-mcp@0.1.42`)
|
|
9
|
+
- `mcp.json`: active config used by your local Codex session
|
|
10
|
+
|
|
11
|
+
## One-command switch for Codex + Claude
|
|
12
|
+
|
|
13
|
+
Use these commands from this repo:
|
|
14
|
+
|
|
15
|
+
- `npm run switch:mcp:dev`
|
|
16
|
+
- `npm run switch:mcp:prod`
|
|
17
|
+
- `npm run switch:mcp:status`
|
|
18
|
+
|
|
19
|
+
What gets updated:
|
|
20
|
+
|
|
21
|
+
- `mcp.json` in this repo
|
|
22
|
+
- `~/.codex/config.toml` (`[mcp_servers.marble-headed]`)
|
|
23
|
+
- `~/.claude.json` (`mcpServers.marble-headed` globally + project entries)
|
|
24
|
+
|
|
25
|
+
After switching, restart active Codex and Claude sessions.
|
|
26
|
+
|
|
27
|
+
## Common workflow
|
|
28
|
+
|
|
29
|
+
1. Install deps once:
|
|
30
|
+
- `npm ci`
|
|
31
|
+
2. Switch to local MCP mode:
|
|
32
|
+
- `npm run use:mcp:local`
|
|
33
|
+
3. Build once (or use watch):
|
|
34
|
+
- `npm run build`
|
|
35
|
+
- or `npm run watch`
|
|
36
|
+
4. Restart Codex session so MCP reloads from `mcp.json`.
|
|
37
|
+
5. Test changes locally without publishing to npm.
|
|
38
|
+
|
|
39
|
+
## Switch back to published MCP
|
|
40
|
+
|
|
41
|
+
- `npm run use:mcp:prod`
|
|
42
|
+
- Restart Codex session.
|
|
43
|
+
|
|
44
|
+
## Publish flow (when ready)
|
|
45
|
+
|
|
46
|
+
1. Keep testing locally until stable.
|
|
47
|
+
2. Bump version:
|
|
48
|
+
- `npm version patch`
|
|
49
|
+
3. Publish:
|
|
50
|
+
- `npm publish --access public`
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,13 @@ import { promisify } from 'util';
|
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
const
|
|
9
|
+
const DEFAULT_BROWSER_APP_BASE_URL = 'http://localhost:3000';
|
|
10
|
+
const DEFAULT_HEADED_SERVER_BASE_URL = 'http://localhost:4000';
|
|
11
|
+
const ENVIRONMENT_BASE_URLS = {
|
|
12
|
+
localhost: DEFAULT_BROWSER_APP_BASE_URL,
|
|
13
|
+
dev: 'https://dev.withmarble.ai',
|
|
14
|
+
production: 'https://withmarble.ai',
|
|
15
|
+
};
|
|
10
16
|
const WEBAPP_LOG_PATH = '/tmp/webapp';
|
|
11
17
|
const DEFAULT_DEV_CONTAINER_PATH = '/Users/akilanbabu/code/marble-container';
|
|
12
18
|
const VSCODE_REPO_PATH = '/Users/akilanbabu/code/vscode-source-marble/code-server-7/lib/vscode';
|
|
@@ -15,6 +21,7 @@ const DEFAULT_CODEX_LOGGER_PATH = path.join(process.env.HOME || '/Users/akilanba
|
|
|
15
21
|
const CODEX_LOGGER_PATH_ENV = 'CODEX_LOGGER_PATH';
|
|
16
22
|
const CODEX_LOG_PREFIX = 'codex-run-';
|
|
17
23
|
const execFileAsync = promisify(execFile);
|
|
24
|
+
let selectedEnvironment = null;
|
|
18
25
|
function parseCodexJsonOutput(output) {
|
|
19
26
|
const result = {};
|
|
20
27
|
if (!output)
|
|
@@ -50,6 +57,15 @@ function parseCodexJsonOutput(output) {
|
|
|
50
57
|
function normalizeBaseUrl(input) {
|
|
51
58
|
return input.replace(/\/+$/, '');
|
|
52
59
|
}
|
|
60
|
+
function parseEnvironment(input) {
|
|
61
|
+
if (typeof input !== 'string')
|
|
62
|
+
return null;
|
|
63
|
+
const normalized = input.trim().toLowerCase();
|
|
64
|
+
if (normalized === 'localhost' || normalized === 'dev' || normalized === 'production') {
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
53
69
|
function shellQuote(value) {
|
|
54
70
|
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
55
71
|
}
|
|
@@ -110,19 +126,25 @@ async function readCodexThreadIdFromLog(filePath) {
|
|
|
110
126
|
return null;
|
|
111
127
|
}
|
|
112
128
|
}
|
|
113
|
-
function
|
|
114
|
-
const base = process.env.HEADED_SERVER_BASE_URL ||
|
|
129
|
+
function resolveHeadedServerRawBaseUrl() {
|
|
130
|
+
const base = process.env.HEADED_SERVER_BASE_URL || DEFAULT_HEADED_SERVER_BASE_URL;
|
|
115
131
|
return normalizeBaseUrl(base);
|
|
116
132
|
}
|
|
117
|
-
function
|
|
118
|
-
const raw =
|
|
133
|
+
function resolveHeadedServerAppBaseUrl() {
|
|
134
|
+
const raw = resolveHeadedServerRawBaseUrl();
|
|
119
135
|
return raw.replace(/\/api\/headed\/?$/, '');
|
|
120
136
|
}
|
|
137
|
+
function resolveBrowserAppBaseUrl() {
|
|
138
|
+
if (selectedEnvironment) {
|
|
139
|
+
return normalizeBaseUrl(ENVIRONMENT_BASE_URLS[selectedEnvironment]);
|
|
140
|
+
}
|
|
141
|
+
return normalizeBaseUrl(ENVIRONMENT_BASE_URLS.localhost);
|
|
142
|
+
}
|
|
121
143
|
function buildUrl(pathname) {
|
|
122
|
-
return `${
|
|
144
|
+
return `${resolveHeadedServerAppBaseUrl()}${pathname}`;
|
|
123
145
|
}
|
|
124
146
|
function buildHeadedUrl(pathname) {
|
|
125
|
-
return `${
|
|
147
|
+
return `${resolveHeadedServerAppBaseUrl()}/api/headed${pathname}`;
|
|
126
148
|
}
|
|
127
149
|
async function postJson(pathname, payload) {
|
|
128
150
|
const response = await fetch(buildUrl(pathname), {
|
|
@@ -166,6 +188,16 @@ async function getHeadedText(pathname) {
|
|
|
166
188
|
const text = await response.text();
|
|
167
189
|
return { status: response.status, ok: response.ok, text };
|
|
168
190
|
}
|
|
191
|
+
function withBrowserAppBaseUrl(payload) {
|
|
192
|
+
const appBaseUrl = resolveBrowserAppBaseUrl();
|
|
193
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
194
|
+
return {
|
|
195
|
+
...payload,
|
|
196
|
+
appBaseUrl,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { appBaseUrl };
|
|
200
|
+
}
|
|
169
201
|
async function getJson(pathname) {
|
|
170
202
|
const response = await fetch(buildUrl(pathname));
|
|
171
203
|
const text = await response.text();
|
|
@@ -398,6 +430,22 @@ async function saveImageFromUrl({ url, filename }) {
|
|
|
398
430
|
return { ok: true, path: targetPath, bytes: buffer.length };
|
|
399
431
|
}
|
|
400
432
|
const TOOLS = [
|
|
433
|
+
{
|
|
434
|
+
name: 'set_environment',
|
|
435
|
+
description: 'Set the in-browser app environment used by headed session actions. localhost -> http://localhost:3000, dev -> https://dev.withmarble.ai, production -> https://withmarble.ai. API calls still go to HEADED_SERVER_BASE_URL (default localhost:4000).',
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
environment: {
|
|
440
|
+
type: 'string',
|
|
441
|
+
enum: ['localhost', 'dev', 'production'],
|
|
442
|
+
description: 'Environment to target.',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ['environment'],
|
|
446
|
+
additionalProperties: false,
|
|
447
|
+
},
|
|
448
|
+
},
|
|
401
449
|
{
|
|
402
450
|
name: 'headed_start_session',
|
|
403
451
|
description: 'Start a headed browser session (if email or password are not provided, they default to akilan@withmarble.ai and marbledebug123, respectively). Remember to call `headed_end_session` when finished to avoid leaving sessions open.',
|
|
@@ -435,20 +483,6 @@ const TOOLS = [
|
|
|
435
483
|
additionalProperties: false,
|
|
436
484
|
},
|
|
437
485
|
},
|
|
438
|
-
{
|
|
439
|
-
name: 'navigate_to_url',
|
|
440
|
-
description: 'Navigate a headed session to a URL. Prefer `headed_navigate_to_project` when possible; use this only for validating code in a separate browser tab (e.g., proxy URL access). If the URL already exists (ignoring query parameters), the existing tab will be focused instead of creating a new one.',
|
|
441
|
-
inputSchema: {
|
|
442
|
-
type: 'object',
|
|
443
|
-
properties: {
|
|
444
|
-
browserSession: { type: 'number', description: 'Headed browser session id.' },
|
|
445
|
-
browserSessionId: { type: 'number', description: 'Alias for browserSession.' },
|
|
446
|
-
url: { type: 'string', description: 'URL to navigate to.' },
|
|
447
|
-
},
|
|
448
|
-
required: ['url'],
|
|
449
|
-
additionalProperties: false,
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
486
|
{
|
|
453
487
|
name: 'start_plan_project',
|
|
454
488
|
description: 'Navigate to a project within a plan. If startProject is true (default), clicks BEGIN SIMULATION; if false, only navigates to the plan project without starting the simulation.',
|
|
@@ -568,7 +602,7 @@ const TOOLS = [
|
|
|
568
602
|
},
|
|
569
603
|
createProgram: { type: 'boolean', description: 'If true (default), auto-generate program projects before save.' },
|
|
570
604
|
createProgramMessage: { type: 'string', description: 'Optional custom message used for program generation turn.' },
|
|
571
|
-
maxOperatorTurns: { type: 'number', description: 'Max auto-follow-up turns after the initial goal (default:
|
|
605
|
+
maxOperatorTurns: { type: 'number', description: 'Max auto-follow-up turns after the initial goal (default: 5).' },
|
|
572
606
|
workflowRunName: { type: 'string', description: 'Override workflow run name.' },
|
|
573
607
|
},
|
|
574
608
|
required: ['goal'],
|
|
@@ -716,37 +750,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
716
750
|
const { name, arguments: args } = request.params;
|
|
717
751
|
try {
|
|
718
752
|
switch (name) {
|
|
753
|
+
case 'set_environment': {
|
|
754
|
+
const environment = parseEnvironment(args?.environment);
|
|
755
|
+
if (!environment) {
|
|
756
|
+
return {
|
|
757
|
+
content: [
|
|
758
|
+
{
|
|
759
|
+
type: 'text',
|
|
760
|
+
text: JSON.stringify({
|
|
761
|
+
ok: false,
|
|
762
|
+
error: 'environment must be one of: localhost, dev, production.',
|
|
763
|
+
}, null, 2),
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
selectedEnvironment = environment;
|
|
769
|
+
const browserAppBaseUrl = resolveBrowserAppBaseUrl();
|
|
770
|
+
const headedServerAppBaseUrl = resolveHeadedServerAppBaseUrl();
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: 'text',
|
|
775
|
+
text: JSON.stringify({
|
|
776
|
+
ok: true,
|
|
777
|
+
environment,
|
|
778
|
+
browserAppBaseUrl,
|
|
779
|
+
headedServerAppBaseUrl,
|
|
780
|
+
headedServerHeadedApiBaseUrl: `${headedServerAppBaseUrl}/api/headed`,
|
|
781
|
+
}, null, 2),
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
};
|
|
785
|
+
}
|
|
719
786
|
case 'headed_start_session': {
|
|
720
|
-
const result = await postHeadedJson('/start_session', args);
|
|
787
|
+
const result = await postHeadedJson('/start_session', withBrowserAppBaseUrl(args));
|
|
721
788
|
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
722
789
|
}
|
|
723
790
|
case 'headed_end_session': {
|
|
724
|
-
const result = await postHeadedJson('/end_session', args);
|
|
791
|
+
const result = await postHeadedJson('/end_session', withBrowserAppBaseUrl(args));
|
|
725
792
|
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
726
793
|
}
|
|
727
794
|
case 'headed_navigate_to_project': {
|
|
728
|
-
const result = await postHeadedJson('/navigate_to_project', args);
|
|
729
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
730
|
-
}
|
|
731
|
-
case 'navigate_to_url': {
|
|
732
|
-
const url = typeof args?.url === 'string' ? args.url.trim() : '';
|
|
733
|
-
const browserSessionRaw = args?.browserSession ?? args?.browserSessionId;
|
|
734
|
-
const browserSessionId = typeof browserSessionRaw === 'number' ? browserSessionRaw : Number(browserSessionRaw);
|
|
735
|
-
if (!url) {
|
|
736
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'url is required.' }, null, 2) }] };
|
|
737
|
-
}
|
|
738
|
-
if (!browserSessionId || Number.isNaN(browserSessionId)) {
|
|
739
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'browserSession is required.' }, null, 2) }] };
|
|
740
|
-
}
|
|
741
|
-
const result = await postHeadedJson('/navigate_to_url', { browserSessionId, url });
|
|
795
|
+
const result = await postHeadedJson('/navigate_to_project', withBrowserAppBaseUrl(args));
|
|
742
796
|
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
743
797
|
}
|
|
744
798
|
case 'start_plan_project': {
|
|
745
|
-
const result = await postHeadedJson('/start_plan_project', args);
|
|
799
|
+
const result = await postHeadedJson('/start_plan_project', withBrowserAppBaseUrl(args));
|
|
746
800
|
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
747
801
|
}
|
|
748
802
|
case 'headed_send_msg': {
|
|
749
|
-
const result = await postHeadedJson('/send_msg_headed', args);
|
|
803
|
+
const result = await postHeadedJson('/send_msg_headed', withBrowserAppBaseUrl(args));
|
|
750
804
|
const payload = (result.json && typeof result.json === 'object') ? result.json : null;
|
|
751
805
|
if (payload?.workflowRunId) {
|
|
752
806
|
const summaryResult = await fetchWorkflowEntries(String(payload.workflowRunId));
|
|
@@ -761,7 +815,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
761
815
|
return { content: [{ type: 'text', text: JSON.stringify(payload || { status: result.status, body: result.text }, null, 2) }] };
|
|
762
816
|
}
|
|
763
817
|
case 'take_screenshot': {
|
|
764
|
-
const result = await postHeadedJson('/take_screenshot', args);
|
|
818
|
+
const result = await postHeadedJson('/take_screenshot', withBrowserAppBaseUrl(args));
|
|
765
819
|
const payload = (result.json && typeof result.json === 'object') ? result.json : null;
|
|
766
820
|
if (payload?.workflowRunId) {
|
|
767
821
|
const summaryResult = await fetchWorkflowEntries(String(payload.workflowRunId));
|
|
@@ -776,14 +830,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
776
830
|
return { content: [{ type: 'text', text: JSON.stringify(payload || { status: result.status, body: result.text }, null, 2) }] };
|
|
777
831
|
}
|
|
778
832
|
case 'take_screenshot': {
|
|
779
|
-
const result = await postHeadedJson('/take_screenshot', args);
|
|
833
|
+
const result = await postHeadedJson('/take_screenshot', withBrowserAppBaseUrl(args));
|
|
780
834
|
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
781
835
|
}
|
|
782
836
|
case 'screenshots_preview': {
|
|
783
837
|
const payload = args?.screenshotUrls
|
|
784
838
|
? { screenshotUrls: args.screenshotUrls }
|
|
785
839
|
: args?.response || args;
|
|
786
|
-
const result = await postHeadedJson('/preview_screenshots', payload);
|
|
840
|
+
const result = await postHeadedJson('/preview_screenshots', withBrowserAppBaseUrl(payload));
|
|
787
841
|
return { content: [{ type: 'text', text: JSON.stringify({ status: result.status, ok: result.ok, html: result.text }, null, 2) }] };
|
|
788
842
|
}
|
|
789
843
|
case 'screenshots_get_preview': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marble-headed-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "MCP server for Marble headed automation endpoints",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"prepare": "npm run build",
|
|
18
18
|
"watch": "tsc --watch",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"dev:mcp": "node dist/index.js",
|
|
21
|
+
"use:mcp:local": "cp mcp.local.json mcp.json",
|
|
22
|
+
"use:mcp:prod": "cp mcp.prod.json mcp.json",
|
|
23
|
+
"switch:mcp:dev": "node scripts/switch-mcp-mode.mjs dev",
|
|
24
|
+
"switch:mcp:prod": "node scripts/switch-mcp-mode.mjs prod",
|
|
25
|
+
"switch:mcp:status": "node scripts/switch-mcp-mode.mjs status"
|
|
20
26
|
},
|
|
21
27
|
"keywords": [
|
|
22
28
|
"mcp",
|