diff --git a/README.md b/README.md index 1fc5626..f11c9cf 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,42 @@ Phase 4: ECOSYSTEM (Batches 11+) → Adjacent tools, community, competitive c | SaaS Product Evaluation | [saas-evaluation.md](templates/saas-evaluation.md) | | Programming Framework | [programming-framework.md](templates/programming-framework.md) | +## MCP Server + +BIF includes a stdio MCP server that exposes the framework as tools for MCP-compatible clients. + +### Required environment + +| Variable | Required | Purpose | +|----------|----------|---------| +| `BIF_SESSIONS_DIR` | No | Overrides the default temp-directory session store. Use this when you want sessions persisted in a known workspace path. | + +If `BIF_SESSIONS_DIR` is not set, sessions are written under the operating system temp directory in `bif-sessions/`. + +### Startup command + +```bash +python mcp_server.py +``` + +Example MCP client configuration: + +```json +{ + "mcpServers": { + "bif": { + "command": "python", + "args": ["/absolute/path/to/bif/mcp_server.py"], + "env": { + "BIF_SESSIONS_DIR": "/absolute/path/to/bif/.sessions" + } + } + } +} +``` + +The server supports `initialize`, `tools/list`, `tools/call`, and `ping` over newline-delimited JSON-RPC on stdio. + ## Proven Results | Project | Batches | Files | Time | Coverage | diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py new file mode 100644 index 0000000..3efa07c --- /dev/null +++ b/tests/test_mcp_server.py @@ -0,0 +1,66 @@ +"""Subprocess tests for the BIF MCP stdio server.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).parent.parent +SERVER = REPO_ROOT / "mcp_server.py" + + +def _request(process: subprocess.Popen, payload: dict) -> dict: + assert process.stdin is not None + assert process.stdout is not None + process.stdin.write(json.dumps(payload) + "\n") + process.stdin.flush() + line = process.stdout.readline() + assert line, "MCP server did not emit a response" + return json.loads(line) + + +def test_mcp_server_starts_and_accepts_tool_invocation(tmp_path) -> None: + env = {**os.environ, "BIF_SESSIONS_DIR": str(tmp_path / "sessions")} + process = subprocess.Popen( + [sys.executable, str(SERVER)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + try: + initialize = _request( + process, + {"jsonrpc": "2.0", "id": "init-1", "method": "initialize", "params": {}}, + ) + assert initialize["result"]["serverInfo"]["name"] == "bif" + + tools = _request( + process, + {"jsonrpc": "2.0", "id": "tools-1", "method": "tools/list", "params": {}}, + ) + tool_names = {tool["name"] for tool in tools["result"]["tools"]} + assert "bif_start_session" in tool_names + + call = _request( + process, + { + "jsonrpc": "2.0", + "id": "call-1", + "method": "tools/call", + "params": { + "name": "bif_start_session", + "arguments": {"domain": "MCP Test Domain"}, + }, + }, + ) + content = json.loads(call["result"]["content"][0]["text"]) + assert content["domain"] == "MCP Test Domain" + assert (tmp_path / "sessions" / f"{content['session_id']}.json").exists() + finally: + process.terminate() + process.wait(timeout=5)