MicroFish/scripts/smoke_mcp_full.py

217 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""MCP lifecycle smoke.
This verifies the FastMCP server can be constructed and then exercises the
same lifecycle service used by MCP tools. If the MCP SDK is missing, the script
fails with a clear dependency blocker.
"""
from __future__ import annotations
import json
import os
import tempfile
import asyncio
from pathlib import Path
from write_mock_agent_response import output_for
async def call_tool(server, name: str, arguments: dict) -> dict:
result = await server.call_tool(name, arguments)
if isinstance(result, tuple) and len(result) > 1 and isinstance(result[1], dict):
return result[1]["result"]
if isinstance(result, dict):
return result.get("result", result)
raise RuntimeError(f"Unexpected MCP tool result for {name}: {result!r}")
async def async_main() -> int:
try:
import mcp # noqa: F401
except ImportError as exc:
print("BLOCKER: MCP Python SDK package 'mcp' is not installed.")
print(f"Import error: {exc}")
return 2
from app.mcp_server.server import create_server
server = create_server()
if server is None:
print("BLOCKER: create_server returned None")
return 2
tmp = Path(tempfile.mkdtemp())
os.environ["MIROFISH_MODE"] = "agent"
os.environ["MIROFISH_LLM_PROVIDER"] = "agent_queue"
os.environ["MIROFISH_GRAPH_PROVIDER"] = "graphiti"
os.environ["MIROFISH_GRAPHITI_STORE"] = "file"
os.environ["MIROFISH_GRAPHITI_COMPAT_PATH"] = str(tmp / "graphiti-store.json")
os.environ.pop("LLM_API_KEY", None)
os.environ.pop("OPENAI_API_KEY", None)
os.environ.pop("ZEP_API_KEY", None)
seed = tmp / "seed.md"
seed.write_text("美国商务部限制先进AI芯片出口。", encoding="utf-8")
run_dir = tmp / "mcp-run"
tools = await server.list_tools()
tool_names = {tool.name for tool in tools}
required = {
"mirofish_create_run",
"mirofish_run",
"mirofish_resume_run",
"mirofish_get_status",
"mirofish_get_current_stage",
"mirofish_update_simulation_settings",
"mirofish_approve_stage",
"mirofish_reject_stage",
"mirofish_rerun_stage",
"mirofish_list_requests",
"mirofish_get_request",
"mirofish_submit_response",
"mirofish_validate_response",
"mirofish_build_graph",
"mirofish_search_graph",
"mirofish_export_graph",
"mirofish_start_simulation",
"mirofish_resume_simulation",
"mirofish_generate_report",
"mirofish_get_report",
"mirofish_ask_followup_question",
"mirofish_get_followup_answer",
"mirofish_list_artifacts",
"mirofish_doctor",
}
missing = sorted(required - tool_names)
if missing:
print(f"BLOCKER: MCP tools missing: {missing}")
return 2
create_tool = next(tool for tool in tools if tool.name == "mirofish_create_run")
create_schema = getattr(create_tool, "inputSchema", {}) or {}
create_props = create_schema.get("properties", {})
if "rounds" not in create_props or "mode" not in create_props:
print(f"BLOCKER: mirofish_create_run schema does not expose rounds/mode: {create_schema}")
return 2
staged_dir = tmp / "mcp-staged-run"
staged = await call_tool(
server,
"mirofish_create_run",
{
"seed": str(seed),
"requirement": "预测未来10年全球芯片能力格局变化",
"output": str(staged_dir),
"mode": "staged",
"rounds": 10,
"round_unit": "year",
},
)
assert staged["status"] == "created", staged
assert staged["state"]["workflow_mode"] == "staged", staged
assert staged["state"]["simulation_settings"]["rounds"] == 10, staged
current_stage = await call_tool(server, "mirofish_get_current_stage", {"run": str(staged_dir)})
assert current_stage["stage"]["current_stage"] == "seed_input", current_stage
approved_stage = await call_tool(server, "mirofish_approve_stage", {"run": str(staged_dir)})
assert approved_stage["next_stage"] == "prediction_requirement", approved_stage
created = await call_tool(
server,
"mirofish_create_run",
{"seed": str(seed), "requirement": "预测未来10年全球芯片能力格局变化", "output": str(run_dir), "rounds": 10},
)
assert created["status"] == "created"
result = await call_tool(server, "mirofish_run", {"run": str(run_dir)})
for _ in range(10):
if result["status"] == "completed":
status = await call_tool(server, "mirofish_get_status", {"run": str(run_dir)})
assert status["status"] == "ok", status
report = await call_tool(server, "mirofish_get_report", {"run": str(run_dir)})
assert report["status"] == "ok"
search = await call_tool(
server,
"mirofish_search_graph",
{"run": str(run_dir), "query": "先进AI芯片出口", "limit": 5},
)
assert search["status"] == "ok", search
exported = await call_tool(server, "mirofish_export_graph", {"run": str(run_dir), "output": None})
assert exported["status"] == "ok", exported
artifacts = await call_tool(server, "mirofish_list_artifacts", {"run": str(run_dir)})
artifact_names = {artifact["name"] for artifact in artifacts["artifacts"]}
assert {"report.md", "verdict.json", "timeline.json", "graph_snapshot.json"}.issubset(artifact_names)
doctor = await call_tool(server, "mirofish_doctor", {"runs_dir": str(tmp / "doctor-runs")})
assert doctor["status"] == "ok", doctor
followup = await call_tool(
server,
"mirofish_ask_followup_question",
{"run": str(run_dir), "question": "先进AI芯片出口限制有什么影响?", "limit": 5},
)
assert followup["status"] == "need_agent_response", followup
followup_request = await call_tool(
server,
"mirofish_get_request",
{"run": str(run_dir), "request_id": followup["request_id"]},
)
request = followup_request["request"]
response_path = run_dir / "responses" / f"{request['request_id']}.json"
response_path.write_text(
json.dumps(
{"request_id": request["request_id"], "status": "ok", "output": output_for(request)},
ensure_ascii=False,
),
encoding="utf-8",
)
submitted = await call_tool(
server,
"mirofish_submit_response",
{"run": str(run_dir), "response": str(response_path)},
)
assert submitted["ok"], submitted
answer = await call_tool(
server,
"mirofish_get_followup_answer",
{"run": str(run_dir), "request_id": request["request_id"]},
)
assert answer["status"] == "ok", answer
print(f"MCP lifecycle smoke passed: {run_dir}")
return 0
assert result["status"] == "need_agent_response", result
listed = await call_tool(server, "mirofish_list_requests", {"run": str(run_dir)})
assert any(item["request_id"] == result["request_id"] for item in listed["requests"])
request_result = await call_tool(
server,
"mirofish_get_request",
{"run": str(run_dir), "request_id": result["request_id"]},
)
request = request_result["request"]
request_id = request["request_id"]
response_path = run_dir / "responses" / f"{request_id}.json"
response_path.write_text(
json.dumps({"request_id": request_id, "status": "ok", "output": output_for(request)}, ensure_ascii=False),
encoding="utf-8",
)
validation = await call_tool(
server,
"mirofish_validate_response",
{"run": str(run_dir), "response": str(response_path)},
)
assert validation["ok"], validation
submitted = await call_tool(
server,
"mirofish_submit_response",
{"run": str(run_dir), "response": str(response_path)},
)
assert submitted["ok"], submitted
result = await call_tool(server, "mirofish_resume_run", {"run": str(run_dir)})
print("MCP lifecycle smoke did not complete within expected steps")
return 1
def main() -> int:
return asyncio.run(async_main())
if __name__ == "__main__":
raise SystemExit(main())