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.
Files changed (3) hide show
  1. package/README.md +49 -0
  2. package/dist/index.js +97 -43
  3. 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 DEFAULT_BASE_URL = 'http://localhost:4000';
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 resolveRawBaseUrl() {
114
- const base = process.env.HEADED_SERVER_BASE_URL || DEFAULT_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 resolveAppBaseUrl() {
118
- const raw = resolveRawBaseUrl();
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 `${resolveAppBaseUrl()}${pathname}`;
144
+ return `${resolveHeadedServerAppBaseUrl()}${pathname}`;
123
145
  }
124
146
  function buildHeadedUrl(pathname) {
125
- return `${resolveAppBaseUrl()}/api/headed${pathname}`;
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: 4).' },
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.43",
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",