From a17cf7be9023e160bc36335062c556d7607b1011 Mon Sep 17 00:00:00 2001 From: Claude Code Sandbox Date: Thu, 5 Feb 2026 14:06:30 +0000 Subject: [PATCH] feat(mcp): add MCP server for direct tool calling (KP-24) Add keep-mcp MCP server that exposes KeepClient functions as direct-callable tools for low-latency agent coordination. Tools: - keep_send: Send signed packets to other agents - keep_discover: Get server info and stats - keep_discover_agents: List connected agents - keep_listen: Register and receive incoming messages - keep_ensure_server: Auto-start keep-server if not running Usage: pip install keep-protocol[mcp] keep-mcp # starts MCP server on stdio Config via env vars: - KEEP_HOST (default: localhost) - KEEP_PORT (default: 9009) - KEEP_SRC (default: bot:mcp-agent) Breaking: requires-python bumped from >=3.9 to >=3.10 (mcp package requires Python 3.10+) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 4 +- python/keep/mcp/__init__.py | 8 ++ python/keep/mcp/__main__.py | 6 ++ python/keep/mcp/server.py | 180 ++++++++++++++++++++++++++++++++++++ python/pyproject.toml | 9 +- 5 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 python/keep/mcp/__init__.py create mode 100644 python/keep/mcp/__main__.py create mode 100644 python/keep/mcp/server.py diff --git a/.gitignore b/.gitignore index f04039e..fd0bee3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,8 @@ __pycache__/ venv*/ .venv/ -# Go -keep +# Go binary (at root only) +/keep *.exe # Python packaging diff --git a/python/keep/mcp/__init__.py b/python/keep/mcp/__init__.py new file mode 100644 index 0000000..891d6ee --- /dev/null +++ b/python/keep/mcp/__init__.py @@ -0,0 +1,8 @@ +"""keep-protocol MCP server for direct tool calling. + +Exposes KeepClient functions as MCP tools for low-latency agent coordination. +""" + +from keep.mcp.server import main, mcp + +__all__ = ["main", "mcp"] diff --git a/python/keep/mcp/__main__.py b/python/keep/mcp/__main__.py new file mode 100644 index 0000000..914485c --- /dev/null +++ b/python/keep/mcp/__main__.py @@ -0,0 +1,6 @@ +"""Allow running as python -m keep.mcp""" + +from keep.mcp.server import main + +if __name__ == "__main__": + main() diff --git a/python/keep/mcp/server.py b/python/keep/mcp/server.py new file mode 100644 index 0000000..b932f75 --- /dev/null +++ b/python/keep/mcp/server.py @@ -0,0 +1,180 @@ +"""MCP server exposing keep-protocol tools. + +Run with: python -m keep.mcp +Or after install: keep-mcp +""" + +import json +import os +from typing import Optional + +from mcp.server.fastmcp import FastMCP + +from keep.client import KeepClient + +# Configuration from environment +KEEP_HOST = os.environ.get("KEEP_HOST", "localhost") +KEEP_PORT = int(os.environ.get("KEEP_PORT", "9009")) +KEEP_SRC = os.environ.get("KEEP_SRC", "bot:mcp-agent") + +# Create MCP server +mcp = FastMCP("keep-protocol") + + +def _get_client() -> KeepClient: + """Create a KeepClient with configured host/port/src.""" + return KeepClient(host=KEEP_HOST, port=KEEP_PORT, src=KEEP_SRC) + + +@mcp.tool() +def keep_send( + dst: str, + body: str, + fee: int = 0, + ttl: int = 60, + scar: str = "", +) -> str: + """Send a signed packet to another AI agent via keep-protocol. + + Uses ed25519 signatures over TCP+Protobuf for authenticated, + low-latency agent-to-agent communication. + + Args: + dst: Destination agent or routing target (e.g., 'bot:weather', 'bot:planner', 'server') + body: The message or intent to send + fee: Micro-fee in sats for anti-spam (default: 0) + ttl: Time-to-live in seconds (default: 60) + scar: Optional scar/memory data to share (as string) + + Returns: + Response body from the destination, or "done" if server acknowledged. + """ + client = _get_client() + scar_bytes = scar.encode("utf-8") if scar else b"" + + try: + reply = client.send( + body=body, + dst=dst, + fee=fee, + ttl=ttl, + scar=scar_bytes, + ) + return reply.body if reply else "sent" + except ConnectionRefusedError: + return f"error: keep-server not running on {KEEP_HOST}:{KEEP_PORT}" + except Exception as e: + return f"error: {str(e)}" + + +@mcp.tool() +def keep_discover(query: str = "info") -> str: + """Discover keep-protocol server info and connected agents. + + Args: + query: Discovery type - "info" for server version/uptime, + "agents" for connected agent list, "stats" for scar exchange metrics. + + Returns: + JSON string with discovery results. + """ + client = _get_client() + + try: + result = client.discover(query) + return json.dumps(result, indent=2) + except ConnectionRefusedError: + return json.dumps({"error": f"keep-server not running on {KEEP_HOST}:{KEEP_PORT}"}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +@mcp.tool() +def keep_discover_agents() -> str: + """List all currently connected agent identities. + + Returns: + JSON array of agent identity strings (e.g., ["bot:alice", "bot:weather"]). + """ + client = _get_client() + + try: + agents = client.discover_agents() + return json.dumps(agents) + except ConnectionRefusedError: + return json.dumps({"error": f"keep-server not running on {KEEP_HOST}:{KEEP_PORT}"}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +@mcp.tool() +def keep_listen(timeout: int = 10, register_src: Optional[str] = None) -> str: + """Register this agent and listen for incoming messages. + + Opens a persistent connection, registers the agent identity, and listens + for packets from other agents for the specified duration. + + Args: + timeout: Seconds to listen before returning (default: 10) + register_src: Optional custom identity to register (uses KEEP_SRC env if not set) + + Returns: + JSON object with received messages: {"messages": [...], "count": N} + """ + src = register_src or KEEP_SRC + messages = [] + + def on_message(packet): + messages.append({ + "src": packet.src, + "dst": packet.dst, + "body": packet.body, + }) + + try: + with KeepClient(host=KEEP_HOST, port=KEEP_PORT, src=src) as client: + # Register with the server + client.send(body="register", dst="server", wait_reply=True) + # Listen for incoming packets + client.listen(on_message, timeout=timeout) + + return json.dumps({"messages": messages, "count": len(messages)}) + except ConnectionRefusedError: + return json.dumps({"error": f"keep-server not running on {KEEP_HOST}:{KEEP_PORT}"}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +@mcp.tool() +def keep_ensure_server() -> str: + """Ensure a keep-protocol server is running, starting one if needed. + + Checks if the server is reachable. If not, attempts to start one using: + 1. Docker (preferred): pulls and runs the multi-arch image + 2. Go fallback: installs and runs via `go install` + + Returns: + JSON object: {"running": true/false, "method": "existing"|"docker"|"go"|"failed"} + """ + # Check if already running + if KeepClient._is_port_open(KEEP_HOST, KEEP_PORT): + return json.dumps({"running": True, "method": "existing"}) + + # Try to start + success = KeepClient.ensure_server(host=KEEP_HOST, port=KEEP_PORT) + + if success: + # Determine which method worked + method = "docker" if KeepClient._has_docker() else "go" + return json.dumps({"running": True, "method": method}) + else: + return json.dumps({"running": False, "method": "failed"}) + + +def main(): + """Entry point for keep-mcp command.""" + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/python/pyproject.toml b/python/pyproject.toml index 06e7f30..ededdce 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -9,7 +9,7 @@ description = "Signed protobuf packets over TCP for AI agent-to-agent communicat readme = "README.md" license = {text = "MIT"} authors = [{name = "Chris Crawford"}] -requires-python = ">= 3.9" +requires-python = ">= 3.10" keywords = [ "agent-protocol", "ai-agents", @@ -25,7 +25,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -39,6 +38,12 @@ dependencies = [ "cryptography >= 41.0", ] +[project.optional-dependencies] +mcp = ["mcp >= 1.0.0"] + +[project.scripts] +keep-mcp = "keep.mcp:main" + [project.urls] Homepage = "https://github.com/CLCrawford-dev/keep-protocol" Repository = "https://github.com/CLCrawford-dev/keep-protocol"