feat: expose local self-correlation tools through MCP
Deploy production / deploy (push) Successful in 57s
Deploy production / deploy (push) Successful in 57s
This commit is contained in:
+106
-1
@@ -162,7 +162,7 @@ async def test_official_sdk_client_and_error_contract(mcp_app):
|
||||
async with ClientSession(streams[0], streams[1]) as client:
|
||||
await client.initialize()
|
||||
listed = await client.list_tools()
|
||||
assert len(listed.tools) == 11
|
||||
assert len(listed.tools) == 13
|
||||
caps = await client.call_tool("get_research_capabilities", {})
|
||||
assert caps.structured_content["max_candidates"] == 100
|
||||
result = await client.call_tool("submit_backtests", submission())
|
||||
@@ -278,3 +278,108 @@ async def test_refresh_job_error_pages_and_kind_isolation(mcp_app):
|
||||
assert result["errors"]["items"][0]["alpha_id"] == "1"
|
||||
error = await mcp_app.state.mcp.invoke(principal, "get_refresh_job", {"job_id": "auth"})
|
||||
assert error.structured_content["error"]["code"] == "NOT_FOUND"
|
||||
|
||||
|
||||
async def test_self_correlation_sdk_workflow_cache_and_staleness(mcp_app, monkeypatch):
|
||||
import httpx2
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
from app.alphas import upsert_alpha
|
||||
from app.models import Job, SelfCorrelation
|
||||
from tests.conftest import alpha
|
||||
from tests.test_alpha_management import points
|
||||
|
||||
data = points([1, -2, 4, 3] * 20)
|
||||
async with mcp_app.state.sessions.begin() as db:
|
||||
for raw in (alpha("target"), alpha("peer", status="ACTIVE"), alpha("pending")):
|
||||
await upsert_alpha(db, raw)
|
||||
db.add(Pnl(alpha_id="target", raw={}, points=data))
|
||||
calls = []
|
||||
|
||||
async def pnl(alpha_id):
|
||||
calls.append(alpha_id)
|
||||
return {"records": [{"date": p["date"], "pnl": p["value"]} for p in data]}
|
||||
|
||||
monkeypatch.setattr(mcp_app.state.runner.client, "pnl", pnl)
|
||||
principal, secret = await credentials(mcp_app, {"research:read", "research:refresh"})
|
||||
async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=mcp_app),
|
||||
headers={"Authorization": f"Bearer {secret}"}) as http:
|
||||
async with streamable_http_client("http://testserver/api/v1/mcp/", http_client=http) as streams:
|
||||
async with ClientSession(streams[0], streams[1]) as client:
|
||||
await client.initialize()
|
||||
listed = {t.name: t for t in (await client.list_tools()).tools}
|
||||
assert listed["get_self_correlation"].annotations.read_only_hint
|
||||
assert not listed["check_self_correlation"].annotations.read_only_hint
|
||||
assert listed["check_self_correlation"].annotations.open_world_hint
|
||||
before = await client.call_tool("get_self_correlation", {"alpha_id": "target"})
|
||||
assert before.structured_content["status"] == "not_cached"
|
||||
assert before.structured_content["result"] is None and calls == []
|
||||
mcp_app.state.runner.wake.clear()
|
||||
started = await client.call_tool("check_self_correlation", {"alpha_ids": ["target", "target"]})
|
||||
assert not started.is_error, started
|
||||
job_id = started.structured_content["job_id"]
|
||||
assert mcp_app.state.runner.wake.is_set() and calls == []
|
||||
assert started.structured_content["alpha_ids"] == ["target"]
|
||||
again = await client.call_tool("check_self_correlation", {"alpha_ids": ["target"]})
|
||||
assert again.structured_content["job_id"] == job_id
|
||||
queued = await client.call_tool("get_refresh_job", {"job_id": job_id})
|
||||
assert queued.structured_content["status"] == "queued"
|
||||
await mcp_app.state.runner.execute(job_id)
|
||||
done = await client.call_tool("get_refresh_job", {"job_id": job_id})
|
||||
assert done.structured_content["status"] == "completed"
|
||||
assert done.structured_content["processed"] == 1
|
||||
result = await client.call_tool("get_self_correlation", {"alpha_id": "target"})
|
||||
body = result.structured_content
|
||||
assert body["source"] == "local" and body["status"] == "available"
|
||||
assert body["result"]["max_correlation"] == pytest.approx(1)
|
||||
assert body["result"]["compared_count"] == 1
|
||||
assert body["result"]["matches"][0]["alpha_id"] == "peer"
|
||||
assert calls == ["peer"]
|
||||
async with mcp_app.state.sessions.begin() as db:
|
||||
row = await db.get(SelfCorrelation, "target")
|
||||
row.stale = True
|
||||
audit = await db.scalar(select(MCPAudit).where(MCPAudit.tool == "check_self_correlation"))
|
||||
assert audit.business_id == job_id and audit.result_code == "OK"
|
||||
assert await db.scalar(select(func.count()).select_from(Job).where(Job.kind == "self_correlation")) == 1
|
||||
assert await db.get(Pnl, "peer") is not None
|
||||
stale = await invoke(mcp_app, principal, "get_self_correlation", {"alpha_id": "target"})
|
||||
assert stale["status"] == "stale" and stale["result"]["stale"] and calls == ["peer"]
|
||||
|
||||
|
||||
async def test_self_correlation_read_only_scope_and_invalid_inputs(mcp_app):
|
||||
from app.alphas import upsert_alpha
|
||||
from app.models import Job
|
||||
from tests.conftest import alpha
|
||||
|
||||
async with mcp_app.state.sessions.begin() as db:
|
||||
await upsert_alpha(db, alpha("known"))
|
||||
_, secret = await credentials(mcp_app, {"research:read"})
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=mcp_app), base_url="http://testserver",
|
||||
headers={"Authorization": f"Bearer {secret}", "Accept": "application/json, text/event-stream"}) as http:
|
||||
listed = await http.post(ENDPOINT, json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
|
||||
names = {t["name"] for t in listed.json()["result"]["tools"]}
|
||||
assert "get_self_correlation" in names and "check_self_correlation" not in names
|
||||
for name, args, status in (
|
||||
("check_self_correlation", {"alpha_ids": ["known"]}, 403),
|
||||
("get_self_correlation", {"alpha_id": "known"}, 200),
|
||||
):
|
||||
response = await http.post(ENDPOINT, json={"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
||||
"params": {"name": name, "arguments": args}})
|
||||
assert response.status_code == status
|
||||
principal, _ = await credentials(mcp_app)
|
||||
for args, code in (
|
||||
({"alpha_ids": []}, "INVALID_INPUT"),
|
||||
({"alpha_ids": ["known"] * 101}, "INVALID_INPUT"),
|
||||
({"alpha_ids": ["../secret"]}, "INVALID_INPUT"),
|
||||
({"alpha_ids": ["known"], "force": True}, "INVALID_INPUT"),
|
||||
({"alpha_ids": ["known", "missing"]}, "NOT_FOUND"),
|
||||
):
|
||||
failure = await mcp_app.state.mcp.invoke(principal, "check_self_correlation", args)
|
||||
assert failure.is_error and failure.structured_content["error"]["code"] == code
|
||||
if code == "NOT_FOUND":
|
||||
assert failure.structured_content["error"]["affected_items"] == ["missing"]
|
||||
missing = await mcp_app.state.mcp.invoke(principal, "get_self_correlation", {"alpha_id": "missing"})
|
||||
assert missing.is_error and missing.structured_content["error"]["code"] == "NOT_FOUND"
|
||||
async with mcp_app.state.sessions() as db:
|
||||
assert await db.scalar(select(func.count()).select_from(Job)) == 0
|
||||
|
||||
Reference in New Issue
Block a user