How the MCP Server Routes Requests to the Correct Workspace and Project Scope
TLDR: The MCP server utilizes a ScopeResolver helper that evaluates explicit request arguments, active-project hooks, and server defaults in a strict precedence chain to determine the target workspace and project for every operation.
The akitaonrails/ai-memory repository implements a Memory-Control-Protocol (MCP) server that isolates data into distinct workspaces and projects. Every incoming request—whether reading a page or writing new content—must be routed to the correct scope without ambiguity. According to the source code, this routing logic is centralized in a dedicated resolution component that evaluates multiple sources of truth to produce a definitive (WorkspaceId, ProjectId) tuple.
The ScopeResolver Architecture
At the heart of the routing logic sits the ScopeResolver struct, defined in crates/ai-memory-store/src/scope.rs. This component encapsulates all lookup logic and maintains references to the data sources needed for scope resolution.
The resolver holds:
reader: A shared read pool for looking up existing scopes.writer: An optional writer used during write operations to create missing workspaces or projects.active_project: A map tracking the most recent hook-published active project for each actor.default_workspace_idanddefault_project_id: The server-wide defaults supplied at startup via--workspaceand--projectflags.
The MCP server instantiates a fresh resolver for each request through the scope_resolver() method in crates/ai-memory-mcp/src/server.rs (lines 1230–1234):
fn scope_resolver(&self) -> ScopeResolver<'_> {
ScopeResolver::new(&self.reader, self.workspace_id, self.project_id)
.with_writer(&self.writer)
.with_active_project(&self.active_project)
}
Read-Side Resolution: Precedence and Fallbacks
For read-only operations, the server determines the effective scope by calling effective_ids_for_read_args_with_actor (server.rs lines 1430–1445). This method delegates to resolve_read_args, which implements a four-step precedence chain:
- Explicit project in request with matching active-project: If the request specifies a project name and the actor has an active-project hook published for that same project, it uses the associated workspace.
- Explicit project with server default workspace: If the project is explicit but no active-project matches, it falls back to the server's baked workspace.
- Active-project from hooks: If no project is specified, it uses the actor's current working directory project published by hooks.
- Server-default project: If no hooks are active, it falls back to the project supplied at server startup.
The explicit resolution code appears as:
async fn effective_ids_for_read_args_with_actor(
&self,
explicit_workspace: Option<&str>,
explicit_project: Option<&str>,
actor: &ai_memory_core::ActorKey,
) -> Result<(WorkspaceId, ProjectId), McpError> {
self.scope_resolver()
.resolve_read_args(explicit_workspace, explicit_project, actor)
.await
.map(ai_memory_store::ResolvedScope::as_tuple)
.map_err(Self::scope_error)
}
Write-Side Resolution: Creating Missing Scopes
Write operations follow a similar logic but with the ability to create non-existent scopes. The write_target_ids_with_actor method (server.rs lines 1488–1492) calls resolve_write_args, which distinguishes itself by automatically creating workspaces or projects when an explicit name does not exist.
A critical difference in write resolution is the fallback behavior: when only a project name is provided without a workspace, the resolver defaults to the actor's active-project workspace rather than the server-wide default workspace. This ensures that new content lands in the context the user is currently working in.
async fn write_target_ids_with_actor(
&self,
explicit_workspace: Option<&str>,
explicit_project: Option<&str>,
actor: &ai_memory_core::ActorKey,
) -> Result<(WorkspaceId, ProjectId), McpError> {
self.scope_resolver()
.resolve_write_args(explicit_workspace, explicit_project, actor)
.await
.map(ai_memory_store::ResolvedScope::as_tuple)
.map_err(Self::scope_error)
}
Multi-Scope Query Resolution
When a request targets multiple scopes simultaneously—such as searching across several projects—the server uses resolve_query_scopes (server.rs lines 1495–1504). This method converts scope arguments into validated workspace/project tuples without creating new scopes.
The implementation de-duplicates the input and validates each name against existing data:
async fn resolve_query_scopes(
&self,
scopes: &[MemoryScopeArg],
) -> Result<Vec<(WorkspaceId, ProjectId)>, McpError> {
let names: Vec<_> = scopes.iter()
.map(|scope| ScopeName::new(&scope.workspace, &scope.project))
.collect();
self.scope_resolver()
.resolve_many_existing(&names, MAX_QUERY_SCOPES)
.await
.map(|scopes| scopes.into_iter()
.map(ai_memory_store::ResolvedScope::as_tuple)
.collect())
.map_err(Self::scope_error)
}
The resolve_many_existing method enforces a maximum limit (MAX_QUERY_SCOPES) to prevent resource exhaustion during broad queries.
Key Resolution Methods in scope.rs
The ScopeResolver implementation in crates/ai-memory-store/src/scope.rs provides the underlying methods invoked by the server:
new(reader, default_ws, default_proj)(lines 1716–1720): Constructs a read-only resolver with server defaults.with_writer(writer)(lines 1630–1634): Attaches a writer for create-on-write paths.with_active_project(active_project)(lines 1636–1640): Attaches the active-project map for implicit defaults.resolve_current_or_project(explicit_project, actor): Implements the current-project fallback logic for reads.resolve_read_args(explicit_ws, explicit_proj, actor): Resolves explicit arguments with full precedence fallback.resolve_write_args(explicit_ws, explicit_proj, actor): Likeresolve_read_argsbut creates missing scopes.resolve_many_existing(names, max): Validates multiple scope names without mutation.
Each method returns a ResolvedScope struct containing the final workspace_id and project_id, which the MCP server converts into the tuple format used by downstream store operations.
Summary
- The ScopeResolver in
ai-memory-store/src/scope.rscentralizes all workspace and project routing logic for the MCP server. - Read operations follow a strict four-step precedence: explicit arguments, active-project hooks, then server defaults.
- Write operations can auto-create missing scopes and prefer the actor's active-project workspace over server defaults when resolving partial arguments.
- Multi-scope queries use
resolve_many_existingto validate and de-duplicate target scopes without side effects. - Resolution always produces a concrete
(WorkspaceId, ProjectId)tuple, ensuring every request operates within a well-defined namespace.
Frequently Asked Questions
What happens if I don't specify a workspace or project in my MCP request?
The resolver falls back through a defined chain: first checking if an active-project was published via hooks (representing your current working directory), then defaulting to the server-supplied defaults specified at startup with --workspace and --project flags. This ensures requests never operate in an undefined scope.
Can the MCP server create new workspaces or projects automatically?
Yes, but only during write operations. When using resolve_write_args, if you specify a workspace or project name that doesn't exist, the resolver uses the attached writer to create the missing scope before completing the operation. Read operations require the scope to already exist.
How does the active-project hook interact with explicit request parameters?
Explicit parameters always take highest precedence. If you specify a project name in the request, the resolver uses that name first, validating it against your active-project hook (if available) or the server defaults. The active-project only serves as the fallback when arguments are omitted entirely.
Is there a limit to how many scopes I can query in a single request?
Yes. The resolve_many_existing method enforces a maximum limit defined by MAX_QUERY_SCOPES to prevent excessive resource consumption. If your query exceeds this limit, the resolver returns an error before executing the database lookup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →