Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MCP

drsg-mcp 是一个 Model Context Protocol 服务,它内嵌 Dr Strange,并将数据库作为一组工具暴露给 LLM 智能体。兼容的宿主中, 智能体可以直接检索、遍历、查询、运行算法、以自然语言提问、导入文档,也可以写入 图,全程无需专门的集成代码。

Dr Strange 如何契合智能体

MCP 本身就是 JSON-RPC 2.0,与 Web 后端说的是同一套协议,因此 MCP 服务是一等的 接口,而不是一层适配器。drsg-mcp 内嵌 dr-strange-core,在进程内打开数据库 (和命令行一样),再通过 stdio 提供该协议:宿主启动这个进程,双方在标准输入 输出上交换 JSON-RPC 消息。日志写入 stderr 与一个滚动文件,绝不写入承载协议的 stdout。

运行与配置

drsg-mcp 是一个独立的二进制。一行命令即可安装发行版本 (第 2 章):

$ curl -fsSL https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.sh | sh -s -- --bin drsg-mcp

在 Windows 的 PowerShell 中:

PS> & ([scriptblock]::Create((irm https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.ps1))) -Bin drsg-mcp

它同样可以从源码构建:cargo build --release -p dr-strange-mcp。数据库以 --db <路径> 指定,也可以直接作为一个裸参数给出;其次读 $DRSG_DB,再退回到默认 值 graph.drsg。--help 与 --version 只打印信息随即退出:

$ drsg-mcp --db /path/to/graph.drsg
$ drsg-mcp /path/to/graph.drsg          # 同样的意思,简写

数据库必须已经存在——这个服务从不创建数据库。用 drsg digest <目录> --apply --db <路径> 建立,或在仓库中运行 drsg init。空数据库对任何问题都只会答「什么也没 找到」,这和一次出了错的图化毫无区别;因此路径不存在时直接报错。

未指名数据库时,它会先去找一个服务。 它读取最近的 .mcp.json——像 git 找自己的 目录那样逐级向上,到仓库根(第一个 .git)为止;不属于当前用户或任何人可写的文件会被 跳过,因为它指名的服务将接收宿主的会话及其携带的 Authorization——若其中声明了一个 drsg 服务且该服务有应答,本进程便把宿主的会话 中继过去,而不再打开任何数据库。于是在一个由 drsg init 准备好的仓库里(它图化 源码树、拉起一个 drsg serve … watch,并把该服务的 URL 写进 .mcp.json),只会说 stdio 的宿主同样能接到那个已经持有数据库的进程,其平面还跟随着仓库的提交(另见 下一节)。

中继按原样双向转发消息,因此宿主看到的是那个服务的工具集——包括较新的服务才有、而 本二进制从未听说过的工具。指名了数据库(--db、$DRSG_DB)则跳过这次查找:既然 调用者说明了想要哪张图,就不该被送去另一张。若无人应答、没有 .mcp.json,或其中 并无 drsg 服务,就依旧在本进程里打开数据库。

它通常由宿主启动,而不是手动运行。宿主通过命令、参数与环境变量来配置它,图工具 所需的 LLM 提供方密钥就放在环境变量里传入:

{
  "mcpServers": {
    "dr-strange": {
      "command": "drsg-mcp",
      "args": ["--db", "/path/to/graph.drsg"],
      "env": {
        "OPENAI_API_KEY": "sk-...",
        "DEEPSEEK_API_KEY": "...",
        "DASHSCOPE_API_KEY": "..."
      }
    }
  }
}

crates/dr-strange-mcp/mcp.json 提供了一份可直接编辑的该配置副本:把 command 设为 drsg-mcp(位于 PATH 中)或该二进制的绝对路径,让 args 指向你的数据库, 只提供你实际用到的提供方密钥。

由于它在进程内打开数据库,同一时刻只能有一个进程能直接打开某个数据库:可以是 一个 drsg-mcp、一条 drsg 命令,或一个 drsg serve,但不能两个同时打开。这 一点是强制的,不只是建议——第二次打开会直接报错失败,而不会把数据库弄坏。

