@solarisdk/mcp 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -20,7 +20,8 @@ registry**: `*_create` tools return a `sessionId`, and the rest take it.
20
20
  Point any MCP client at the hosted Streamable HTTP endpoint with your API key:
21
21
 
22
22
  - **Prod:** `https://mcp.getsolari.com/mcp`
23
- - **Staging:** `https://mcp-sta.solaribrowser.com/mcp`
23
+ - **Staging:** currently NOT deployed — the us-east-1 ALB it rode was
24
+ removed in the us-west-1 migration and its DNS record has been deleted.
24
25
 
25
26
  Claude Desktop / claude.ai: **Settings → Connectors → Add custom connector**,
26
27
  URL as above, and send `Authorization: Bearer slr_live_…`. Or in
@@ -68,8 +69,9 @@ lifecycles — re-attach with `solari_connect`).
68
69
  "env": {
69
70
  "SOLARI_API_KEY": "slr_live_…"
70
71
  // optional overrides (defaults hit prod https://api.getsolari.com):
71
- // "SOLARI_BASE_URL": "https://desktops-sta.solaribrowser.com", // sandbox/desktop gateway
72
- // "SOLARI_BROWSER_URL": "https://api-sta.solaribrowser.com" // browser gateway
72
+ // optional staging override — ONE host now serves both gateways:
73
+ // "SOLARI_BASE_URL": "https://api-sta.getsolari.com",
74
+ // "SOLARI_BROWSER_URL": "https://api-sta.getsolari.com"
73
75
  }
74
76
  }
75
77
  }
@@ -85,7 +87,7 @@ Chat, Code and Cowork alike.
85
87
  |---|---|---|---|
86
88
  | `SOLARI_API_KEY` | ✅ | — | unified `slr_live_…` key (works on both gateways) |
87
89
  | `SOLARI_BASE_URL` | | `https://api.getsolari.com` | sandbox/desktop gateway |
88
- | `SOLARI_BROWSER_URL` | | `SOLARI_BASE_URL` → prod | cloud-browser gateway (differs from `SOLARI_BASE_URL` on staging) |
90
+ | `SOLARI_BROWSER_URL` | | `SOLARI_BASE_URL` → prod | cloud-browser gateway. Both prod (`api.getsolari.com`) and staging (`api-sta.getsolari.com`) now path-route browser AND desktop on one host, so this rarely needs setting |
89
91
  | `SOLARI_BROWSER_API_KEY` | | `SOLARI_API_KEY` | separate key for the browser gateway, for environments where the browser and desktop key stores are split |
90
92
 
91
93
  ## Tools
@@ -94,7 +96,7 @@ Chat, Code and Cowork alike.
94
96
 
95
97
  | Tool | What it does |
96
98
  |---|---|
97
- | `solari_browser_create` | Start a browser session (`stealth`, `proxy`, `captcha`, `recording`, `profileId`) → `sessionId` |
99
+ | `solari_browser_create` | Start a browser session (`mode`, `proxy`, `captcha`, `recording`, `profileId`) → `{sessionId, mode, captcha}` |
98
100
  | `solari_browser_navigate` | Go to a URL → `{url, title, status}` |
99
101
  | `solari_browser_read_page` | Page text / links / HTML (30k-char cap) |
100
102
  | `solari_browser_screenshot` | JPEG screenshot (image content) |
@@ -103,6 +105,39 @@ Chat, Code and Cowork alike.
103
105
  | `solari_browser_replay_url` | rrweb replay URL (needs `recording: true`) |
104
106
  | `solari_browser_close` | Release the session |
105
107
 
