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:
Huixin615
2026-06-16 23:20:20 +08:00
committed by GitHub
parent 0966131b31
commit 1896722e66
6 changed files with 137 additions and 8 deletions
+1
View File
@@ -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 |
+30
View File
@@ -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)
+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 ### Skills
#### List Skills #### List Skills
+8 -8
View File
@@ -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
``` ```
+10
View File
@@ -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
+68
View File
@@ -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(