Merge branch 'main' into 2.0.0-release

This commit is contained in:
Willem Jiang
2026-06-17 00:12:48 +08:00
committed by GitHub
11 changed files with 297 additions and 194 deletions
+1
View File
@@ -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 |
+1 -1
View File
@@ -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."""
+30
View File
@@ -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)
+20
View File
@@ -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
+8 -8
View File
@@ -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
```
+10
View File
@@ -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
+21
View File
@@ -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()
+68
View File
@@ -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(