108
+ ### Browser mode — stealth by default
109
+
110
+ `solari_browser_create` defaults to **`mode: "stealth"`** (the anti-bot hardened pool). A bare
111
+ call with no arguments gets it; the model does not have to ask.
112
+
113
+ This used to be an optional `stealth: true` boolean, and in practice models did not opt in — so
114
+ sessions quietly landed on the fast pool and got blocked by bot detection. The failure looked
115
+ like "the site is broken" rather than "we chose the wrong pool", which is the worst way for a
116
+ default to be wrong.
117
+
118
+ `mode: "fast"` is the explicit opt-out: lower latency, for sites you already know do not
119
+ fingerprint or block automation (internal tools, localhost, your own app, plain docs). If a page
120
+ returns a block/captcha/challenge in fast mode, close the session and retry on stealth.
121
+
122
+ `proxy` and `captcha` are stealth-only upstream and are now **rejected** in fast mode rather than
123
+ silently dropped — a quietly unproxied request leaks the origin IP, which is the one thing the
124
+ caller was trying to avoid. The response echoes the resolved `mode`, so a later block is
125
+ actionable.
126
+
127
+ The deprecated `stealth` boolean still works in both directions for existing callers
128
+ (`stealth: false` still means fast); an explicit `mode` wins when both are supplied.
129
+
130
+ **Captcha auto-solving is on by default too**, for the same reason: an option a model has to opt
131
+ into does not get used, and the resulting failure ("the page is a challenge screen") reads as a
132
+ broken site rather than a missing flag. Pass **`captcha: false`** to opt out — solving costs money
133
+ and adds latency, so turn it off for sites you know never challenge.
134
+
135
+ The default *follows the pool* rather than being a flat `true`: captcha is stealth-only upstream,
136
+ so a flat default would make a plain `mode: "fast"` call throw on an option the caller never
137
+ asked for. Stealth ⇒ on, fast ⇒ off, explicit value always wins. Asking for `captcha: true`
138
+ *together with* `mode: "fast"` is a real contradiction and is refused. The response echoes the
139
+ resolved `captcha` alongside `mode`.
140
+
106
141
  ### Sandboxes + desktops
107
142
 
108
143
  | Tool | What it does |
@@ -137,11 +172,46 @@ npm run build:bundle # esbuild → dist/solari-mcp.bundle.cjs (single file, no n
137
172
 
138
173
  ```bash
139
174
  SOLARI_API_KEY=slr_live_… \
140
- SOLARI_BASE_URL=https://desktops-sta.solaribrowser.com \
141
- SOLARI_BROWSER_URL=https://api-sta.solaribrowser.com \
175
+ SOLARI_BASE_URL=https://api-sta.getsolari.com \
176
+ SOLARI_BROWSER_URL=https://api-sta.getsolari.com \
142
177
  node test/live-smoke.mjs # add SMOKE_SERVER=dist/solari-mcp.bundle.cjs to test the bundle
143
178
  ```
144
179
 
145
180
  Round-trips browser create→navigate→read→screenshot→search→close, sandbox
146
181
  create→exec→run_code→kill, and desktop create→screenshot→kill as a real MCP
147
182
  client over stdio.
