@@ -74,14 +74,47 @@ export function registerMcpProvider(
7474 try {
7575 // ── HTTP definition branch (D-03) ──────────────────────────────
7676 const settings = getSettings ( ) ;
77+ // Declared out here so the stdio fallback below can tell "the daemon is
78+ // serving us" from "we fell through" — the credential branch depends on it.
79+ let daemon : { port : number ; bearerToken : string } | null = null ;
7780 if ( settings . mcp . useDaemon && daemonManager ) {
78- const daemonStatus = await daemonManager . getDaemonStatus ( ) ;
79- if ( daemonStatus . healthy && daemonStatus . port != null && daemonStatus . bearerToken != null ) {
81+ // START the daemon, don't just look for one. A passive getDaemonStatus()
82+ // meant an MCP request could never bring the daemon up, so unless the user
83+ // had opened the dashboard (or run a formula command) EVERY session fell
84+ // through to the stdio branch below — and that branch is the one that
85+ // contends for the shared .chrome-profile, once per VS Code window. One
86+ // shared daemon is the configuration that avoids Chrome exit 21, so
87+ // starting it here makes profile collisions LESS likely, not more.
88+ //
89+ // implicit:true is load-bearing: it keeps the _userStopped latch (a
90+ // deliberate Stop must not be undone by the next tool call) and the
91+ // SPAWN_SUPPRESS_MS window that closed the runaway-spawn OOM documented
92+ // in daemon-manager.ts. Never pass implicit:false from here.
93+ //
94+ // Timeout matches ensureDaemon's own default and SPAWN_SUPPRESS_MS. It is
95+ // a ceiling, not a cost: a healthy daemon returns on the first poll.
96+ // ponytail: on failure we fall through to stdio rather than returning [],
97+ // so MCP still works when the daemon can't start. That leaves a narrow
98+ // window where a late-arriving daemon and the stdio server both drive the
99+ // same profile; give the fallback its own AIRTABLE_PROFILE_DIR if that
100+ // ever shows up in the wild.
101+ try {
102+ const info = await daemonManager . ensureDaemon ( { implicit : true , timeoutMs : 15_000 } ) ;
103+ if ( info ?. port != null && info . bearerToken != null ) {
104+ daemon = { port : info . port , bearerToken : info . bearerToken } ;
105+ }
106+ } catch {
107+ daemon = null ;
108+ }
109+ if ( daemon ) {
80110 const httpDef = createHttpDefinition (
81- `http://127.0.0.1:${ daemonStatus . port } /mcp` ,
82- `Bearer ${ daemonStatus . bearerToken } ` ,
111+ `http://127.0.0.1:${ daemon . port } /mcp` ,
112+ `Bearer ${ daemon . bearerToken } ` ,
83113 ) ;
84114 if ( httpDef ) return [ httpDef ] ;
115+ // A definition we could not construct is not a served daemon — fall
116+ // through to stdio AND let the credential branch know it must inject.
117+ daemon = null ;
85118 }
86119 }
87120
@@ -124,11 +157,17 @@ export function registerMcpProvider(
124157 if ( authManager ) {
125158 const authMode = settings . mcp . authMode ;
126159 const isCredMode = authMode === 'byo' || authMode === 'direct-login' ;
127- if ( isCredMode && ! settings . mcp . useDaemon ) {
128- // Pure stdio mode (no daemon): byo/direct-login REQUIRE credentials
129- // (no browser), and env is the only channel VS Code gives a stdio
130- // spawn. In daemon mode the daemon endpoint (/daemon/auth-credentials)
131- // is the SINGLE credential channel — never duplicate secrets into env.
160+ if ( isCredMode && ! daemon ) {
161+ // Gate on "did we actually fall through to stdio", NOT on the useDaemon
162+ // SETTING. Those used to be the same thing; they no longer are. useDaemon
163+ // defaults true, so with the setting-based test a byo/direct-login user
164+ // who reached this branch — daemon failed to start, or they pressed Stop —
165+ // got a stdio server with NO credentials and no browser to fall back on:
166+ // *_CREDENTIALS_MISSING on every tool call. `daemon` is null here exactly
167+ // when the daemon is not serving us, which is precisely when env is once
168+ // again the only credential channel VS Code gives a stdio spawn.
169+ // When the daemon IS serving, /daemon/auth-credentials stays the SINGLE
170+ // channel — secrets are still never duplicated into env on that path.
132171 const credEnv = await authManager . getCredentialsEnv ( authMode ) ;
133172 if ( credEnv ) Object . assign ( env , credEnv ) ;
134173 } else if ( ! isCredMode && settings . auth . loginMode === 'auto' ) {
0 commit comments