Skip to main content
Glama
llego
by llego

Anchor MCP

一个小型 MCP 边车(sidecar)的实现计划,通过运行在 Anchor 同一 Docker Compose 栈中的隧道客户端,向 ChatGPT 暴露安全的 Anchor Notes 工具。

研究依据:Anchor 上游仓库 ZhFahim/anchor,默认分支 main,检查日期 2026-08-20。Anchor 是一个 Nest.js 后端,在 /api/* 下提供经过认证的 REST 端点。

目标

在 Anchor 旁边运行一个 MCP 服务器,使外部助手能够列出、搜索、读取、创建、更新、导入 Anchor 笔记并为其附加文件,而无需直接暴露 Anchor 的数据库或私有 API。

Related MCP server: NotesBridge

当前状态

第一个里程碑已实现:

  • 位于 POST /mcp 的 Streamable HTTP MCP 端点。

  • 位于 GET /healthz 的健康检查端点。

  • 只读 Anchor 工具:anchor_list_notes、anchor_search_notes、anchor_get_note、anchor_list_tags、anchor_list_attachments。

  • 使用 ANCHOR_MCP_TOKEN 的可选 MCP bearer 守卫。

  • Anchor API 调用使用 ANCHOR_TOKEN 和 ANCHOR_BASE_URL。

  • 已包含 Dockerfile。

写入工具有意尚未实现。

开发

在 NixOS 上,使用 nix-shell 执行 Node/npm 命令:

nix-shell -p nodejs --run 'npm install'
nix-shell -p nodejs --run 'npm run typecheck'
nix-shell -p nodejs --run 'npm run build'

本地运行:

ANCHOR_BASE_URL=https://anchor.cri.su \
ANCHOR_TOKEN=... \
ANCHOR_MCP_TOKEN=... \
nix-shell -p nodejs --run 'npm run dev'

MCP 端点为 http://localhost:8000/mcp。如果设置了 ANCHOR_MCP_TOKEN,调用方必须发送 Authorization: Bearer <token>。

部署模型

预期的栈包含三个服务:

services:
  anchor:
    # Existing Anchor service.

  anchor-mcp:
    build: /path/to/anchor-mcp
    environment:
      ANCHOR_BASE_URL: http://anchor:3000
      ANCHOR_TOKEN: ${ANCHOR_TOKEN}
      ANCHOR_MCP_TOKEN: ${ANCHOR_MCP_TOKEN}
    expose:
      - "8000"
    depends_on:
      - anchor

  chatgpt-tunnel-client:
    # Outbound tunnel client.
    environment:
      MCP_TARGET_URL: http://anchor-mcp:8000/mcp
      MCP_TARGET_TOKEN: ${ANCHOR_MCP_TOKEN}
    depends_on:
      - anchor-mcp

MCP 服务器应仅在 Docker 网络上可达。隧道客户端是唯一的外部桥梁。

已确认的 Anchor API 表面

以下所有端点均由 Anchor 的 AuthGuard 保护,并期望 Authorization: Bearer <token>。该守卫接受可解析为活动用户的 Anchor 令牌。

笔记:

  • POST /api/notes

  • GET /api/notes?search=<query>&tagId=<tagId>&limit=<limit>

  • GET /api/notes/:id

  • PATCH /api/notes/:id

  • DELETE /api/notes/:id

  • DELETE /api/notes/:id/permanent

  • PATCH /api/notes/:id/restore

  • GET /api/notes/trash

  • GET /api/notes/archive

  • POST /api/notes/bulk/delete

  • POST /api/notes/bulk/archive

  • POST /api/notes/bulk/pin

  • POST /api/notes/bulk/tags

标签:

  • POST /api/tags

  • GET /api/tags

  • GET /api/tags/:id

  • GET /api/tags/:id/notes

  • PATCH /api/tags/:id

  • DELETE /api/tags/:id

附件:

  • POST /api/notes/:noteId/attachments

  • GET /api/notes/:noteId/attachments

  • GET /api/notes/:noteId/attachments/:id

  • DELETE /api/notes/:noteId/attachments/:id

  • PATCH /api/notes/:noteId/attachments/reorder

导入/导出:

  • POST /api/import/notes

  • POST /api/import/notes/:noteId/attachments

  • GET /api/export

同步 API:

  • POST /api/sync

  • GET /api/sync/events 作为服务器发送事件(server-sent events)

共享:

  • POST /api/notes/:id/shares

  • GET /api/notes/:id/shares

  • PATCH /api/notes/:id/shares/:shareId

  • DELETE /api/notes/:id/shares/:shareId

MCP 服务器应从常规的笔记/标签/附件/导入端点开始。同步 API 对需要冲突感知的离线客户端很有用,但 MCP 边车可以暂不涉及。

数据形状

创建笔记请求体:

{
  "title": "string",
  "content": "optional string",
  "isPinned": false,
  "isArchived": false,
  "background": "optional string",
  "tagIds": ["tag-id"]
}

更新笔记请求体是部分创建请求体加上可选的乐观锁:

{
  "title": "optional string",
  "content": "optional string",
  "isPinned": false,
  "isArchived": false,
  "background": "optional string",
  "tagIds": ["tag-id"],
  "baseVersion": 1
}

Anchor 返回转换后的笔记,包含以下重要字段:

{
  "id": "uuid",
  "title": "string",
  "content": "string or null",
  "version": 1,
  "isPinned": false,
  "isArchived": false,
  "background": null,
  "state": "active",
  "createdAt": "iso timestamp",
  "updatedAt": "iso timestamp",
  "userId": "uuid",
  "tagIds": ["tag-id"],
  "permission": "owner",
  "attachmentCount": 0,
  "imagePreviewIds": []
}

导入笔记请求体:

{
  "notes": [
    {
      "ref": "external stable reference, max 256 chars",
      "id": "optional uuid",
      "title": "string",
      "content": "stringified Quill Delta JSON",
      "isPinned": false,
      "isArchived": false,
      "isTrashed": false,
      "background": "optional background id",
      "tagNames": ["tag name"],
      "createdAt": "iso timestamp",
      "updatedAt": "iso timestamp"
    }
  ],
  "tags": [{ "name": "tag", "color": "#8B5CF6" }],
  "skipExisting": true
}

导入结果形状:

{
  "results": [
    {
      "ref": "external reference",
      "status": "created | skipped | remapped | failed",
      "noteId": "uuid",
      "warning": "optional string",
      "error": "optional string"
    }
  ],
  "tags": { "created": 0, "reused": 0 }
}

附件上传形状:

  • 普通笔记上传:multipart file 字段,发送到 POST /api/notes/:noteId/attachments。

  • 导入附件上传:multipart file 加上 position 表单字段,发送到 POST /api/import/notes/:noteId/attachments。

  • 附件响应包含 id、noteId、type、originalFilename、mimeType、fileSize、position、uploadedByUserId 和 createdAt。

限制与验证

笔记列表限制:

  • GET /api/notes 将 limit 限制在 1..200 范围内。

批量限制:

  • noteIds:最多 200 个。

  • tagIds:最多 50 个。

导入限制:

  • 每批笔记数:50。

  • 字符串化 Delta 内容长度:1,000,000 字节/字符。

  • 标题长度:1000。

  • 每篇笔记的标签数:50。

  • 每批导入的标签数:500。

  • 标签名称长度:100。

附件限制:

  • 最大文件大小:50 MB。

  • 允许的图片:image/jpeg、image/png、image/webp、image/gif。

  • 允许的音频:audio/mpeg、audio/wav、audio/mp4、audio/x-m4a、audio/ogg、audio/aac、audio/webm。

  • 当前源码拒绝 PDF、JSON、ZIP 和通用的 application/octet-stream。

导入允许的背景 ID:

  • color_red、color_orange、color_yellow、color_green、color_teal、color_blue、color_dark_blue、color_purple、color_pink、color_brown。

  • pattern_dots、pattern_grid、pattern_lines、pattern_waves、pattern_groceries、pattern_music、pattern_travel、pattern_code。

内容格式

Anchor 将笔记 content 存储为字符串。现有的导入工作确认,对于富文本导入,这应该是字符串化的 Quill Delta JSON。

MCP 服务器应暴露对 Markdown 友好的工具,并在内部将 Markdown 转换为 Quill Delta。以后还可以暴露专家模式的原生 Delta 工具。

推荐的转换策略:

  • anchor_create_note 接受 Markdown,转换为 Delta,调用 POST /api/notes。

  • anchor_update_note 接受 Markdown,转换为 Delta,调用 PATCH /api/notes/:id,可带 baseVersion。

  • anchor_import_notes 接受 Markdown 或原生 Delta,通过 POST /api/import/notes 分批处理。

  • anchor_get_note 返回原始内容,外加一个尽力而为的文本/Markdown 投影,便于 LLM 阅读。

认证模型

Anchor 源码使用从 Authorization: Bearer <token> 提取 bearer 令牌的方式。因此 MCP 边车应维护两层认证:

  • ANCHOR_TOKEN:anchor-mcp 调用 Anchor 时使用的令牌。

  • ANCHOR_MCP_TOKEN:隧道客户端在请求被服务之前必须提供的令牌。

MCP 服务器绝不应将任意调用方令牌转发给 Anchor。

源码参考

上游检查的主要文件:

  • server/src/notes/controllers/notes.controller.ts

  • server/src/notes/controllers/note-attachments.controller.ts

  • server/src/notes/controllers/note-shares.controller.ts

  • server/src/tags/tags.controller.ts

  • server/src/import-export/import.controller.ts

  • server/src/import-export/export.controller.ts

  • server/src/sync/sync.controller.ts

  • server/src/sync/sync-events.controller.ts

  • server/src/notes/dto/create-note.dto.ts

  • server/src/notes/dto/update-note.dto.ts

  • server/src/import-export/dto/import-notes.dto.ts

  • server/src/import-export/dto/import-attachment.dto.ts

  • server/src/notes/constants/notes.constants.ts

  • server/src/import-export/constants/import.constants.ts

  • server/src/notes/utils/note-transformer.util.ts

  • server/src/notes/utils/attachment-storage.util.ts

MCP 工具

第一阶段只读工具:

  • anchor_list_notes(limit, offset)

  • anchor_search_notes(query, limit)

  • anchor_get_note(note_id)

  • anchor_list_tags()

  • anchor_list_attachments(note_id)

已实现工具详情:

  • anchor_list_notes 支持 limit、offset、include_content 和 tag_id。由于 Anchor 只暴露基于 limit 的列表,offset + limit 最多必须为 200。

  • anchor_search_notes 支持 query、limit、include_content 和 tag_id。

  • anchor_get_note 支持 note_id 和 include_content。

  • anchor_list_tags 不接受任何输入。

  • anchor_list_attachments 仅返回元数据,不下载附件字节。

第二阶段写入工具:

  • anchor_create_note(title, markdown)

  • anchor_update_note(note_id, markdown, base_version)

  • anchor_import_notes(notes)

  • anchor_create_tag(name, color)

  • anchor_upload_attachment(note_id, file, filename, mime_type)

第三阶段管理工具:

  • anchor_archive_notes(note_ids)

  • anchor_pin_notes(note_ids, is_pinned)

  • anchor_add_tags(note_ids, tag_ids)

  • anchor_export()(如果隧道客户端能处理流式归档)

避免或限制破坏性工具:

  • anchor_delete_note(note_id, confirm) 映射到软删除,应要求 confirm=true。

  • anchor_permanent_delete_note(note_id, confirm) 最初应省略。

  • anchor_delete_tag(tag_id, confirm) 最初应省略。

  • 不要暴露任意的原始 HTTP 代理工具。

安全

  • 仅将 ANCHOR_TOKEN 存储在 Docker 栈环境或 .env 中;不要将其烘焙到镜像中。

  • 为从隧道客户端到 anchor-mcp 的调用添加单独的 ANCHOR_MCP_TOKEN。

  • 仅将 MCP 服务器绑定到容器网络;除非有意暴露,否则不要添加 Traefik 标签。

  • 保持工具窄而类型化。不允许调用方选择任意的 Anchor API 路径。

  • 记录请求元数据,而不是笔记内容或令牌。

  • 在隧道认证路径验证之前,默认只读工具。

  • 对软删除和批量破坏性操作要求显式的 confirm=true。

  • 除非存在单独的 ENABLE_DANGEROUS_TOOLS=true 设置,否则拒绝永久删除。

实现阶段

  1. 创建一个最小的 TypeScript MCP HTTP 服务器。

  2. 从环境添加配置:ANCHOR_BASE_URL、ANCHOR_TOKEN、ANCHOR_MCP_TOKEN、绑定主机/端口。

  3. 为 Docker 和隧道诊断实现 /healthz。

  4. 实现一个小型 Anchor API 客户端,具有类型化方法且没有任意路径逃生口。

  5. 实现 anchor_list_notes、anchor_search_notes、anchor_get_note 和 anchor_list_tags。

  6. 添加响应整形,除非显式请求,否则剥离重字段。

  7. 实现 Markdown 到 Delta 的转换辅助函数和测试。

  8. 实现创建/更新,通过 baseVersion 支持可选的乐观锁。

  9. 按照已知的导入限制实现导入批处理。

  10. 仅对允许的图片/音频实现附件上传。

  11. 添加 Dockerfile 和 Compose 示例,包括隧道客户端占位符。

  12. 使用模拟的 Anchor 响应和验证失败添加测试。

  13. 添加关于轮换令牌和接线 ChatGPT 隧道客户端的运维文档。

未决问题

  • 确切的隧道客户端镜像、环境变量和认证头格式。

  • Anchor 是否可以配置或修补以允许 PDF 和其他文件类型。

  • 笔记内容应接受 Markdown 并转换为 Quill Delta,还是 MCP 应直接暴露 Anchor 的原生内容格式。

  • 隧道客户端是否能足够好地传递二进制负载以支持附件上传和导出下载。

  • 是否应在客户端模拟 offset,因为 GET /api/notes 只暴露 limit,而不暴露偏移分页。

推荐的第一里程碑

构建一个只读 MCP 服务器,包含 anchor_list_notes、anchor_search_notes、anchor_get_note 和 anchor_list_tags。将其私有部署在 Anchor 栈中、隧道客户端之后。仅在读取路径和认证模型验证通过后,再添加创建/更新/导入。

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server for AI agents to read, write, and organize notes in a local-first, human-in-the-loop note-taking app.
    1 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Read-only MCP bridge that exposes secure search and fetch tools over an Obsidian-compatible Markdown vault, enabling ChatGPT to query notes without write access.
    1
    Apache 2.0