183
+
184
+ ## Hosted connector — where it runs (ops)
185
+
186
+ All of this is **manual**, not terraform.
187
+
188
+ | | Prod | Staging |
189
+ |---|---|---|
190
+ | URL | `https://mcp.getsolari.com/mcp` | *(not deployed — see above)* |
191
+ | Region | **us-west-1** | — |
192
+ | Cluster | `solari-desktops-prod-usw1-gw` | — |
193
+ | Service / taskdef | `solari-mcp-prod-usw1` | — |
194
+ | Image | ECR `solari-mcp:prod` (us-west-1) | — |
195
+ | Ingress | listener rule **prio 20** on the gateway ALB, host-header match | — |
196
+
197
+ It rides the desktop gateway's ALB, so it is **coupled to that ALB's lifetime**.
198
+
199
+ ### ⚠️ `/health` returning 200 does NOT mean the connector is up
200
+
201
+ The desktop gateway answers `GET /health` with `{"ok":true}` too. If DNS points at the
202
+ gateway instead of the connector — which is exactly what happened on 2026-07-28, when the
203
+ us-west-1 migration deleted the old us-east-1 gateway ALB and re-aliased
204
+ `mcp.getsolari.com` to the new one — `/health` still returns 200 while **every MCP call
205
+ 404s**. The connector was down and the health check was green.
206
+
207
+ Two checks that actually discriminate:
208
+
209
+ ```sh
210
+ curl -s https://mcp.getsolari.com/health # connector's body has a "sessions" field; the gateway's does not
211
+ curl -s -o /dev/null -w '%{http_code}\n' -XPOST https://mcp.getsolari.com/mcp \
212
+ -H 'content-type: application/json' -d '{}'
213
+ # 401 = the connector (auth required) 404 = the gateway is answering — connector NOT reachable
214
+ ```
215
+
216
+ **If you move or rebuild the desktop gateway ALB, you must re-point this service too** —
217
+ target group, listener rule, and the Route53 alias all live on that ALB.
package/dist/browser.js CHANGED
@@ -131,21 +131,69 @@ export function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
131
131
  return {
132
132
  solari_browser_create: {
133
133
  description: "Start a Solari cloud browser session. Use this whenever you need to browse the web. " +
134
- "Returns a sessionId for the other solari_browser_* tools. Options: stealth (anti-bot " +
135
- "hardened pool), proxy ('smart' | country code like 'us' requires stealth), captcha " +
136
- "auto-solving (requires stealth), recording (rrweb replay).",
134
+ "Returns a sessionId for the other solari_browser_* tools.\n" +
135
+ "mode defaults to 'stealth' the anti-bot hardened pool. Just call this with no " +
136
+ "arguments and you get it; you do NOT need to ask for stealth explicitly. Stealth is " +
137
+ "the right default for the open web: ordinary sites work fine on it, and it is what " +
138
+ "keeps bot-detection from blocking the session.\n" +
139
+ "Pass mode:'fast' ONLY for a site you already know does not fingerprint or block " +
140
+ "automation (internal tools, localhost, your own app, plain docs/API pages) and you " +
141
+ "want the lower-latency pool. If a page in fast mode returns a block/captcha/challenge, " +
142
+ "close the session and retry with mode:'stealth'.\n" +
143
+ "captcha auto-solving is ALSO on by default (it follows the pool: on for stealth, off " +
144
+ "for fast). Pass captcha:false to turn it off — solving a challenge costs money and " +
145
+ "adds latency, so switch it off for sites you know never challenge.\n" +
146
+ "proxy ('smart' | country code like 'us') REQUIRES stealth, as does captcha; both are " +
147
+ "rejected if you ask for them explicitly in fast mode. recording gives an rrweb replay.",
137
148
  inputSchema: {
149
+ mode: z.enum(["stealth", "fast"]).optional(),
150
+ /** @deprecated use mode. Kept so existing callers keep working. */
138
151
  stealth: z.boolean().optional(),
139
152
  proxy: z.string().optional(),
153
+ /** Defaults to ON (following the pool). Pass false to opt out. */
140
154
  captcha: z.boolean().optional(),
141
155
  recording: z.boolean().optional(),
142
156
  profileId: z.string().optional(),
143
157
  },
144
158
  handler: async (a) => {
159
+ // STEALTH IS THE DEFAULT. Models consistently declined to opt in when it
160
+ // was an optional boolean, so sessions silently landed on the fast pool
161
+ // and got blocked by bot detection — the failure looked like "the site
162
+ // is broken" rather than "we picked the wrong pool". Defaulting on, with
163
+ // an explicit escape hatch, removes that whole class of confusion.
164
+ //
165
+ // Precedence: explicit mode wins; then the deprecated stealth boolean
166
+ // (so `stealth:false` still means fast for old callers); then stealth.
167
+ const stealth = a.mode !== undefined ? a.mode === "stealth" : (a.stealth ?? true);
168
+ // CAPTCHA IS ALSO ON BY DEFAULT — same reasoning as stealth: a model
169
+ // that has to opt in does not, and the resulting failure ("the page is
170
+ // a challenge screen") reads as a broken site rather than a missing
171
+ // option.
172
+ //
173
+ // The default FOLLOWS the pool rather than being a flat `true`, because
174
+ // captcha is stealth-only upstream. A flat true would make plain
175
+ // mode:'fast' throw on an option the caller never asked for, turning the
176
+ // escape hatch into a dead end. So: stealth ⇒ on, fast ⇒ off, and an
177
+ // explicit value always wins (captcha:false is the opt-out).
178
+ const captcha = a.captcha ?? stealth;
179
+ // proxy/captcha are stealth-only upstream. Previously a fast-pool
180
+ // session with proxy came back with the proxy silently dropped and only
181
+ // a note in the response; fail loudly instead — a silently unproxied
182
+ // request can leak the origin IP, which is the one thing the caller was
183
+ // trying to avoid. Only an EXPLICIT ask conflicts; the captcha default
184
+ // simply turns itself off in fast mode.
185
+ if (!stealth) {
186
+ const needsStealth = [a.proxy ? "proxy" : "", a.captcha === true ? "captcha" : ""].filter(Boolean);
187
+ if (needsStealth.length) {
188
+ throw new Error(`${needsStealth.join(" and ")} ${needsStealth.length > 1 ? "require" : "requires"} ` +
189
+ `stealth, but mode is 'fast'. ` +
190
+ `Drop ${needsStealth.length > 1 ? "them" : "it"} or use mode:'stealth'.`);
191
+ }
192
+ }
145
193
  const body = {};
146
- if (a.stealth)
194
+ if (stealth)
147
195
  body.stealth = true;
148
- if (a.captcha)
196
+ if (captcha)
149
197
  body.captcha = true;
150
198
  if (a.recording)
151
199
  body.recording = true;
@@ -196,10 +244,16 @@ export function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
196
244
  throw new Error(`browser session started but could not be attached (released it): ${redact(err instanceof Error ? err.message : String(err))}`);
197
245
  }
198
246
  // Proxy silently degrades rather than erroring — surface what we got.
247
+ // Echo the resolved mode back: the caller did not necessarily pick it
248
+ // (stealth is the default), and knowing which pool it landed on is what
249
+ // makes a later block actionable — "retry on stealth" vs "this site
250
+ // blocks us even hardened".
199
251
  return text({
200
252
  sessionId: s.sessionId,
253
+ mode: stealth ? "stealth" : "fast",
254
+ captcha,
201
255
  expiresAt: s.expiresAt,
202
- proxy: s.proxy ?? (a.proxy ? "NOT APPLIED (check plan/stealth)" : undefined),
256
+ proxy: s.proxy ?? (a.proxy ? "NOT APPLIED (check plan)" : undefined),
203
257
  });
204
258
  },
205
259
  },
