mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-09 13:39:26 +00:00
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:
parent
2bb230b334
commit
a263af2845
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -14,6 +14,72 @@ DeerFlow supports configurable MCP servers and skills to extend its capabilities
|
||||
3. Configure each server’s 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
|
||||
|
||||
172
backend/tests/test_openviking_mcp_integration.py
Normal file
172
backend/tests/test_openviking_mcp_integration.py
Normal 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
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user