Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion capabilities/ghostwriter-readonly/capability.yaml
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
8 changes: 5 additions & 3 deletions capabilities/ghostwriter-readonly/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
# ]
Expand Down Expand Up @@ -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 {}


Expand Down
53 changes: 48 additions & 5 deletions capabilities/ghostwriter-readonly/mcp/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
# ]
Expand All @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
# ]
Expand Down Expand Up @@ -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]


Expand Down
Loading