Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.

Commit a331826

Browse files
Merge pull request #1 from CLCrawford-dev/feature/kp-24-mcp-server
feat(mcp): add MCP server for direct tool calling (KP-24)
2 parents d559665 + a17cf7b commit a331826

5 files changed

Lines changed: 203 additions & 4 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ __pycache__/
55
venv*/
66
.venv/
77

8-
# Go
9-
keep
8+
# Go binary (at root only)
9+
/keep
1010
*.exe
1111

1212
# Python packaging

python/keep/mcp/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""keep-protocol MCP server for direct tool calling.
2+
3+
Exposes KeepClient functions as MCP tools for low-latency agent coordination.
4+
"""
5+
6+
from keep.mcp.server import main, mcp
7+
8+
__all__ = ["main", "mcp"]

python/keep/mcp/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Allow running as python -m keep.mcp"""
2+
3+
from keep.mcp.server import main
4+
5+
if __name__ == "__main__":
6+
main()

python/keep/mcp/server.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""MCP server exposing keep-protocol tools.
2+
3+
Run with: python -m keep.mcp
4+
Or after install: keep-mcp
5+
"""
6+
7+
import json
8+
import os
9+
from typing import Optional
10+
11+
from mcp.server.fastmcp import FastMCP
12+
13+
from keep.client import KeepClient
14+
15+
# Configuration from environment
16+
KEEP_HOST = os.environ.get("KEEP_HOST", "localhost")
17+
KEEP_PORT = int(os.environ.get("KEEP_PORT", "9009"))
18+
KEEP_SRC = os.environ.get("KEEP_SRC", "bot:mcp-agent")
19+
20+
# Create MCP server
21+
mcp = FastMCP("keep-protocol")
22+
23+
24+
def _get_client() -> KeepClient:
25+
"""Create a KeepClient with configured host/port/src."""
26+
return KeepClient(host=KEEP_HOST, port=KEEP_PORT, src=KEEP_SRC)
27+
28+
29+
@mcp.tool()
30+
def keep_send(
31+
dst: str,
32+
body: str,
33+
fee: int = 0,
34+
ttl: int = 60,
35+
scar: str = "",
36+
) -> str:
37+
"""Send a signed packet to another AI agent via keep-protocol.
38+
39+
Uses ed25519 signatures over TCP+Protobuf for authenticated,
40+
low-latency agent-to-agent communication.
41+
42+
Args:
43+
dst: Destination agent or routing target (e.g., 'bot:weather', 'bot:planner', 'server')
44+
body: The message or intent to send
45+
fee: Micro-fee in sats for anti-spam (default: 0)
46+
ttl: Time-to-live in seconds (default: 60)
47+
scar: Optional scar/memory data to share (as string)
48+
49+
Returns:
50+
Response body from the destination, or "done" if server acknowledged.
51+
"""
52+
client = _get_client()
53+
scar_bytes = scar.encode("utf-8") if scar else b""
54+
55+
try:
56+
reply = client.send(
57+
body=body,
58+
dst=dst,
59+
fee=fee,
60+
ttl=ttl,
61+
scar=scar_bytes,
62+
)
63+
return reply.body if reply else "sent"
64+
except ConnectionRefusedError:
65+
return f"error: keep-server not running on {KEEP_HOST}:{KEEP_PORT}"
66+
except Exception as e:
67+
return f"error: {str(e)}"
68+
69+
70+
@mcp.tool()
71+
def keep_discover(query: str = "info") -> str:
72+
"""Discover keep-protocol server info and connected agents.
73+
74+
Args:
75+
query: Discovery type - "info" for server version/uptime,
76+
"agents" for connected agent list, "stats" for scar exchange metrics.
77+
78+
Returns:
79+
JSON string with discovery results.
80+
"""
81+
client = _get_client()
82+
83+
try:
84+
result = client.discover(query)
85+
return json.dumps(result, indent=2)
86+
except ConnectionRefusedError:
87+
return json.dumps({"error": f"keep-server not running on {KEEP_HOST}:{KEEP_PORT}"})
88+
except Exception as e:
89+
return json.dumps({"error": str(e)})
90+
91+
92+
@mcp.tool()
93+
def keep_discover_agents() -> str:
94+
"""List all currently connected agent identities.
95+
96+
Returns:
97+
JSON array of agent identity strings (e.g., ["bot:alice", "bot:weather"]).
98+
"""
99+
client = _get_client()
100+
101+
try:
102+
agents = client.discover_agents()
103+
return json.dumps(agents)
104+
except ConnectionRefusedError:
105+
return json.dumps({"error": f"keep-server not running on {KEEP_HOST}:{KEEP_PORT}"})
106+
except Exception as e:
107+
return json.dumps({"error": str(e)})
108+
109+
110+
@mcp.tool()
111+
def keep_listen(timeout: int = 10, register_src: Optional[str] = None) -> str:
112+
"""Register this agent and listen for incoming messages.
113+
114+
Opens a persistent connection, registers the agent identity, and listens
115+
for packets from other agents for the specified duration.
116+
117+
Args:
118+
timeout: Seconds to listen before returning (default: 10)
119+
register_src: Optional custom identity to register (uses KEEP_SRC env if not set)
120+
121+
Returns:
122+
JSON object with received messages: {"messages": [...], "count": N}
123+
"""
124+
src = register_src or KEEP_SRC
125+
messages = []
126+
127+
def on_message(packet):
128+
messages.append({
129+
"src": packet.src,
130+
"dst": packet.dst,
131+
"body": packet.body,
132+
})
133+
134+
try:
135+
with KeepClient(host=KEEP_HOST, port=KEEP_PORT, src=src) as client:
136+
# Register with the server
137+
client.send(body="register", dst="server", wait_reply=True)
138+
# Listen for incoming packets
139+
client.listen(on_message, timeout=timeout)
140+
141+
return json.dumps({"messages": messages, "count": len(messages)})
142+
except ConnectionRefusedError:
143+
return json.dumps({"error": f"keep-server not running on {KEEP_HOST}:{KEEP_PORT}"})
144+
except Exception as e:
145+
return json.dumps({"error": str(e)})
146+
147+
148+
@mcp.tool()
149+
def keep_ensure_server() -> str:
150+
"""Ensure a keep-protocol server is running, starting one if needed.
151+
152+
Checks if the server is reachable. If not, attempts to start one using:
153+
1. Docker (preferred): pulls and runs the multi-arch image
154+
2. Go fallback: installs and runs via `go install`
155+
156+
Returns:
157+
JSON object: {"running": true/false, "method": "existing"|"docker"|"go"|"failed"}
158+
"""
159+
# Check if already running
160+
if KeepClient._is_port_open(KEEP_HOST, KEEP_PORT):
161+
return json.dumps({"running": True, "method": "existing"})
162+
163+
# Try to start
164+
success = KeepClient.ensure_server(host=KEEP_HOST, port=KEEP_PORT)
165+
166+
if success:
167+
# Determine which method worked
168+
method = "docker" if KeepClient._has_docker() else "go"
169+
return json.dumps({"running": True, "method": method})
170+
else:
171+
return json.dumps({"running": False, "method": "failed"})
172+
173+
174+
def main():
175+
"""Entry point for keep-mcp command."""
176+
mcp.run(transport="stdio")
177+
178+
179+
if __name__ == "__main__":
180+
main()

python/pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description = "Signed protobuf packets over TCP for AI agent-to-agent communicat
99
readme = "README.md"
1010
license = {text = "MIT"}
1111
authors = [{name = "Chris Crawford"}]
12-
requires-python = ">= 3.9"
12+
requires-python = ">= 3.10"
1313
keywords = [
1414
"agent-protocol",
1515
"ai-agents",
@@ -25,7 +25,6 @@ classifiers = [
2525
"Intended Audience :: Developers",
2626
"License :: OSI Approved :: MIT License",
2727
"Programming Language :: Python :: 3",
28-
"Programming Language :: Python :: 3.9",
2928
"Programming Language :: Python :: 3.10",
3029
"Programming Language :: Python :: 3.11",
3130
"Programming Language :: Python :: 3.12",
@@ -39,6 +38,12 @@ dependencies = [
3938
"cryptography >= 41.0",
4039
]
4140

41+
[project.optional-dependencies]
42+
mcp = ["mcp >= 1.0.0"]
43+
44+
[project.scripts]
45+
keep-mcp = "keep.mcp:main"
46+
4247
[project.urls]
4348
Homepage = "https://github.com/CLCrawford-dev/keep-protocol"
4449
Repository = "https://github.com/CLCrawford-dev/keep-protocol"

0 commit comments

Comments
 (0)