feat(mcp): add official OpenViking tools integration (#4745)

* feat(mcp): add OpenViking tools integration

* fix(mcp): warn on ineffective tool overrides

* docs(mcp): clarify OpenViking resource removal

* fix(mcp): expose native OpenViking forget tool

* docs(mcp): document OpenViking forget guardrail
This commit is contained in:
Hao Zhe 2026-08-11 13:56:02 +08:00 committed by GitHub
parent 2bb230b334
commit a263af2845
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 258 additions and 0 deletions

View File

@ -157,6 +157,8 @@ This section accumulates work toward the **2.1.0** milestone
[#3866])
- **mcp:** Per-server `tool_call_timeout` for MCP tool calls, and routing hints
that guide the model to the right server. ([#3843], [#4004])
- **mcp:** Add an official OpenViking `/mcp` example that exposes the native
tool set through DeerFlow's generic MCP client. ([#4745])
- **community:** Agentic browser control as a first-class thread capability -
Playwright-backed browser sessions the agent operates while the user observes
or takes over from the workspace. ([#4187])
@ -1417,3 +1419,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
[#4471]: https://github.com/bytedance/deer-flow/pull/4471
[#4516]: https://github.com/bytedance/deer-flow/pull/4516
[#4611]: https://github.com/bytedance/deer-flow/issues/4611
[#4745]: https://github.com/bytedance/deer-flow/pull/4745

View File

@ -417,6 +417,14 @@ Targeted updates accept both DeerFlow's `type` field and the MCP-spec `transport
Runtime MCP and skill updates replace `extensions_config.json` atomically, so an interrupted write cannot leave the shared configuration truncated or partially written.
MCP routing hints can also prefer a specific MCP tool for matching requests without forbidding other tools. When `tool_search` defers MCP schemas, matching routing metadata can auto-promote up to `tool_search.auto_promote_top_k` deferred schemas before the model call.
OpenViking users can register the official Streamable HTTP endpoint at `/mcp`
with an owner-bound USER API key. The native `forget` tool is exposed for
capability parity; deletion is irreversible, so it should be called only after
explicit user confirmation. DeerFlow does not enforce that confirmation. This
explicit, model-selected MCP tool path can run alongside the separate automatic
OpenViking memory backend; it does not replace automatic turn capture or recall. See the
[OpenViking MCP tools configuration](backend/docs/MCP_SERVER.md#openviking-mcp-tools).
The Gateway also includes a disabled-by-default, protocol-neutral foundation for durable long-running MCP tasks. It stores remote task handles outside model context, polls them under cross-worker leases, rejects results returned after their lease expires, schedules the next attempt from the time a remote status call finishes, isolates unexpected failures between claimed tasks, cancels in-flight polling during Gateway shutdown, and makes expired claims recoverable after restart. If remote submission succeeds but the handle cannot be persisted, the runtime makes a best-effort cancellation so an untracked task is not silently left running. The exact scoped duplicate-handle conflict is surfaced without cancellation because an existing durable row already owns that remote task. Durable recovery requires a SQL database backend (`sqlite` or `postgres`); the in-memory backend does not initialize this task repository. This foundation does not make existing MCP tools asynchronous by itself: `mcp_tasks.enabled` should remain `false` until a compatible task driver is configured. Ordinary `submit/status/cancel` tools and the future MCP Tasks extension can share the same runtime without making the model remember remote task IDs.
See the [MCP Server Guide](backend/docs/MCP_SERVER.md) for detailed instructions.

View File

@ -14,6 +14,72 @@ DeerFlow supports configurable MCP servers and skills to extend its capabilities
3. Configure each servers command, arguments, and environment variables as needed.
4. Restart the application to load and register MCP tools.
## OpenViking MCP Tools
OpenViking's official server exposes a Streamable HTTP MCP endpoint at `/mcp`.
DeerFlow connects to it through the same generic MCP client used for other HTTP
servers:
```json
{
"mcpServers": {
"openviking": {
"enabled": true,
"type": "http",
"url": "http://127.0.0.1:1933/mcp",
"headers": {
"X-API-Key": "$OPENVIKING_API_KEY"
}
}
}
}
```
Set `OPENVIKING_API_KEY` to a normal owner-bound OpenViking **USER API key**.
The key determines the OpenViking account and user. Do not use a root/admin
key, trusted mode, or add `X-OpenViking-Account`, `X-OpenViking-User`, or
`X-OpenViking-Actor-Peer` headers for this personal single-owner setup.
`X-API-Key` is used here because DeerFlow expands a whole-string `$ENV_VAR`
value without storing a credential in the checked-in configuration.
If `OPENVIKING_API_KEY` is missing or empty during initialization, OpenViking
authentication fails and DeerFlow skips that MCP server, so no OpenViking tools
appear. Changing only the environment variable does not invalidate DeerFlow's
already-populated, file-signature-based MCP tool cache; after setting or fixing
the key, restart DeerFlow, modify and re-save the extensions config, or call the
MCP cache-reset endpoint at `POST /api/mcp/cache/reset`.
OpenViking owns the tool schemas and behavior. DeerFlow performs the standard
MCP initialization and discovery flow, prefixes the discovered names with
`openviking_` by default, and routes calls back through the generic MCP client.
For capability parity with other official OpenViking harnesses, DeerFlow exposes
the native `forget` tool with the other discovered tools. `forget` permanently
deletes a `viking://` URI and should be called only after explicit user
confirmation; DeerFlow does not enforce that confirmation.
Operators who do not want agents to call `forget` can block its default visible
name with DeerFlow's existing guardrail configuration:
```yaml
guardrails:
enabled: true
provider:
use: deerflow.guardrails.builtin:AllowlistProvider
config:
denied_tools: ["openviking_forget"]
```
If `tool_name_prefix` is disabled for the OpenViking server, block `forget`
instead.
This explicit tool path is separate from the automatic OpenViking memory backend
configured under `config.yaml -> memory`. Both may be enabled at the same time:
the memory backend handles automatic turn capture and recall, while MCP tools
are model-selected operations.
For Docker, point `url` at the OpenViking address reachable from the Gateway
container, such as `http://openviking:1933/mcp` for a shared Compose network or
`http://host.docker.internal:1933/mcp` for a host-installed server.
## Routing Hints
Use `routing` when an MCP server should be preferred for specific requests, such

View File

@ -0,0 +1,172 @@
from __future__ import annotations
import asyncio
import json
import socket
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch
import pytest
import uvicorn
from mcp.server.fastmcp import FastMCP
from app.gateway.routers.mcp import McpServerConfigResponse, _mask_server_config
from deerflow.config.extensions_config import ExtensionsConfig
from deerflow.mcp.client import build_server_params
from deerflow.mcp.tools import get_mcp_tools
_IDENTITY_HEADERS = {
"x-openviking-account",
"x-openviking-user",
"x-openviking-actor-peer",
}
class _RequestCapture:
def __init__(self, app) -> None:
self._app = app
self.headers: list[dict[str, str]] = []
self.methods: list[str] = []
async def __call__(self, scope, receive, send) -> None:
if scope["type"] != "http":
await self._app(scope, receive, send)
return
messages = []
body = bytearray()
while True:
message = await receive()
messages.append(message)
body.extend(message.get("body", b""))
if not message.get("more_body", False):
break
self.headers.append({key.decode("latin-1").lower(): value.decode("latin-1") for key, value in scope.get("headers", [])})
if body:
payload = json.loads(body)
requests = payload if isinstance(payload, list) else [payload]
self.methods.extend(request["method"] for request in requests if isinstance(request, dict) and "method" in request)
message_iterator = iter(messages)
async def replay_receive():
return next(message_iterator, {"type": "http.disconnect"})
await self._app(scope, replay_receive, send)
@asynccontextmanager
async def _openviking_mcp_server() -> AsyncIterator[tuple[str, _RequestCapture, list[str]]]:
calls: list[str] = []
mcp = FastMCP("OpenViking test server", stateless_http=True, json_response=True)
@mcp.tool(name="find")
async def find(query: str) -> dict[str, list[str]]:
calls.append(query)
return {"matches": [query]}
@mcp.tool(name="forget")
async def forget(uri: str) -> dict[str, str]:
return {"forgotten": uri}
capture = _RequestCapture(mcp.streamable_http_app())
server_socket = socket.socket()
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("127.0.0.1", 0))
server_socket.listen()
server_socket.setblocking(False)
port = server_socket.getsockname()[1]
server = uvicorn.Server(
uvicorn.Config(
capture,
log_level="error",
lifespan="on",
)
)
server_task = asyncio.create_task(server.serve(sockets=[server_socket]))
try:
for _ in range(500):
if server.started:
break
if server_task.done():
await server_task
await asyncio.sleep(0.01)
else:
raise TimeoutError("Timed out starting the test MCP server")
yield f"http://127.0.0.1:{port}/mcp", capture, calls
finally:
server.should_exit = True
await server_task
server_socket.close()
def _write_openviking_extensions_config(path: Path, url: str) -> None:
example_path = Path(__file__).resolve().parents[2] / "extensions_config.example.json"
example = json.loads(example_path.read_text(encoding="utf-8"))
openviking = deepcopy(example["mcpServers"]["openviking"])
openviking["enabled"] = True
openviking["url"] = url
path.write_text(
json.dumps({"mcpServers": {"openviking": openviking}}),
encoding="utf-8",
)
def test_openviking_mcp_config_resolves_headers_omits_identity_and_masks_secrets(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
api_key = "openviking-user-test-secret"
monkeypatch.setenv("OPENVIKING_API_KEY", api_key)
config_path = tmp_path / "extensions_config.json"
_write_openviking_extensions_config(config_path, "http://127.0.0.1:1933/mcp")
extensions = ExtensionsConfig.from_file(str(config_path))
server = extensions.mcp_servers["openviking"]
params = build_server_params("openviking", server)
masked = _mask_server_config(McpServerConfigResponse.model_validate(server.model_dump()))
assert params["headers"] == {"X-API-Key": api_key}
assert _IDENTITY_HEADERS.isdisjoint({name.lower() for name in params["headers"]})
assert masked.headers == {"X-API-Key": "***"}
assert server.tools == {}
assert api_key not in masked.model_dump_json()
assert api_key not in caplog.text
@pytest.mark.asyncio
async def test_openviking_http_mcp_discovers_exposes_and_calls_native_tools(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
api_key = "openviking-user-test-secret"
monkeypatch.setenv("OPENVIKING_API_KEY", api_key)
async with _openviking_mcp_server() as (url, capture, calls):
config_path = tmp_path / "extensions_config.json"
_write_openviking_extensions_config(config_path, url)
extensions = ExtensionsConfig.from_file(str(config_path))
with patch("deerflow.mcp.tools.ExtensionsConfig.from_file", return_value=extensions):
tools = await get_mcp_tools()
assert {tool.name for tool in tools} == {"openviking_find", "openviking_forget"}
find_tool = next(tool for tool in tools if tool.name == "openviking_find")
result = await find_tool.ainvoke({"query": "needle"})
assert calls == ["needle"]
assert any(block.get("type") == "text" and "needle" in block.get("text", "") for block in result)
assert {"initialize", "notifications/initialized", "tools/list", "tools/call"} <= set(capture.methods)
assert capture.headers
for headers in capture.headers:
assert headers["x-api-key"] == api_key
assert _IDENTITY_HEADERS.isdisjoint(headers)
assert api_key not in caplog.text

View File

@ -20,6 +20,15 @@
"tool_call_timeout": 60,
"description": "GitHub MCP server for repository operations"
},
"openviking": {
"enabled": false,
"type": "http",
"url": "http://127.0.0.1:1933/mcp",
"headers": {
"X-API-Key": "$OPENVIKING_API_KEY"
},
"description": "Official OpenViking tools for explicit memory and resource operations"
},
"postgres": {
"enabled": false,
"type": "stdio",