@@ -113768,18 +113768,31 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
113768
113768
  \u2026[truncated ${s.length - MAX_PAGE_TEXT} of ${s.length} ${what}]` : s;
113769
113769
  return {
113770
113770
  solari_browser_create: {
113771
- description: "Start a Solari cloud browser session. Use this whenever you need to browse the web. Returns a sessionId for the other solari_browser_* tools. Options: stealth (anti-bot hardened pool), proxy ('smart' | country code like 'us' \u2014 requires stealth), captcha auto-solving (requires stealth), recording (rrweb replay).",
113771
+ description: "Start a Solari cloud browser session. Use this whenever you need to browse the web. Returns a sessionId for the other solari_browser_* tools.\nmode defaults to 'stealth' \u2014 the anti-bot hardened pool. Just call this with no arguments and you get it; you do NOT need to ask for stealth explicitly. Stealth is the right default for the open web: ordinary sites work fine on it, and it is what keeps bot-detection from blocking the session.\nPass mode:'fast' ONLY for a site you already know does not fingerprint or block automation (internal tools, localhost, your own app, plain docs/API pages) and you want the lower-latency pool. If a page in fast mode returns a block/captcha/challenge, close the session and retry with mode:'stealth'.\ncaptcha auto-solving is ALSO on by default (it follows the pool: on for stealth, off for fast). Pass captcha:false to turn it off \u2014 solving a challenge costs money and adds latency, so switch it off for sites you know never challenge.\nproxy ('smart' | country code like 'us') REQUIRES stealth, as does captcha; both are rejected if you ask for them explicitly in fast mode. recording gives an rrweb replay.",
113772
113772
  inputSchema: {
113773
+ mode: external_exports.enum(["stealth", "fast"]).optional(),
113774
+ /** @deprecated use mode. Kept so existing callers keep working. */
113773
113775
  stealth: external_exports.boolean().optional(),
113774
113776
  proxy: external_exports.string().optional(),
113777
+ /** Defaults to ON (following the pool). Pass false to opt out. */
113775
113778
  captcha: external_exports.boolean().optional(),
113776
113779
  recording: external_exports.boolean().optional(),
113777
113780
  profileId: external_exports.string().optional()
113778
113781
  },
113779
113782
  handler: async (a2) => {
113783
+ const stealth = a2.mode !== void 0 ? a2.mode === "stealth" : a2.stealth ?? true;
113784
+ const captcha = a2.captcha ?? stealth;
113785
+ if (!stealth) {
113786
+ const needsStealth = [a2.proxy ? "proxy" : "", a2.captcha === true ? "captcha" : ""].filter(Boolean);
113787
+ if (needsStealth.length) {
113788
+ throw new Error(
113789
+ `${needsStealth.join(" and ")} ${needsStealth.length > 1 ? "require" : "requires"} stealth, but mode is 'fast'. Drop ${needsStealth.length > 1 ? "them" : "it"} or use mode:'stealth'.`
113790
+ );
113791
+ }
113792
+ }
113780
113793
  const body = {};
113781
- if (a2.stealth) body.stealth = true;
113782
- if (a2.captcha) body.captcha = true;
113794
+ if (stealth) body.stealth = true;
113795
+ if (captcha) body.captcha = true;
113783
113796
  if (a2.recording) body.recording = true;
113784
113797
  if (a2.proxy) body.proxy = a2.proxy;
113785
113798
  if (a2.profileId) body.profileId = a2.profileId;
@@ -113819,8 +113832,10 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
113819
113832
  }
113820
113833
  return text({
113821
113834
  sessionId: s.sessionId,
113835
+ mode: stealth ? "stealth" : "fast",
113836
+ captcha,
113822
113837
  expiresAt: s.expiresAt,
113823
- proxy: s.proxy ?? (a2.proxy ? "NOT APPLIED (check plan/stealth)" : void 0)
113838
+ proxy: s.proxy ?? (a2.proxy ? "NOT APPLIED (check plan)" : void 0)
113824
113839
  });
113825
113840
  }
113826
113841
  },
@@ -112437,18 +112437,31 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
112437
112437
  \u2026[truncated ${s.length - MAX_PAGE_TEXT} of ${s.length} ${what}]` : s;
