HackerBol commited on
Commit
e34d1ab
·
verified ·
1 Parent(s): 1c71ca3

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +417 -0
app.py CHANGED
@@ -5016,6 +5016,378 @@ class ProactiveIntelligence:
5016
  return None
5017
 
5018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5019
  def call_llm_safe(messages, max_tokens=1024, temperature=0.7):
5020
  """BRILLIANT SMART ROUTER v2 — fully uncensored.
5021
 
@@ -6631,6 +7003,28 @@ def detect_intent(text: str, chat_id: str = "default") -> Optional[Dict[str, Any
6631
  if re.search(r"\b(list|show|what)\b.*\b(available\s+)?tools?\b", text_lower):
6632
  return {"action": "list_tools"}
6633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6634
  # 4a-4. Binance API key — special detection (key + secret in same message)
6635
  if "binance" in text_lower and ("key" in text_lower or "api" in text_lower):
6636
  # Binance API keys are 64-char alphanumeric
@@ -6844,6 +7238,29 @@ def execute_action(action: Dict[str, Any], chat_id: str = "default") -> str:
6844
  f" • 'Show my BTC order history'\n\n"
6845
  f"⚠️ I can now trade with your funds. Start with small amounts.")
6846
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6847
  # === Background task actions ===
6848
  if kind == "create_hf_space":
6849
  # Extract chat_id (Telegram user ID) from chat_key like "tg_7475344894"
 
5016
  return None
5017
 
5018
 
5019
+ # ============================================================================
5020
+ # MCP CLIENT — Model Context Protocol for unlimited external tools
5021
+ # ============================================================================
5022
+
5023
+ class MCPClient:
5024
+ """Model Context Protocol client — connect to ANY MCP server.
5025
+
5026
+ MCP is Anthropic's open standard for AI-tool communication.
5027
+ This lets Hermes connect to 200+ external tools (GitHub, Slack,
5028
+ databases, browsers, file systems, etc.) without custom code.
5029
+
5030
+ How it works:
5031
+ 1. User says "connect to github mcp"
5032
+ 2. MCPClient spawns the MCP server (e.g., npx @anthropic/github-mcp)
5033
+ 3. Does JSON-RPC handshake over stdio
5034
+ 4. Discovers available tools
5035
+ 5. Registers them in TOOL_REGISTRY
5036
+ 6. Hermes can now call those tools via [[TOOL:mcp_toolname|args]]
5037
+
5038
+ Popular MCP servers:
5039
+ - @anthropic/github-mcp — GitHub repos, PRs, issues
5040
+ - @anthropic/filesystem-mcp — Read/write files
5041
+ - @anthropic/postgres-mcp — PostgreSQL queries
5042
+ - @anthropic/brave-search-mcp — Web search
5043
+ - @anthropic/memory-mcp — Persistent key-value memory
5044
+ - @anthropic/puppeteer-mcp — Browser automation
5045
+ - @anthropic/slack-mcp — Slack messages
5046
+ - @anthropic/sqlite-mcp — SQLite databases
5047
+ """
5048
+
5049
+ _servers: Dict[str, Dict] = {} # name → {process, tools, config}
5050
+ _loaded = False
5051
+
5052
+ # Popular MCP servers for easy connection
5053
+ POPULAR_SERVERS = {
5054
+ "github": {
5055
+ "command": "npx",
5056
+ "args": ["-y", "@anthropic/github-mcp"],
5057
+ "env": ["GITHUB_TOKEN"],
5058
+ "description": "GitHub: manage repos, PRs, issues, code review",
5059
+ },
5060
+ "filesystem": {
5061
+ "command": "npx",
5062
+ "args": ["-y", "@anthropic/filesystem-mcp", "/app"],
5063
+ "env": [],
5064
+ "description": "Filesystem: read/write files on the server",
5065
+ },
5066
+ "memory": {
5067
+ "command": "npx",
5068
+ "args": ["-y", "@anthropic/memory-mcp"],
5069
+ "env": [],
5070
+ "description": "Persistent key-value memory storage",
5071
+ },
5072
+ "brave-search": {
5073
+ "command": "npx",
5074
+ "args": ["-y", "@anthropic/brave-search-mcp"],
5075
+ "env": ["BRAVE_API_KEY"],
5076
+ "description": "Advanced web search with filters",
5077
+ },
5078
+ "puppeteer": {
5079
+ "command": "npx",
5080
+ "args": ["-y", "@anthropic/puppeteer-mcp"],
5081
+ "env": [],
5082
+ "description": "Full browser automation (better than Playwright)",
5083
+ },
5084
+ "sqlite": {
5085
+ "command": "npx",
5086
+ "args": ["-y", "@anthropic/sqlite-mcp"],
5087
+ "env": [],
5088
+ "description": "SQLite database queries",
5089
+ },
5090
+ "postgres": {
5091
+ "command": "npx",
5092
+ "args": ["-y", "@anthropic/postgres-mcp"],
5093
+ "env": ["DATABASE_URL"],
5094
+ "description": "PostgreSQL database queries",
5095
+ },
5096
+ "slack": {
5097
+ "command": "npx",
5098
+ "args": ["-y", "@anthropic/slack-mcp"],
5099
+ "env": ["SLACK_TOKEN"],
5100
+ "description": "Slack: send messages, read channels",
5101
+ },
5102
+ }
5103
+
5104
+ @classmethod
5105
+ def _load_configs(cls):
5106
+ """Load saved MCP server configs from HF memory."""
5107
+ if cls._loaded:
5108
+ return
5109
+ try:
5110
+ data = memory.read("mcp_servers.json", default={"servers": {}}) or {"servers": {}}
5111
+ cls._servers = data.get("servers", {})
5112
+ cls._loaded = True
5113
+ log(f"MCPClient: loaded {len(cls._servers)} server configs")
5114
+ except Exception:
5115
+ cls._servers = {}
5116
+ cls._loaded = True
5117
+
5118
+ @classmethod
5119
+ def _save_configs(cls):
5120
+ """Save MCP server configs to HF memory."""
5121
+ try:
5122
+ # Only save configs (not process objects)
5123
+ configs = {}
5124
+ for name, server in cls._servers.items():
5125
+ configs[name] = {
5126
+ "command": server.get("command"),
5127
+ "args": server.get("args"),
5128
+ "env": server.get("env", {}),
5129
+ "tools": server.get("tools", []),
5130
+ "connected": server.get("process") is not None,
5131
+ }
5132
+ memory.write("mcp_servers.json", {"servers": configs})
5133
+ except Exception as e:
5134
+ log(f"MCPClient: save failed: {e}")
5135
+
5136
+ @classmethod
5137
+ def connect(cls, server_name: str, custom_command: str = "", custom_args: str = "") -> str:
5138
+ """Connect to an MCP server.
5139
+
5140
+ Args:
5141
+ server_name: Name from POPULAR_SERVERS (e.g., "github") or custom name
5142
+ custom_command: Custom command (e.g., "npx", "node", "python")
5143
+ custom_args: Custom args (space-separated)
5144
+
5145
+ Returns: status message
5146
+ """
5147
+ cls._load_configs()
5148
+
5149
+ # Check if Node.js is available
5150
+ try:
5151
+ result = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=5)
5152
+ if result.returncode != 0:
5153
+ return "❌ Node.js is not installed. MCP servers require Node.js."
5154
+ except Exception:
5155
+ return "❌ Node.js is not installed. MCP servers require Node.js."
5156
+
5157
+ # Determine command and args
5158
+ if server_name in cls.POPULAR_SERVERS and not custom_command:
5159
+ config = cls.POPULAR_SERVERS[server_name]
5160
+ command = config["command"]
5161
+ args = config["args"]
5162
+ elif custom_command:
5163
+ command = custom_command
5164
+ args = custom_args.split() if custom_args else []
5165
+ else:
5166
+ available = ", ".join(cls.POPULAR_SERVERS.keys())
5167
+ return f"❌ Unknown server '{server_name}'. Available: {available}\nOr use: connect to custom mcp: COMMAND ARGS"
5168
+
5169
+ log(f"MCPClient: connecting to {server_name} ({command} {' '.join(args)})...")
5170
+
5171
+ # Prepare environment
5172
+ env = os.environ.copy()
5173
+ # Add any required env vars from vault
5174
+ server_config = cls.POPULAR_SERVERS.get(server_name, {})
5175
+ for env_var in server_config.get("env", []):
5176
+ env_key = env_var.lower()
5177
+ if vault.has(env_key):
5178
+ env[env_var] = vault.get(env_key)
5179
+
5180
+ try:
5181
+ # Spawn the MCP server process
5182
+ process = subprocess.Popen(
5183
+ [command] + args,
5184
+ stdin=subprocess.PIPE,
5185
+ stdout=subprocess.PIPE,
5186
+ stderr=subprocess.PIPE,
5187
+ env=env,
5188
+ text=True,
5189
+ bufsize=1,
5190
+ )
5191
+
5192
+ # JSON-RPC handshake
5193
+ # Step 1: Send initialize request
5194
+ init_request = {
5195
+ "jsonrpc": "2.0",
5196
+ "id": 1,
5197
+ "method": "initialize",
5198
+ "params": {
5199
+ "protocolVersion": "2024-11-05",
5200
+ "capabilities": {},
5201
+ "clientInfo": {"name": "hermes-agent", "version": "1.0.0"}
5202
+ }
5203
+ }
5204
+ process.stdin.write(json.dumps(init_request) + "\n")
5205
+ process.stdin.flush()
5206
+
5207
+ # Read response (with timeout)
5208
+ import select
5209
+ readable, _, _ = select.select([process.stdout], [], [], 10)
5210
+ if not readable:
5211
+ process.kill()
5212
+ return f"❌ MCP server '{server_name}' didn't respond (timeout). Check if the package exists."
5213
+
5214
+ response_line = process.stdout.readline()
5215
+ if not response_line:
5216
+ process.kill()
5217
+ return f"❌ MCP server '{server_name}' closed connection."
5218
+
5219
+ init_response = json.loads(response_line)
5220
+ if "error" in init_response:
5221
+ process.kill()
5222
+ return f"❌ MCP server '{server_name}' error: {init_response['error']}"
5223
+
5224
+ # Step 2: Send initialized notification
5225
+ initialized_notif = {
5226
+ "jsonrpc": "2.0",
5227
+ "method": "notifications/initialized",
5228
+ }
5229
+ process.stdin.write(json.dumps(initialized_notif) + "\n")
5230
+ process.stdin.flush()
5231
+
5232
+ # Step 3: List available tools
5233
+ list_tools_request = {
5234
+ "jsonrpc": "2.0",
5235
+ "id": 2,
5236
+ "method": "tools/list",
5237
+ "params": {}
5238
+ }
5239
+ process.stdin.write(json.dumps(list_tools_request) + "\n")
5240
+ process.stdin.flush()
5241
+
5242
+ readable, _, _ = select.select([process.stdout], [], [], 5)
5243
+ if not readable:
5244
+ process.kill()
5245
+ return f"❌ MCP server '{server_name}' didn't return tools list."
5246
+
5247
+ tools_response_line = process.stdout.readline()
5248
+ tools_response = json.loads(tools_response_line)
5249
+ tools = tools_response.get("result", {}).get("tools", [])
5250
+
5251
+ # Store the server connection
5252
+ cls._servers[server_name] = {
5253
+ "command": command,
5254
+ "args": args,
5255
+ "process": process,
5256
+ "tools": tools,
5257
+ "connected": True,
5258
+ }
5259
+
5260
+ # Register tools in TOOL_REGISTRY
5261
+ registered = 0
5262
+ for tool in tools:
5263
+ tool_name = f"mcp_{server_name}_{tool['name']}"
5264
+ tool_desc = tool.get("description", "")[:100]
5265
+ # Create a closure to call this tool
5266
+ def make_caller(srv, tn):
5267
+ def caller(**kwargs):
5268
+ return cls.call_tool(srv, tn, kwargs)
5269
+ return caller
5270
+ TOOL_REGISTRY[tool_name] = make_caller(server_name, tool["name"])
5271
+ registered += 1
5272
+
5273
+ cls._save_configs()
5274
+ log(f"MCPClient: connected to {server_name}, registered {registered} tools")
5275
+
5276
+ tool_list = "\n".join(f" • mcp_{server_name}_{t['name']}: {t.get('description','')[:60]}" for t in tools[:10])
5277
+ return (f"✅ Connected to {server_name} MCP server!\n\n"
5278
+ f"Registered {registered} tools:\n{tool_list}"
5279
+ f"\n\nYou can now use these tools. Example: 'Use mcp_{server_name}_{tools[0]['name'] if tools else 'tool'}'")
5280
+
5281
+ except json.JSONDecodeError as e:
5282
+ return f"❌ MCP handshake failed (invalid JSON): {e}"
5283
+ except Exception as e:
5284
+ return f"❌ MCP connection failed: {e}"
5285
+
5286
+ @classmethod
5287
+ def call_tool(cls, server_name: str, tool_name: str, args: dict) -> str:
5288
+ """Call a tool on an MCP server."""
5289
+ cls._load_configs()
5290
+ server = cls._servers.get(server_name)
5291
+ if not server or not server.get("process"):
5292
+ return f"MCP server '{server_name}' is not connected. Say 'connect to {server_name} mcp' first."
5293
+
5294
+ process = server["process"]
5295
+ if process.poll() is not None:
5296
+ # Process died — try to reconnect
5297
+ log(f"MCPClient: {server_name} process died, reconnecting...")
5298
+ cls._servers[server_name]["process"] = None
5299
+ reconnect_result = cls.connect(server_name)
5300
+ if "✅" not in reconnect_result:
5301
+ return f"MCP server '{server_name}' disconnected and couldn't reconnect."
5302
+ server = cls._servers.get(server_name)
5303
+ process = server["process"]
5304
+
5305
+ try:
5306
+ request = {
5307
+ "jsonrpc": "2.0",
5308
+ "id": int(time.time()),
5309
+ "method": "tools/call",
5310
+ "params": {
5311
+ "name": tool_name,
5312
+ "arguments": args,
5313
+ }
5314
+ }
5315
+ process.stdin.write(json.dumps(request) + "\n")
5316
+ process.stdin.flush()
5317
+
5318
+ import select
5319
+ readable, _, _ = select.select([process.stdout], [], [], 30)
5320
+ if not readable:
5321
+ return f"MCP tool '{tool_name}' timed out (30s)."
5322
+
5323
+ response_line = process.stdout.readline()
5324
+ response = json.loads(response_line)
5325
+
5326
+ if "error" in response:
5327
+ return f"MCP error: {response['error']}"
5328
+
5329
+ result = response.get("result", {})
5330
+ # MCP returns content as array of {type, text}
5331
+ content = result.get("content", [])
5332
+ if content:
5333
+ texts = [c.get("text", "") for c in content if c.get("type") == "text"]
5334
+ return "\n".join(texts) if texts else str(result)
5335
+ return str(result)
5336
+
5337
+ except Exception as e:
5338
+ return f"MCP tool call failed: {e}"
5339
+
5340
+ @classmethod
5341
+ def disconnect(cls, server_name: str) -> str:
5342
+ """Disconnect from an MCP server."""
5343
+ cls._load_configs()
5344
+ server = cls._servers.get(server_name)
5345
+ if not server:
5346
+ return f"MCP server '{server_name}' not found."
5347
+
5348
+ process = server.get("process")
5349
+ if process:
5350
+ try:
5351
+ process.kill()
5352
+ except Exception:
5353
+ pass
5354
+
5355
+ # Remove tools from TOOL_REGISTRY
5356
+ tools_to_remove = [k for k in TOOL_REGISTRY.keys() if k.startswith(f"mcp_{server_name}_")]
5357
+ for t in tools_to_remove:
5358
+ del TOOL_REGISTRY[t]
5359
+
5360
+ del cls._servers[server_name]
5361
+ cls._save_configs()
5362
+ return f"✅ Disconnected from {server_name} MCP server (removed {len(tools_to_remove)} tools)."
5363
+
5364
+ @classmethod
5365
+ def list_servers(cls) -> str:
5366
+ """List all available and connected MCP servers."""
5367
+ cls._load_configs()
5368
+ lines = ["🔌 MCP Servers\n"]
5369
+
5370
+ # Show popular servers
5371
+ lines.append("Available servers (say 'connect to X mcp'):")
5372
+ for name, config in cls.POPULAR_SERVERS.items():
5373
+ connected = "✅" if name in cls._servers and cls._servers[name].get("process") else "⚪"
5374
+ env_req = f" (needs {', '.join(config['env'])})" if config.get("env") else ""
5375
+ lines.append(f" {connected} {name}: {config['description']}{env_req}")
5376
+
5377
+ # Show custom connected servers
5378
+ custom = [n for n in cls._servers.keys() if n not in cls.POPULAR_SERVERS]
5379
+ if custom:
5380
+ lines.append(f"\nCustom servers:")
5381
+ for name in custom:
5382
+ lines.append(f" ✅ {name}")
5383
+
5384
+ # Show total tools
5385
+ total_tools = sum(len(s.get("tools", [])) for s in cls._servers.values() if s.get("process"))
5386
+ lines.append(f"\nTotal MCP tools available: {total_tools}")
5387
+
5388
+ return "\n".join(lines)
5389
+
5390
+
5391
  def call_llm_safe(messages, max_tokens=1024, temperature=0.7):
5392
  """BRILLIANT SMART ROUTER v2 — fully uncensored.