上面的中继,正是为了让这条规则不再挡路而存在;而在中继不适用之处,它依旧生效。同一 个项目开着两个编辑器,就会各自派生一个 drsg-mcp:有 watch 服务在跑时,两者都中继 到它,共享同一个数据库;没有时,先来的打开文件,后到的拒绝启动。若干智能体需要共享 同一份记忆的场景,见下一节。

跨智能体共享:drsg serve 上的 /mcp

drsg-mcp 按设计内嵌运行:把宿主指向一个路径即可工作,无需运行任何附加服务,也 无需配置。这对单个智能体是正确答案,但也正因为如此,两个宿主没法直接共享一个 数据库:两边都要打开同一个文件。

drsg serve(arch/08)已经为 JSON-RPC 接口和仪表盘解决了"一个写者、多个客户端" 的问题。它在 POST /mcp 上暴露完全相同的工具集,走 MCP 的 Streamable HTTP 传输, 让多个智能体宿主可以指向同一个服务,不必各自内嵌一份数据库副本:

{
  "mcpServers": {
    "dr-strange": {
      "url": "http://127.0.0.1:7700/mcp",
      "headers": { "Authorization": "Bearer <DRSG_TOKEN>" }
    }
  }
}

(远程 MCP 配置的确切格式请查阅你所用宿主的文档。上面的 url/headers 字段只是 示例,不是通用格式。)

/mcp 的鉴权方式与 /rpc 完全一致:未设置 DRSG_TOKEN 时,只有同源的浏览器 UI 受信任,任何程序化客户端都会被拒绝,读操作也不例外,因为零配置的桌面安装不该在 本地悄悄暴露一个开放的 API。要从其他地方(包括另一个智能体宿主)访问 /mcp,请 在服务端设置 DRSG_TOKEN,并以 bearer token 的形式传递它。

把它放到某个域名后面之前,有一点需要先知道:MCP 传输层会校验请求的 Host 头, 借此削弱针对本机服务的 DNS 重绑定攻击,对不认识的名字返回 403。回环地址 (localhost、127.0.0.1、::1)始终放行。配置了 bearer token 之后,服务端绑定 的地址也会放行,DRSG_ALLOWED_HOSTS(逗号分隔,主机名或 host:port)里的每个 名字同样放行——所以把 /mcp 放到 https://memory.example.com 后面,只需 DRSG_ALLOWED_HOSTS=memory.example.com。未设 token 时,无论环境变量怎么写,列表 都只有回环地址:未设 token 的服务端信任自己的同源 UI,而重绑定页面要冒充的正是 这个身份,Host 检查就是用来挡它的。有了 token,每个请求都必须携带这种页面读不 到的秘密,这道检查就不再比 token 多挡住什么,运营者可以放宽它。ROADMAP §10 针对 的场景——同一台机器上的多个智能体宿主——什么都不用配。

每个会话都会拿到自己的 DrStrange 实例,但它们共享服务端打开的同一个 Database:一个会话的写入会立刻对其他所有会话可见,就像两个浏览器标签页访问 /rpc 那样。这正是关键所在——核心内部的 write_gate 会把并发写者串行化,所以 这么做是安全的,不只是方便。

rebuild 会重新读取某个 plane 建立时所依据的整棵树——当一个回答说「自本 plane 建立 以来插件发生了变化」,指的正是这件事:折叠只会把新装的解析器作用于某次提交触及的文 件。它只接受一个 plane 名字:目录来自该 plane 自己记录的 synced_root,因此重建无 法被指向一棵它从未据以建立的树。代价是把整个仓库重新解析一遍,完成后促使它被调用的 那条警告也随之消失。这条警告的含义见插件一章。

