Skip to content

MCP tool envelope: workspaceId is not session-sticky — multi-call sessions default into the "default" workspace #214

Description

@gcp007-ops

Summary

The MCP CLI envelope (getTools / useTools) takes workspaceId on every call and falls back to 'default' when it is not explicitly passed. There is no per-session stickiness: a multi-call MCP session working in one territory must re-specify workspaceId on every envelope, or its traces, sessions and states silently pile into the default workspace.

The infrastructure to avoid this already exists (SessionContextManager), but the envelope path doesn't use it. Raising as a design issue with options rather than a cold PR, since the cleanest fix touches a boundary you may have an opinion on.

Evidence

Profiling the workspace event store (.nexus-data/data/workspaces/) on a long-lived vault:

  • ws_default = 119 MB / 26 shards; 91% of it is trace_added (9,933 events).
  • ws_default trace_added per month: 147 → 2,381 → 3,351 → 4,054 (Feb→May) — steady, growing.
  • Territory workspaces (Academico, Advocacia, Desenvolvedor, …) stay small.

Almost all of that is MCP-session activity that belongs to a territory but was filed under default. It then feeds the cold-start fullRebuild cost — see #158 (the bloat is what turns that design deficiency into a hard failure).

Why it happens

  1. Schema nudges. The "Known-good example" in getTools.ts and useTools.ts hardcodes "workspaceId":"default"; the param description is 'Workspace ID. Optional. Defaults to "default".'; toolManager/types.ts says 'Use "default" for the global workspace. Do not invent workspace IDs.'. An LLM client mirrors the example.
  2. The fallback. ToolCliNormalizer.ts (v5.9.5, ~L556): workspaceId: params.workspaceId || 'default'.
  3. Existing infra is not consulted. SessionContextManager already maps sessionId → WorkspaceContext (getWorkspaceContext / setWorkspaceContext / updateFromResult). baseTool.getInheritedWorkspaceContext + a workspaceContext param already give workspace inheritance between internal tools. Neither is wired into the MCP envelope.
  4. Secondary — name vs id. workspaceId: "Desenvolvedor" (name) and the workspace UUID resolve to distinct event stores, so even a careful caller can fragment a workspace's history by alternating the two forms.

#159 fixed the handler that ignored the envelope workspaceId; this is the layer above — the envelope having no notion of "the workspace this session is already working in."

Options

Prerequisite for any of the below: stop hardcoding default in the schema examples/descriptions — otherwise any stickiness mechanism just sticks to default on the first example-mimicking call.

  • Option A — session-sticky resolution in ToolCliNormalizer. Resolution chain: explicit params.workspaceIdSessionContextManager.getWorkspaceContext(sessionId)'default'. An explicit workspaceId also writes the session context. First explicit call (typically getTools) establishes it; the rest inherit. Smallest change; one resolution point.
  • Option B — A, plus loadWorkspace binds the session. loadWorkspace calls setWorkspaceContext(sessionId, …), so loading a workspace also means "this session is now in that workspace." Two explicit bind points, inheritance for the rest.
  • Option C — surface the existing workspaceContext inheritance to the MCP envelope. Extend the internal workspaceContext mechanism (getInheritedWorkspaceContext / updateFromResult) to the getTools / useTools boundary, so the MCP path uses the same continuation Nexus already uses internally instead of a separate code path.

I lean toward C — it extends a mechanism that already exists rather than bolting session state onto the normalizer — but it touches the most surface and the envelope/result contract, so it's your call. Happy to prototype whichever option you choose, and to share the full trace profile / repro.

Environment

Nexus 5.9.5, Obsidian, macOS; used as MCP server via connector.js. Code references from the 5.9.5 tag.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions