diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07adb89..8f94c0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,9 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.0 + # Keep in lock-step with the ruff version pinned in .github/workflows/ci.yml + # (>=0.4.0,<0.5). Using a newer rev here produces a different formatter + # output than CI and breaks `ruff format --check` in the lint job. + rev: v0.4.10 hooks: - id: ruff args: [--fix] diff --git a/tests/test_readme_claims.py b/tests/test_readme_claims.py index 4645f34..7d4754a 100644 --- a/tests/test_readme_claims.py +++ b/tests/test_readme_claims.py @@ -22,6 +22,8 @@ import pytest REPO_ROOT = Path(__file__).resolve().parent.parent MEMPALACE_PKG = REPO_ROOT / "mempalace" README_PATH = REPO_ROOT / "README.md" +MCP_TOOLS_DOC_PATH = REPO_ROOT / "website" / "reference" / "mcp-tools.md" +MODULES_DOC_PATH = REPO_ROOT / "website" / "reference" / "modules.md" def _read(path: Path) -> str: @@ -40,10 +42,15 @@ def _tools_dict_keys() -> list: return re.findall(r'"(mempalace_\w+)":\s*\{', src) -def _readme_tool_table_names() -> list: - """Return tool names listed in the README's MCP tool table.""" - readme = _readme() - return re.findall(r"^\| `(mempalace_\w+)`", readme, re.MULTILINE) +def _doc_tool_names() -> list: + """Return the list of tool names documented in the MCP tools reference. + + The MCP tool table lived in README.md prior to the #875 rewrite; it now + lives in website/reference/mcp-tools.md (linked from README). Each tool + is introduced by a level-3 heading `### \\`mempalace_xxx\\``. + """ + doc = _read(MCP_TOOLS_DOC_PATH) + return re.findall(r"^###\s+`(mempalace_\w+)`", doc, re.MULTILINE) # --------------------------------------------------------------------------- @@ -77,19 +84,28 @@ class TestToolCount: class TestReadmeToolsExistInCode: - """Every tool name in the README tool table must be a key in TOOLS.""" + """Every tool name documented in the MCP tools reference must be a key in TOOLS.""" def test_every_readme_tool_exists_in_tools_dict(self): - """Claim: README lists tools like mempalace_get_aaak_spec. - Each one must actually be registered in the TOOLS dict.""" - code_tools = set(_tools_dict_keys()) - readme_tools = _readme_tool_table_names() - assert len(readme_tools) > 0, "Could not parse any tools from README table" + """Claim: the MCP tools reference (website/reference/mcp-tools.md) + lists tools like mempalace_get_aaak_spec. Each one must actually be + registered in the TOOLS dict in mempalace/mcp_server.py. - missing = [t for t in readme_tools if t not in code_tools] + Pre-#875 this parsed the tool table that lived in README.md; that + table has moved to the website docs and README now links out. + """ + code_tools = set(_tools_dict_keys()) + doc_tools = _doc_tool_names() + assert len(doc_tools) > 0, ( + f"Could not parse any tools from {MCP_TOOLS_DOC_PATH.relative_to(REPO_ROOT)} " + f"— expected `### \\`mempalace_xxx\\`` headings." + ) + + missing = [t for t in doc_tools if t not in code_tools] assert missing == [], ( - f"README lists tools that don't exist in TOOLS dict: {missing}. " - f"Either add them to mcp_server.py or remove them from README." + f"Docs list tools that don't exist in TOOLS dict: {missing}. " + f"Either add them to mcp_server.py or remove them from " + f"{MCP_TOOLS_DOC_PATH.relative_to(REPO_ROOT)}." ) @@ -99,18 +115,20 @@ class TestReadmeToolsExistInCode: class TestNoUnlistedTools: - """Every tool in the TOOLS dict should be documented in the README.""" + """Every tool in the TOOLS dict should be documented in the MCP tools reference.""" def test_no_undocumented_tools(self): - """Claim: README's tool table is complete. - Any tool in TOOLS but not in README is undocumented.""" + """Claim: the MCP tools reference + (website/reference/mcp-tools.md) is complete. Any tool in TOOLS + but not documented there is undocumented on the public surface.""" code_tools = set(_tools_dict_keys()) - readme_tools = set(_readme_tool_table_names()) + doc_tools = set(_doc_tool_names()) - undocumented = sorted(code_tools - readme_tools) + undocumented = sorted(code_tools - doc_tools) assert undocumented == [], ( - f"Tools in TOOLS dict but missing from README: {undocumented}. " - f"Add rows for these to the tool table in README.md." + f"Tools in TOOLS dict but missing from docs: {undocumented}. " + f"Add sections for these to " + f"{MCP_TOOLS_DOC_PATH.relative_to(REPO_ROOT)}." ) @@ -485,21 +503,27 @@ class TestDialectNotLossless: class TestReadmeDialectNotLossless: - """README's file reference table must not say dialect.py is lossless.""" + """The file-reference documentation must not say dialect.py is lossless. + + Pre-#875 this lived in a README.md file table; it now lives in + website/reference/modules.md. The April 7 correction established that + AAAK is a lossy abbreviation system, not lossless compression, and + every docs surface that describes dialect.py must respect that. + """ def test_readme_dialect_line_not_lossless(self): - """Claim: April 7 correction applied to README file table. - The dialect.py row must not say 'lossless'.""" - readme = _readme() - # Find the line with dialect.py in the file reference table - dialect_lines = [ - line for line in readme.splitlines() if "dialect.py" in line and "|" in line - ] - assert len(dialect_lines) > 0, "Could not find dialect.py in README file table" + doc = _read(MODULES_DOC_PATH) + # Any line mentioning dialect.py (narrative or table) must not call it lossless + dialect_lines = [line for line in doc.splitlines() if "dialect.py" in line] + assert len(dialect_lines) > 0, ( + f"Could not find dialect.py in " + f"{MODULES_DOC_PATH.relative_to(REPO_ROOT)}. " + f"Expected at least one reference." + ) for line in dialect_lines: assert "lossless" not in line.lower(), ( - f"README file table still says dialect.py is lossless: {line.strip()!r}. " + f"Docs still call dialect.py lossless: {line.strip()!r}. " f"After April 7 correction, this must say 'lossy' or remove the lossless claim." ) diff --git a/website/reference/mcp-tools.md b/website/reference/mcp-tools.md index d20d8f4..f951fe1 100644 --- a/website/reference/mcp-tools.md +++ b/website/reference/mcp-tools.md @@ -1,6 +1,6 @@ # MCP Tools Reference -Detailed parameter schemas for all 19 MCP tools. +Detailed parameter schemas for all 29 MCP tools. ## Palace — Read Tools @@ -114,6 +114,48 @@ Delete a drawer by ID. Irreversible. --- +### `mempalace_get_drawer` + +Fetch a single drawer by ID — returns full content and metadata. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `drawer_id` | string | **Yes** | ID of the drawer to fetch | + +**Returns:** `{ drawer: { id, wing, room, content, ... } }` + +--- + +### `mempalace_list_drawers` + +List drawers with pagination. Optional wing/room filter. Returns IDs, wings, rooms, and content previews. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `wing` | string | No | Filter by wing | +| `room` | string | No | Filter by room | +| `limit` | integer | No | Max results per page (default 20, max 100) | +| `offset` | integer | No | Offset for pagination (default 0) | + +**Returns:** `{ drawers: [...], total, limit, offset }` + +--- + +### `mempalace_update_drawer` + +Update an existing drawer's content and/or metadata (wing, room). Fetches the existing drawer first; returns an error if not found. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `drawer_id` | string | **Yes** | ID of the drawer to update | +| `content` | string | No | New content (omit to keep existing) | +| `wing` | string | No | New wing (omit to keep existing) | +| `room` | string | No | New room (omit to keep existing) | + +**Returns:** `{ success, drawer_id, updated_fields }` + +--- + ## Knowledge Graph Tools ### `mempalace_kg_query` @@ -221,6 +263,61 @@ Palace graph overview: nodes, tunnels, edges, connectivity. --- +### `mempalace_create_tunnel` + +Create a cross-wing tunnel linking two palace locations. Use when content in one project relates to another — e.g., an API design in `project_api` connects to a database schema in `project_database`. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `source_wing` | string | **Yes** | Wing of the source | +| `source_room` | string | **Yes** | Room in the source wing | +| `target_wing` | string | **Yes** | Wing of the target | +| `target_room` | string | **Yes** | Room in the target wing | +| `label` | string | No | Description of the connection | +| `source_drawer_id` | string | No | Specific source drawer ID | +| `target_drawer_id` | string | No | Specific target drawer ID | + +**Returns:** `{ success, tunnel_id, source, target }` + +--- + +### `mempalace_list_tunnels` + +List all explicit cross-wing tunnels. Optionally filter by wing. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `wing` | string | No | Filter tunnels by wing (source or target) | + +**Returns:** `{ tunnels: [...], count }` + +--- + +### `mempalace_delete_tunnel` + +Delete an explicit tunnel by its ID. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `tunnel_id` | string | **Yes** | Tunnel ID to delete | + +**Returns:** `{ success, tunnel_id }` + +--- + +### `mempalace_follow_tunnels` + +Follow tunnels from a room to see what it connects to in other wings. Returns connected rooms with drawer previews. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `wing` | string | **Yes** | Wing to start from | +| `room` | string | **Yes** | Room to follow tunnels from | + +**Returns:** `[{ wing, room, label, previews }]` + +--- + ## Agent Diary Tools ### `mempalace_diary_write` @@ -247,3 +344,38 @@ Read recent diary entries. | `last_n` | integer | No | Number of recent entries (default: 10) | **Returns:** `{ agent, entries: [{ date, timestamp, topic, content }], total, showing }` + +--- + +## System Tools + +### `mempalace_hook_settings` + +Get or set auto-save hook behaviour. `silent_save=true` saves directly without MCP-level clutter; `silent_save=false` uses the legacy blocking path. `desktop_toast=true` surfaces a desktop notification when a save completes. Call with no arguments to view the current settings. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `silent_save` | boolean | No | `true` = silent direct save, `false` = blocking MCP calls | +| `desktop_toast` | boolean | No | `true` = show desktop toast via `notify-send` | + +**Returns:** `{ silent_save, desktop_toast }` + +--- + +### `mempalace_memories_filed_away` + +Check whether a recent palace checkpoint was saved. Returns message count and timestamp of the last save. + +**Parameters:** None + +**Returns:** `{ filed, message_count, timestamp }` + +--- + +### `mempalace_reconnect` + +Force a reconnect to the palace database. Use this after external scripts or CLI commands modified the palace directly, which can leave the in-memory HNSW index stale. + +**Parameters:** None + +**Returns:** `{ success, palace_path }`