有一项能力的差异来自传输方式本身,而不是配置。digest 接受一个 path,由服务端 读取该文档(Word、PowerPoint、Excel、OpenDocument、RTF、EPUB、CSV、PDF、Markdown 或纯文本),但只有 stdio 服务接受这种用法:这个进程运行在你自己的机器上、以你 自己的身份运行,它读到的文件本来就是 agent 能打开的文件。共享的 drsg serve 就 会拒绝这么做——一旦允许调用方指定任意路径,一个已通过认证的远程 agent 就能把 服务器上的任意文件读进图里,再用查询取回。在 /mcp 上,请改为直接以 text 传入 文档内容。

digest 的 LLM 调用,花的始终是服务进程自身的提供方密钥(不管本地还是远程,都 绝不会来自工具参数)。write_nodes/write_edges 仍然保留逐次调用的批量原子性, 因为不管走哪条路径,工具代码都在同一进程内针对同一个 Database 运行,不会代理到 /rpc,也不会改变任何工具的行为。[digest] 配置段对 digest 工具的作用,和它对 /rpc 上 digest.run 的作用完全一致:调低 concurrency 来规避提供方限流时, 两个入口都会遵守,不存在一个听、另一个不听的情况。(内嵌的 drsg-mcp 二进制没有 配置文件,沿用内置默认值。)

会话生命周期

正常退出的 host 会发送 DELETE /mcp,会话随即消失。被 SIGKILL 的 host(比如 编辑器重启它的 MCP 子进程)什么都不会发,服务端就改用定时器回收:空闲超过 10 分钟,或者会话创建后 60 秒内没有收到 initialize。这时该会话的 worker task、它的 DrStrange,以及缓冲的消息都会一并释放。

空闲窗口取 10 分钟而不是 5 分钟,原因很具体:传输层会把正在执行的工具调用也算作 空闲,因为它的 keep-alive 计时器只认流量,而一次工具调用从派发到返回结果之间不会 发送任何东西。所以在一个本就安静的会话上,运行时间超过这个窗口的工具会被中途 拆断。10 分钟足以覆盖任何现实场景中的 digest;如果你经常要跑更久的任务,要么让 会话保持有流量,要么就要预期它可能需要重试。

回收之后,每个死会话都会留下一条 map 条目。它只有几十字节,但并非无害:这个 会话 id 上的下一个请求会收到 500,而不是规范要求的 404,于是本该据此重新 initialize 的客户端不会重连,这个会话就一直坏着,直到 host 重启为止。这两点都是 传输层的问题,该在上游修,而不是在这里绕过去。如果你在脚本里循环创建会话,记得 手动关闭,就能完全避开这个问题。

工具并发

工具调用和 HTTP 请求是分开限流的:全进程同时最多 16 个工具调用(如果 max_concurrent 更小,则取更小的那个)。二者不是一回事——max_concurrent 统计的 是请求,其中大多数开销很低;而每次工具调用,要么是一次全图扫描、一次批量写入, 要么是一次向 LLM 扇出的 digest。而且传输层在工具调用刚被入队时就会应答,请求 上限在工作真正开始之前就已经释放了,根本管不到它。超出的调用会排队而不是直接 失败:服务器繁忙时该让 agent 等,而不是把它拒之门外——但不会无限期地等:每次 调用有一个覆盖排队与执行的截止时间,默认 300 秒,可用 DRSG_MCP_TOOL_DEADLINE_SECS 调整(0 关闭)。超时的调用以工具级错误返回,并说明 是服务器繁忙还是执行过慢;已在运行的调用体不会被中断,但会一直占着它的名额直到 结束,所以上限仍然算得上它。

工具

