mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-12 19:50:57 +05:00
170 lines
5.8 KiB
Python
170 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for the ArchiveBox MCP server and CLI entry point."""
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from archivebox.mcp.server import MCPServer
|
|
from archivebox.tests.conftest import run_archivebox_cmd
|
|
|
|
|
|
pytestmark = pytest.mark.django_db(transaction=True)
|
|
|
|
|
|
def test_mcp_help_runs_successfully(tmp_path):
|
|
"""The mcp command should be registered and expose help."""
|
|
|
|
result = run_archivebox_cmd(["mcp", "--help"])
|
|
|
|
assert result.returncode == 0
|
|
assert "mcp" in result.stdout.lower()
|
|
|
|
|
|
def test_mcp_stdio_handles_handshake_notification_and_ping(initialized_archive):
|
|
requests = [
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": {
|
|
"protocolVersion": "2025-11-25",
|
|
"capabilities": {},
|
|
"clientInfo": {"name": "archivebox-test", "version": "1"},
|
|
},
|
|
},
|
|
{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}},
|
|
{"jsonrpc": "2.0", "id": 2, "method": "ping", "params": {}},
|
|
]
|
|
result = run_archivebox_cmd(
|
|
["mcp"],
|
|
cwd=initialized_archive,
|
|
input="".join(json.dumps(request) + "\n" for request in requests),
|
|
default_cli_env=True,
|
|
)
|
|
responses = [json.loads(line) for line in result.stdout.splitlines()]
|
|
|
|
assert result.returncode == 0
|
|
assert [response["id"] for response in responses] == [1, 2]
|
|
assert responses[0]["result"]["protocolVersion"] == "2025-11-25"
|
|
assert responses[1]["result"] == {}
|
|
|
|
|
|
def test_mcp_exposes_six_focused_tools():
|
|
tools = MCPServer().handle_tools_list({})["tools"]
|
|
tools_by_name = {tool["name"]: tool for tool in tools}
|
|
|
|
assert set(tools_by_name) == {"add", "search", "crawl", "snapshot", "archiveresult", "shell"}
|
|
assert len(tools) == 6
|
|
assert all("outputSchema" in tool for tool in tools)
|
|
|
|
crawl_schema = tools_by_name["crawl"]["inputSchema"]
|
|
assert crawl_schema["properties"]["action"]["enum"] == ["create", "delete", "list", "update"]
|
|
assert crawl_schema["properties"]["urls"]["type"] == "array"
|
|
assert crawl_schema["properties"]["records"]["type"] == "array"
|
|
assert tools_by_name["search"]["annotations"]["readOnlyHint"] is True
|
|
assert tools_by_name["shell"]["inputSchema"]["required"] == ["code"]
|
|
assert tools_by_name["shell"]["annotations"]["destructiveHint"] is True
|
|
|
|
|
|
def test_mcp_crawl_create_returns_structured_json(initialized_archive):
|
|
os.chdir(initialized_archive)
|
|
result = MCPServer().handle_tools_call(
|
|
{
|
|
"name": "crawl",
|
|
"arguments": {
|
|
"action": "create",
|
|
"urls": ["https://mcp-test.example.com/"],
|
|
"depth": 1,
|
|
"tag": "mcp-test",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert result["isError"] is False, result
|
|
assert result["structuredContent"]["success"] is True
|
|
assert result["structuredContent"]["error"] is None
|
|
assert result["structuredContent"]["command"] == "archivebox crawl create"
|
|
assert result["structuredContent"]["exitCode"] == 0
|
|
assert result["structuredContent"]["records"][0]["urls"] == "https://mcp-test.example.com/"
|
|
assert result["structuredContent"]["records"][0]["max_depth"] == 1
|
|
assert json.loads(result["content"][0]["text"]) == result["structuredContent"]
|
|
|
|
|
|
def test_mcp_snapshot_update_accepts_records_without_a_jsonl_pipeline(initialized_archive):
|
|
os.chdir(initialized_archive)
|
|
server = MCPServer()
|
|
created = server.handle_tools_call(
|
|
{
|
|
"name": "snapshot",
|
|
"arguments": {
|
|
"action": "create",
|
|
"urls": ["https://mcp-update.example.com/"],
|
|
},
|
|
},
|
|
)
|
|
assert created["isError"] is False, created
|
|
snapshot = created["structuredContent"]["records"][0]
|
|
|
|
updated = server.handle_tools_call(
|
|
{
|
|
"name": "snapshot",
|
|
"arguments": {
|
|
"action": "update",
|
|
"records": [{"id": snapshot["id"]}],
|
|
"tag": "updated-through-mcp",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert updated["isError"] is False
|
|
assert updated["structuredContent"]["records"][0]["id"] == snapshot["id"]
|
|
assert "updated-through-mcp" in updated["structuredContent"]["records"][0]["tags"]
|
|
|
|
|
|
def test_mcp_cli_errors_are_structured_for_agents(initialized_archive):
|
|
os.chdir(initialized_archive)
|
|
result = MCPServer().handle_tools_call(
|
|
{
|
|
"name": "crawl",
|
|
"arguments": {"action": "create"},
|
|
},
|
|
)
|
|
|
|
assert result["isError"] is True
|
|
assert result["structuredContent"]["success"] is False
|
|
assert result["structuredContent"]["exitCode"] == 1
|
|
assert "No URLs provided" in result["structuredContent"]["error"]
|
|
assert json.loads(result["content"][0]["text"]) == result["structuredContent"]
|
|
|
|
|
|
def test_mcp_invalid_action_is_a_protocol_error():
|
|
response = MCPServer().handle_request(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {"name": "crawl", "arguments": {"action": "bogus"}},
|
|
},
|
|
)
|
|
|
|
assert response["error"]["code"] == -32602
|
|
assert "Choose one of: create, delete, list, update" in response["error"]["message"]
|
|
|
|
|
|
def test_mcp_shell_runs_python_through_archivebox_shell(initialized_archive):
|
|
os.chdir(initialized_archive)
|
|
result = MCPServer().handle_tools_call(
|
|
{
|
|
"name": "shell",
|
|
"arguments": {
|
|
"code": "from archivebox.core.models import Snapshot; print(f'shell_ok={Snapshot.objects.count() >= 0}')",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert result["isError"] is False, result
|
|
assert result["structuredContent"]["command"] == "archivebox shell"
|
|
assert result["structuredContent"]["stdout"] == "shell_ok=True\n"
|