diff --git a/capabilities/ghostwriter-readonly/capability.yaml b/capabilities/ghostwriter-readonly/capability.yaml index 816bbe1..a453338 100644 --- a/capabilities/ghostwriter-readonly/capability.yaml +++ b/capabilities/ghostwriter-readonly/capability.yaml @@ -1,6 +1,6 @@ schema: 1 name: ghostwriter-readonly -version: "1.0.0" +version: "1.0.1" description: > Read-only GhostWriter integration. Query clients, projects, findings, objectives, targets, scope, deconflictions, evidence, observations, diff --git a/capabilities/ghostwriter-readonly/mcp/server.py b/capabilities/ghostwriter-readonly/mcp/server.py index 16ba370..2f26008 100644 --- a/capabilities/ghostwriter-readonly/mcp/server.py +++ b/capabilities/ghostwriter-readonly/mcp/server.py @@ -2,8 +2,8 @@ # /// script # requires-python = ">=3.11" # dependencies = [ -# "fastmcp>=2.0", -# "gql[aiohttp,websockets]>=3.0,<4.0", +# "fastmcp>=3.2.0", +# "gql[aiohttp,websockets]>=4.0", # "aiohttp>=3.9,<4.0", # "pydantic>=2.0,<3.0", # ] @@ -595,7 +595,9 @@ async def _ensure_connected() -> AsyncClientSession: async def _run_query(query: str, variables: GqlVariables | None = None) -> JsonObject: session = await _ensure_connected() - result = await session.execute(gql_parse(query), variable_values=variables) + request = gql_parse(query) + request.variable_values = variables + result = await session.execute(request) return result if isinstance(result, dict) else {} diff --git a/capabilities/ghostwriter-readonly/mcp/test_server.py b/capabilities/ghostwriter-readonly/mcp/test_server.py index da9f51c..629230f 100644 --- a/capabilities/ghostwriter-readonly/mcp/test_server.py +++ b/capabilities/ghostwriter-readonly/mcp/test_server.py @@ -4,8 +4,8 @@ # dependencies = [ # "pytest>=8.0", # "pytest-asyncio>=0.23", -# "fastmcp>=2.0", -# "gql[aiohttp,websockets]>=3.0,<4.0", +# "fastmcp>=3.2.0", +# "gql[aiohttp,websockets]>=4.0", # "aiohttp>=3.9,<4.0", # "pydantic>=2.0,<3.0", # ] @@ -15,9 +15,11 @@ from __future__ import annotations import sys +import warnings from pathlib import Path import pytest +from aiohttp import web sys.path.insert(0, str(Path(__file__).parent)) import server @@ -27,8 +29,8 @@ class TestToolRegistration: def test_expected_tools_registered(self): import asyncio - tools = asyncio.run(server.mcp.get_tools()) - tool_names = set(tools) if isinstance(tools, dict) else {t.name for t in tools} + tools = asyncio.run(server.mcp.list_tools()) + tool_names = {t.name for t in tools} expected = { "get_status", "list_clients", @@ -60,7 +62,7 @@ def test_expected_tools_registered(self): def test_tool_count(self): import asyncio - tools = asyncio.run(server.mcp.get_tools()) + tools = asyncio.run(server.mcp.list_tools()) assert len(tools) == 22 @@ -103,6 +105,47 @@ async def test_ensure_connected_errors_without_credentials(self, monkeypatch): await server._ensure_connected() +class TestRunQuery: + @pytest.mark.asyncio + async def test_variables_sent_without_deprecation_warning(self, monkeypatch): + """Variables reach the wire on the gql 4 request object, with no deprecation warning.""" + bodies: list[dict] = [] + data = {"client_aggregate": {"aggregate": {"count": 0}}, "finding_by_pk": None} + + async def handler(request: web.Request) -> web.Response: + bodies.append(await request.json()) + return web.json_response({"data": data}) + + app = web.Application() + app.router.add_post(server.GRAPHQL_PATH, handler) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, "127.0.0.1", 0) + await site.start() + port = runner.addresses[0][1] + + monkeypatch.setenv("GHOSTWRITER_URL", f"http://127.0.0.1:{port}") + monkeypatch.setenv("GHOSTWRITER_API_TOKEN", "tok") + monkeypatch.setattr(server, "_gql_client", None) + monkeypatch.setattr(server, "_gql_session", None) + monkeypatch.setattr(server, "_config", None) + try: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + result = await server._run_query( + "query q($id: bigint!) { finding_by_pk(id: $id) { id } }", + {"id": 7}, + ) + assert result == data + assert bodies[-1]["variables"] == {"id": 7} + assert not [w for w in caught if "variable_values" in str(w.message)] + assert not [w for w in caught if str(w.message).startswith("Using variable_values")] + finally: + if server._gql_client is not None: + await server._gql_client.close_async() + await runner.cleanup() + + class TestNoteTables: def test_all_note_types_mapped(self): for note_type in ("client", "project", "domain", "server"): diff --git a/capabilities/ghostwriter-readonly/skills/ghostwriter-readonly/scripts/ghostwriter_read.py b/capabilities/ghostwriter-readonly/skills/ghostwriter-readonly/scripts/ghostwriter_read.py index 5bd9946..510d171 100755 --- a/capabilities/ghostwriter-readonly/skills/ghostwriter-readonly/scripts/ghostwriter_read.py +++ b/capabilities/ghostwriter-readonly/skills/ghostwriter-readonly/scripts/ghostwriter_read.py @@ -2,7 +2,7 @@ # /// script # requires-python = ">=3.11" # dependencies = [ -# "gql[aiohttp,websockets]>=3.0,<4.0", +# "gql[aiohttp,websockets]>=4.0", # "aiohttp>=3.9,<4.0", # "pydantic>=2.0,<3.0", # ] @@ -428,7 +428,9 @@ async def run_query( variables: dict[str, int | str] | None = None, ) -> dict[str, object]: """Execute a GraphQL query and return the result dict.""" - result = await session.execute(gql_parse(query), variable_values=variables) + request = gql_parse(query) + request.variable_values = variables + result = await session.execute(request) return result # type: ignore[return-value]