Files
forge/apps/api/tests/test_mcp_server.py
nihalashetty ae67bff5a3 feat: deep-agent canvas, live observability, and multi-environment tooling
Self-hosted platform for building, testing, and shipping LangChain/LangGraph agents. Deep-agent sub-agents on the canvas, a live tracing/observability timeline, auto-provisioned built-in tools with import/export, per-environment tool variables, streamed evaluations, and per-user auth token forwarding.
2026-07-28 01:49:19 +05:30

110 lines
5.0 KiB
Python

"""Forge-as-an-MCP-server: initialize / tools/list / tools/call over JSON-RPC."""
from __future__ import annotations
import httpx
from forge.db.base import SessionLocal
from forge.main import create_app
from forge.models import Project, Tool
async def _seed_project_with_tool(slug="mcp-proj") -> str:
from forge.services.tool_sets import ToolSetService
async with SessionLocal() as s:
proj = Project(tenant_id="t_mcps", name="MCP Proj", slug=slug, config={})
s.add(proj)
await s.flush()
tool = Tool(tenant_id="t_mcps", project_id=proj.id, name="calculator", kind="builtin",
config={"builtin": "calculator", "description": "Evaluate arithmetic."})
s.add(tool)
await s.commit()
await s.refresh(proj)
await s.refresh(tool)
# The MCP surface is the enabled tools of EXPOSED tool sets, so publish via a set.
await ToolSetService.create(s, "t_mcps", proj.id, name="General", tool_ids=[tool.id])
return proj.id
async def test_mcp_initialize_and_list_and_call():
pid = await _seed_project_with_tool()
app = create_app()
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as c:
# initialize
r = await c.post(f"/v1/mcp/{pid}", json={"jsonrpc": "2.0", "id": 1, "method": "initialize"})
assert r.status_code == 200 and r.json()["result"]["serverInfo"]["name"].startswith("forge-")
# tools/list exposes the project's tools
r = await c.post(f"/v1/mcp/{pid}", json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
names = [t["name"] for t in r.json()["result"]["tools"]]
assert "calculator" in names
# tools/call runs it
r = await c.post(f"/v1/mcp/{pid}", json={
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "calculator", "arguments": {"expression": "6*7"}},
})
body = r.json()["result"]
assert body["isError"] is False and "42" in body["content"][0]["text"]
async def test_mcp_exposes_project_tools():
"""Project-level tools (workflow / knowledge / Q&A) are published on the base endpoint when
their project.config flags are set, and never on a per-set (toolset) endpoint."""
async with SessionLocal() as s:
proj = Project(tenant_id="t_mcps3", name="P3", slug="p3", config={
"mcp_expose_workflow": True, "mcp_workflow_tool_name": "run_it",
"mcp_expose_knowledge": True, "mcp_expose_faq": True,
})
s.add(proj)
await s.commit()
await s.refresh(proj)
pid = proj.id
app = create_app()
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as c:
# base endpoint: all three project tools are listed (custom workflow tool name honored)
r = await c.post(f"/v1/mcp/{pid}", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
names = [t["name"] for t in r.json()["result"]["tools"]]
assert "run_it" in names
assert "search_knowledge_base" in names
assert "lookup_faq" in names
# per-toolset endpoint: project tools are a whole-project surface, so none of them appear
r = await c.post(f"/v1/mcp/{pid}/toolset/general", json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
names = [t["name"] for t in r.json()["result"]["tools"]]
assert not ({"run_it", "search_knowledge_base", "lookup_faq"} & set(names))
async def test_mcp_project_tools_off_by_default():
"""No flags => no project tools (unchanged surface for existing projects)."""
async with SessionLocal() as s:
proj = Project(tenant_id="t_mcps4", name="P4", slug="p4", config={})
s.add(proj)
await s.commit()
await s.refresh(proj)
pid = proj.id
app = create_app()
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as c:
r = await c.post(f"/v1/mcp/{pid}", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
names = [t["name"] for t in r.json()["result"]["tools"]]
assert not ({"run_workflow", "search_knowledge_base", "lookup_faq"} & set(names))
async def test_mcp_requires_key_when_configured():
async with SessionLocal() as s:
proj = Project(tenant_id="t_mcps2", name="P2", slug="p2", config={"mcp_api_key": "secret-key"})
s.add(proj)
await s.commit()
await s.refresh(proj)
pid = proj.id
app = create_app()
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as c:
# no key -> 401
r = await c.post(f"/v1/mcp/{pid}", json={"jsonrpc": "2.0", "id": 1, "method": "initialize"})
assert r.status_code == 401
# correct key -> ok
r = await c.post(f"/v1/mcp/{pid}", headers={"Authorization": "Bearer secret-key"},
json={"jsonrpc": "2.0", "id": 1, "method": "initialize"})
assert r.status_code == 200