工具类别用途
list_planes读列出各平面及其节点/边计数
describe_plane读某个平面的软 schema(标签、属性、边类型、键前缀)
get_node读按 id 或外部键获取一个节点
search读语义查找——嵌入查询文本,返回最近的 k 个节点
context读已图化代码平面上一个符号的完整邻域——首选的智能体动词
describe读一个符号的属性,只看节点的轻量视图
grep读在被监视的源码树上做文本检索——字面或正则,可用 path 限定范围并带上下文行;每个命中都标出所在的符号;被 ignore 的文件除非指定 all_files 否则不在范围内
trace读两个符号之间图中记录的最短调用路径
impact读所有能到达该符号的东西,按距离分组,解析无法确定绑定的那些只计在总数旁边、从不计入总数;across(attached 或 all)还会指出这个半径触及了哪些其他仓库——依据它们构建声明的坐标,以及它们源码为之立下替身的符号
fathom读一个符号周边区域的构成——标签、边类型与枢纽
snippet读一个符号的源码文本,或文件的某个行区间(path:start-end);会说明区间落在哪个符号里、如何继续往下读
history读一个仓库历史的概览——HEAD 在哪、分支与标签指向何处、哪些分支被变基过,以及最新的若干提交;读的是 git 检出目录被图化时一并写入的 _git 平面
deployed读某个运行中的东西是由哪次提交构建的,以及哪些东西在跑某次提交——把清单被图化时写入的 _ops 平面,经由镜像标签所携带的修订号,连到它旁边的 _git 平面;浮动标签、只带 digest 的引用,以及这段历史并不持有的修订号,都会被如实说出来,而不是用“什么都没有”来回答
recall读某个 git 修订版本时的代码——一个文件、一个行区间、一个目录,或一个符号当时的声明(按当时的文件重新解析得到);pattern 在那棵树上检索,vs 给出一个名字相对更早修订版本的差异;修订版本经由 _git 平面解析
wire读什么在提供某个 HTTP 路径、什么在请求它——跨平面内的每种语言匹配路由,也跨它所附着的平面;每次调用时计算,从不存储
attach写记录本平面可以询问的平面:本实例上的,或由另一个 drsg 提供服务的(url);both 会把返程也记下来
detach写按附着时所用的名称移除一条附着
attachments读本平面附着了什么;probe 会逐个询问位于别处的附着现在是否应答
mount写立即把另一个数据库的平面与本数据库的一同提供服务,无需重启——一个进程服务多个仓库,每个仓库保留自己的存储、自己的写入者与自己的平面名。受 [server] mount_root 限制
unmount写停止提供某个已挂载的数据库,释放其存储的锁,并停止向它折叠的那个 watcher;freed 说明该锁是否已经释放——仍在进行中的折叠会持有它直到结束。不删除任何东西
mounts读本服务器提供了什么、各部分从何而来——每个挂载及其目录与平面,以及每个平面被解析自的那棵树;客户端正是据此把工作目录映射到平面
traverse读从某个节点进行邻域扩展(1 跳及以上)
query读运行一个序列化的逻辑计划
cypher读 / 写运行一条 openCypher 子集语句——任何具名动词都未预见的问题,由它兜底;DELETE/REMOVE 需要 confirm: true
algo读一个图算法(pagerank / components / shortest_path / louvain)
hybrid读融合的向量 + 关键词 + 图邻近度检索
ask读一个自然语言问题,编译为计划并执行
write_nodes写创建节点(批量)
write_edges写按端点键创建边(批量)
create_plane写创建一个空平面
drop_plane写删除一个平面及其内容(需确认)
digest写导入一篇文档(默认为 dry-run;apply: true 需要 confirm: true;mode 决定抽取精度)
rebuild写重新读取某个 plane 建立时所依据的整棵树,让一张被两套插件分别解析过的图重新成为一次完整的解析

与系统其余部分的映射

这些工具做的事,和命令行、JSON-RPC 接口能做的完全一样,只是照顾了智能体的使用 方式:search / traverse / query / cypher / algo / hybrid / ask 对应 第 4 章与第 3 章的查询与检索接口; write_nodes / write_edges / create_plane / drop_plane / digest 对应写入 与导入接口。每个工具都依据该平面的软 schema 行事,而 describe_plane 就是用来 暴露这份 schema 的,让智能体能在动手之前先了解这张图。