112438
112438
  return {
112439
112439
  solari_browser_create: {
112440
- description: "Start a Solari cloud browser session. Use this whenever you need to browse the web. Returns a sessionId for the other solari_browser_* tools. Options: stealth (anti-bot hardened pool), proxy ('smart' | country code like 'us' \u2014 requires stealth), captcha auto-solving (requires stealth), recording (rrweb replay).",
112440
+ description: "Start a Solari cloud browser session. Use this whenever you need to browse the web. Returns a sessionId for the other solari_browser_* tools.\nmode defaults to 'stealth' \u2014 the anti-bot hardened pool. Just call this with no arguments and you get it; you do NOT need to ask for stealth explicitly. Stealth is the right default for the open web: ordinary sites work fine on it, and it is what keeps bot-detection from blocking the session.\nPass mode:'fast' ONLY for a site you already know does not fingerprint or block automation (internal tools, localhost, your own app, plain docs/API pages) and you want the lower-latency pool. If a page in fast mode returns a block/captcha/challenge, close the session and retry with mode:'stealth'.\ncaptcha auto-solving is ALSO on by default (it follows the pool: on for stealth, off for fast). Pass captcha:false to turn it off \u2014 solving a challenge costs money and adds latency, so switch it off for sites you know never challenge.\nproxy ('smart' | country code like 'us') REQUIRES stealth, as does captcha; both are rejected if you ask for them explicitly in fast mode. recording gives an rrweb replay.",
112441
112441
  inputSchema: {
112442
+ mode: external_exports.enum(["stealth", "fast"]).optional(),
112443
+ /** @deprecated use mode. Kept so existing callers keep working. */
112442
112444
  stealth: external_exports.boolean().optional(),
112443
112445
  proxy: external_exports.string().optional(),
112446
+ /** Defaults to ON (following the pool). Pass false to opt out. */
112444
112447
  captcha: external_exports.boolean().optional(),
112445
112448
  recording: external_exports.boolean().optional(),
112446
112449
  profileId: external_exports.string().optional()
112447
112450
  },
112448
112451
  handler: async (a2) => {
112452
+ const stealth = a2.mode !== void 0 ? a2.mode === "stealth" : a2.stealth ?? true;
112453
+ const captcha = a2.captcha ?? stealth;
112454
+ if (!stealth) {
112455
+ const needsStealth = [a2.proxy ? "proxy" : "", a2.captcha === true ? "captcha" : ""].filter(Boolean);
112456
+ if (needsStealth.length) {
112457
+ throw new Error(
112458
+ `${needsStealth.join(" and ")} ${needsStealth.length > 1 ? "require" : "requires"} stealth, but mode is 'fast'. Drop ${needsStealth.length > 1 ? "them" : "it"} or use mode:'stealth'.`
112459
+ );
112460
+ }
112461
+ }
112449
112462
  const body = {};
112450
- if (a2.stealth) body.stealth = true;
112451
- if (a2.captcha) body.captcha = true;
112463
+ if (stealth) body.stealth = true;
112464
+ if (captcha) body.captcha = true;
112452
112465
  if (a2.recording) body.recording = true;
112453
112466
  if (a2.proxy) body.proxy = a2.proxy;
112454
112467
  if (a2.profileId) body.profileId = a2.profileId;
@@ -112488,8 +112501,10 @@ function makeBrowserToolset(cfg, reg, deps = defaultDeps) {
112488
112501
  }
112489
112502
  return text({
112490
112503
  sessionId: s.sessionId,
112504
+ mode: stealth ? "stealth" : "fast",
112505
+ captcha,
112491
112506
  expiresAt: s.expiresAt,
112492
- proxy: s.proxy ?? (a2.proxy ? "NOT APPLIED (check plan/stealth)" : void 0)
112507
+ proxy: s.proxy ?? (a2.proxy ? "NOT APPLIED (check plan)" : void 0)
112493
112508
  });
112494
112509
  }
112495
112510
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solarisdk/mcp",
3
- "version": "0.3.0",
4
- "description": "Model Context Protocol server for the Solari cloud browser, sandboxes + desktops drive them from Claude Desktop/Cowork, Claude Code, Cursor, etc.",
3
+ "version": "0.3.1",
4
+ "description": "Model Context Protocol server for the Solari cloud browser, sandboxes + desktops \u2014 drive them from Claude Desktop/Cowork, Claude Code, Cursor, etc.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "solari-mcp": "./dist/cli.js"