mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-17 04:56:04 +00:00
fix: add MCP tools cache reset endpoint (#3602)
* fix: add MCP tools cache reset endpoint * docs: clarify MCP cache reset scope
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 /api/models` | List available LLM models |
|
||||||
| `GET/PUT /api/mcp/config` | Manage MCP server configurations |
|
| `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 |
|
| `GET/PUT /api/skills` | List and manage skills |
|
||||||
| `POST /api/skills/install` | Install skill from `.skill` archive |
|
| `POST /api/skills/install` | Install skill from `.skill` archive |
|
||||||
| `GET /api/memory` | Retrieve memory data |
|
| `GET /api/memory` | Retrieve memory data |
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter, HTTPException, Request, status
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from deerflow.config.extensions_config import ExtensionsConfig, get_extensions_config, reload_extensions_config
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/api", tags=["mcp"])
|
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 = "***"
|
_MASKED_VALUE = "***"
|
||||||
|
|
||||||
|
|
||||||
@@ -269,6 +277,27 @@ async def get_mcp_configuration(request: Request) -> McpConfigResponse:
|
|||||||
return McpConfigResponse(mcp_servers=servers)
|
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(
|
@router.put(
|
||||||
"/mcp/config",
|
"/mcp/config",
|
||||||
response_model=McpConfigResponse,
|
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
|
# agent runtime lives in Gateway, so this keeps API reads and tool
|
||||||
# execution aligned after extensions_config.json changes.
|
# execution aligned after extensions_config.json changes.
|
||||||
reloaded_config = reload_extensions_config()
|
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()}
|
servers = {name: _mask_server_config(McpServerConfigResponse(**server.model_dump())) for name, server in reloaded_config.mcp_servers.items()}
|
||||||
return McpConfigResponse(mcp_servers=servers)
|
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
|
### Skills
|
||||||
|
|
||||||
#### List Skills
|
#### List Skills
|
||||||
|
|||||||
@@ -427,17 +427,17 @@ SKILL.md Format:
|
|||||||
### Configuration Reload
|
### Configuration Reload
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Client updates MCP config
|
1. Client updates MCP config or requests a cache reset
|
||||||
PUT /api/mcp/config
|
PUT /api/mcp/config
|
||||||
|
POST /api/mcp/cache/reset
|
||||||
|
|
||||||
2. Gateway writes extensions_config.json
|
2. Gateway updates runtime state
|
||||||
- Updates mcpServers section
|
- PUT writes extensions_config.json and reloads configuration
|
||||||
- File mtime changes
|
- Both endpoints reset the MCP tools cache and persistent sessions
|
||||||
|
|
||||||
3. MCP Manager detects change
|
3. MCP Manager reloads on next use
|
||||||
- get_cached_mcp_tools() checks mtime
|
- get_cached_mcp_tools() lazily reinitializes MCP tools
|
||||||
- If changed: reinitializes MCP client
|
- Loads current server configurations and tool lists
|
||||||
- Loads updated server configurations
|
|
||||||
|
|
||||||
4. Next agent run uses new tools
|
4. Next agent run uses new tools
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def test_public_paths(path: str):
|
|||||||
[
|
[
|
||||||
"/api/models",
|
"/api/models",
|
||||||
"/api/mcp/config",
|
"/api/mcp/config",
|
||||||
|
"/api/mcp/cache/reset",
|
||||||
"/api/memory",
|
"/api/memory",
|
||||||
"/api/skills",
|
"/api/skills",
|
||||||
"/api/threads/123",
|
"/api/threads/123",
|
||||||
@@ -149,6 +150,10 @@ def _make_app():
|
|||||||
async def mcp_put():
|
async def mcp_put():
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
@app.post("/api/mcp/cache/reset")
|
||||||
|
async def mcp_cache_reset():
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
@app.delete("/api/threads/abc")
|
@app.delete("/api/threads/abc")
|
||||||
async def thread_delete():
|
async def thread_delete():
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -360,6 +365,11 @@ def test_protected_post_no_cookie_returns_401(client):
|
|||||||
assert res.status_code == 401
|
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():
|
def test_protected_post_with_internal_auth_header_passes():
|
||||||
from app.gateway.internal_auth import create_internal_auth_headers
|
from app.gateway.internal_auth import create_internal_auth_headers
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from types import SimpleNamespace
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from app.gateway.routers import mcp as mcp_router
|
||||||
from app.gateway.routers.mcp import (
|
from app.gateway.routers.mcp import (
|
||||||
_MCP_STDIO_COMMAND_ALLOWLIST_ENV,
|
_MCP_STDIO_COMMAND_ALLOWLIST_ENV,
|
||||||
McpConfigUpdateRequest,
|
McpConfigUpdateRequest,
|
||||||
@@ -21,6 +22,8 @@ from app.gateway.routers.mcp import (
|
|||||||
_merge_preserving_secrets,
|
_merge_preserving_secrets,
|
||||||
_require_admin_user,
|
_require_admin_user,
|
||||||
_validate_mcp_update_request,
|
_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
|
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):
|
def test_validate_mcp_update_allows_default_npx_stdio_command(monkeypatch):
|
||||||
monkeypatch.delenv(_MCP_STDIO_COMMAND_ALLOWLIST_ENV, raising=False)
|
monkeypatch.delenv(_MCP_STDIO_COMMAND_ALLOWLIST_ENV, raising=False)
|
||||||
request = McpConfigUpdateRequest(
|
request = McpConfigUpdateRequest(
|
||||||
|
|||||||
Reference in New Issue
Block a user