pub struct Workspace { /* private fields */ }Expand description
Workspace runtime state. Shared across MCP request clones via Arc.
Implementations§
Source§impl Workspace
impl Workspace
Sourcepub fn open(
workspace_dir: PathBuf,
stale_after_days: u32,
post_activate: Option<PostActivateHook>,
) -> Result<Self>
pub fn open( workspace_dir: PathBuf, stale_after_days: u32, post_activate: Option<PostActivateHook>, ) -> Result<Self>
Open a github-flavoured workspace (clone + track flow).
Sourcepub fn open_local(
root: PathBuf,
post_activate: Option<PostActivateHook>,
) -> Result<Self>
pub fn open_local( root: PathBuf, post_activate: Option<PostActivateHook>, ) -> Result<Self>
Open a local-directory workspace.
Binds root as the active source root immediately and fires the
post-activate hook (subject to last-built-sha gating). inventory.json
is kept under <root>/.mcp-workspace/ so the local mode mirrors
the same gating / fingerprinting infra without polluting the
user’s tree with a repos/ directory.
Sourcepub fn open_local_unanchored(
post_activate: Option<PostActivateHook>,
) -> Result<Self>
pub fn open_local_unanchored( post_activate: Option<PostActivateHook>, ) -> Result<Self>
Open a local-directory workspace with no active root.
The unanchored sibling of open_local, for the
case where the root is expected to arrive later from an MCP client
(workspace.adopt_client_roots, see
adopt_client_root). Nothing is bound,
nothing is created on disk, and no hook fires: until something
activates, active_repo_path is None
and the source tools behave exactly as they do with no root — which
is the designed outcome when no root ever arrives.
The inventory directory is deliberately not created here: with no
root there is nowhere to put it. The first activation that
commits picks the directory (<root>/.mcp-workspace/) and it is
fixed from then on, mirroring the anchored mode’s rule that the
inventory survives later root swaps. An activation that fails its
build picks nothing and creates nothing — a client-proposed root
that never activated must not end up owning the inventory, nor
gain a directory it did not have.
root_ownership starts at
RootOwnership::Unowned — the only constructor that does.
Sourcepub fn with_adopt_client_roots(self) -> Self
pub fn with_adopt_client_roots(self) -> Self
Allow this workspace to adopt a client-advertised MCP root as a
fallback when the operator configured none (manifest key
workspace.adopt_client_roots).
Off by default. With it off no roots/list request is ever issued,
which is what keeps clients that do not advertise roots — and
deployments that never opt in — bit-for-bit unaffected.
Setting it on a workspace that already has a root is harmless and
intentional: ownership is already RootOwnership::Operator, so
adoption is refused. Call before the workspace is cloned into
crate::server::ServerOptions, like the other builders.
MCP roots is deprecated as of protocol revision 2026-07-28
(SEP-2577) and is eligible for removal in the first revision
released on or after 2027-07-28. New deployments should prefer the
spec’s own migration path — pass directories via tool parameters,
resource URIs, or server configuration (workspace.root).
Sourcepub fn adopts_client_roots(&self) -> bool
pub fn adopts_client_roots(&self) -> bool
Is client-root adoption enabled (workspace.adopt_client_roots)?
Sourcepub fn root_ownership(&self) -> RootOwnership
pub fn root_ownership(&self) -> RootOwnership
Who chose the active root — see RootOwnership.
Never blocks on an in-flight activation: the flag has a lock of its own, distinct from the swap ordering lock that a root swap holds across its (arbitrarily long) build.
Sourcepub fn with_sandbox_root(self, boundary: &Path) -> Result<Self>
pub fn with_sandbox_root(self, boundary: &Path) -> Result<Self>
Bound runtime root swaps to a containment boundary (local mode).
With a boundary attached, set_root_dir
refuses any target whose canonical path is not inside boundary
— .. traversals and symlinks out of the tree are therefore
rejected too, and the rejection happens before any state is
touched. Without it (the default) set_root_dir stays unbounded,
which is the historical behaviour.
Call immediately after open_local, before the workspace is cloned
into crate::server::ServerOptions — like
with_activation_summary it mutates
the still-unique inner Arc. Unlike the hook builders, a late call
is an error rather than a warning: a containment boundary that
silently failed to attach is worse than no boundary at all.
Errors when the boundary does not exist, when the workspace is not local-flavoured, or when the already-active root lies outside the boundary — a config that contradicts itself must die at boot, not at the first swap.
Sourcepub fn with_activation_summary(self, hook: ActivationSummaryHook) -> Self
pub fn with_activation_summary(self, hook: ActivationSummaryHook) -> Self
Attach an ActivationSummaryHook. Call immediately after
open/open_local (before the workspace is cloned into
ServerOptions): it mutates the still-unique inner Arc. Calling
it after the workspace has been cloned is a no-op with a warning —
the summary simply won’t be attached.
Sourcepub fn with_post_activate_revs(self, hook: PostActivateRevsHook) -> Self
pub fn with_post_activate_revs(self, hook: PostActivateRevsHook) -> Self
Attach a PostActivateRevsHook. Call immediately after
open/open_local (before the workspace is cloned into
ServerOptions): it mutates the still-unique inner Arc, exactly
like with_activation_summary.
Calling it after the workspace has been cloned is a no-op with a
warning. Additive — consumers that don’t set it keep the plain
single-rev activation behaviour.
Sourcepub fn with_activation_transaction(
self,
hook: ActivationTransactionHook,
) -> Self
pub fn with_activation_transaction( self, hook: ActivationTransactionHook, ) -> Self
Attach the request-scoped activation prepare/commit contract.
When set, this hook owns plain builds, revision-set builds, cheap-skip
summaries, and atomic product publication. It replaces the legacy
post_activate, post_activate_revs, and activation_summary
callbacks for activation calls. Configure it before cloning the
workspace into crate::server::ServerOptions.
pub fn kind(&self) -> WorkspaceKind
Sourcepub fn workspace_dir(&self) -> &Path
pub fn workspace_dir(&self) -> &Path
The directory this workspace keeps its bookkeeping under.
After an unanchored local boot (open_local_unanchored)
this is the empty path until the first activation anchors it; every
other constructor knows it up front.
pub fn repos_dir(&self) -> PathBuf
Sourcepub fn active_repo_name(&self) -> Option<String>
pub fn active_repo_name(&self) -> Option<String>
Active repo’s full org/repo name, or None if nothing is active.
Sourcepub fn active_repo_path(&self) -> Option<PathBuf>
pub fn active_repo_path(&self) -> Option<PathBuf>
Active repo’s filesystem path, or None.
Sourcepub fn default_github_repo(&self) -> Option<String>
pub fn default_github_repo(&self) -> Option<String>
Default org/repo for the GitHub tools when the caller passes none.
Github mode: the active repo — there the inventory key is the
org/repo. Local mode: the active root’s origin remote parsed
to org/repo, or None when there’s no GitHub remote. Crucially
it is never the local/<dir> inventory key (see
active_repo_name), which is a
filesystem-derived key, not a valid repo slug.
Sourcepub fn last_built_sha(&self, name: &str) -> Option<String>
pub fn last_built_sha(&self, name: &str) -> Option<String>
Read the SHA recorded after the last successful post-activate hook
for the named repo. None if the repo was never built (or the
hook last failed). Useful for downstream consumers gating
“is the active graph up to date with the repo HEAD?” checks.
Sourcepub fn last_built_revs(&self, name: &str) -> Option<RevsRequest>
pub fn last_built_revs(&self, name: &str) -> Option<RevsRequest>
Read the revisions request last successfully built for the named
repo — Some when the last build was multi-rev (revs=), None
for a plain / HEAD-only build or a never-built repo. Drives the
rev-set-aware skip gate and the update-preserves-rev-set path.
Sourcepub fn repo_management(
&self,
name: Option<&str>,
delete: bool,
update: bool,
force_rebuild: bool,
revs: Option<&RevsRequest>,
) -> String
pub fn repo_management( &self, name: Option<&str>, delete: bool, update: bool, force_rebuild: bool, revs: Option<&RevsRequest>, ) -> String
Public entry for the repo_management MCP tool.
name:org/repoto activate (None = list / refresh mode).delete: remove the named repo + inventory entry. Github only.update: refresh the active repo (auto-rebuild gated).force_rebuild: withupdate=true(or initial activation), re-run the post-activate hook even when the HEAD SHA matcheslast_built_sha. Useful after the builder itself has been upgraded.
Local mode behaviour: name and delete are rejected; pass
update=true (or no args after the initial activation) to
re-fingerprint the root and rebuild if anything changed.
Sourcepub fn set_root_dir(
&self,
new_root: &Path,
revs: Option<&RevsRequest>,
) -> String
pub fn set_root_dir( &self, new_root: &Path, revs: Option<&RevsRequest>, ) -> String
Swap the active root (local mode only). Re-fires the post-activate hook against the new root. Errors if the workspace is github-flavoured.
revs (optional): resolve revisions against the new root (which
must be a git repo) and fire the revs-aware hook — see
activate / RevsRequest.
Concurrency: two set_root_dir calls may overlap — the newer
activation supersedes the older one. A client-root adoption may not
overlap either of them: it is exclusive with every operator swap
for its whole check-swap-publish sequence, which is what makes
“the operator always wins” true rather than merely likely. Must
not be called from inside an activation hook (that has always been
true — the legacy hook path already serializes on its own lock).
Sourcepub fn adopt_client_root(&self, new_root: &Path) -> Result<String, String>
pub fn adopt_client_root(&self, new_root: &Path) -> Result<String, String>
Adopt a root proposed by the MCP client (roots/list).
Routes through the same validation and containment path as
set_root_dir — one code path, so a
workspace.sandbox_root boundary applies identically to an
operator swap and to a path proposed by an external party. The MCP
spec is explicit that roots are “informational guidance rather than
an access-control mechanism”, so the boundary, not the client, is
what bounds this.
Refuses (without touching any state) when ownership is already
RootOwnership::Operator — an operator-chosen root always wins.
On success ownership becomes RootOwnership::Adopted, leaving a
later roots/list_changed free to replace it.
Err is a human-readable reason for the log; adoption failure is
never fatal to the server.