Phase G5: Add platform_bridge MCP package (mcp_server + sync_client)
This commit is contained in:
1 parent
891d1ce527
commit
46fb67026d
3 files changed
+465
No files matched your search
@@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
Platform Bridge - Cloud-to-Local Agent Bridge
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
A Python package providing MCP server and sync client for bridging
|
||||||
|
Goose (local agent) with the Qihu-AIFlow cloud platform.
|
||||||
|
|
||||||
|
Modules:
|
||||||
|
- mcp_server: MCP-compatible server for Goose (stdio transport)
|
||||||
|
- sync_client: Periodic sync manager for KB, license, usage
|
||||||
|
- __init__: Package interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .mcp_server import PlatformBridgeClient, PlatformBridgeMCPServer, main
|
||||||
|
from .sync_client import SyncManager, create_sync_manager
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__all__ = [
|
||||||
|
"PlatformBridgeClient",
|
||||||
|
"PlatformBridgeMCPServer",
|
||||||
|
"SyncManager",
|
||||||
|
"create_sync_manager",
|
||||||
|
"main",
|
||||||
|
]
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
"""
|
||||||
|
Platform Bridge MCP Server
|
||||||
|
===========================
|
||||||
|
Goose MCP extension that bridges local Goose with the cloud platform.
|
||||||
|
|
||||||
|
Provides tools:
|
||||||
|
- fetch_agent_config: Get agent configuration from platform
|
||||||
|
- sync_knowledge_base: Sync knowledge base to local
|
||||||
|
- report_usage: Report anonymized usage statistics
|
||||||
|
- validate_license: Validate offline license
|
||||||
|
- get_platform_status: Check platform health
|
||||||
|
"""
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__name__ = "platform_bridge"
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformBridgeClient:
|
||||||
|
"""Client for the cloud platform API"""
|
||||||
|
|
||||||
|
def __init__(self, api_url: str = None, user_id: str = None, api_key: str = None):
|
||||||
|
self.api_url = api_url or os.environ.get(
|
||||||
|
"PLATFORM_API_URL", "http://210.16.161.218:18080"
|
||||||
|
)
|
||||||
|
self.user_id = user_id or os.environ.get("PLATFORM_USER_ID", "local-agent")
|
||||||
|
self.api_key = api_key or os.environ.get("PLATFORM_API_KEY", "")
|
||||||
|
self.base_path = "/app/api/admin/privacy"
|
||||||
|
|
||||||
|
def _request(self, method: str, path: str, params: dict = None) -> dict:
|
||||||
|
"""Make an HTTP request to the platform API"""
|
||||||
|
url = f"{self.api_url}{path}"
|
||||||
|
if params:
|
||||||
|
query_parts = []
|
||||||
|
for k, v in params.items():
|
||||||
|
if v is not None:
|
||||||
|
query_parts.append(f"{k}={urllib.request.quote(str(v))}")
|
||||||
|
if query_parts:
|
||||||
|
url += "?" + "&".join(query_parts)
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, method=method)
|
||||||
|
if self.api_key:
|
||||||
|
req.add_header("Authorization", f"Bearer {self.api_key}")
|
||||||
|
req.add_header("Accept", "application/json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
data = json.loads(resp.read().decode())
|
||||||
|
return data
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return {"code": 1, "error": f"HTTP {e.code}: {e.reason}"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"code": 1, "error": str(e)}
|
||||||
|
|
||||||
|
def fetch_agent_config(self, agent_id: str = None) -> dict:
|
||||||
|
"""Fetch agent configuration from platform"""
|
||||||
|
params = {"user_id": self.user_id}
|
||||||
|
if agent_id:
|
||||||
|
params["agent_id"] = agent_id
|
||||||
|
return self._request("GET", f"{self.base_path}/local-agents", params)
|
||||||
|
|
||||||
|
def sync_knowledge_base(self, kb_id: int, direction: str = "pull") -> dict:
|
||||||
|
"""Sync knowledge base between platform and local"""
|
||||||
|
return self._request("POST", f"{self.base_path}/kb-sync/trigger", {
|
||||||
|
"knowledge_base_id": kb_id,
|
||||||
|
"sync_direction": direction,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def report_usage(self, metrics: dict, report_type: str = "daily") -> dict:
|
||||||
|
"""Submit anonymized usage report"""
|
||||||
|
return self._request("POST", f"{self.base_path}/usage/report", {
|
||||||
|
"metrics": json.dumps(metrics),
|
||||||
|
"report_type": report_type,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def validate_license(self, license_key: str, device_fingerprint: str = None) -> dict:
|
||||||
|
"""Validate offline license"""
|
||||||
|
params = {"license_key": license_key, "user_id": self.user_id}
|
||||||
|
if device_fingerprint:
|
||||||
|
params["device_fingerprint"] = device_fingerprint
|
||||||
|
return self._request("POST", f"{self.base_path}/licenses/validate", params)
|
||||||
|
|
||||||
|
def get_privacy_config(self) -> dict:
|
||||||
|
"""Get current privacy configuration"""
|
||||||
|
return self._request("GET", f"{self.base_path}/config", {"user_id": self.user_id})
|
||||||
|
|
||||||
|
def get_bridge_config(self) -> dict:
|
||||||
|
"""Get Platform Bridge configuration for export"""
|
||||||
|
return self._request("GET", f"{self.base_path}/bridge-config", {"user_id": self.user_id})
|
||||||
|
|
||||||
|
def refresh_license(self, license_id: int, extend_days: int = 7) -> dict:
|
||||||
|
"""Refresh an existing license"""
|
||||||
|
return self._request("POST", f"{self.base_path}/licenses/{license_id}/refresh", {
|
||||||
|
"extend_days": extend_days,
|
||||||
|
})
|
||||||
|
|
||||||
|
def sync_all_knowledge_bases(self) -> List[dict]:
|
||||||
|
"""Sync all configured knowledge bases"""
|
||||||
|
config = self.get_privacy_config()
|
||||||
|
if config.get("code") != 0:
|
||||||
|
return [{"error": "Failed to get config"}]
|
||||||
|
|
||||||
|
data = config.get("data", {})
|
||||||
|
kb_list = data.get("sync_knowledge_bases", [])
|
||||||
|
if isinstance(kb_list, str):
|
||||||
|
try:
|
||||||
|
kb_list = json.loads(kb_list)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
kb_list = []
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for kb_id in (kb_list or []):
|
||||||
|
result = self.sync_knowledge_base(kb_id)
|
||||||
|
results.append({"kb_id": kb_id, "result": result})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def check_health(self) -> dict:
|
||||||
|
"""Check platform health and connectivity"""
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
result = self._request("GET", f"{self.base_path}/stats")
|
||||||
|
elapsed = time.time() - start
|
||||||
|
return {
|
||||||
|
"connected": True,
|
||||||
|
"latency_ms": round(elapsed * 1000),
|
||||||
|
"platform_version": "qihuaiflow-g5",
|
||||||
|
"stats": result.get("data", {}),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"connected": False,
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# MCP Server Interface (_stdio transport)
|
||||||
|
# ========================================================================
|
||||||
|
class PlatformBridgeMCPServer:
|
||||||
|
"""MCP-compatible server using stdio transport"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.client = PlatformBridgeClient()
|
||||||
|
self.tools = {
|
||||||
|
"fetch_agent_config": {
|
||||||
|
"name": "fetch_agent_config",
|
||||||
|
"description": "Fetch agent configuration from the cloud platform",
|
||||||
|
"parameters": {
|
||||||
|
"agent_id": {"type": "string", "description": "Optional agent ID", "required": False}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sync_knowledge_base": {
|
||||||
|
"name": "sync_knowledge_base",
|
||||||
|
"description": "Sync knowledge base documents from platform to local",
|
||||||
|
"parameters": {
|
||||||
|
"kb_id": {"type": "integer", "description": "Knowledge base ID", "required": True},
|
||||||
|
"direction": {"type": "string", "description": "Sync direction: pull/push/bidirectional", "required": False}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"report_usage": {
|
||||||
|
"name": "report_usage",
|
||||||
|
"description": "Report anonymized usage statistics to platform",
|
||||||
|
"parameters": {
|
||||||
|
"metrics": {"type": "object", "description": "Usage metrics JSON", "required": True},
|
||||||
|
"report_type": {"type": "string", "description": "Report type: daily/weekly/monthly/realtime", "required": False}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate_license": {
|
||||||
|
"name": "validate_license",
|
||||||
|
"description": "Validate offline license key",
|
||||||
|
"parameters": {
|
||||||
|
"license_key": {"type": "string", "description": "The license key to validate", "required": True},
|
||||||
|
"device_fingerprint": {"type": "string", "description": "Device fingerprint for binding", "required": False}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get_platform_status": {
|
||||||
|
"name": "get_platform_status",
|
||||||
|
"description": "Check cloud platform connectivity and health",
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
"get_agent_list": {
|
||||||
|
"name": "get_agent_list",
|
||||||
|
"description": "List available agents from the platform",
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
"sync_all_kb": {
|
||||||
|
"name": "sync_all_kb",
|
||||||
|
"description": "Sync all configured knowledge bases",
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
"get_privacy_config": {
|
||||||
|
"name": "get_privacy_config",
|
||||||
|
"description": "Get current privacy mode configuration",
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _handle_request(self, request: dict) -> dict:
|
||||||
|
"""Process an MCP tool request"""
|
||||||
|
tool_name = request.get("name", "")
|
||||||
|
args = request.get("arguments", {}) or {}
|
||||||
|
|
||||||
|
handler_map = {
|
||||||
|
"fetch_agent_config": lambda: self.client.fetch_agent_config(args.get("agent_id")),
|
||||||
|
"sync_knowledge_base": lambda: self.client.sync_knowledge_base(
|
||||||
|
args["kb_id"], args.get("direction", "pull")
|
||||||
|
),
|
||||||
|
"report_usage": lambda: self.client.report_usage(
|
||||||
|
args["metrics"], args.get("report_type", "daily")
|
||||||
|
),
|
||||||
|
"validate_license": lambda: self.client.validate_license(
|
||||||
|
args["license_key"], args.get("device_fingerprint")
|
||||||
|
),
|
||||||
|
"get_platform_status": lambda: self.client.check_health(),
|
||||||
|
"get_agent_list": lambda: self.client.fetch_agent_config(),
|
||||||
|
"sync_all_kb": lambda: self.client.sync_all_knowledge_bases(),
|
||||||
|
"get_privacy_config": lambda: self.client.get_privacy_config(),
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = handler_map.get(tool_name)
|
||||||
|
if not handler:
|
||||||
|
return {"error": f"Unknown tool: {tool_name}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = handler()
|
||||||
|
return result if isinstance(result, dict) else {"result": result}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def handle_tools_list(self) -> dict:
|
||||||
|
"""Return list of available tools (MCP tools/list)"""
|
||||||
|
return {"tools": list(self.tools.values())}
|
||||||
|
|
||||||
|
def handle_tools_call(self, request: dict) -> dict:
|
||||||
|
"""Handle a tool call (MCP tools/call)"""
|
||||||
|
tool_name = request.get("name", "")
|
||||||
|
args = request.get("arguments", {}) or {}
|
||||||
|
|
||||||
|
result = self._handle_request({"name": tool_name, "arguments": args})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run MCP server in stdio mode"""
|
||||||
|
print("Platform Bridge MCP Server v1.0", file=sys.stderr)
|
||||||
|
print(f"API URL: {self.client.api_url}", file=sys.stderr)
|
||||||
|
print(f"User ID: {self.client.user_id}", file=sys.stderr)
|
||||||
|
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
request = json.loads(line)
|
||||||
|
method = request.get("method", "")
|
||||||
|
|
||||||
|
if method == "tools/list":
|
||||||
|
response = self.handle_tools_list()
|
||||||
|
elif method == "tools/call":
|
||||||
|
response = self.handle_tools_call(request.get("params", {}))
|
||||||
|
elif method == "initialize":
|
||||||
|
response = {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"serverInfo": {"name": "platform-bridge", "version": "1.0.0"},
|
||||||
|
"capabilities": {"tools": {}}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response = {"error": f"Unknown method: {method}"}
|
||||||
|
|
||||||
|
if "id" in request:
|
||||||
|
response["id"] = request["id"]
|
||||||
|
response["jsonrpc"] = "2.0"
|
||||||
|
|
||||||
|
print(json.dumps(response), flush=True)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
err_resp = {"jsonrpc": "2.0", "error": {"code": -32700, "message": f"Parse error: {e}"}}
|
||||||
|
print(json.dumps(err_resp), flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
err_resp = {"jsonrpc": "2.0", "error": {"code": -32603, "message": str(e)}}
|
||||||
|
print(json.dumps(err_resp), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point for python -m platform_bridge.mcp_server"""
|
||||||
|
server = PlatformBridgeMCPServer()
|
||||||
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
Platform Bridge - Cloud-to-Local Agent Bridge
|
||||||
|
==============================================
|
||||||
|
WorkBuddy-compatible sync client for local Goose agent.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- Periodic knowledge base sync
|
||||||
|
- Usage metrics collection and reporting
|
||||||
|
- License validation and refresh
|
||||||
|
- Platform health monitoring
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Import the MCP server client
|
||||||
|
from .mcp_server import PlatformBridgeClient
|
||||||
|
|
||||||
|
|
||||||
|
class SyncManager:
|
||||||
|
"""Manages periodic synchronization between local agent and cloud platform"""
|
||||||
|
|
||||||
|
def __init__(self, client: PlatformBridgeClient = None):
|
||||||
|
self.client = client or PlatformBridgeClient()
|
||||||
|
self.sync_interval = 3600 # Default: 1 hour
|
||||||
|
self.last_sync = {}
|
||||||
|
self.last_license_check = None
|
||||||
|
self.last_usage_report = None
|
||||||
|
self.usage_buffer = []
|
||||||
|
|
||||||
|
def configure(self, config: dict):
|
||||||
|
"""Configure sync parameters from privacy config"""
|
||||||
|
frequency_map = {
|
||||||
|
"realtime": 60,
|
||||||
|
"hourly": 3600,
|
||||||
|
"daily": 86400,
|
||||||
|
"manual": None, # No auto sync
|
||||||
|
}
|
||||||
|
freq = config.get("sync_frequency", "daily")
|
||||||
|
self.sync_interval = frequency_map.get(freq, 86400)
|
||||||
|
self.client.user_id = config.get("user_id", self.client.user_id)
|
||||||
|
|
||||||
|
def should_sync_kb(self, kb_id: int) -> bool:
|
||||||
|
"""Check if a KB needs syncing"""
|
||||||
|
last = self.last_sync.get(kb_id, 0)
|
||||||
|
return (time.time() - last) >= self.sync_interval
|
||||||
|
|
||||||
|
def sync_kb(self, kb_id: int) -> dict:
|
||||||
|
"""Sync a single knowledge base"""
|
||||||
|
result = self.client.sync_knowledge_base(kb_id)
|
||||||
|
self.last_sync[kb_id] = time.time()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def sync_all(self, kb_ids: list) -> list:
|
||||||
|
"""Sync all configured knowledge bases"""
|
||||||
|
results = []
|
||||||
|
for kb_id in kb_ids:
|
||||||
|
if self.should_sync_kb(kb_id):
|
||||||
|
result = self.sync_kb(kb_id)
|
||||||
|
results.append({"kb_id": kb_id, "result": result})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def check_license(self, license_key: str) -> dict:
|
||||||
|
"""Check if license is still valid"""
|
||||||
|
now = time.time()
|
||||||
|
if self.last_license_check and (now - self.last_license_check) < 3600:
|
||||||
|
return {"valid": True, "cached": True}
|
||||||
|
|
||||||
|
result = self.client.validate_license(license_key)
|
||||||
|
self.last_license_check = now
|
||||||
|
return result
|
||||||
|
|
||||||
|
def track_usage(self, event_type: str, metadata: dict = None):
|
||||||
|
"""Buffer a usage event for reporting"""
|
||||||
|
self.usage_buffer.append({
|
||||||
|
"type": event_type,
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"metadata": metadata or {},
|
||||||
|
})
|
||||||
|
|
||||||
|
def report_usage(self) -> dict:
|
||||||
|
"""Submit buffered usage events"""
|
||||||
|
if not self.usage_buffer:
|
||||||
|
return {"reported": 0}
|
||||||
|
|
||||||
|
events = self.usage_buffer[:]
|
||||||
|
self.usage_buffer = []
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
"event_count": len(events),
|
||||||
|
"event_types": {},
|
||||||
|
"time_range": {
|
||||||
|
"start": min(e["timestamp"] for e in events),
|
||||||
|
"end": max(e["timestamp"] for e in events),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for e in events:
|
||||||
|
t = e["type"]
|
||||||
|
metrics["event_types"][t] = metrics["event_types"].get(t, 0) + 1
|
||||||
|
|
||||||
|
result = self.client.report_usage(metrics)
|
||||||
|
self.last_usage_report = time.time()
|
||||||
|
result["reported"] = len(events)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def health_check(self) -> dict:
|
||||||
|
"""Run a full health check"""
|
||||||
|
platform = self.client.check_health()
|
||||||
|
config = self.client.get_privacy_config()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"platform": platform,
|
||||||
|
"privacy_mode": config.get("data", {}).get("privacy_mode", "unknown"),
|
||||||
|
"pending_events": len(self.usage_buffer),
|
||||||
|
"last_license_check": self.last_license_check,
|
||||||
|
"last_usage_report": self.last_usage_report,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_sync_manager(api_url: str = None, user_id: str = None) -> SyncManager:
|
||||||
|
"""Factory function to create a configured SyncManager"""
|
||||||
|
client = PlatformBridgeClient(api_url=api_url, user_id=user_id)
|
||||||
|
manager = SyncManager(client)
|
||||||
|
|
||||||
|
# Load config
|
||||||
|
config_result = client.get_privacy_config()
|
||||||
|
if config_result.get("code") == 0:
|
||||||
|
manager.configure(config_result["data"])
|
||||||
|
|
||||||
|
return manager
|
||||||
Reference in new issue
Block a user