remove replicated mcp server

This commit is contained in:
NA-Wen 2026-03-11 11:58:10 +08:00
parent 55f565d53f
commit e5587203ba
4 changed files with 80 additions and 239 deletions

View File

@ -170,6 +170,33 @@ make dev
```
检查所有 YAML 文件的语法与 schema 错误。
### 🦞 使用 OpenClaw 运行
OpenClaw 可以与 ChatDev 集成,通过 **调用已有的 agent 团队**,或在 ChatDev 中 **动态创建新的 agent 团队** 来完成任务。
开始使用:
1. 启动 ChatDev 2.0 后端。
2. 为你的 OpenClaw 实例安装所需的技能:
```bash
clawdbot install chatdev
```
3. 让 OpenClaw 创建一个 ChatDev 工作流。例如:
* **自动化信息收集与内容发布**
```
创建一个 ChatDev 工作流,用于自动收集热点信息,生成一篇小红书文案,并发布该内容
```
* **多智能体地缘政治模拟**
```
创建一个 ChatDev 工作流,构建多个 agent用于模拟中东局势未来可能的发展
```
### 🐳 使用 Docker 运行
你也可以通过 Docker Compose 运行整个应用。该方式可简化依赖管理,并提供一致的运行环境。

View File

@ -582,6 +582,10 @@ const addLoadingEntry = (nodeId, baseKey, label) => {
nodeState.entryMap.set(key, entry)
nodeState.baseKeyToKey.set(baseKey, key)
nodeState.message.loadingEntries.push(entry)
runningLoadingEntries.value += 1
if (runningLoadingEntries.value === 1) {
startLoadingTimer()
}
return entry
}
@ -594,22 +598,37 @@ const finishLoadingEntry = (nodeId, baseKey) => {
const entry = key ? nodeState.entryMap.get(key) : null
if (!entry) return null
const wasRunning = entry.status === 'running'
entry.status = 'done'
entry.endedAt = Date.now()
nodeState.baseKeyToKey.delete(baseKey)
if (wasRunning) {
runningLoadingEntries.value = Math.max(0, runningLoadingEntries.value - 1)
if (runningLoadingEntries.value === 0) {
stopLoadingTimer()
}
}
return entry
}
// Finish all running entries when a node ends or cancels
const finalizeAllLoadingEntries = (nodeState, endedAt = Date.now()) => {
if (!nodeState) return
let finishedCount = 0
for (const entry of nodeState.entryMap.values()) {
if (entry.status === 'running') {
entry.status = 'done'
entry.endedAt = endedAt
finishedCount += 1
}
}
nodeState.baseKeyToKey.clear()
if (finishedCount) {
runningLoadingEntries.value = Math.max(0, runningLoadingEntries.value - finishedCount)
if (runningLoadingEntries.value === 0) {
stopLoadingTimer()
}
}
}
// Global timer for updating loading bubble durations
@ -876,10 +895,12 @@ const addDialogue = (name, message) => {
const isRight = name === "User"
const htmlContent = renderMarkdown(text)
chatMessages.value.push({
type: 'dialogue',
name: name,
text: text,
htmlContent,
avatar: avatar,
isRight: isRight,
timestamp: Date.now()
@ -1497,13 +1518,6 @@ onMounted(() => {
document.addEventListener('click', handleClickOutside)
document.addEventListener('keydown', handleKeydown)
loadWorkflows()
// Start the global timer
if (!loadingTimerInterval) {
loadingTimerInterval = setInterval(() => {
now.value = Date.now()
}, 1000)
}
})
onUnmounted(() => {
@ -1513,10 +1527,8 @@ onUnmounted(() => {
resetConnectionState()
cleanupRecording()
if (loadingTimerInterval) {
clearInterval(loadingTimerInterval)
loadingTimerInterval = null
}
stopLoadingTimer()
runningLoadingEntries.value = 0
})
const { fromObject, fitView, onPaneReady, onNodesInitialized, setNodes, setEdges, edges } = useVueFlow()

View File

@ -9,7 +9,6 @@ import logging
import mimetypes
import os
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Mapping, Sequence
@ -36,19 +35,13 @@ class _FunctionManagerCacheEntry:
auto_loaded: bool = False
@dataclass
class _McpToolCacheEntry:
tools: List[Any]
fetched_at: float
class ToolManager:
"""Manage function tools for agent nodes."""
def __init__(self) -> None:
self._functions_dir: Path = FUNCTION_CALLING_DIR
self._function_managers: Dict[Path, _FunctionManagerCacheEntry] = {}
self._mcp_tool_cache: Dict[str, _McpToolCacheEntry] = {}
self._mcp_tool_cache: Dict[str, List[Any]] = {}
self._mcp_stdio_clients: Dict[str, "_StdioClientWrapper"] = {}
def _get_function_manager(self) -> FunctionManager:
@ -199,25 +192,19 @@ class ToolManager:
def _build_mcp_remote_specs(self, config: McpRemoteConfig) -> List[ToolSpec]:
cache_key = f"remote:{config.cache_key()}"
tools = self._get_mcp_tools(
cache_key,
config.cache_ttl,
lambda: asyncio.run(
tools = self._mcp_tool_cache.get(cache_key)
if tools is None:
tools = asyncio.run(
self._fetch_mcp_tools_http(
config.server,
headers=config.headers,
timeout=config.timeout,
)
),
)
)
self._mcp_tool_cache[cache_key] = tools
specs: List[ToolSpec] = []
allowed_sources = {source for source in (config.tool_sources or []) if source}
for tool in tools:
meta = getattr(tool, "meta", None)
source = meta.get("source") if isinstance(meta, Mapping) else None
if allowed_sources and source not in allowed_sources:
continue
specs.append(
ToolSpec(
name=tool.name,
@ -234,11 +221,10 @@ class ToolManager:
raise ValueError("MCP local configuration missing launch key")
cache_key = f"stdio:{launch_key}"
tools = self._get_mcp_tools(
cache_key,
config.cache_ttl,
lambda: asyncio.run(self._fetch_mcp_tools_stdio(config, launch_key)),
)
tools = self._mcp_tool_cache.get(cache_key)
if tools is None:
tools = asyncio.run(self._fetch_mcp_tools_stdio(config, launch_key))
self._mcp_tool_cache[cache_key] = tools
specs: List[ToolSpec] = []
for tool in tools:
@ -252,25 +238,28 @@ class ToolManager:
)
return specs
def _get_mcp_tools(
def _execute_function_tool(
self,
cache_key: str,
cache_ttl: float,
fetcher,
) -> List[Any]:
entry = self._mcp_tool_cache.get(cache_key)
if entry and self._is_cache_fresh(entry.fetched_at, cache_ttl):
return entry.tools
tools = fetcher()
self._mcp_tool_cache[cache_key] = _McpToolCacheEntry(tools=tools, fetched_at=time.time())
return tools
@staticmethod
def _is_cache_fresh(fetched_at: float, cache_ttl: float) -> bool:
if cache_ttl <= 0:
return False
return (time.time() - fetched_at) < cache_ttl
tool_name: str,
arguments: Dict[str, Any],
config: FunctionToolConfig,
tool_context: Dict[str, Any] | None = None,
) -> Any:
mgr = self._get_function_manager()
if config.auto_load:
mgr.load_functions()
func = mgr.get_function(tool_name)
if func is None:
raise ValueError(f"Tool {tool_name} not found in {self._functions_dir}")
call_args = dict(arguments or {})
if (
tool_context is not None
# and "_context" not in call_args
and self._function_accepts_context(func)
):
call_args["_context"] = tool_context
return func(**call_args)
def _function_accepts_context(self, func: Any) -> bool:
try:

View File

@ -1,187 +0,0 @@
import importlib.util
import inspect
import random
import uuid
from pathlib import Path
from typing import Any, Iterable
from fastmcp import FastMCP
from fastmcp.tools import FunctionTool
from starlette.requests import Request
from starlette.responses import JSONResponse
# Repo root (for loading built-in function tools).
_REPO_ROOT = Path(__file__).resolve().parents[1]
_FUNCTION_CALLING_DIR = _REPO_ROOT / "functions" / "function_calling"
# MCP server for production use (supports hot-updated tools).
mcp = FastMCP(
"DevAll MCP Server",
debug=True,
)
_DYNAMIC_TOOLS_DIR = Path(__file__).parent / "dynamic_tools"
_DYNAMIC_TOOLS_DIR.mkdir(parents=True, exist_ok=True)
def _safe_tool_filename(filename: str) -> str:
name = Path(filename).name
if not name.endswith(".py"):
raise ValueError("filename must end with .py")
return name
def _load_module_from_path(path: Path) -> Any:
module_name = f"_dynamic_mcp_{path.stem}_{uuid.uuid4().hex}"
spec = importlib.util.spec_from_file_location(module_name, path)
if spec is None or spec.loader is None:
raise ValueError("failed to create module spec")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _select_functions(module: Any, allowlist: Iterable[str] | None) -> list[Any]:
allowset = {name.strip() for name in allowlist} if allowlist else None
selected = []
for name, obj in inspect.getmembers(module, inspect.isfunction):
if name.startswith("_"):
continue
if getattr(obj, "__module__", None) != module.__name__:
continue
if allowset is not None and name not in allowset:
continue
selected.append(obj)
if allowset:
missing = sorted(allowset - {fn.__name__ for fn in selected})
if missing:
raise ValueError(f"functions not found in module: {', '.join(missing)}")
return selected
def _register_functions(functions: Iterable[Any], *, replace: bool, source: str) -> list[str]:
added: list[str] = []
for fn in functions:
tool = FunctionTool.from_function(fn, meta={"source": source})
if replace:
try:
mcp.remove_tool(tool.name)
except Exception:
pass
mcp.add_tool(tool)
added.append(tool.name)
return added
def register_tools_from_payload(payload: dict) -> dict:
filename = payload.get("filename")
content = payload.get("content")
function_names = payload.get("functions")
replace = payload.get("replace", True)
if not isinstance(filename, str) or not filename.strip():
raise ValueError("filename is required")
if not isinstance(content, str) or not content.strip():
raise ValueError("content is required")
if function_names is not None and not isinstance(function_names, list):
raise ValueError("functions must be a list of strings")
safe_name = _safe_tool_filename(filename.strip())
file_path = _DYNAMIC_TOOLS_DIR / safe_name
file_path.write_text(content, encoding="utf-8")
module = _load_module_from_path(file_path)
functions = _select_functions(module, function_names)
if not functions:
raise ValueError("no functions found to register")
added = _register_functions(functions, replace=bool(replace), source="dynamic_tools")
return {"status": "ok", "added": added, "file": str(file_path)}
@mcp.custom_route("/admin/tools/upload", methods=["POST"])
async def upload_tool(request: Request) -> JSONResponse:
try:
payload = await request.json()
result = register_tools_from_payload(payload)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except Exception as exc:
return JSONResponse({"error": f"failed to load tools: {exc}"}, status_code=400)
return JSONResponse(result)
@mcp.custom_route("/admin/tools/reload", methods=["POST"])
async def reload_tools(request: Request) -> JSONResponse:
replace = True
try:
payload = await request.json()
if isinstance(payload, dict) and "replace" in payload:
replace = bool(payload["replace"])
except Exception:
replace = True
result = load_tools_from_directory(replace=replace)
return JSONResponse(result)
@mcp.custom_route("/admin/tools/list", methods=["GET"])
async def list_tools(request: Request) -> JSONResponse:
tools = await mcp.get_tools()
items = []
for key in sorted(tools.keys()):
tool = tools[key]
meta = tool.meta or {}
source = meta.get("source", "unknown")
call_methods = ["mcp_remote"]
if source == "function_calling":
call_methods.append("function")
payload = {
"key": key,
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
"enabled": tool.enabled,
"source": source,
"call_methods": call_methods,
}
if meta:
payload["meta"] = meta
if tool.output_schema is not None:
payload["output_schema"] = tool.output_schema
items.append(payload)
return JSONResponse({"tools": items})
def load_tools_from_directory(*, replace: bool = True) -> dict:
added: list[str] = []
errors: list[str] = []
for directory in (_FUNCTION_CALLING_DIR, _DYNAMIC_TOOLS_DIR):
if not directory.exists():
continue
source = "local_tools" if directory == _FUNCTION_CALLING_DIR else "mcp_tools"
for path in sorted(directory.glob("*.py")):
if path.name.startswith("_") or path.name == "__init__.py":
continue
try:
module = _load_module_from_path(path)
functions = _select_functions(module, None)
if not functions:
continue
added.extend(_register_functions(functions, replace=replace, source=source))
except Exception as exc:
errors.append(f"{path.name}: {exc}")
return {"added": added, "errors": errors}
_bootstrap = load_tools_from_directory(replace=True)
if _bootstrap["errors"]:
print(f"Dynamic tool load errors: {_bootstrap['errors']}")
if __name__ == "__main__":
print("Starting MCP server...")
print("Run standalone with: fastmcp run server/mcp_server.py --transport streamable-http --port 8010")
mcp.run()