@swarmclawai/swarmclaw 1.5.69 → 1.5.71

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
@@ -399,6 +399,25 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.5.71 Highlights
403
+
404
+ Fast-follow release for [#60](https://github.com/swarmclawai/swarmclaw/pull/60) by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!
405
+
406
+ - **Browser MCP works from standalone builds again.** The Next standalone output now includes the Playwright MCP runtime packages required by packaged SwarmClaw builds.
407
+ - **Host browser launches use cached Chromium.** Local Playwright MCP startup now selects Chromium explicitly instead of depending on a system Chrome install.
408
+ - **Standalone repair is more robust.** The build repair step now fills partially traced Playwright MCP package directories, and the packaging and browser startup paths are covered by regression tests.
409
+
410
+ ### v1.5.70 Highlights
411
+
412
+ Fast-follow release for [#56](https://github.com/swarmclawai/swarmclaw/pull/56) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
413
+
414
+ Also includes fixes for [#57](https://github.com/swarmclawai/swarmclaw/issues/57) and [#58](https://github.com/swarmclawai/swarmclaw/issues/58) reported by [@zantak](https://github.com/zantak). Thanks zantak!
415
+
416
+ - **Builtin provider saves work again.** Saving a builtin provider no longer sends the strict-schema rejected `type` field, and the provider update route is now covered by the runtime test script.
417
+ - **Knowledge sources appear on direct visits.** Panel-backed routes such as Knowledge now auto-open their source/sidebar panel on desktop route changes, while mobile keeps the drawer closed by default.
418
+ - **Reasoning content stays out of the reply body.** OpenAI-compatible `reasoning_content` and `reasoning` stream deltas now flow into the existing collapsed Thinking panel instead of being appended before the visible answer.
419
+ - **macOS install guidance remains explicit.** Ad-hoc signed macOS desktop builds still document the quarantine workaround until Developer ID signing and notarization are available. Thanks [@yagudaev](https://github.com/yagudaev) for confirming the current workaround on Apple Silicon.
420
+
402
421
  ### v1.5.69 Highlights
403
422
 
404
423
  Fast-follow release for [#55](https://github.com/swarmclawai/swarmclaw/pull/55) by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.5.69",
3
+ "version": "1.5.71",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -86,8 +86,8 @@
86
86
  "cli": "node ./bin/swarmclaw.js",
87
87
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
88
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
- "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
89
+ "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
+ "test:runtime": "tsx --test src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/app/view-constants.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
91
91
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
92
  "test:e2e": "tsx .workbench/browser-e2e/run.ts",
93
93
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -35,6 +35,11 @@ export const REQUIRED_NEXT_METADATA_FILES = [
35
35
  'get-metadata-route.js',
36
36
  'is-metadata-route.js',
37
37
  ]
38
+ export const REQUIRED_STANDALONE_BROWSER_PACKAGES = [
39
+ '@playwright/mcp',
40
+ 'playwright',
41
+ 'playwright-core',
42
+ ]
38
43
 
39
44
  function parsePositiveInteger(value) {
40
45
  const parsed = Number.parseInt(String(value ?? '').trim(), 10)
@@ -187,6 +192,27 @@ export function repairStandaloneCssTreeData(cwd = process.cwd()) {
187
192
  return repaired
188
193
  }
189
194
 
195
+ export function repairStandaloneBrowserMcpRuntime(cwd = process.cwd()) {
196
+ const standaloneDir = path.join(cwd, '.next', 'standalone')
197
+ if (!fs.existsSync(standaloneDir)) return false
198
+
199
+ let repaired = false
200
+ const standaloneNodeModules = path.join(standaloneDir, 'node_modules')
201
+ for (const packageName of REQUIRED_STANDALONE_BROWSER_PACKAGES) {
202
+ const sourceDir = path.join(cwd, 'node_modules', ...packageName.split('/'))
203
+ const targetDir = path.join(standaloneNodeModules, ...packageName.split('/'))
204
+ if (!fs.existsSync(sourceDir)) {
205
+ throw new Error(`Missing required browser MCP runtime package under ${sourceDir}.`)
206
+ }
207
+
208
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true })
209
+ fs.cpSync(sourceDir, targetDir, { recursive: true, force: true })
210
+ repaired = true
211
+ }
212
+
213
+ return repaired
214
+ }
215
+
190
216
  export function repairStandaloneNextMetadata(cwd = process.cwd()) {
191
217
  const standaloneDir = path.join(cwd, '.next', 'standalone')
192
218
  if (!fs.existsSync(standaloneDir)) return false
@@ -248,6 +274,9 @@ function main() {
248
274
  if (result.status === 0 && repairStandaloneCssTreeData(process.cwd())) {
249
275
  console.error('Copied css-tree/data/ into standalone build output.')
250
276
  }
277
+ if (result.status === 0 && repairStandaloneBrowserMcpRuntime(process.cwd())) {
278
+ console.error('Copied Playwright MCP runtime packages into standalone build output.')
279
+ }
251
280
  process.exit(result.status)
252
281
  }
253
282
  if (result.signal) {
@@ -47,3 +47,23 @@ test('provider route upserts builtin override records for enablement changes', (
47
47
  assert.equal(output.responsePayload.type, 'builtin')
48
48
  assert.equal(output.responsePayload.isEnabled, false)
49
49
  })
50
+
51
+ test('provider route rejects unknown fields per ProviderUpdateSchema.strict()', () => {
52
+ const output = runWithTempDataDir<{ status: number }>(`
53
+ const routeMod = await import('./src/app/api/providers/[id]/route')
54
+ const route = routeMod.default || routeMod
55
+
56
+ const response = await route.PUT(
57
+ new Request('http://local/api/providers/openai', {
58
+ method: 'PUT',
59
+ headers: { 'content-type': 'application/json' },
60
+ body: JSON.stringify({ type: 'builtin', isEnabled: true }),
61
+ }),
62
+ { params: Promise.resolve({ id: 'openai' }) },
63
+ )
64
+
65
+ console.log(JSON.stringify({ status: response.status }))
66
+ `, { prefix: 'swarmclaw-provider-route-strict-test-' })
67
+
68
+ assert.equal(output.status, 400)
69
+ })
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback } from 'react'
3
+ import { useEffect, useRef, useState, useCallback } from 'react'
4
4
  import { useRouter, usePathname } from 'next/navigation'
5
5
  import { initAudioContext } from '@/lib/tts'
6
6
  import { clearStoredAccessKey } from '@/lib/app/api-client'
@@ -13,6 +13,7 @@ import { useSwipe } from '@/hooks/use-swipe'
13
13
  import { useWs } from '@/hooks/use-ws'
14
14
  import { api } from '@/lib/app/api-client'
15
15
  import { pathToView, useNavigate } from '@/lib/app/navigation'
16
+ import { shouldAutoOpenPanelSidebar } from '@/lib/app/view-constants'
16
17
 
17
18
  import { FullScreenLoader } from '@/components/ui/full-screen-loader'
18
19
  import { SidebarRail } from '@/components/layout/sidebar-rail'
@@ -45,6 +46,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
45
46
 
46
47
  const [bootTimedOut, setBootTimedOut] = useState(false)
47
48
  const [profileSheetOpen, setProfileSheetOpen] = useState(false)
49
+ const lastAutoOpenedPanelPathRef = useRef<string | null>(null)
48
50
  const sidebarOpen = useAppStore((s) => s.sidebarOpen)
49
51
  const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
50
52
  const appSettings = useAppStore((s) => s.appSettings)
@@ -165,6 +167,18 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
165
167
  }
166
168
  }, [pathname, isViewEnabled, router, isAuthPage])
167
169
 
170
+ useEffect(() => {
171
+ if (isAuthPage) return
172
+ const currentView = pathToView(pathname)
173
+ if (!shouldAutoOpenPanelSidebar(currentView, isDesktop)) {
174
+ lastAutoOpenedPanelPathRef.current = null
175
+ return
176
+ }
177
+ if (lastAutoOpenedPanelPathRef.current === pathname) return
178
+ lastAutoOpenedPanelPathRef.current = pathname
179
+ setSidebarOpen(true)
180
+ }, [isAuthPage, isDesktop, pathname, setSidebarOpen])
181
+
168
182
  // Extension sidebar items
169
183
  const refreshExtensionState = useCallback(() => {
170
184
  void loadExtensions()
@@ -9,7 +9,7 @@ import { DaemonIndicator } from '@/components/layout/daemon-indicator'
9
9
  import { NotificationCenter } from '@/components/shared/notification-center'
10
10
  import { NavItem, RailTooltip } from '@/components/layout/nav-item'
11
11
  import { useWs } from '@/hooks/use-ws'
12
- import { FULL_WIDTH_VIEWS } from '@/lib/app/view-constants'
12
+ import { FULL_WIDTH_VIEWS, isPanelSidebarView } from '@/lib/app/view-constants'
13
13
  import { pathToView, useNavigate } from '@/lib/app/navigation'
14
14
  import { safeStorageGet, safeStorageSet } from '@/lib/app/safe-storage'
15
15
  import type { AppView } from '@/types'
@@ -80,9 +80,9 @@ export function SidebarRail({
80
80
  setSidebarOpen(false)
81
81
  return
82
82
  }
83
- if (FULL_WIDTH_VIEWS.has(view)) {
84
- setSidebarOpen(false)
85
- } else if (activeView === view && sidebarOpen) {
83
+ if (isPanelSidebarView(view)) {
84
+ setSidebarOpen(!(activeView === view && sidebarOpen))
85
+ } else if (FULL_WIDTH_VIEWS.has(view)) {
86
86
  setSidebarOpen(false)
87
87
  } else {
88
88
  setSidebarOpen(true)
@@ -85,7 +85,6 @@ export function useSaveBuiltinProviderMutation() {
85
85
  mutationFn: async ({ id, models, isEnabled, baseUrl }: SaveBuiltinProviderInput) => {
86
86
  await api('PUT', `/providers/${id}/models`, { models })
87
87
  return api('PUT', `/providers/${id}`, {
88
- type: 'builtin',
89
88
  isEnabled,
90
89
  ...(baseUrl ? { baseUrl } : {}),
91
90
  })
@@ -0,0 +1,21 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { isPanelSidebarView, shouldAutoOpenPanelSidebar } from './view-constants'
5
+
6
+ describe('panel sidebar route helpers', () => {
7
+ it('treats knowledge as a panel-backed view', () => {
8
+ assert.equal(isPanelSidebarView('knowledge'), true)
9
+ })
10
+
11
+ it('auto-opens panel-backed views only on desktop', () => {
12
+ assert.equal(shouldAutoOpenPanelSidebar('knowledge', true), true)
13
+ assert.equal(shouldAutoOpenPanelSidebar('knowledge', false), false)
14
+ })
15
+
16
+ it('does not auto-open full-width views without panel layouts', () => {
17
+ assert.equal(shouldAutoOpenPanelSidebar('home', true), false)
18
+ assert.equal(shouldAutoOpenPanelSidebar('settings', true), false)
19
+ assert.equal(shouldAutoOpenPanelSidebar(null, true), false)
20
+ })
21
+ })
@@ -245,3 +245,28 @@ export const FULL_WIDTH_VIEWS = new Set<AppView>([
245
245
  'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'extensions',
246
246
  'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects', 'swarmfeed', 'marketplace', 'missions',
247
247
  ])
248
+
249
+ export const PANEL_SIDEBAR_VIEWS = new Set<AppView>([
250
+ 'agents',
251
+ 'connectors',
252
+ 'extensions',
253
+ 'knowledge',
254
+ 'logs',
255
+ 'mcp_servers',
256
+ 'memory',
257
+ 'providers',
258
+ 'runs',
259
+ 'schedules',
260
+ 'secrets',
261
+ 'skills',
262
+ 'tasks',
263
+ 'webhooks',
264
+ ])
265
+
266
+ export function isPanelSidebarView(view: AppView | null | undefined): boolean {
267
+ return Boolean(view && PANEL_SIDEBAR_VIEWS.has(view))
268
+ }
269
+
270
+ export function shouldAutoOpenPanelSidebar(view: AppView | null | undefined, isDesktop: boolean): boolean {
271
+ return isDesktop && isPanelSidebarView(view)
272
+ }
@@ -0,0 +1,54 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { streamOpenAiChat } from './openai'
5
+
6
+ function sseChunk(data: unknown) {
7
+ return `data: ${JSON.stringify(data)}\n\n`
8
+ }
9
+
10
+ function parseSseEvents(frames: string[]) {
11
+ return frames
12
+ .flatMap((frame) => frame.trim().split('\n\n'))
13
+ .filter(Boolean)
14
+ .map((frame) => JSON.parse(frame.replace(/^data: /, '')) as { t: string; text?: string })
15
+ }
16
+
17
+ test('OpenAI-compatible reasoning deltas stream as thinking instead of visible text', async () => {
18
+ const originalFetch = globalThis.fetch
19
+ const encoded = new TextEncoder()
20
+ const frames = [
21
+ sseChunk({ choices: [{ delta: { reasoning_content: 'internal reasoning ' } }] }),
22
+ sseChunk({ choices: [{ delta: { content: 'visible answer' } }] }),
23
+ 'data: [DONE]\n\n',
24
+ ]
25
+ const writes: string[] = []
26
+
27
+ globalThis.fetch = async () => new Response(new ReadableStream({
28
+ start(controller) {
29
+ for (const frame of frames) controller.enqueue(encoded.encode(frame))
30
+ controller.close()
31
+ },
32
+ }), {
33
+ status: 200,
34
+ headers: { 'content-type': 'text/event-stream' },
35
+ })
36
+
37
+ try {
38
+ const result = await streamOpenAiChat({
39
+ session: { id: 'session-1', provider: 'openai', model: 'test-model' },
40
+ message: 'hello',
41
+ write: (data) => writes.push(data),
42
+ active: new Map(),
43
+ loadHistory: () => [],
44
+ } as Parameters<typeof streamOpenAiChat>[0])
45
+
46
+ assert.equal(result, 'visible answer')
47
+ assert.deepEqual(parseSseEvents(writes), [
48
+ { t: 'thinking', text: 'internal reasoning ' },
49
+ { t: 'd', text: 'visible answer' },
50
+ ])
51
+ } finally {
52
+ globalThis.fetch = originalFetch
53
+ }
54
+ })
@@ -166,13 +166,17 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
166
166
  try {
167
167
  const parsed = JSON.parse(data)
168
168
  const choice = parsed.choices?.[0]?.delta
169
- const delta = choice?.content
170
- // Thinking/reasoning models (kimi-k2, etc.) put output in reasoning fields
171
- || choice?.reasoning_content
172
- || choice?.reasoning
173
- if (delta) {
174
- fullResponse += delta
175
- writeSSE(write, 'd', delta)
169
+ const contentDelta = typeof choice?.content === 'string' ? choice.content : ''
170
+ const reasoningDelta =
171
+ typeof choice?.reasoning_content === 'string' ? choice.reasoning_content
172
+ : typeof choice?.reasoning === 'string' ? choice.reasoning
173
+ : ''
174
+ if (reasoningDelta) {
175
+ writeSSE(write, 'thinking', reasoningDelta)
176
+ }
177
+ if (contentDelta) {
178
+ fullResponse += contentDelta
179
+ writeSSE(write, 'd', contentDelta)
176
180
  }
177
181
  // Extract usage from the final chunk (stream_options: include_usage)
178
182
  if (usageEnabled && parsed.usage && onUsage) {
@@ -18,9 +18,12 @@ describe('browser tool connection config', () => {
18
18
  const params = buildBrowserStdioServerParams('/tmp/swarmclaw-browser-profile')
19
19
 
20
20
  assert.equal(params.command, process.execPath)
21
+ assert.equal(params.args.includes('--browser'), true)
22
+ assert.equal(params.args[params.args.indexOf('--browser') + 1], 'chromium')
21
23
  assert.equal(params.args.includes('--headless'), true)
22
24
  assert.equal(params.args.includes('--shared-browser-context'), false)
23
25
  assert.equal(params.args.includes('/tmp/swarmclaw-browser-profile'), true)
26
+ assert.equal((params.env as Record<string, string | undefined>).PLAYWRIGHT_MCP_BROWSER, 'chromium')
24
27
  assert.equal((params.env as Record<string, string | undefined>).PLAYWRIGHT_MCP_USER_DATA_DIR, '/tmp/swarmclaw-browser-profile')
25
28
  assert.equal(params.env.PLAYWRIGHT_MCP_OUTPUT_MODE, 'file')
26
29
  assert.equal(params.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION, '60000')
@@ -39,10 +42,12 @@ describe('browser tool connection config', () => {
39
42
  assert.equal(params.args.includes('--cdp-header'), true)
40
43
  assert.equal(params.args.includes('Authorization: Bearer token-123'), true)
41
44
  assert.equal(params.args.includes('--allow-unrestricted-file-access'), true)
45
+ assert.equal(params.args.includes('--browser'), false)
42
46
  assert.equal(params.args.includes('--user-data-dir'), false)
43
47
  const env = params.env as Record<string, string | undefined>
44
48
  assert.equal(env.PLAYWRIGHT_MCP_CDP_ENDPOINT, 'http://127.0.0.1:44001')
45
49
  assert.equal(params.env.PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS, '1')
50
+ assert.equal(env.PLAYWRIGHT_MCP_BROWSER, undefined)
46
51
  assert.equal(env.PLAYWRIGHT_MCP_USER_DATA_DIR, undefined)
47
52
  })
48
53
 
@@ -102,6 +102,7 @@ export function buildBrowserStdioServerParams(
102
102
  allowUnrestrictedFileAccess?: boolean
103
103
  },
104
104
  ) {
105
+ const browserName = 'chromium'
105
106
  const cliCandidates = [
106
107
  path.join(process.cwd(), 'node_modules', '@playwright', 'mcp', 'cli.js'),
107
108
  path.join(process.cwd(), '[project]', 'node_modules', '@playwright', 'mcp', 'cli.js'),
@@ -134,6 +135,7 @@ export function buildBrowserStdioServerParams(
134
135
  }
135
136
  } else {
136
137
  args.push(
138
+ '--browser', browserName,
137
139
  '--headless',
138
140
  '--user-data-dir', profileDir,
139
141
  )
@@ -145,6 +147,7 @@ export function buildBrowserStdioServerParams(
145
147
  env: {
146
148
  ...env,
147
149
  ...(cdpEndpoint ? { PLAYWRIGHT_MCP_CDP_ENDPOINT: cdpEndpoint } : {
150
+ PLAYWRIGHT_MCP_BROWSER: browserName,
148
151
  PLAYWRIGHT_MCP_USER_DATA_DIR: profileDir,
149
152
  PLAYWRIGHT_MCP_HEADLESS: '1',
150
153
  }),