# How the MCP Server Routes Requests to the Correct Workspace and Project Scope

> Understand how the MCP server routes requests to the correct workspace and project scope using a strict precedence chain of ScopeResolver helpers. Learn more.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: architecture
- Published: 2026-08-25

---

**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`](https://github.com/akitaonrails/ai-memory/blob/main/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_id`** and **`default_project_id`**: The server-wide defaults supplied at startup via `--workspace` and `--project` flags.

The MCP server instantiates a fresh resolver for each request through the `scope_resolver()` method in [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs) (lines 1230–1234):

```rust
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`](https://github.com/akitaonrails/ai-memory/blob/main/server.rs) lines 1430–1445). This method delegates to `resolve_read_args`, which implements a four-step precedence chain:

1. **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.
2. **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.
3. **Active-project from hooks**: If no project is specified, it uses the actor's current working directory project published by hooks.
4. **Server-default project**: If no hooks are active, it falls back to the project supplied at server startup.

The explicit resolution code appears as:

```rust
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`](https://github.com/akitaonrails/ai-memory/blob/main/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.

```rust
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`](https://github.com/akitaonrails/ai-memory/blob/main/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:

```rust
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`](https://github.com/akitaonrails/ai-memory/blob/main/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)`**: Like `resolve_read_args` but 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.rs`](https://github.com/akitaonrails/ai-memory/blob/main/ai-memory-store/src/scope.rs) centralizes 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_existing`** to 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.