Run the complete core and MCP documentation suites

This commit is contained in:
Nick Sweeting 2026-07-21 19:26:35 -07:00
parent c36d3c1575
commit bcab0bfa6e
No known key found for this signature in database
4 changed files with 58 additions and 36 deletions

View File

@ -206,7 +206,7 @@ jobs:
uv run --directory "$DATA_DIR" --no-sync --no-sources archivebox status
- name: Run consolidated core suite
if: matrix.os_name == 'macOS' || matrix.python == '3.14.6'
if: matrix.os_name == 'macOS' || matrix.python == '3.14'
run: |
mkdir -p tests/out
uv run --no-sync --no-sources pytest -q archivebox/tests --basetemp="tests/out/${{ matrix.os_name }}-python-${{ matrix.python }}"

View File

@ -19,7 +19,9 @@ This is a lightweight, stateless MCP server that dynamically introspects Archive
### Start the MCP Server
```bash
archivebox mcp
request='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
response="$(printf '%s\n' "$request" | "$UV_BINARY" run --project "$ARCHIVEBOX_PROJECT_DIR" --no-sync archivebox mcp)"
"$JQ_BINARY" -e '.id == 1 and .result.serverInfo.name == "archivebox-mcp"' <<< "$response"
```
The server runs in stdio mode, reading JSON-RPC 2.0 requests from stdin and writing responses to stdout.
@ -27,25 +29,30 @@ The server runs in stdio mode, reading JSON-RPC 2.0 requests from stdin and writ
### Example Client
```python
import subprocess
import json
import os
import subprocess
# Start MCP server
proc = subprocess.Popen(
['archivebox', 'mcp'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
# Send initialize request
request = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
proc.stdin.write(json.dumps(request) + '\n')
proc.stdin.flush()
# Read response
response = json.loads(proc.stdout.readline())
print(response)
completed = subprocess.run(
[
os.environ["UV_BINARY"],
"run",
"--project",
os.environ["ARCHIVEBOX_PROJECT_DIR"],
"--no-sync",
"archivebox",
"mcp",
],
input=json.dumps(request) + "\n",
capture_output=True,
text=True,
check=True,
timeout=30,
)
response = json.loads(completed.stdout)
assert response["id"] == 1
assert response["result"]["serverInfo"]["name"] == "archivebox-mcp"
```
### Example Requests
@ -98,16 +105,21 @@ The server exposes all ArchiveBox CLI commands:
Instead of manually defining schemas, the server uses Click's introspection API to automatically generate MCP tool definitions:
```python
# Auto-discover commands
from archivebox.cli import ArchiveBoxGroup
cli_group = ArchiveBoxGroup()
all_commands = cli_group.all_subcommands
import click
# Auto-generate schemas from Click metadata
for cmd_name in all_commands:
click_cmd = cli_group.get_command(None, cmd_name)
# Extract params, types, help text, etc.
tool_schema = click_command_to_mcp_tool(cmd_name, click_cmd)
from archivebox.cli import ArchiveBoxGroup
from archivebox.mcp.server import click_command_to_mcp_tool
cli_group = ArchiveBoxGroup()
context = click.Context(cli_group)
tools = []
for command_name in cli_group.all_subcommands:
command = cli_group.get_command(context, command_name)
assert command is not None
tools.append(click_command_to_mcp_tool(command_name, command))
assert {tool["name"] for tool in tools} == set(cli_group.all_subcommands)
```
### Tool Execution
@ -115,10 +127,14 @@ for cmd_name in all_commands:
Commands are executed using Click's `CliRunner`:
```python
from click.testing import CliRunner
from archivebox.mcp.server import MCPServer
runner = CliRunner()
result = runner.invoke(click_command, args)
response = MCPServer().handle_request(
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
)
assert response["id"] == 2
assert response["result"]["tools"]
assert all("name" in tool and "inputSchema" in tool for tool in response["result"]["tools"])
```
## Files

View File

@ -4,7 +4,7 @@ environments = ["core", "merge", "publishing", "docker", "root", "macos", "freeb
[syntax]
executed = ["bash", "sh", "python"]
shell_syntax_only = ["console"]
structured = ["yaml", "ini", "sql", "nginx", "mermaid"]
structured = ["json", "yaml", "ini", "sql", "nginx", "mermaid"]
prose = ["text"]
directive_prefixes = ["{"]
console_blocks = [
@ -18,9 +18,6 @@ console_blocks = [
"README.md::line 1524",
"README.md::line 1537",
"README.md::line 1601",
"README.md::line 1617",
"AGENTS.md::line 84",
"skills/archivebox/SKILL.md::line 78",
]
[ci.standard]
@ -34,6 +31,7 @@ macos = "macos-15"
[ci.core_shards]
metadata = [
"README.md",
"archivebox/mcp/README.md",
"docs/Changelog.md",
"docs/Chromium-Install.md",
]
@ -64,6 +62,7 @@ openbsd = "vmactions/openbsd-vm@v1.4.5"
[files]
"README.md" = "core"
"AGENTS.md" = "core"
"archivebox/mcp/README.md" = "core"
"skills/archivebox/SKILL.md" = "core"
"docs/Changelog.md" = "core"
"docs/Chromium-Install.md" = "core"

View File

@ -1,5 +1,6 @@
import configparser
from collections import Counter
import json
from pathlib import Path
import sqlite3
import subprocess
@ -15,7 +16,11 @@ WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "docs.yml"
def markdown_paths() -> tuple[Path, ...]:
candidates = [REPO_ROOT / "README.md", REPO_ROOT / "AGENTS.md"]
candidates = [
REPO_ROOT / "README.md",
REPO_ROOT / "AGENTS.md",
REPO_ROOT / "archivebox" / "mcp" / "README.md",
]
candidates.extend(sorted((REPO_ROOT / "skills").rglob("*.md")))
candidates.extend(sorted((REPO_ROOT / "docs").rglob("*.md")))
@ -106,7 +111,9 @@ def test_structured_data_fences_parse() -> None:
)
for nodeid, block in blocks.items():
if block.syntax == "yaml":
if block.syntax == "json":
json.loads(block.code)
elif block.syntax == "yaml":
list(yaml.safe_load_all(block.code))
elif block.syntax == "ini":
parser = configparser.ConfigParser()