Add project-aware chat startup defaults
Make repo-root chat startup native by letting MCP servers carry a default project source for workspace creation. When a chat host starts from a Git checkout, workspace_create can now omit seed_path and inherit the server startup source; explicit --project-path and clean-clone --repo-url/--repo-ref paths are supported as fallbacks. Add project startup resolution and materialization, surface origin_kind/origin_ref in workspace_seed, update chat-host docs and the repro/fix smoke to use project-aware workspace creation, and switch dist-check to uv run pyro so verification stays stable after uv reinstalls. Validated with uv lock, focused startup/server/CLI pytest coverage, UV_CACHE_DIR=.uv-cache make check, UV_CACHE_DIR=.uv-cache make dist-check, and real guest-backed smokes for both explicit project_path and bare repo-root auto-detection.
This commit is contained in:
parent
9b9b83ebeb
commit
535efc6919
28 changed files with 968 additions and 67 deletions
|
|
@ -13,6 +13,12 @@ from pyro_mcp.contract import (
|
|||
PUBLIC_MCP_WORKSPACE_CORE_PROFILE_TOOLS,
|
||||
PUBLIC_MCP_WORKSPACE_FULL_PROFILE_TOOLS,
|
||||
)
|
||||
from pyro_mcp.project_startup import (
|
||||
ProjectStartupSource,
|
||||
describe_project_startup_source,
|
||||
materialize_project_startup_source,
|
||||
resolve_project_startup_source,
|
||||
)
|
||||
from pyro_mcp.vm_manager import (
|
||||
DEFAULT_ALLOW_HOST_COMPAT,
|
||||
DEFAULT_MEM_MIB,
|
||||
|
|
@ -39,6 +45,18 @@ def _validate_mcp_profile(profile: str) -> McpToolProfile:
|
|||
return cast(McpToolProfile, profile)
|
||||
|
||||
|
||||
def _workspace_create_description(startup_source: ProjectStartupSource | None) -> str:
|
||||
if startup_source is None:
|
||||
return "Create and start a persistent workspace."
|
||||
described_source = describe_project_startup_source(startup_source)
|
||||
if described_source is None:
|
||||
return "Create and start a persistent workspace."
|
||||
return (
|
||||
"Create and start a persistent workspace. If `seed_path` is omitted, "
|
||||
f"the server seeds from {described_source}."
|
||||
)
|
||||
|
||||
|
||||
class Pyro:
|
||||
"""High-level facade over the ephemeral VM runtime."""
|
||||
|
||||
|
|
@ -462,14 +480,31 @@ class Pyro:
|
|||
allow_host_compat=allow_host_compat,
|
||||
)
|
||||
|
||||
def create_server(self, *, profile: McpToolProfile = "workspace-core") -> FastMCP:
|
||||
def create_server(
|
||||
self,
|
||||
*,
|
||||
profile: McpToolProfile = "workspace-core",
|
||||
project_path: str | Path | None = None,
|
||||
repo_url: str | None = None,
|
||||
repo_ref: str | None = None,
|
||||
no_project_source: bool = False,
|
||||
) -> FastMCP:
|
||||
"""Create an MCP server for one of the stable public tool profiles.
|
||||
|
||||
`workspace-core` is the default stable chat-host profile in 4.x. Use
|
||||
`profile="workspace-full"` only when the host truly needs the full
|
||||
advanced workspace surface.
|
||||
advanced workspace surface. By default, the server auto-detects the
|
||||
nearest Git worktree root from its current working directory and uses
|
||||
that source when `workspace_create` omits `seed_path`. `project_path`,
|
||||
`repo_url`, and `no_project_source` override that behavior explicitly.
|
||||
"""
|
||||
normalized_profile = _validate_mcp_profile(profile)
|
||||
startup_source = resolve_project_startup_source(
|
||||
project_path=project_path,
|
||||
repo_url=repo_url,
|
||||
repo_ref=repo_ref,
|
||||
no_project_source=no_project_source,
|
||||
)
|
||||
enabled_tools = set(_PROFILE_TOOLS[normalized_profile])
|
||||
server = FastMCP(name="pyro_mcp")
|
||||
|
||||
|
|
@ -583,9 +618,56 @@ class Pyro:
|
|||
return self.reap_expired()
|
||||
|
||||
if _enabled("workspace_create"):
|
||||
workspace_create_description = _workspace_create_description(startup_source)
|
||||
|
||||
def _create_workspace_from_server_defaults(
|
||||
*,
|
||||
environment: str,
|
||||
vcpu_count: int,
|
||||
mem_mib: int,
|
||||
ttl_seconds: int,
|
||||
network_policy: str,
|
||||
allow_host_compat: bool,
|
||||
seed_path: str | None,
|
||||
secrets: list[dict[str, str]] | None,
|
||||
name: str | None,
|
||||
labels: dict[str, str] | None,
|
||||
) -> dict[str, Any]:
|
||||
if seed_path is not None or startup_source is None:
|
||||
return self.create_workspace(
|
||||
environment=environment,
|
||||
vcpu_count=vcpu_count,
|
||||
mem_mib=mem_mib,
|
||||
ttl_seconds=ttl_seconds,
|
||||
network_policy=network_policy,
|
||||
allow_host_compat=allow_host_compat,
|
||||
seed_path=seed_path,
|
||||
secrets=secrets,
|
||||
name=name,
|
||||
labels=labels,
|
||||
)
|
||||
with materialize_project_startup_source(startup_source) as resolved_seed_path:
|
||||
prepared_seed = self._manager._prepare_workspace_seed( # noqa: SLF001
|
||||
resolved_seed_path,
|
||||
origin_kind=startup_source.kind,
|
||||
origin_ref=startup_source.origin_ref,
|
||||
)
|
||||
return self._manager.create_workspace(
|
||||
environment=environment,
|
||||
vcpu_count=vcpu_count,
|
||||
mem_mib=mem_mib,
|
||||
ttl_seconds=ttl_seconds,
|
||||
network_policy=network_policy,
|
||||
allow_host_compat=allow_host_compat,
|
||||
secrets=secrets,
|
||||
name=name,
|
||||
labels=labels,
|
||||
_prepared_seed=prepared_seed,
|
||||
)
|
||||
|
||||
if normalized_profile == "workspace-core":
|
||||
|
||||
@server.tool(name="workspace_create")
|
||||
@server.tool(name="workspace_create", description=workspace_create_description)
|
||||
async def workspace_create_core(
|
||||
environment: str,
|
||||
vcpu_count: int = DEFAULT_VCPU_COUNT,
|
||||
|
|
@ -596,8 +678,7 @@ class Pyro:
|
|||
name: str | None = None,
|
||||
labels: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create and start a persistent workspace."""
|
||||
return self.create_workspace(
|
||||
return _create_workspace_from_server_defaults(
|
||||
environment=environment,
|
||||
vcpu_count=vcpu_count,
|
||||
mem_mib=mem_mib,
|
||||
|
|
@ -612,7 +693,7 @@ class Pyro:
|
|||
|
||||
else:
|
||||
|
||||
@server.tool(name="workspace_create")
|
||||
@server.tool(name="workspace_create", description=workspace_create_description)
|
||||
async def workspace_create_full(
|
||||
environment: str,
|
||||
vcpu_count: int = DEFAULT_VCPU_COUNT,
|
||||
|
|
@ -625,8 +706,7 @@ class Pyro:
|
|||
name: str | None = None,
|
||||
labels: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create and start a persistent workspace."""
|
||||
return self.create_workspace(
|
||||
return _create_workspace_from_server_defaults(
|
||||
environment=environment,
|
||||
vcpu_count=vcpu_count,
|
||||
mem_mib=mem_mib,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue