mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-17 13:05:58 +00:00
Merge branch 'main' into 2.0.0-release
This commit is contained in:
@@ -113,6 +113,7 @@ FastAPI application providing REST endpoints for frontend integration:
|
||||
|-------|---------|
|
||||
| `GET /api/models` | List available LLM models |
|
||||
| `GET/PUT /api/mcp/config` | Manage MCP server configurations |
|
||||
| `POST /api/mcp/cache/reset` | Reset cached MCP tools so they reload on next use |
|
||||
| `GET/PUT /api/skills` | List and manage skills |
|
||||
| `POST /api/skills/install` | Install skill from `.skill` archive |
|
||||
| `GET /api/memory` | Retrieve memory data |
|
||||
|
||||
@@ -172,7 +172,7 @@ class MessageBus:
|
||||
|
||||
def unsubscribe_outbound(self, callback: OutboundCallback) -> None:
|
||||
"""Remove a previously registered outbound callback."""
|
||||
self._outbound_listeners = [cb for cb in self._outbound_listeners if cb is not callback]
|
||||
self._outbound_listeners = [cb for cb in self._outbound_listeners if cb != callback]
|
||||
|
||||
async def publish_outbound(self, msg: OutboundMessage) -> None:
|
||||
"""Dispatch an outbound message to all registered listeners."""
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter, HTTPException, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from deerflow.config.extensions_config import ExtensionsConfig, get_extensions_config, reload_extensions_config
|
||||
from deerflow.mcp.cache import reset_mcp_tools_cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api", tags=["mcp"])
|
||||
@@ -69,6 +70,13 @@ class McpConfigUpdateRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class McpCacheResetResponse(BaseModel):
|
||||
"""Response model for resetting the MCP tools cache."""
|
||||
|
||||
success: bool = Field(description="Whether the MCP tools cache was reset")
|
||||
message: str = Field(description="Human-readable reset status")
|
||||
|
||||
|
||||
_MASKED_VALUE = "***"
|
||||
|
||||
|
||||
@@ -269,6 +277,27 @@ async def get_mcp_configuration(request: Request) -> McpConfigResponse:
|
||||
return McpConfigResponse(mcp_servers=servers)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/mcp/cache/reset",
|
||||
response_model=McpCacheResetResponse,
|
||||
summary="Reset MCP Tools Cache",
|
||||
description=("Reset cached MCP tools and pooled sessions process-wide so tools are reloaded on next use. This affects all threads and users in the current Gateway process."),
|
||||
)
|
||||
async def reset_mcp_tools_cache_endpoint(request: Request) -> McpCacheResetResponse:
|
||||
"""Reset cached MCP tools and persistent sessions process-wide.
|
||||
|
||||
The next agent run or tool lookup will reload tools from the configured MCP
|
||||
servers. This affects all threads and users in the current Gateway process,
|
||||
and avoids relying on extensions_config.json mtime changes.
|
||||
"""
|
||||
await _require_admin_user(request)
|
||||
reset_mcp_tools_cache()
|
||||
return McpCacheResetResponse(
|
||||
success=True,
|
||||
message="MCP tools cache reset. Tools will reload on next use.",
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/mcp/config",
|
||||
response_model=McpConfigResponse,
|
||||
@@ -363,6 +392,7 @@ async def update_mcp_configuration(request: Request, body: McpConfigUpdateReques
|
||||
# agent runtime lives in Gateway, so this keeps API reads and tool
|
||||
# execution aligned after extensions_config.json changes.
|
||||
reloaded_config = reload_extensions_config()
|
||||
reset_mcp_tools_cache()
|
||||
servers = {name: _mask_server_config(McpServerConfigResponse(**server.model_dump())) for name, server in reloaded_config.mcp_servers.items()}
|
||||
return McpConfigResponse(mcp_servers=servers)
|
||||
|
||||
|
||||
@@ -299,6 +299,26 @@ deployment needs additional trusted launchers.
|
||||
}
|
||||
```
|
||||
|
||||
#### Reset MCP Tools Cache
|
||||
|
||||
Clear cached MCP tools and persistent MCP sessions process-wide. This affects
|
||||
all threads and users in the current Gateway process. Tools are loaded again
|
||||
from configured MCP servers on the next agent run or tool lookup.
|
||||
|
||||
```http
|
||||
POST /api/mcp/cache/reset
|
||||
```
|
||||
|
||||
Requires an authenticated admin session.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "MCP tools cache reset. Tools will reload on next use."
|
||||
}
|
||||
```
|
||||
|
||||
### Skills
|
||||
|
||||
#### List Skills
|
||||
|
||||
@@ -427,17 +427,17 @@ SKILL.md Format:
|
||||
### Configuration Reload
|
||||
|
||||
```
|
||||
1. Client updates MCP config
|
||||
1. Client updates MCP config or requests a cache reset
|
||||
PUT /api/mcp/config
|
||||
POST /api/mcp/cache/reset
|
||||
|
||||
2. Gateway writes extensions_config.json
|
||||
- Updates mcpServers section
|
||||
- File mtime changes
|
||||
2. Gateway updates runtime state
|
||||
- PUT writes extensions_config.json and reloads configuration
|
||||
- Both endpoints reset the MCP tools cache and persistent sessions
|
||||
|
||||
3. MCP Manager detects change
|
||||
- get_cached_mcp_tools() checks mtime
|
||||
- If changed: reinitializes MCP client
|
||||
- Loads updated server configurations
|
||||
3. MCP Manager reloads on next use
|
||||
- get_cached_mcp_tools() lazily reinitializes MCP tools
|
||||
- Loads current server configurations and tool lists
|
||||
|
||||
4. Next agent run uses new tools
|
||||
```
|
||||
|
||||
@@ -33,6 +33,7 @@ def test_public_paths(path: str):
|
||||
[
|
||||
"/api/models",
|
||||
"/api/mcp/config",
|
||||
"/api/mcp/cache/reset",
|
||||
"/api/memory",
|
||||
"/api/skills",
|
||||
"/api/threads/123",
|
||||
@@ -149,6 +150,10 @@ def _make_app():
|
||||
async def mcp_put():
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/api/mcp/cache/reset")
|
||||
async def mcp_cache_reset():
|
||||
return {"ok": True}
|
||||
|
||||
@app.delete("/api/threads/abc")
|
||||
async def thread_delete():
|
||||
return {"ok": True}
|
||||
@@ -360,6 +365,11 @@ def test_protected_post_no_cookie_returns_401(client):
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_mcp_cache_reset_post_no_cookie_returns_401(client):
|
||||
res = client.post("/api/mcp/cache/reset")
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_protected_post_with_internal_auth_header_passes():
|
||||
from app.gateway.internal_auth import create_internal_auth_headers
|
||||
|
||||
|
||||
@@ -148,6 +148,27 @@ class TestMessageBus:
|
||||
|
||||
_run(go())
|
||||
|
||||
def test_unsubscribe_outbound_removes_fresh_bound_method_reference(self):
|
||||
bus = MessageBus()
|
||||
received = []
|
||||
|
||||
class Handler:
|
||||
async def callback(self, msg):
|
||||
received.append((self, msg))
|
||||
|
||||
handler = Handler()
|
||||
other_handler = Handler()
|
||||
|
||||
async def go():
|
||||
bus.subscribe_outbound(handler.callback)
|
||||
bus.subscribe_outbound(other_handler.callback)
|
||||
bus.unsubscribe_outbound(handler.callback)
|
||||
out = OutboundMessage(channel_name="test", chat_id="c1", thread_id="t1", text="reply")
|
||||
await bus.publish_outbound(out)
|
||||
assert received == [(other_handler, out)]
|
||||
|
||||
_run(go())
|
||||
|
||||
def test_outbound_error_does_not_crash(self):
|
||||
bus = MessageBus()
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.gateway.routers import mcp as mcp_router
|
||||
from app.gateway.routers.mcp import (
|
||||
_MCP_STDIO_COMMAND_ALLOWLIST_ENV,
|
||||
McpConfigUpdateRequest,
|
||||
@@ -21,6 +22,8 @@ from app.gateway.routers.mcp import (
|
||||
_merge_preserving_secrets,
|
||||
_require_admin_user,
|
||||
_validate_mcp_update_request,
|
||||
reset_mcp_tools_cache_endpoint,
|
||||
update_mcp_configuration,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -339,6 +342,71 @@ async def test_mcp_config_requires_admin_user():
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_mcp_tools_cache_endpoint_requires_admin_user(monkeypatch):
|
||||
called = False
|
||||
|
||||
def fake_reset_mcp_tools_cache():
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
monkeypatch.setattr(mcp_router, "reset_mcp_tools_cache", fake_reset_mcp_tools_cache)
|
||||
|
||||
response = await reset_mcp_tools_cache_endpoint(_request_with_role("admin"))
|
||||
|
||||
assert called is True
|
||||
assert response.success is True
|
||||
assert "next use" in response.message
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await reset_mcp_tools_cache_endpoint(_request_with_role("user"))
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_mcp_configuration_resets_tools_cache(monkeypatch, tmp_path):
|
||||
reset_calls = 0
|
||||
config_path = tmp_path / "extensions_config.json"
|
||||
config_path.write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
|
||||
|
||||
current_config = SimpleNamespace(skills={}, mcp_servers={})
|
||||
reloaded_config = SimpleNamespace(
|
||||
mcp_servers={
|
||||
"github": McpServerConfigResponse(
|
||||
type="stdio",
|
||||
command="npx",
|
||||
args=["-y", "@modelcontextprotocol/server-github"],
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def fake_reset_mcp_tools_cache():
|
||||
nonlocal reset_calls
|
||||
reset_calls += 1
|
||||
|
||||
monkeypatch.setattr(mcp_router.ExtensionsConfig, "resolve_config_path", lambda: config_path)
|
||||
monkeypatch.setattr(mcp_router, "get_extensions_config", lambda: current_config)
|
||||
monkeypatch.setattr(mcp_router, "reload_extensions_config", lambda: reloaded_config)
|
||||
monkeypatch.setattr(mcp_router, "reset_mcp_tools_cache", fake_reset_mcp_tools_cache)
|
||||
|
||||
response = await update_mcp_configuration(
|
||||
_request_with_role("admin"),
|
||||
McpConfigUpdateRequest(
|
||||
mcp_servers={
|
||||
"github": McpServerConfigResponse(
|
||||
type="stdio",
|
||||
command="npx",
|
||||
args=["-y", "@modelcontextprotocol/server-github"],
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert reset_calls == 1
|
||||
assert list(response.mcp_servers) == ["github"]
|
||||
|
||||
|
||||
def test_validate_mcp_update_allows_default_npx_stdio_command(monkeypatch):
|
||||
monkeypatch.delenv(_MCP_STDIO_COMMAND_ALLOWLIST_ENV, raising=False)
|
||||
request = McpConfigUpdateRequest(
|
||||
|
||||
Reference in New Issue
Block a user