精确地指定一个节点

节点的外部键带前缀,而前缀恰恰是任何命名习惯都看不出来的那一段。Rust 键以 crate 为前缀(dr_strange_core::compact::resolve);TypeScript 键以最近一个 package.json 里声明的 name 为前缀,其后是该文件相对于这个清单所在目录的 路径,去掉扩展名——结尾的 /index 折叠为其所在目录,而根目录的 index 就代表这 个包本身。若文件之上没有任何 package.json,前缀取这棵树的标签,对本地目录来说 就是目录名。

于是有一条值得记住的推论:TypeScript 键并不以仓库目录为前缀。一个目录名叫 bridge、而清单里声明为 highway_bridge-3.0 的项目,它的键都在 highway_bridge-3.0/ 之下,按目录名去查将一无所获。

有两处让这件事可被发现,而不必靠记:describe_plane 会给出 key_prefixes——每个 产出方的节点实际使用的前缀,由扫描得来而非声明得来,因此不会与已存的键脱节;而一 次没有命中的查找,会列出该平面确实持有的前缀,因为名字是按键的片段匹配的: src/App 无需知道前缀,也能找到 highway_bridge-3.0/src/App。

安全

  • 提供方密钥从服务端环境读取,绝不从工具参数读取——智能体无法通过一次调用 外泄或提供密钥。
  • 读操作是非破坏性的。 尤其是 ask,它编译为一个只读计划,无法改动图。
  • 破坏性写入受到保护,增量写入则不受。 drop_plane、带 DELETE 或 REMOVE 的 cypher 语句、以及 apply: true 的 digest 都需要 confirm: true,缺少时会 拒绝并指出该标志;digest 默认为 dry-run,返回拟议的节点与边以供检视,而非直接 写入。write_nodes、write_edges 和 CREATE/MERGE/SET 无需确认:它们添加的 内容可由之后的删除撤销,而一个事事都要的标志只会被客户端不假思索地传上。
  • 文件只从源码树内读取。 grep 与 snippet 打开的每个路径都必须解析到它所属 的树之内:..、绝对路径、指向树外的符号链接一律拒绝。操作者挂接的树(serve watch 的目录或 [server] source_root)始终可读;平面自身的 synced_root 是 数据,stdio 服务以你的身份运行、可以照读,而共享的 drsg serve 只在它位于挂接 的树内时才读;未挂接任何树时,/mcp 不读任何文件。
  • 镜像平面不接受手工改写。 由 digest 或 serve watch 与源码树保持同步的 平面记录着它所反映的提交,cypher 在这类平面上拒绝 CREATE/MERGE/SET/REMOVE/DELETE:下一次折叠会把平面与源码树重新对 账,覆盖对解析器所有节点的修改、重建被删除的节点,这样的写入会被无声地撤销。 write_nodes / write_edges 在那里仍然可用——折叠不会动它不拥有的节点—— 而智能体自己的平面接受任何语句。

在同样这些镜像平面上,cypher 对 RETURN n 以智能体动词共用的紧凑文本作答—— 一个计数、同步的提交、每个节点一行——而不是每个节点一条 JSON 记录;投影查询在 任何平面上都返回表格。语句解析失败时,返回会附上它所触及的那几条子句的语法, 因此工具清单只需承载意图与两个例子,而不必每一轮都携带整门语言。

示例:一个智能体工作流

一次典型的智能体会话会组合这些工具:

  1. describe_plane,以了解作用范围内的标签、属性与边类型。
  2. search 或 hybrid 检索相关节点,再以 traverse 收集其邻域,为模型提供扎实 的上下文。
  3. ask 或 cypher,针对图回答一个具体问题。
  4. digest(先 dry-run,再应用),将新的原始材料作为实体与关系并入图。

由于存储的是一张图,智能体可以在同一个会话里从检索直接转入遍历——这正是 第 1 章所说的、由模型端到端驱动的 GraphRAG 循环。