generated from coulomb/repo-seed
Rename MCP server identifier from state-hub to dev-hub
Introduce canonical MCP_SERVER_NAME constants, shared registration helpers, and a migrate_mcp_config.py script for ~/.claude.json upgrades. Registration, patch, and custodian CLI checks accept both dev-hub and legacy state-hub during transition. API root metadata and session-protocol template reflect the new name.
This commit is contained in:
@@ -136,4 +136,4 @@ app.include_router(policy.router)
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def root():
|
||||
return {"service": "state-hub", "docs": "/docs"}
|
||||
return {"service": "dev-hub", "docs": "/docs"}
|
||||
|
||||
@@ -126,11 +126,9 @@ def _detect_domain(project_path: Path) -> str | None:
|
||||
|
||||
|
||||
def _check_mcp() -> bool:
|
||||
claude_json = Path.home() / ".claude.json"
|
||||
if not claude_json.exists():
|
||||
return False
|
||||
config = json.loads(claude_json.read_text())
|
||||
return "state-hub" in config.get("mcpServers", {})
|
||||
from scripts.mcp_registration import load_claude_json, mcp_server_registered
|
||||
|
||||
return mcp_server_registered(load_claude_json())
|
||||
|
||||
|
||||
# ── Subcommands ────────────────────────────────────────────────────────────────
|
||||
@@ -193,7 +191,8 @@ def cmd_register(args: argparse.Namespace) -> None:
|
||||
if _check_mcp():
|
||||
print(" MCP OK")
|
||||
else:
|
||||
print("WARNING: 'state-hub' not in ~/.claude.json.")
|
||||
print("WARNING: 'dev-hub' (or legacy 'state-hub') not in ~/.claude.json.")
|
||||
print(" Run: python scripts/migrate_mcp_config.py # if upgrading legacy config")
|
||||
print(" See ~/.claude/CLAUDE.md → MCP Server Registration section.")
|
||||
|
||||
# ── Step 5: Write CLAUDE.custodian.md ─────────────────────────────────────
|
||||
|
||||
4
mcp_server/constants.py
Normal file
4
mcp_server/constants.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Canonical MCP server identifiers for State Hub / dev-hub."""
|
||||
|
||||
MCP_SERVER_NAME = "dev-hub"
|
||||
LEGACY_MCP_SERVER_NAME = "state-hub"
|
||||
@@ -19,10 +19,12 @@ import httpx
|
||||
from fastmcp import FastMCP
|
||||
from hub_core.mcp import HubCoreMCPServer
|
||||
|
||||
from mcp_server.constants import MCP_SERVER_NAME
|
||||
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||
|
||||
mcp = FastMCP(
|
||||
name="state-hub",
|
||||
name=MCP_SERVER_NAME,
|
||||
instructions=(
|
||||
"Custodian State Hub: tracks topics, workstreams, tasks, decisions, and progress events. "
|
||||
"Start every session with get_state_summary() for orientation. "
|
||||
@@ -45,7 +47,7 @@ _HUB_CORE_MCP_EXCLUDE = frozenset({
|
||||
"ingest_tpsc_tool",
|
||||
})
|
||||
HubCoreMCPServer(
|
||||
name="state-hub",
|
||||
name=MCP_SERVER_NAME,
|
||||
api_base=API_BASE,
|
||||
register_tools=False,
|
||||
).attach_to(mcp, exclude=_HUB_CORE_MCP_EXCLUDE)
|
||||
|
||||
30
scripts/mcp_registration.py
Normal file
30
scripts/mcp_registration.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Helpers for ~/.claude.json MCP server registration checks."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from mcp_server.constants import LEGACY_MCP_SERVER_NAME, MCP_SERVER_NAME
|
||||
|
||||
|
||||
def load_claude_json(path: Path | None = None) -> dict | None:
|
||||
claude_json = path or Path.home() / ".claude.json"
|
||||
if not claude_json.exists():
|
||||
return None
|
||||
return json.loads(claude_json.read_text())
|
||||
|
||||
|
||||
def mcp_server_registered(config: dict | None) -> bool:
|
||||
if not config:
|
||||
return False
|
||||
servers = config.get("mcpServers", {})
|
||||
return MCP_SERVER_NAME in servers or LEGACY_MCP_SERVER_NAME in servers
|
||||
|
||||
|
||||
def resolve_mcp_server_name(config: dict) -> str | None:
|
||||
servers = config.get("mcpServers", {})
|
||||
if MCP_SERVER_NAME in servers:
|
||||
return MCP_SERVER_NAME
|
||||
if LEGACY_MCP_SERVER_NAME in servers:
|
||||
return LEGACY_MCP_SERVER_NAME
|
||||
return None
|
||||
51
scripts/migrate_mcp_config.py
Normal file
51
scripts/migrate_mcp_config.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rename legacy state-hub MCP registration to dev-hub in ~/.claude.json."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from mcp_server.constants import LEGACY_MCP_SERVER_NAME, MCP_SERVER_NAME
|
||||
|
||||
CLAUDE_JSON = Path.home() / ".claude.json"
|
||||
|
||||
|
||||
def migrate_config(config: dict) -> tuple[dict, bool]:
|
||||
servers = config.setdefault("mcpServers", {})
|
||||
if MCP_SERVER_NAME in servers:
|
||||
return config, False
|
||||
if LEGACY_MCP_SERVER_NAME not in servers:
|
||||
return config, False
|
||||
servers[MCP_SERVER_NAME] = servers.pop(LEGACY_MCP_SERVER_NAME)
|
||||
return config, True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not CLAUDE_JSON.exists():
|
||||
print(f"ERROR: {CLAUDE_JSON} not found.")
|
||||
raise SystemExit(1)
|
||||
|
||||
config = json.loads(CLAUDE_JSON.read_text())
|
||||
migrated_config, changed = migrate_config(config)
|
||||
if not changed:
|
||||
if MCP_SERVER_NAME in config.get("mcpServers", {}):
|
||||
print(f"OK: '{MCP_SERVER_NAME}' already registered.")
|
||||
else:
|
||||
print(
|
||||
f"Nothing to migrate: neither '{LEGACY_MCP_SERVER_NAME}' nor "
|
||||
f"'{MCP_SERVER_NAME}' found in mcpServers."
|
||||
)
|
||||
return
|
||||
|
||||
stamp = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
backup = CLAUDE_JSON.with_name(f".claude.json.bak-{stamp}")
|
||||
shutil.copy2(CLAUDE_JSON, backup)
|
||||
CLAUDE_JSON.write_text(json.dumps(migrated_config, indent=2) + "\n")
|
||||
print(f"Backed up: {backup}")
|
||||
print(f"Migrated: mcpServers['{LEGACY_MCP_SERVER_NAME}'] → mcpServers['{MCP_SERVER_NAME}']")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Patch ~/.claude.json to add the cwd field to the state-hub MCP entry.
|
||||
Patch ~/.claude.json to add the cwd field to the dev-hub MCP entry.
|
||||
|
||||
claude mcp add-json silently drops the cwd field. Run this script after
|
||||
any claude mcp add-json call to restore it.
|
||||
@@ -11,22 +11,35 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from mcp_server.constants import LEGACY_MCP_SERVER_NAME, MCP_SERVER_NAME
|
||||
from scripts.mcp_registration import load_claude_json, resolve_mcp_server_name
|
||||
|
||||
CLAUDE_JSON = Path.home() / ".claude.json"
|
||||
STATE_HUB_DIR = Path(__file__).resolve().parent.parent # state-hub/
|
||||
STATE_HUB_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not CLAUDE_JSON.exists():
|
||||
config = load_claude_json(CLAUDE_JSON)
|
||||
if config is None:
|
||||
print(f"ERROR: {CLAUDE_JSON} not found. Run 'claude mcp add-json' first.")
|
||||
raise SystemExit(1)
|
||||
|
||||
config = json.loads(CLAUDE_JSON.read_text())
|
||||
servers = config.setdefault("mcpServers", {})
|
||||
|
||||
if "state-hub" not in servers:
|
||||
print("ERROR: 'state-hub' not found in ~/.claude.json. Run 'claude mcp add-json' first.")
|
||||
server_name = resolve_mcp_server_name(config)
|
||||
if server_name is None:
|
||||
print(
|
||||
f"ERROR: neither '{MCP_SERVER_NAME}' nor '{LEGACY_MCP_SERVER_NAME}' "
|
||||
f"found in ~/.claude.json. Run 'claude mcp add-json' first."
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
entry = servers["state-hub"]
|
||||
if server_name == LEGACY_MCP_SERVER_NAME:
|
||||
print(
|
||||
f"NOTE: patching legacy '{LEGACY_MCP_SERVER_NAME}' entry. "
|
||||
"Run scripts/migrate_mcp_config.py to rename to dev-hub."
|
||||
)
|
||||
|
||||
entry = servers[server_name]
|
||||
cwd_str = str(STATE_HUB_DIR)
|
||||
|
||||
if entry.get("cwd") == cwd_str:
|
||||
@@ -35,8 +48,8 @@ def main() -> None:
|
||||
|
||||
entry["cwd"] = cwd_str
|
||||
CLAUDE_JSON.write_text(json.dumps(config, indent=2) + "\n")
|
||||
print(f"Patched: ~/.claude.json state-hub.cwd = {cwd_str}")
|
||||
print(f"Patched: ~/.claude.json {server_name}.cwd = {cwd_str}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
@@ -1,6 +1,7 @@
|
||||
## Session Protocol
|
||||
|
||||
State Hub: http://127.0.0.1:8000
|
||||
Dev Hub (State Hub API): http://127.0.0.1:8000
|
||||
MCP server name in `~/.claude.json`: `dev-hub`
|
||||
|
||||
**Step 1 — Orient**
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
||||
|
||||
STATE_HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CLAUDE_JSON="${CLAUDE_JSON:-$HOME/.claude.json}"
|
||||
SERVER_NAME="${STATE_HUB_MCP_NAME:-state-hub}"
|
||||
SERVER_NAME="${STATE_HUB_MCP_NAME:-dev-hub}"
|
||||
API_BASE="${API_BASE:-}"
|
||||
MCP_URL="${MCP_URL:-}"
|
||||
DRY_RUN=0
|
||||
|
||||
@@ -112,11 +112,12 @@ if not f.exists():
|
||||
print('MISSING_FILE')
|
||||
else:
|
||||
d = json.loads(f.read_text())
|
||||
print('OK' if 'state-hub' in d.get('mcpServers', {}) else 'NOT_REGISTERED')
|
||||
servers = d.get('mcpServers', {})
|
||||
print('OK' if 'dev-hub' in servers or 'state-hub' in servers else 'NOT_REGISTERED')
|
||||
")"
|
||||
case "$MCP_OK" in
|
||||
MISSING_FILE) echo "WARNING: ~/.claude.json not found. MCP server not registered." ;;
|
||||
NOT_REGISTERED) echo "WARNING: 'state-hub' not in ~/.claude.json. See global CLAUDE.md §MCP Server Registration." ;;
|
||||
NOT_REGISTERED) echo "WARNING: 'dev-hub' not in ~/.claude.json. See global CLAUDE.md §MCP Server Registration." ;;
|
||||
*) echo " MCP OK" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
74
tests/test_mcp_registration.py
Normal file
74
tests/test_mcp_registration.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Tests for dev-hub MCP registration helpers and config migration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import mcp_server.server as server
|
||||
from mcp_server.constants import LEGACY_MCP_SERVER_NAME, MCP_SERVER_NAME
|
||||
from scripts.mcp_registration import (
|
||||
mcp_server_registered,
|
||||
resolve_mcp_server_name,
|
||||
)
|
||||
from scripts.migrate_mcp_config import migrate_config
|
||||
|
||||
|
||||
def test_mcp_server_name_is_dev_hub() -> None:
|
||||
assert server.mcp.name == MCP_SERVER_NAME
|
||||
|
||||
|
||||
def test_mcp_server_registered_accepts_dev_hub() -> None:
|
||||
config = {"mcpServers": {MCP_SERVER_NAME: {"type": "sse"}}}
|
||||
assert mcp_server_registered(config) is True
|
||||
|
||||
|
||||
def test_mcp_server_registered_accepts_legacy_state_hub() -> None:
|
||||
config = {"mcpServers": {LEGACY_MCP_SERVER_NAME: {"type": "sse"}}}
|
||||
assert mcp_server_registered(config) is True
|
||||
|
||||
|
||||
def test_resolve_mcp_server_name_prefers_dev_hub() -> None:
|
||||
config = {
|
||||
"mcpServers": {
|
||||
MCP_SERVER_NAME: {"type": "sse"},
|
||||
LEGACY_MCP_SERVER_NAME: {"type": "sse"},
|
||||
}
|
||||
}
|
||||
assert resolve_mcp_server_name(config) == MCP_SERVER_NAME
|
||||
|
||||
|
||||
def test_migrate_config_renames_legacy_entry() -> None:
|
||||
legacy_entry = {"type": "sse", "url": "http://127.0.0.1:8001/sse"}
|
||||
config = {"mcpServers": {LEGACY_MCP_SERVER_NAME: legacy_entry}}
|
||||
migrated, changed = migrate_config(config)
|
||||
assert changed is True
|
||||
assert LEGACY_MCP_SERVER_NAME not in migrated["mcpServers"]
|
||||
assert migrated["mcpServers"][MCP_SERVER_NAME] == legacy_entry
|
||||
|
||||
|
||||
def test_migrate_config_noop_when_dev_hub_present() -> None:
|
||||
config = {"mcpServers": {MCP_SERVER_NAME: {"type": "sse"}}}
|
||||
migrated, changed = migrate_config(config)
|
||||
assert changed is False
|
||||
assert migrated is config
|
||||
|
||||
|
||||
def test_migrate_config_noop_when_neither_present() -> None:
|
||||
config = {"mcpServers": {"ops-bridge": {"type": "sse"}}}
|
||||
migrated, changed = migrate_config(config)
|
||||
assert changed is False
|
||||
assert migrated is config
|
||||
|
||||
|
||||
def test_migrate_config_preserves_other_servers(tmp_path) -> None:
|
||||
claude_json = tmp_path / ".claude.json"
|
||||
config = {
|
||||
"mcpServers": {
|
||||
LEGACY_MCP_SERVER_NAME: {"type": "sse"},
|
||||
"ops-bridge": {"type": "sse", "url": "http://127.0.0.1:8002/sse"},
|
||||
}
|
||||
}
|
||||
claude_json.write_text(json.dumps(config))
|
||||
migrated, changed = migrate_config(json.loads(claude_json.read_text()))
|
||||
assert changed is True
|
||||
assert "ops-bridge" in migrated["mcpServers"]
|
||||
assert migrated["mcpServers"]["ops-bridge"]["url"] == "http://127.0.0.1:8002/sse"
|
||||
Reference in New Issue
Block a user