@aliwey/bmo 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/bin/bmo.js +188 -0
- package/cli.py +1129 -0
- package/config/__init__.py +0 -0
- package/config/__pycache__/__init__.cpython-313.pyc +0 -0
- package/config/__pycache__/settings.cpython-313.pyc +0 -0
- package/config/__pycache__/system-prompt.cpython-313.pyc +0 -0
- package/config/settings.py +104 -0
- package/config/system-prompt.json +18 -0
- package/core/__init__.py +0 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_a2a_bridge.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_agent.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_agent_card.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_connector.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_discovery.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_identity.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_tasks.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_transport.cpython-313.pyc +0 -0
- package/core/__pycache__/bmo_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
- package/core/__pycache__/budget_tracker.cpython-313.pyc +0 -0
- package/core/__pycache__/cli_renderer.cpython-313.pyc +0 -0
- package/core/__pycache__/goal_runner.cpython-313.pyc +0 -0
- package/core/__pycache__/request_worker.cpython-313.pyc +0 -0
- package/core/__pycache__/security.cpython-313.pyc +0 -0
- package/core/__pycache__/shared_state.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_multiproc.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_protocol.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_subprocess.cpython-313.pyc +0 -0
- package/core/bfp_a2a_bridge.py +399 -0
- package/core/bfp_agent.py +98 -0
- package/core/bfp_agent_card.py +161 -0
- package/core/bfp_connector.py +177 -0
- package/core/bfp_discovery.py +105 -0
- package/core/bfp_identity.py +83 -0
- package/core/bfp_tasks.py +70 -0
- package/core/bfp_transport.py +368 -0
- package/core/bmo_engine.py +405 -0
- package/core/bot_client.py +838 -0
- package/core/budget_tracker.py +62 -0
- package/core/cli_renderer.py +177 -0
- package/core/goal_runner.py +129 -0
- package/core/request_worker.py +242 -0
- package/core/security.py +42 -0
- package/core/shared_state.py +4 -0
- package/core/worker_manager.py +71 -0
- package/core/worker_multiproc.py +155 -0
- package/core/worker_protocol.py +30 -0
- package/core/worker_subprocess.py +222 -0
- package/handlers/__init__.py +0 -0
- package/handlers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/handlers/__pycache__/messages.cpython-313.pyc +0 -0
- package/handlers/messages.py +2761 -0
- package/main.py +125 -0
- package/memory.md +43 -0
- package/models/__init__.py +0 -0
- package/models/__pycache__/__init__.cpython-313.pyc +0 -0
- package/models/__pycache__/chat_models.cpython-313.pyc +0 -0
- package/models/chat_models.py +143 -0
- package/package.json +50 -0
- package/registry/worker.js +108 -0
- package/registry/wrangler.toml +11 -0
- package/requirements.txt +13 -0
- package/scripts/bmo_init.js +115 -0
- package/scripts/postinstall.js +265 -0
- package/scripts/relay_cmd.js +276 -0
- package/scripts/web_cmd.js +136 -0
- package/setup.py +26 -0
- package/storage/__init__.py +0 -0
- package/storage/__pycache__/__init__.cpython-313.pyc +0 -0
- package/storage/__pycache__/sqlite_storage.cpython-313.pyc +0 -0
- package/storage/__pycache__/storage.cpython-313.pyc +0 -0
- package/storage/sqlite_storage.py +658 -0
- package/storage/storage.py +265 -0
- package/tools/__pycache__/bfp_relay.cpython-313.pyc +0 -0
- package/tools/__pycache__/get_session_summaries.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_bridge.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_server.cpython-313.pyc +0 -0
- package/tools/__pycache__/run_mcp_standalone.cpython-313.pyc +0 -0
- package/tools/__pycache__/task_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/test_mcp_connection.cpython-313.pyc +0 -0
- package/tools/bfp_relay.py +359 -0
- package/tools/bot.db +0 -0
- package/tools/get_session_summaries.py +45 -0
- package/tools/mcp_bridge.py +109 -0
- package/tools/mcp_server.py +531 -0
- package/tools/register_mcp_task.py +20 -0
- package/tools/run_detached.bat +32 -0
- package/tools/run_mcp_standalone.py +16 -0
- package/tools/task_registry.py +184 -0
- package/tools/test_mcp_connection.py +80 -0
- package/webchat/package-lock.json +1528 -0
- package/webchat/package.json +12 -0
- package/webchat/public/app.js +1293 -0
- package/webchat/public/index.html +226 -0
- package/webchat/public/index.html.bak +416 -0
- package/webchat/public/styles.css +2435 -0
- package/webchat/server.js +645 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Run just the MCP SSE server standalone (without Telegram bot)."""
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env"))
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO)
|
|
10
|
+
|
|
11
|
+
import uvicorn
|
|
12
|
+
from tools.mcp_server import mcp
|
|
13
|
+
|
|
14
|
+
print("Starting BMO MCP Server on port 4097 (standalone)...")
|
|
15
|
+
print(f"TELEGRAM_BOT_TOKEN: {'SET' if os.getenv('TELEGRAM_BOT_TOKEN') or os.getenv('TELEGRAM_TOKEN') else 'NOT SET'}")
|
|
16
|
+
uvicorn.run(mcp.sse_app, host="127.0.0.1", port=4097, log_level="info")
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Background Task Registry — tracks all running background tasks (servers, tunnels, etc.)
|
|
3
|
+
Auto-cleans dead PIDs on read. Thread-safe with file locking.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import psutil
|
|
10
|
+
import threading
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
15
|
+
REGISTRY_PATH = os.path.join(PROJECT_ROOT, "data", "background_tasks.json")
|
|
16
|
+
_lock = threading.Lock()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ensure_registry():
|
|
20
|
+
"""Create registry file if it doesn't exist."""
|
|
21
|
+
if not os.path.exists(REGISTRY_PATH):
|
|
22
|
+
os.makedirs(os.path.dirname(REGISTRY_PATH), exist_ok=True)
|
|
23
|
+
with open(REGISTRY_PATH, "w", encoding="utf-8") as f:
|
|
24
|
+
json.dump({"tasks": []}, f, indent=2)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_process_alive(pid: int) -> bool:
|
|
28
|
+
"""Check if a process with the given PID is still running."""
|
|
29
|
+
try:
|
|
30
|
+
proc = psutil.Process(pid)
|
|
31
|
+
return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE
|
|
32
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cleanup_dead_tasks(tasks: list) -> list:
|
|
37
|
+
"""Mark dead PIDs as stopped. Returns cleaned list."""
|
|
38
|
+
cleaned = []
|
|
39
|
+
for task in tasks:
|
|
40
|
+
if task.get("status") == "running" and not _is_process_alive(task["pid"]):
|
|
41
|
+
task["status"] = "stopped"
|
|
42
|
+
task["stopped_at"] = datetime.now().isoformat()
|
|
43
|
+
task["stop_reason"] = "process_terminated"
|
|
44
|
+
cleaned.append(task)
|
|
45
|
+
return cleaned
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def read_registry() -> dict:
|
|
49
|
+
"""Read the task registry, auto-cleaning dead PIDs."""
|
|
50
|
+
with _lock:
|
|
51
|
+
_ensure_registry()
|
|
52
|
+
try:
|
|
53
|
+
with open(REGISTRY_PATH, "r", encoding="utf-8") as f:
|
|
54
|
+
data = json.load(f)
|
|
55
|
+
except (json.JSONDecodeError, IOError):
|
|
56
|
+
data = {"tasks": []}
|
|
57
|
+
|
|
58
|
+
data["tasks"] = _cleanup_dead_tasks(data.get("tasks", []))
|
|
59
|
+
_save_unlocked(data)
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _save_unlocked(data: dict):
|
|
64
|
+
"""Save registry without acquiring lock (caller must hold lock)."""
|
|
65
|
+
with open(REGISTRY_PATH, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write_registry(data: dict):
|
|
70
|
+
"""Write the task registry (thread-safe)."""
|
|
71
|
+
with _lock:
|
|
72
|
+
_ensure_registry()
|
|
73
|
+
data["tasks"] = _cleanup_dead_tasks(data.get("tasks", []))
|
|
74
|
+
_save_unlocked(data)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def register_task(pid: int, command: str, port: int = None, task_type: str = "server",
|
|
78
|
+
description: str = "", log_file: str = "") -> dict:
|
|
79
|
+
"""Register a new background task."""
|
|
80
|
+
data = read_registry()
|
|
81
|
+
task = {
|
|
82
|
+
"pid": pid,
|
|
83
|
+
"command": command,
|
|
84
|
+
"port": port,
|
|
85
|
+
"type": task_type,
|
|
86
|
+
"status": "running",
|
|
87
|
+
"url": "",
|
|
88
|
+
"log_file": log_file,
|
|
89
|
+
"start_time": datetime.now().isoformat(),
|
|
90
|
+
"description": description,
|
|
91
|
+
}
|
|
92
|
+
data["tasks"].append(task)
|
|
93
|
+
write_registry(data)
|
|
94
|
+
return task
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def update_task(pid: int, **kwargs) -> bool:
|
|
98
|
+
"""Update fields of an existing task by PID."""
|
|
99
|
+
data = read_registry()
|
|
100
|
+
for task in data["tasks"]:
|
|
101
|
+
if task["pid"] == pid:
|
|
102
|
+
task.update(kwargs)
|
|
103
|
+
write_registry(data)
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def remove_task(pid: int) -> bool:
|
|
109
|
+
"""Remove a task from the registry."""
|
|
110
|
+
data = read_registry()
|
|
111
|
+
original_len = len(data["tasks"])
|
|
112
|
+
data["tasks"] = [t for t in data["tasks"] if t["pid"] != pid]
|
|
113
|
+
if len(data["tasks"]) < original_len:
|
|
114
|
+
write_registry(data)
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_task(pid: int) -> dict | None:
|
|
120
|
+
"""Get a single task by PID."""
|
|
121
|
+
data = read_registry()
|
|
122
|
+
for task in data["tasks"]:
|
|
123
|
+
if task["pid"] == pid:
|
|
124
|
+
return task
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_active_tasks() -> list:
|
|
129
|
+
"""Get all running tasks (auto-cleans dead ones)."""
|
|
130
|
+
data = read_registry()
|
|
131
|
+
return [t for t in data["tasks"] if t.get("status") == "running"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_tasks_by_port(port: int) -> list:
|
|
135
|
+
"""Get all tasks using a specific port."""
|
|
136
|
+
data = read_registry()
|
|
137
|
+
return [t for t in data["tasks"] if t.get("port") == port]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_used_ports() -> list:
|
|
141
|
+
"""Get all ports currently in use by running tasks."""
|
|
142
|
+
data = read_registry()
|
|
143
|
+
return [t["port"] for t in data["tasks"] if t.get("status") == "running" and t.get("port")]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_port_conflict(port: int) -> dict | None:
|
|
147
|
+
"""Check if a port is already in use. Returns conflicting task or None."""
|
|
148
|
+
tasks = get_tasks_by_port(port)
|
|
149
|
+
for t in tasks:
|
|
150
|
+
if t.get("status") == "running":
|
|
151
|
+
return t
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def format_task_list(tasks: list = None) -> str:
|
|
156
|
+
"""Format task list for display to the user."""
|
|
157
|
+
if tasks is None:
|
|
158
|
+
tasks = get_active_tasks()
|
|
159
|
+
|
|
160
|
+
if not tasks:
|
|
161
|
+
return "No active background tasks."
|
|
162
|
+
|
|
163
|
+
lines = ["📋 Active Background Tasks:", ""]
|
|
164
|
+
for i, task in enumerate(tasks, 1):
|
|
165
|
+
status_icon = "🟢" if task.get("status") == "running" else "🔴"
|
|
166
|
+
type_icon = {
|
|
167
|
+
"tunnel": "🌐",
|
|
168
|
+
"server": "🖥️",
|
|
169
|
+
"webchat": "💬",
|
|
170
|
+
"static": "📁",
|
|
171
|
+
}.get(task.get("type", "server"), "⚙️")
|
|
172
|
+
|
|
173
|
+
url_str = f" → {task['url']}" if task.get("url") else ""
|
|
174
|
+
desc_str = f" — {task['description']}" if task.get("description") else ""
|
|
175
|
+
|
|
176
|
+
lines.append(
|
|
177
|
+
f"{i}. {status_icon} {type_icon} [{task['type']}] "
|
|
178
|
+
f"PID: {task['pid']}, Port: {task.get('port', 'N/A')}"
|
|
179
|
+
f"{url_str}{desc_str}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
lines.append("")
|
|
183
|
+
lines.append(f"Total: {len(tasks)} active task(s)")
|
|
184
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Test MCP SSE connection and call send_telegram_message."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import anyio
|
|
5
|
+
from mcp.client.sse import sse_client
|
|
6
|
+
from mcp.types import JSONRPCMessage
|
|
7
|
+
from mcp.shared.message import SessionMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def main():
|
|
11
|
+
print("Connecting to MCP SSE server at http://127.0.0.1:4097/sse ...")
|
|
12
|
+
async with sse_client(url="http://127.0.0.1:4097/sse") as (read, write):
|
|
13
|
+
print("Connected!")
|
|
14
|
+
|
|
15
|
+
# 1. Initialize
|
|
16
|
+
init = {
|
|
17
|
+
"jsonrpc": "2.0",
|
|
18
|
+
"id": 1,
|
|
19
|
+
"method": "initialize",
|
|
20
|
+
"params": {
|
|
21
|
+
"protocolVersion": "2024-11-05",
|
|
22
|
+
"capabilities": {},
|
|
23
|
+
"clientInfo": {"name": "bmo-bridge", "version": "1.0.0"},
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
msg = JSONRPCMessage.model_validate(init)
|
|
27
|
+
await write.send(SessionMessage(message=msg))
|
|
28
|
+
|
|
29
|
+
async for msg in read:
|
|
30
|
+
data = msg.message.model_dump()
|
|
31
|
+
print(f"Initialize response: {json.dumps(data, indent=2)[:300]}")
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
# 2. Send initialized notification
|
|
35
|
+
notif = {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
|
|
36
|
+
msg = JSONRPCMessage.model_validate(notif)
|
|
37
|
+
await write.send(SessionMessage(message=msg))
|
|
38
|
+
|
|
39
|
+
# 3. List tools
|
|
40
|
+
list_tools = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
|
|
41
|
+
msg = JSONRPCMessage.model_validate(list_tools)
|
|
42
|
+
await write.send(SessionMessage(message=msg))
|
|
43
|
+
|
|
44
|
+
async for msg in read:
|
|
45
|
+
data = msg.message.model_dump()
|
|
46
|
+
if "result" in data:
|
|
47
|
+
tools = data["result"].get("tools", [])
|
|
48
|
+
print(f"\nFound {len(tools)} tools:")
|
|
49
|
+
for t in tools:
|
|
50
|
+
print(f" - {t['name']}: {t.get('description', '')[:80]}")
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
# 4. Call send_telegram_message
|
|
54
|
+
call = {
|
|
55
|
+
"jsonrpc": "2.0",
|
|
56
|
+
"id": 3,
|
|
57
|
+
"method": "tools/call",
|
|
58
|
+
"params": {
|
|
59
|
+
"name": "send_telegram_message",
|
|
60
|
+
"arguments": {
|
|
61
|
+
"chat_id": 732356803,
|
|
62
|
+
"message": "TEST from BMO MCP bridge ✅ Server tools connected!",
|
|
63
|
+
"parse_mode": "HTML",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
print("\nCalling send_telegram_message...")
|
|
68
|
+
msg = JSONRPCMessage.model_validate(call)
|
|
69
|
+
await write.send(SessionMessage(message=msg))
|
|
70
|
+
|
|
71
|
+
async for msg in read:
|
|
72
|
+
data = msg.message.model_dump()
|
|
73
|
+
print(f"Result: {json.dumps(data, indent=2)[:500]}")
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
print("\nDone!")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
asyncio.run(main())
|