marble-headed-mcp 0.1.42 → 0.1.43

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 (2) hide show
  1. package/dist/index.js +41 -94
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,12 +6,7 @@ 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:3000';
10
- const ENVIRONMENT_BASE_URLS = {
11
- localhost: 'http://localhost:3000',
12
- dev: 'https://dev.withmarble.ai',
13
- production: 'https://withmarble.ai',
14
- };
9
+ const DEFAULT_BASE_URL = 'http://localhost:4000';
15
10
  const WEBAPP_LOG_PATH = '/tmp/webapp';
16
11
  const DEFAULT_DEV_CONTAINER_PATH = '/Users/akilanbabu/code/marble-container';
17
12
  const VSCODE_REPO_PATH = '/Users/akilanbabu/code/vscode-source-marble/code-server-7/lib/vscode';
@@ -20,7 +15,6 @@ const DEFAULT_CODEX_LOGGER_PATH = path.join(process.env.HOME || '/Users/akilanba
20
15
  const CODEX_LOGGER_PATH_ENV = 'CODEX_LOGGER_PATH';
21
16
  const CODEX_LOG_PREFIX = 'codex-run-';
22
17
  const execFileAsync = promisify(execFile);
23
- let selectedEnvironment = null;
24
18
  function parseCodexJsonOutput(output) {
25
19
  const result = {};
26
20
  if (!output)
@@ -56,15 +50,6 @@ function parseCodexJsonOutput(output) {
56
50
  function normalizeBaseUrl(input) {
57
51
  return input.replace(/\/+$/, '');
58
52
  }
59
- function parseEnvironment(input) {
60
- if (typeof input !== 'string')
61
- return null;
62
- const normalized = input.trim().toLowerCase();
63
- if (normalized === 'localhost' || normalized === 'dev' || normalized === 'production') {
64
- return normalized;
65
- }
66
- return null;
67
- }
68
53
  function shellQuote(value) {
69
54
  return `'${value.replace(/'/g, `'\"'\"'`)}'`;
70
55
  }
@@ -125,25 +110,19 @@ async function readCodexThreadIdFromLog(filePath) {
125
110
  return null;
126
111
  }
127
112
  }
128
- function resolveHeadedServerRawBaseUrl() {
113
+ function resolveRawBaseUrl() {
129
114
  const base = process.env.HEADED_SERVER_BASE_URL || DEFAULT_BASE_URL;
130
115
  return normalizeBaseUrl(base);
131
116
  }
132
- function resolveHeadedServerAppBaseUrl() {
133
- const raw = resolveHeadedServerRawBaseUrl();
117
+ function resolveAppBaseUrl() {
118
+ const raw = resolveRawBaseUrl();
134
119
  return raw.replace(/\/api\/headed\/?$/, '');
135
120
  }
136
- function resolveBrowserAppBaseUrl() {
137
- if (selectedEnvironment) {
138
- return normalizeBaseUrl(ENVIRONMENT_BASE_URLS[selectedEnvironment]);
139
- }
140
- return normalizeBaseUrl(ENVIRONMENT_BASE_URLS.localhost);
141
- }
142
121
  function buildUrl(pathname) {
143
- return `${resolveHeadedServerAppBaseUrl()}${pathname}`;
122
+ return `${resolveAppBaseUrl()}${pathname}`;
144
123
  }
145
124
  function buildHeadedUrl(pathname) {
146
- return `${resolveHeadedServerAppBaseUrl()}/api/headed${pathname}`;
125
+ return `${resolveAppBaseUrl()}/api/headed${pathname}`;
147
126
  }
148
127
  async function postJson(pathname, payload) {
149
128
  const response = await fetch(buildUrl(pathname), {
@@ -187,16 +166,6 @@ async function getHeadedText(pathname) {
187
166
  const text = await response.text();
188
167
  return { status: response.status, ok: response.ok, text };
189
168
  }
190
- function withBrowserAppBaseUrl(payload) {
191
- const appBaseUrl = resolveBrowserAppBaseUrl();
192
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
193
- return {
194
- ...payload,
195
- appBaseUrl,
196
- };
197
- }
198
- return { appBaseUrl };
199
- }
200
169
  async function getJson(pathname) {
201
170
  const response = await fetch(buildUrl(pathname));
202
171
  const text = await response.text();
@@ -429,22 +398,6 @@ async function saveImageFromUrl({ url, filename }) {
429
398
  return { ok: true, path: targetPath, bytes: buffer.length };
430
399
  }
431
400
  const TOOLS = [
432
- {
433
- name: 'set_environment',
434
- 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:3000).',
435
- inputSchema: {
436
- type: 'object',
437
- properties: {
438
- environment: {
439
- type: 'string',
440
- enum: ['localhost', 'dev', 'production'],
441
- description: 'Environment to target.',
442
- },
443
- },
444
- required: ['environment'],
445
- additionalProperties: false,
446
- },
447
- },
448
401
  {
449
402
  name: 'headed_start_session',
450
403
  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.',
@@ -482,6 +435,20 @@ const TOOLS = [
482
435
  additionalProperties: false,
483
436
  },
484
437
  },
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
+ },
485
452
  {
486
453
  name: 'start_plan_project',
487
454
  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.',
@@ -749,57 +716,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
749
716
  const { name, arguments: args } = request.params;
750
717
  try {
751
718
  switch (name) {
752
- case 'set_environment': {
753
- const environment = parseEnvironment(args?.environment);
754
- if (!environment) {
755
- return {
756
- content: [
757
- {
758
- type: 'text',
759
- text: JSON.stringify({
760
- ok: false,
761
- error: 'environment must be one of: localhost, dev, production.',
762
- }, null, 2),
763
- },
764
- ],
765
- };
766
- }
767
- selectedEnvironment = environment;
768
- const browserAppBaseUrl = resolveBrowserAppBaseUrl();
769
- const headedServerAppBaseUrl = resolveHeadedServerAppBaseUrl();
770
- return {
771
- content: [
772
- {
773
- type: 'text',
774
- text: JSON.stringify({
775
- ok: true,
776
- environment,
777
- browserAppBaseUrl,
778
- headedServerAppBaseUrl,
779
- headedServerHeadedApiBaseUrl: `${headedServerAppBaseUrl}/api/headed`,
780
- }, null, 2),
781
- },
782
- ],
783
- };
784
- }
785
719
  case 'headed_start_session': {
786
- const result = await postHeadedJson('/start_session', withBrowserAppBaseUrl(args));
720
+ const result = await postHeadedJson('/start_session', args);
787
721
  return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
788
722
  }
789
723
  case 'headed_end_session': {
790
- const result = await postHeadedJson('/end_session', withBrowserAppBaseUrl(args));
724
+ const result = await postHeadedJson('/end_session', args);
791
725
  return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
792
726
  }
793
727
  case 'headed_navigate_to_project': {
794
- const result = await postHeadedJson('/navigate_to_project', withBrowserAppBaseUrl(args));
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
742
  return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
796
743
  }
797
744
  case 'start_plan_project': {
798
- const result = await postHeadedJson('/start_plan_project', withBrowserAppBaseUrl(args));
745
+ const result = await postHeadedJson('/start_plan_project', args);
799
746
  return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
800
747
  }
801
748
  case 'headed_send_msg': {
802
- const result = await postHeadedJson('/send_msg_headed', withBrowserAppBaseUrl(args));
749
+ const result = await postHeadedJson('/send_msg_headed', args);
803
750
  const payload = (result.json && typeof result.json === 'object') ? result.json : null;
804
751
  if (payload?.workflowRunId) {
805
752
  const summaryResult = await fetchWorkflowEntries(String(payload.workflowRunId));
@@ -814,7 +761,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
814
761
  return { content: [{ type: 'text', text: JSON.stringify(payload || { status: result.status, body: result.text }, null, 2) }] };
815
762
  }
816
763
  case 'take_screenshot': {
817
- const result = await postHeadedJson('/take_screenshot', withBrowserAppBaseUrl(args));
764
+ const result = await postHeadedJson('/take_screenshot', args);
818
765
  const payload = (result.json && typeof result.json === 'object') ? result.json : null;
819
766
  if (payload?.workflowRunId) {
820
767
  const summaryResult = await fetchWorkflowEntries(String(payload.workflowRunId));
@@ -829,14 +776,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
829
776
  return { content: [{ type: 'text', text: JSON.stringify(payload || { status: result.status, body: result.text }, null, 2) }] };
830
777
  }
831
778
  case 'take_screenshot': {
832
- const result = await postHeadedJson('/take_screenshot', withBrowserAppBaseUrl(args));
779
+ const result = await postHeadedJson('/take_screenshot', args);
833
780
  return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
834
781
  }
835
782
  case 'screenshots_preview': {
836
783
  const payload = args?.screenshotUrls
837
784
  ? { screenshotUrls: args.screenshotUrls }
838
785
  : args?.response || args;
839
- const result = await postHeadedJson('/preview_screenshots', withBrowserAppBaseUrl(payload));
786
+ const result = await postHeadedJson('/preview_screenshots', payload);
840
787
  return { content: [{ type: 'text', text: JSON.stringify({ status: result.status, ok: result.ok, html: result.text }, null, 2) }] };
841
788
  }
842
789
  case 'screenshots_get_preview': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marble-headed-mcp",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "MCP server for Marble headed automation endpoints",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",