"""Tier 3 — JSON-RPC E2E + Claude Desktop integration evidence.""" from __future__ import annotations import json import pathlib import subprocess import sys import pytest from muse.mcp.server import MuseMCPServer SMOKE = pathlib.Path("/Users/aaronrenecarvajal/MUSE_HUB/knowtation-vault-smoke") def _rpc(server: MuseMCPServer, method: str, params: dict | None = None, req_id: int = 1) -> dict: raw = {"jsonrpc": "2.0", "id": req_id, "method": method} if params is not None: raw["params"] = params resp = server.handle_request(raw) assert resp is not None return resp class TestJsonRpcE2E: @pytest.fixture def server(self) -> MuseMCPServer: return MuseMCPServer(SMOKE) def test_initialize(self, server: MuseMCPServer) -> None: resp = _rpc(server, "initialize") assert resp["result"]["serverInfo"]["name"] == "muse-mcp" def test_tools_call_search_and_resource_prime(self, server: MuseMCPServer) -> None: tools = _rpc(server, "tools/list")["result"]["tools"] assert len(tools) == 9 resources = _rpc(server, "resources/list")["result"]["resources"] assert len(resources) >= 6 prompts = _rpc(server, "prompts/list")["result"]["prompts"] assert len(prompts) == 4 search = _rpc( server, "tools/call", {"name": "knowtation/search", "arguments": {"query": "overview", "mode": "lexical"}}, req_id=2, ) payload = json.loads(search["result"]["content"][0]["text"]) assert payload["mode"] == "lexical" prime = _rpc(server, "resources/read", {"uri": "knowtation://prime"}, req_id=3) prime_body = json.loads(prime["result"]["contents"][0]["text"]) assert prime_body["schema_version"] == "1.0.0" prompt = _rpc( server, "prompts/get", {"name": "vault/daily-brief", "arguments": {"since_hours": 24}}, req_id=4, ) assert prompt["result"]["messages"] class TestClaudeDesktopIntegration: """Evidence matrix: stdio subprocess bound to knowtation-vault-smoke.""" def test_stdio_subprocess_list_and_roundtrip(self) -> None: init = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}) proc = subprocess.run( [sys.executable, "-m", "muse.mcp.stdio_server"], input=init + "\n", cwd=str(SMOKE), capture_output=True, text=True, timeout=30, check=False, ) assert proc.returncode == 0, proc.stderr line = proc.stdout.strip().splitlines()[0] data = json.loads(line) names = {t["name"] for t in data["result"]["tools"]} assert len(names) == 9 call = json.dumps( { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "knowtation/search", "arguments": {"query": "kd-smoke", "mode": "lexical"}, }, } ) proc2 = subprocess.run( [sys.executable, "-m", "muse.mcp.stdio_server"], input=init + "\n" + call + "\n", cwd=str(SMOKE), capture_output=True, text=True, timeout=30, check=False, ) assert proc2.returncode == 0, proc2.stderr lines = [json.loads(ln) for ln in proc2.stdout.strip().splitlines() if ln.strip()] search_resp = next(r for r in lines if r.get("id") == 2) body = json.loads(search_resp["result"]["content"][0]["text"]) assert body["query"] == "kd-smoke" res_list = json.dumps({"jsonrpc": "2.0", "id": 3, "method": "resources/list"}) proc3 = subprocess.run( [sys.executable, "-m", "muse.mcp.stdio_server"], input=res_list + "\n", cwd=str(SMOKE), capture_output=True, text=True, timeout=30, check=False, ) resources = json.loads(proc3.stdout.strip())["result"]["resources"] assert any(r["uri"] == "knowtation://prime" for r in resources) prompt_list = json.dumps({"jsonrpc": "2.0", "id": 4, "method": "prompts/list"}) proc4 = subprocess.run( [sys.executable, "-m", "muse.mcp.stdio_server"], input=prompt_list + "\n", cwd=str(SMOKE), capture_output=True, text=True, timeout=30, check=False, ) prompts = json.loads(proc4.stdout.strip())["result"]["prompts"] assert len(prompts) == 4