You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MCP stdio transport pins its localization to the manifest defaults — the settings read happens in start(), inside the pre-bind window under every composition order #11580
The handle is acquired and consumed during start(). SettingsServicePlugin binds its data engine from its own start()-registered kernel:ready hook (settings-service-plugin.ts:153), which is strictly after every plugin's start(). So this read is inside the pre-bind window under every composition order — being started after the settings plugin does not help, and optionalDependencies: ['com.objectstack.service.settings'] would not move it.
Consequence
resolveLocalizationContext calls settings.getMany('localization', ['timezone','locale','currency']) (packages/core/src/security/resolve-authz-context.ts:757). In the window that resolves from the empty in-memory fallback plus the manifest defaults with source: 'default' — it does not throw, so failed stays false and the direct sys_setting$in fallback below it never runs. The declared defaults are non-empty, so the function returns { timezone: 'UTC', locale: 'en-US' } and reports success.
That value is then held for the life of the transport by design (#7279: "resolved ONCE for the life of the transport … the key's tenant cannot change mid-session"). A long-lived stdio MCP server therefore serves every call with UTC / en-US on a workspace whose persisted localization settings say otherwise, and never self-corrects. SettingsService.reportPreBindRead (#10250) does emit one warn line for the localization namespace at boot, which is the only symptom today.
Repair — one of
Move the localization hoist out of start() into a kernel:bootstrapped hook. That is the earliest phase after the bind, and the one reportPreBindRead's own remedy text names. The transport would need to either start from that hook too, or take the localization lazily.
Option 2 looks smaller and keeps the fail-closed identity probe where it is; whoever owns packages/mcp should pick.
⚠️ Do not repair this by adding optionalDependencies: ['com.objectstack.service.settings']. It would look like the #10250 fix and change nothing — packages/mcp is already ordered after the settings plugin in the serve composition (measured with the real resolvePluginOrder) and the read is still pre-bind.
Verification: after the fix, pnpm check:settings-bind-window must go green with the com.objectstack.mcp entry deleted from KNOWN_PRE_BIND_READS in scripts/check-settings-bind-window.mjs — that ledger is shrink-only and errors on a stale entry, so the deletion is part of the fix.
Found by the step-1 measurement of #11045 (the settings bind-window gate). Filed per-plugin rather than fixed there, per that card's ruling.
This one is not the #10250 defect wearing a different hat: no dependency declaration can repair it.
What happens
packages/mcp/src/plugin.ts:301, insideMCPServerPlugin.start()on the stdio auto-start path (OS_MCP_STDIO_ENABLED/autoStart):The handle is acquired and consumed during
start().SettingsServicePluginbinds its data engine from its ownstart()-registeredkernel:readyhook (settings-service-plugin.ts:153), which is strictly after every plugin'sstart(). So this read is inside the pre-bind window under every composition order — being started after the settings plugin does not help, andoptionalDependencies: ['com.objectstack.service.settings']would not move it.Consequence
resolveLocalizationContextcallssettings.getMany('localization', ['timezone','locale','currency'])(packages/core/src/security/resolve-authz-context.ts:757). In the window that resolves from the empty in-memory fallback plus the manifest defaults withsource: 'default'— it does not throw, sofailedstays false and the directsys_setting$infallback below it never runs. The declared defaults are non-empty, so the function returns{ timezone: 'UTC', locale: 'en-US' }and reports success.That value is then held for the life of the transport by design (#7279: "resolved ONCE for the life of the transport … the key's tenant cannot change mid-session"). A long-lived stdio MCP server therefore serves every call with
UTC/en-USon a workspace whose persistedlocalizationsettings say otherwise, and never self-corrects.SettingsService.reportPreBindRead(#10250) does emit one warn line for thelocalizationnamespace at boot, which is the only symptom today.Repair — one of
start()into akernel:bootstrappedhook. That is the earliest phase after the bind, and the onereportPreBindRead's own remedy text names. The transport would need to either start from that hook too, or take the localization lazily.localizationlazy: resolve it on first use and memoize, instead of eagerly at transport construction. This keeps finding: a THIRD hand-written ExecutionContext assembly survives in the stdio MCP plugin, and it dropstabPermissions/accessToken#7279's property (not re-resolved per call) while moving the read out of the window.Option 2 looks smaller and keeps the fail-closed identity probe where it is; whoever owns
packages/mcpshould pick.optionalDependencies: ['com.objectstack.service.settings']. It would look like the #10250 fix and change nothing —packages/mcpis already ordered after the settings plugin in theservecomposition (measured with the realresolvePluginOrder) and the read is still pre-bind.Verification: after the fix,
pnpm check:settings-bind-windowmust go green with thecom.objectstack.mcpentry deleted fromKNOWN_PRE_BIND_READSinscripts/check-settings-bind-window.mjs— that ledger is shrink-only and errors on a stale entry, so the deletion is part of the fix.Suggested lane: whoever owns
packages/mcp.