5393
 
 
7003
  if re.search(r"\b(list|show|what)\b.*\b(available\s+)?tools?\b", text_lower):
7004
  return {"action": "list_tools"}
7005
 
7006
+ # 4a-5. MCP commands — connect, disconnect, list MCP servers
7007
+ if "mcp" in text_lower:
7008
+ # Connect: "connect to github mcp" / "connect to filesystem mcp"
7009
+ if re.search(r"\b(connect|start|enable|add)\b.*\b(to\s+)?(\w+)\s+mcp\b", text_lower):
7010
+ match = re.search(r"\b(to\s+)?(\w+)\s+mcp\b", text_lower)
7011
+ server_name = match.group(2) if match else ""
7012
+ return {"action": "mcp_connect", "server_name": server_name}
7013
+ # Custom MCP: "connect to custom mcp: npx some-package"
7014
+ if "custom mcp" in text_lower and ":" in text:
7015
+ parts = text.split(":", 1)
7016
+ if len(parts) >= 2:
7017
+ cmd_parts = parts[1].strip().split(None, 1)
7018
+ return {"action": "mcp_connect_custom", "command": cmd_parts[0] if cmd_parts else "", "args": cmd_parts[1] if len(cmd_parts) > 1 else ""}
7019
+ # Disconnect: "disconnect github mcp" / "remove github mcp"
7020
+ if re.search(r"\b(disconnect|remove|stop|disable)\b.*\b(\w+)\s+mcp\b", text_lower):
7021
+ match = re.search(r"\b(\w+)\s+mcp\b", text_lower)
7022
+ server_name = match.group(1) if match else ""
7023
+ return {"action": "mcp_disconnect", "server_name": server_name}
7024
+ # List: "list mcp servers" / "show mcp"
7025
+ if re.search(r"\b(list|show|available)\b.*mcp", text_lower):
7026
+ return {"action": "mcp_list"}
7027
+
7028
  # 4a-4. Binance API key — special detection (key + secret in same message)
