npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@4399ywkf/editor-mcp

v0.3.1

Published

把 @4399ywkf/editor 的结构化编辑面接出去:工具定义(transport 无关)+ 浏览器桥 + hub + stdio MCP server

Readme

@4399ywkf/editor-mcp

@4399ywkf/editor 的结构化编辑面接出去,让 AI 操作浏览器里那个真实的 编辑器实例(文档是 tiptap,表格是 Univer)—— AI 改的和用户看的是同一个。

分层

MCP 只是最外面那一层的一种传输。中间两层跟它没关系:

| 层 | 在哪 | 知道 MCP 吗 | |---|---|---| | runtime:LiteXML 编解码 + 8 类操作 | @4399ywkf/editor/doc-runtime | 不知道 | | runtime:表格快照读 + facade 写 | @4399ywkf/editor/sheet-runtime | 不知道 | | 工具面:name / JSON Schema / description | 本包 ./tools | 不知道 | | 传输:浏览器桥 / hub / stdio MCP | 本包 ./bridgebin/ | 知道 |

runtime 住在编辑器包里而不是这儿,是因为 NODE_TAG 镜像的是那个编辑器自己的 schema。跨包之后每加一个节点类型,序列化就静默降级成 <node type="..."/> 占位符 —— 不报错、不丢数据、也不告诉你。表格同理:对 Univer facade 的探测全钉在 sheet-runtime/facade.ts,Univer 版本一漂只改那一个文件。

两个 runtime 的 call(tool, args) 是同构的,桥不需要知道自己面对的是文档还是表格。

用法一:不走 MCP(推荐给已有 agent loop 的项目)

jizhi_ai 那种 builtin-tool-* executor 直接调的形态,只要工具定义和 runtime, 一行传输代码都不需要:

import { DOC_TOOLS } from "@4399ywkf/editor-mcp/tools"
import { DocRuntime } from "@4399ywkf/editor/doc-runtime"

const runtime = new DocRuntime()
// <NotionEditor onEditorReady={(editor) => runtime.setEditor(editor)} />

// 把 DOC_TOOLS 喂给模型,回调里:
const result = await runtime.call(toolName, args)

DocRuntime 的方法名对齐了 jizhi_ai 的 @jizhi/editor-runtimegetPageContent / modifyNodes / replaceText / setEditor / isReady), 两套编辑器可以被同一套工具面驱动。

表格换成 SHEET_TOOLS + SheetRuntime,形状一模一样:

import { SHEET_TOOLS } from "@4399ywkf/editor-mcp/tools"
import { SheetRuntime } from "@4399ywkf/editor/sheet-runtime"

const runtime = new SheetRuntime()
// <DocumentView format="sheet" onReady={(univerAPI) => runtime.setUniverAPI(univerAPI)} />

两个都挂就喂 ALL_TOOLS(= DOC_TOOLS + SHEET_TOOLS), 执行侧用 composeRuntimes 按前缀分发(见下)。

用法二:走 MCP

agent ──stdio MCP──▶ ywkf-editor-mcp ──POST /api/call──▶ ywkf-editor-hub ──WS──▶ 浏览器
                     (只转发)                             (中继)                (执行体)

1. 页面侧接上桥

import { DocRuntime } from "@4399ywkf/editor/doc-runtime"
import { attachBridge } from "@4399ywkf/editor-mcp/bridge"

const runtime = useMemo(() => new DocRuntime(), [])

useEffect(
  () => attachBridge({
    runtime,
    url: "ws://127.0.0.1:4399/bridge",
    onStatus: (s) => setConn(s),          // connecting | primary | observer | disconnected
  }),
  [runtime],
)

<NotionEditor onEditorReady={(editor) => runtime.setEditor(editor)} />

样式记得引一次 @4399ywkf/editor/styles(AI 改动块的闪烁提示 .ai-flash 在里面)。

