Skip to content

Commit e977589

Browse files
committed
chore: update project files
1 parent d9fb025 commit e977589

3 files changed

Lines changed: 375 additions & 68 deletions

File tree

src/specsmith/cli.py

Lines changed: 164 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,16 @@ def init(config_path: str | None, output_dir: str, no_git: bool, guided: bool) -
350350

351351
write_phase(target, "inception")
352352

353+
# Auto-register with the MCP governance server (best-effort, never blocks)
354+
with contextlib.suppress(Exception):
355+
from specsmith.mcp_server import register_project
356+
357+
if register_project(str(target)):
358+
console.print(
359+
" [dim]\u2713 Registered with MCP server "
360+
"([bold]specsmith mcp projects[/bold] to view)[/dim]"
361+
)
362+
353363

354364
def _load_config_with_inheritance(config_path: str) -> dict[str, object]:
355365
"""Load scaffold.yml, merging parent config if `extends` is set."""
@@ -1746,6 +1756,16 @@ def import_project(
17461756
console.print(f"\n[bold green]Done.[/bold green] {len(created)} governance files generated.")
17471757
console.print("Governance files generated. Review project configuration.")
17481758

1759+
# Auto-register with the MCP governance server (best-effort, never blocks)
1760+
with contextlib.suppress(Exception):
1761+
from specsmith.mcp_server import register_project
1762+
1763+
if register_project(str(root)):
1764+
console.print(
1765+
" [dim]\u2713 Registered with MCP server "
1766+
"([bold]specsmith mcp projects[/bold] to view)[/dim]"
1767+
)
1768+
17491769

17501770
def _run_guided_architecture(cfg: ProjectConfig, target: Path) -> list[Path]:
17511771
"""Run interactive architecture definition and generate REQ/TEST stubs."""
@@ -9049,46 +9069,45 @@ def mcp_list_cmd(project_dir: str, as_json: bool) -> None:
90499069

90509070

90519071
@mcp_group.command(name="serve")
9052-
@click.option("--project-dir", type=click.Path(), default=".", show_default=True)
9072+
@click.option(
9073+
"--project-dir",
9074+
type=click.Path(),
9075+
default=None,
9076+
help=(
9077+
"Set this path as the *primary* project (first slot / default for tool calls "
9078+
"that omit project_dir). Omit to use the registry automatically."
9079+
),
9080+
)
90539081
@click.option(
90549082
"--project-dirs",
90559083
default="",
90569084
help=(
9057-
"Additional project directories to register (comma-separated absolute paths). "
9058-
"All registered projects are accessible to any tool call via their "
9059-
"absolute path as the project_dir argument."
9085+
"Extra project directories to add on top of the registry "
9086+
"(comma-separated absolute paths)."
90609087
),
90619088
)
9062-
def mcp_serve_cmd(project_dir: str, project_dirs: str) -> None:
9089+
def mcp_serve_cmd(project_dir: str | None, project_dirs: str) -> None:
90639090
"""Start the native governance MCP stdio server (REQ-363).
90649091
90659092
Implements MCP 2024-11-05 over stdin/stdout (JSON-RPC 2.0).
9066-
Exposes seven governance tools to any MCP client:
9093+
Exposes seven governance tools to any MCP client.
90679094
90689095
\b
9069-
governance_project_list List all registered projects
9070-
governance_audit Run governance audit
9071-
governance_checkpoint Emit GOVERNANCE ANCHOR JSON
9072-
governance_preflight Preflight a change intent
9073-
governance_phase Current AEE phase + readiness %
9074-
governance_req_list List all requirements
9075-
governance_trace_seal Seal a milestone/decision
9096+
Recommended Warp config (set once, never touch again)::
9097+
9098+
{"specsmith-governance": {"command": "specsmith", "args": ["mcp", "serve"]}}
90769099
90779100
\b
9078-
Single-project Warp config (Settings → Agents → MCP servers)::
9101+
Then register each project once from inside that project::
90799102
9080-
{"specsmith-governance": {"command": "specsmith",
9081-
"args": ["mcp", "serve", "--project-dir", "/path/to/project"]}}
9103+
specsmith mcp register
90829104
90839105
\b
9084-
Multi-project Warp config::
9085-
9086-
{"specsmith-governance": {"command": "specsmith",
9087-
"args": ["mcp", "serve", "--project-dir", "/path/to/proj1",
9088-
"--project-dirs", "/path/to/proj2,/path/to/proj3"]}}
9106+
The server reads the registry at startup and serves all registered
9107+
projects automatically — no config changes needed for new projects.
90899108
90909109
\b
9091-
Or pass inline to oz (use ``specsmith mcp install-warp`` for the full snippet)::
9110+
Or pass inline to oz (use ``specsmith mcp install-warp`` for the snippet)::
90929111
90939112
oz agent run --mcp "$(specsmith mcp install-warp --json)" --prompt "..."
90949113
"""
@@ -9099,21 +9118,17 @@ def mcp_serve_cmd(project_dir: str, project_dirs: str) -> None:
90999118

91009119

91019120
@mcp_group.command(name="install-warp")
9102-
@click.option("--project-dir", type=click.Path(), default=".", show_default=True)
9103-
@click.option(
9104-
"--project-dirs",
9105-
default="",
9106-
help="Additional project dirs (comma-separated) to include in the multi-project config.",
9107-
)
91089121
@click.option("--json", "as_json", is_flag=True, default=False, help="Emit JSON config only.")
9109-
def mcp_install_warp_cmd(project_dir: str, project_dirs: str, as_json: bool) -> None:
9122+
def mcp_install_warp_cmd(as_json: bool) -> None:
91109123
"""Print the Warp MCP config snippet for the governance server (REQ-363).
91119124
9125+
Generates a minimal, registry-aware config — paste it into Warp once
9126+
and never change it again. Register new projects with::
9127+
9128+
specsmith mcp register # in the project directory
9129+
91129130
Copy the output into Warp Settings → Agents → MCP servers, or pass it
91139131
to ``oz agent run --mcp '<json>'`` for a one-off cloud agent run.
9114-
9115-
For multi-project setups pass ``--project-dirs`` with comma-separated
9116-
absolute paths; one server instance will serve all projects.
91179132
"""
91189133
import json as _json
91199134
import shutil
@@ -9124,17 +9139,10 @@ def mcp_install_warp_cmd(project_dir: str, project_dirs: str, as_json: bool) ->
91249139
specsmith_exe = shutil.which("specsmith") or str(
91259140
Path(sys.executable).parent / ("specsmith.exe" if sys.platform == "win32" else "specsmith")
91269141
)
9127-
default_dir = str(Path(project_dir).resolve())
9128-
args: list[str] = ["mcp", "serve", "--project-dir", default_dir]
9129-
9130-
extra_dirs = [p.strip() for p in project_dirs.split(",") if p.strip()] if project_dirs else []
9131-
if extra_dirs:
9132-
args += ["--project-dirs", ",".join(extra_dirs)]
9133-
91349142
config = {
91359143
"specsmith-governance": {
91369144
"command": specsmith_exe,
9137-
"args": args,
9145+
"args": ["mcp", "serve"],
91389146
}
91399147
}
91409148

@@ -9148,22 +9156,125 @@ def mcp_install_warp_cmd(project_dir: str, project_dirs: str, as_json: bool) ->
91489156
"or pass inline to [bold]oz agent run --mcp '<json>'[/bold]:\n"
91499157
)
91509158
console.print(_json.dumps(config, indent=2))
9151-
if extra_dirs:
9152-
console.print(
9153-
f"\n[dim]Serving {1 + len(extra_dirs)} projects. "
9154-
"Call [bold]governance_project_list[/bold] to see all registered paths,\n"
9155-
"then pass any path as [bold]project_dir[/bold] to target a specific project.[/dim]"
9156-
)
9159+
console.print(
9160+
"\n[dim][bold]One-time setup[/bold] — paste this config into Warp once," # noqa: E501
9161+
" then never touch it again.\n"
9162+
"\nTo add each project, run this inside the project directory:\n"
9163+
" [bold]specsmith mcp register[/bold]\n"
9164+
"\nThe server reads [bold]~/.specsmith/mcp-projects.json[/bold] at startup\n"
9165+
"and serves all registered projects automatically.\n"
9166+
"\nView registered projects: [bold]specsmith mcp projects[/bold]\n"
9167+
"\nVerify server: specsmith mcp serve (then send an initialize message).[/dim]"
9168+
)
9169+
9170+
9171+
@mcp_group.command(name="register")
9172+
@click.argument("path", default=".", required=False)
9173+
def mcp_register_cmd(path: str) -> None:
9174+
"""Register a project directory with the MCP server registry.
9175+
9176+
Run once inside a project directory to add it to
9177+
``~/.specsmith/mcp-projects.json``. The next ``specsmith mcp serve``
9178+
invocation will automatically include it — no Warp config changes needed.
9179+
9180+
\b
9181+
Examples::
9182+
9183+
specsmith mcp register # register current directory
9184+
specsmith mcp register /path/to/myproject
9185+
"""
9186+
from specsmith.mcp_server import register_project
9187+
9188+
root = Path(path).resolve()
9189+
if not root.exists():
9190+
console.print(f"[red]\u2717[/red] Path does not exist: {root}")
9191+
raise SystemExit(1)
9192+
9193+
added = register_project(str(root))
9194+
if added:
9195+
console.print(f"[green]\u2713[/green] Registered: [bold]{root}[/bold]")
9196+
if not (root / ".specsmith").exists():
9197+
console.print(
9198+
" [yellow]\u26a0[/yellow] No .specsmith/ found. "
9199+
"Run [bold]specsmith init[/bold] or [bold]specsmith import[/bold] first."
9200+
)
91579201
else:
9202+
console.print(f"[dim]Already registered: {root}[/dim]")
9203+
console.print(
9204+
" [dim]specsmith mcp projects ← view all registered[/dim]\n"
9205+
" [dim]specsmith mcp serve ← start the server[/dim]"
9206+
)
9207+
9208+
9209+
@mcp_group.command(name="unregister")
9210+
@click.argument("path", default=".", required=False)
9211+
def mcp_unregister_cmd(path: str) -> None:
9212+
"""Remove a project directory from the MCP server registry.
9213+
9214+
\b
9215+
Examples::
9216+
9217+
specsmith mcp unregister # unregister current directory
9218+
specsmith mcp unregister /path/to/myproject
9219+
"""
9220+
from specsmith.mcp_server import unregister_project
9221+
9222+
root = Path(path).resolve()
9223+
removed = unregister_project(str(root))
9224+
if removed:
9225+
console.print(f"[green]\u2713[/green] Unregistered: [bold]{root}[/bold]")
9226+
else:
9227+
console.print(f"[yellow]Not registered: {root}[/yellow]")
9228+
9229+
9230+
@mcp_group.command(name="projects")
9231+
@click.option("--json", "as_json", is_flag=True, default=False, help="Emit as JSON.")
9232+
def mcp_projects_cmd(as_json: bool) -> None:
9233+
"""List all projects registered with the MCP server.
9234+
9235+
Shows each registered project path and whether it still exists on disk.
9236+
The first entry is the default (used when a tool call omits project_dir).
9237+
"""
9238+
import json as _json
9239+
import os
9240+
9241+
from specsmith.mcp_server import _registry_file, read_registry
9242+
9243+
projects = read_registry()
9244+
reg_path = _registry_file()
9245+
9246+
if as_json:
9247+
entries = [
9248+
{"path": p, "exists": Path(p).exists(), "is_default": i == 0}
9249+
for i, p in enumerate(projects)
9250+
]
9251+
click.echo(_json.dumps({"registry": str(reg_path), "projects": entries}, indent=2))
9252+
return
9253+
9254+
if not projects:
9255+
console.print("[yellow]No projects registered.[/yellow]")
91589256
console.print(
9159-
"\n[dim]After adding, Warp/Oz can call governance_project_list, "
9160-
"governance_audit, governance_preflight,\n"
9161-
"governance_checkpoint, governance_phase, governance_req_list, and\n"
9162-
"governance_trace_seal as structured MCP tool calls.\n"
9163-
"\nFor multiple projects add --project-dirs /path/a,/path/b to serve them "
9164-
"all from one server instance.\n"
9165-
"\nVerify with: specsmith mcp serve (then send an initialize message).[/dim]"
9257+
"[dim]Run [bold]specsmith mcp register[/bold] inside a project to add it.[/dim]"
91669258
)
9259+
return
9260+
9261+
console.print(
9262+
f"[bold]Registered MCP projects[/bold] ({len(projects)}) "
9263+
f"[dim]{reg_path}[/dim]\n"
9264+
)
9265+
for i, p in enumerate(projects):
9266+
exists = Path(p).exists()
9267+
default_tag = " [bold cyan][default][/bold cyan]" if i == 0 else ""
9268+
health = "[green]\u2713 exists[/green]" if exists else "[red]\u2717 not found[/red]"
9269+
# Abbreviate long paths using ~ for home
9270+
display = p.replace(os.path.expanduser("~"), "~")
9271+
console.print(f" {health} {display}{default_tag}")
9272+
9273+
console.print(
9274+
"\n[dim] specsmith mcp register [path] ← add a project"
9275+
"\n specsmith mcp unregister [path] ← remove a project"
9276+
"\n specsmith mcp serve ← start the server[/dim]"
9277+
)
91679278

91689279

91699280
main.add_command(mcp_group)

0 commit comments

Comments
 (0)