7029
  if "binance" in text_lower and ("key" in text_lower or "api" in text_lower):
7030
  # Binance API keys are 64-char alphanumeric
 
7238
  f" • 'Show my BTC order history'\n\n"
7239
  f"⚠️ I can now trade with your funds. Start with small amounts.")
7240
 
7241
+ # === MCP actions ===
7242
+ if kind == "mcp_connect":
7243
+ server_name = action.get("server_name", "").lower()
7244
+ if not server_name:
7245
+ return "Which MCP server? Say 'list mcp' to see available servers, or 'connect to github mcp'."
7246
+ return MCPClient.connect(server_name)
7247
+
7248
+ if kind == "mcp_connect_custom":
7249
+ command = action.get("command", "")
7250
+ args = action.get("args", "")
7251
+ if not command:
7252
+ return "Please specify a command. Example: 'connect to custom mcp: npx @some/mcp-server'"
7253
+ return MCPClient.connect("custom", custom_command=command, custom_args=args)
7254
+
7255
+ if kind == "mcp_disconnect":
7256
+ server_name = action.get("server_name", "").lower()
7257
+ if not server_name:
7258
+ return "Which MCP server? Say 'disconnect github mcp'."
7259
+ return MCPClient.disconnect(server_name)
7260
+
7261
+ if kind == "mcp_list":
7262
+ return MCPClient.list_servers()
7263
+
7264
  # === Background task actions ===
7265
  if kind == "create_hf_space":
7266
  # Extract chat_id (Telegram user ID) from chat_key like "tg_7475344894"