同一个页面上既有文档又有表格时,用 composeRuntimes 把两个运行时并成一个挂上去 —— 按工具名前缀路由,doc_* 去 tiptap,sheet_* 去 Univer:

import { attachBridge, composeRuntimes } from "@4399ywkf/editor-mcp/bridge"

const doc = useMemo(() => new DocRuntime(), [])
const sheet = useMemo(() => new SheetRuntime(), [])

useEffect(
  () => attachBridge({ runtime: composeRuntimes({ doc, sheet }), url: "ws://127.0.0.1:4399/bridge" }),
  [doc, sheet],
)

只挂一个也行 —— 打到没挂的前缀时报的是「本页面没有挂 sheet_* 的运行时,可用的工具面: doc_*」,而不是「未知工具」。差别在于前者不会让模型换个工具名反复重试。

2. 起 hub

npx ywkf-editor-hub --port 4399 --token $(openssl rand -hex 16)

默认只听 127.0.0.1--token 可选但建议开:这个口的权限是「以用户身份重写他正在 编辑的文档」,不开的话本机任何进程都能调。--static <dir> 可以顺便托管一个页面, 本地调试方便。--out-dir <dir> 指定文档的读写目录(默认 hub 的工作目录), doc_export_docx / doc_import_docx / sheet_export_xlsx 只能碰这个目录里的文件。

3. 注册 MCP

{
  "mcpServers": {
    "ywkf-editor": {
      "command": "npx",
      "args": ["ywkf-editor-mcp", "--url", "http://127.0.0.1:4399"],
      "env": { "EDITOR_MCP_TOKEN": "刚才那个 token" }
    }
  }
}

codex exec 下需要 --dangerously-bypass-approvals-and-sandboxapproval: never 会把 MCP 调用直接判成 "user cancelled"。交互式 codex 里可以逐次批准。

工具(doc 10 个 + sheet 6 个)

文档(tiptap)

| 工具 | 用途 | |---|---| | doc_read | 读全文,xml / json / both | | doc_schema | 运行时反射出的节点·标记·标签映射·哪些类型带 id。永不与实现漂移 | | doc_find | 检索,返回可直接回填的 nodeId | | doc_get_selection | 用户此刻的选区;无选区时显式声明「不要沿用历史」 | | doc_replace_text | 块内文本替换(首选),只改命中的那一段,其余格式保住 | | doc_format_text | 加/去 bold·italic·underline·strike·code·sup·sub(改格式首选) | | doc_modify_nodes | 结构化 insert / remove / modify | | doc_table_edit | 表格:增删行列、合并拆分、批量写值 | | doc_export_docx | 导出成 .docx 落盘,返回路径 + 统计 + 警告 | | doc_import_docx | 读一份 .docx 灌进编辑器(整篇替换) |

刻意保持小。参照系:BlockNote AI 只有 3 个工具;把 tiptap 每个 command 都包成工具的 tiptap-apcore 做到 79 个,GitHub 1 star。后两个之所以够格占位置:没有它们, 模型能把文档改好却交付不出去 —— 「改完了,然后呢」是个死胡同。

工具描述里的可用标签清单是从 runtime 的标签表生成的,不是手抄的 —— 编辑器加一个 节点类型,description 自动跟着变(src/tools.test.ts 守这条)。

表格(Univer)

| 工具 | 用途 | |---|---| | sheet_read | 工作簿概览 + 一张表的 CSV 视图与公式清单。写之前必须先调它 | | sheet_find | 全簿检索(值和公式都搜),返回可直接回填的 sheet + A1 ref | | sheet_set_values | 从某格起写一块二维值,"=…" 是公式、null 清空 | | sheet_edit_rows_cols | 插入 / 删除整行整列(0-based) | | sheet_manage_sheets | 工作表 add / rename / delete / activate | | sheet_export_xlsx | 导出成 .xlsx 落盘,返回路径 + 统计 + 警告 |

定位一律用 A1 引用,不用 node id —— 表格的坐标系本来就是稳定的,不需要 doc 那套 「读了才有 id」的仪式。但先读后写这条纪律不变,而且比 doc 更要紧:A1 引用不读也能 编出来,模型会照着想象中的表结构盲写。所以写工具的 description 里都写死了「先 sheet_read」。

读走快照、写走 facade 是有意分的两条路:读是纯数据(Univer 版本漂移伤不到), 且 CSV 文本的 token 成本只有 JSON 单元格矩阵的几分之一;写必须进 Univer 的 command 体系才有撤销栈和协同广播,直接改快照是死路。

sheet_manage_sheetsdelete 不允许省略 sheet —— 「默认删活动表」是脚枪。

文档字节不进模型上下文

doc_export_docx / doc_import_docx / sheet_export_xlsx 的参数和返回值里没有文件内容, 只有路径:

模型 ──path──▶ hub ──base64──▶ 浏览器(真正的 serializeDocx / parseDocx / serializeXlsx)
模型 ◀──path── hub ◀──base64── 浏览器

一份带图的 docx / xlsx 动辄几 MB,base64 之后更大,塞进上下文会直接把窗口撑爆。所以字节 只在 hub ↔ 浏览器之间走一趟,hub 负责落盘 / 读盘。这是 hub 唯一一处不"纯中继"的地方。

文件钉在 --out-dir(默认 hub 的工作目录)里,../ 和绝对路径一律拒 —— 那几个 路径参数是模型给的,「能读写用户整块磁盘」和「能改用户正在编辑的文档」是两个 量级的权限。

前提:编辑器实例得注册了 ExportDocx / ImportDocx 扩展(从 @4399ywkf/editor/docx 引入,通过 <NotionEditor extensions={[...]}> 注入)。 没注册时工具会报错并说明怎么装,而不是静默失败。表格侧没有这道手续 —— sheet_export_xlsx 直接读 Univer 快照拼 OOXML,但它返回的 warnings导出时丢了什么(图片、条件格式等插件数据),非空就得如实转述,别当成功报。

表格导入不在工具面里:.xlsx / .csv / .tsv<DocumentView source={file}> (实现在 @4399ywkf/editor/xlsximportSheetSource)—— 那是用户打开文件的动作, 不是 AI 的动作。

两条硬规则

LiteXML 对 ProseMirror JSON 是有损压缩(省 1.55x 以上 token),所以:

  1. modify 以原节点 attrs 为底做合并,只让 XML 里出现过的键覆盖。否则 textAlign / backgroundColor / language 会被静默抹掉 —— 「AI 改一个词顺手毁排版」。
  2. 未知节点降级成 <node type="..."/>,不抛错、不丢失。

原则:读可以有损压缩,写绝不要求模型重述它没看见的状态。 所以 doc_replace_text / doc_format_text(外科手术式)优先于 doc_modify_nodes(整块替换)。

已知限制

都源自同一件事:文档活在浏览器内存里

  1. 刷新页面文档就回到初始内容。 真架构里文档应该活在浏览器之外(yjs / 服务端持有)。
  2. 多标签页 = 多份互不相干的文档。 现在是「第一个连上的持有桥,后开的降级旁观」, onStatus 会回 observer,请在界面上显示出来。接了 yjs 之后自然消失。
  3. 没有 AI 编辑的审计与回滚。 ProseMirror 的 Step 可 invert,应该持久化 inverted steps 做 doc_revert_turn,目前没做 —— 想兜底的话用 attachBridgeonCall 钩子自己存快照。表格侧写操作都走 Univer 的 command 体系,用户按 Ctrl+Z 撤得掉, 但同样没有「这一轮 AI 改了哪些格子」的审计记录。

开发

pnpm --filter @4399ywkf/editor build     # 测试要读它的构建产物(标签表 / SheetRuntime)
pnpm --filter @4399ywkf/editor-mcp test  # hub 端到端 + 工具面形状 + 两个 runtime 的不漂移