Skip to main content
Glama
llego
by llego

Anchor MCP

Implementation plan for a small MCP sidecar that exposes safe Anchor Notes tools to ChatGPT through a tunnel client running in the same Docker Compose stack as Anchor.

Research basis: Anchor upstream repository ZhFahim/anchor, default branch main, inspected 2026-08-20. Anchor is a Nest.js backend with authenticated REST endpoints under /api/*.

Goal

Run an MCP server next to Anchor so external assistants can list, search, read, create, update, import, and attach files to Anchor notes without exposing Anchor's database or private API directly.

Related MCP server: NotesBridge

Current Status

First milestone is implemented:

  • Streamable HTTP MCP endpoint at POST /mcp.

  • Health endpoint at GET /healthz.

  • Read-only Anchor tools: anchor_list_notes, anchor_search_notes, anchor_get_note, anchor_list_tags, anchor_list_attachments.

  • Optional MCP bearer guard using ANCHOR_MCP_TOKEN.

  • Anchor API calls use ANCHOR_TOKEN and ANCHOR_BASE_URL.

  • Dockerfile is included.

Write tools are intentionally not implemented yet.

Development

On NixOS, use nix-shell for Node/npm commands:

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

Run locally:

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

The MCP endpoint is http://localhost:8000/mcp. If ANCHOR_MCP_TOKEN is set, callers must send Authorization: Bearer <token>.

Deployment Model

The intended stack has three services:

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

The MCP server should only be reachable on the Docker network. The tunnel client is the only external bridge.

Confirmed Anchor API Surface

All endpoints below are guarded by Anchor's AuthGuard and expect Authorization: Bearer <token>. The guard accepts Anchor tokens that resolve to an active user.

Notes:

  • 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

Tags:

  • POST /api/tags

  • GET /api/tags

  • GET /api/tags/:id

  • GET /api/tags/:id/notes

  • PATCH /api/tags/:id

  • DELETE /api/tags/:id

Attachments:

  • 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

Import/export:

  • POST /api/import/notes

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

  • GET /api/export

Sync API:

  • POST /api/sync

  • GET /api/sync/events as server-sent events

Sharing:

  • POST /api/notes/:id/shares

  • GET /api/notes/:id/shares

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

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

The MCP server should start with normal notes/tags/attachments/import endpoints. The sync API is useful for conflict-aware offline clients, but an MCP sidecar can avoid it initially.

Data Shapes

Create note body:

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

Update note body is a partial create body plus optional optimistic lock:

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

Anchor returns transformed notes with these important fields:

{
  "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": []
}

Import notes body:

{
  "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
}

Import result shape:

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

Attachment upload shapes:

  • Normal note upload: multipart file field to POST /api/notes/:noteId/attachments.

  • Import attachment upload: multipart file plus position form field to POST /api/import/notes/:noteId/attachments.

  • Attachment response includes id, noteId, type, originalFilename, mimeType, fileSize, position, uploadedByUserId, and createdAt.

Limits And Validation

Notes list limit:

  • GET /api/notes clamps limit to 1..200.

Bulk limits:

  • noteIds: max 200.

  • tagIds: max 50.

Import limits:

  • Notes per batch: 50.

  • Stringified Delta content length: 1,000,000 bytes/chars.

  • Title length: 1000.

  • Tags per note: 50.

  • Tags per import batch: 500.

  • Tag name length: 100.

Attachment limits:

  • Max file size: 50 MB.

  • Allowed images: image/jpeg, image/png, image/webp, image/gif.

  • Allowed audio: audio/mpeg, audio/wav, audio/mp4, audio/x-m4a, audio/ogg, audio/aac, audio/webm.

  • PDFs, JSON, ZIP, and generic application/octet-stream are rejected by current source.

Background IDs allowed by import:

  • 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.

Content Format

Anchor stores note content as a string. Existing import work confirms this should be stringified Quill Delta JSON for rich-text import.

The MCP server should expose Markdown-friendly tools and convert Markdown to Quill Delta internally. It can also expose expert-mode native Delta tools later.

Recommended conversion policy:

  • anchor_create_note accepts Markdown, converts to Delta, calls POST /api/notes.

  • anchor_update_note accepts Markdown, converts to Delta, calls PATCH /api/notes/:id with optional baseVersion.

  • anchor_import_notes accepts Markdown or native Delta, batches through POST /api/import/notes.

  • anchor_get_note returns raw content plus a best-effort text/Markdown projection for LLM readability.

Authentication Model

Anchor source uses bearer-token extraction from Authorization: Bearer <token>. The MCP sidecar should therefore maintain two auth layers:

  • ANCHOR_TOKEN: token used by anchor-mcp when calling Anchor.

  • ANCHOR_MCP_TOKEN: token expected from the tunnel client before any MCP request is served.

The MCP server should never forward arbitrary caller tokens to Anchor.

Source References

Primary files inspected upstream:

  • 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 Tools

Phase 1 read tools:

  • anchor_list_notes(limit, offset)

  • anchor_search_notes(query, limit)

  • anchor_get_note(note_id)

  • anchor_list_tags()

  • anchor_list_attachments(note_id)

Implemented tool details:

  • anchor_list_notes supports limit, offset, include_content, and tag_id. Because Anchor only exposes limit-based listing, offset + limit must be at most 200.

  • anchor_search_notes supports query, limit, include_content, and tag_id.

  • anchor_get_note supports note_id and include_content.

  • anchor_list_tags takes no input.

  • anchor_list_attachments returns metadata only and does not download attachment bytes.

Phase 2 write tools:

  • 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)

Phase 3 management tools:

  • anchor_archive_notes(note_ids)

  • anchor_pin_notes(note_ids, is_pinned)

  • anchor_add_tags(note_ids, tag_ids)

  • anchor_export() if the tunnel client can handle a streamed archive.

Avoid or gate destructive tools:

  • anchor_delete_note(note_id, confirm) maps to soft delete and should require confirm=true.

  • anchor_permanent_delete_note(note_id, confirm) should be omitted initially.

  • anchor_delete_tag(tag_id, confirm) should be omitted initially.

  • Do not expose a raw arbitrary HTTP proxy tool.

Security

  • Store ANCHOR_TOKEN only in the Docker stack environment or .env; do not bake it into the image.

  • Add a separate ANCHOR_MCP_TOKEN for calls from the tunnel client to anchor-mcp.

  • Bind the MCP server to the container network only; do not add Traefik labels unless intentionally exposing it.

  • Keep tools narrow and typed. Do not allow callers to choose arbitrary Anchor API paths.

  • Log request metadata, not note content or tokens.

  • Default to read-only tools until the tunnel auth path is verified.

  • Require explicit confirm=true for soft-delete and bulk destructive actions.

  • Refuse permanent deletion unless a separate ENABLE_DANGEROUS_TOOLS=true setting is present.

Implementation Phases

  1. Create a minimal TypeScript MCP HTTP server.

  2. Add configuration from environment: ANCHOR_BASE_URL, ANCHOR_TOKEN, ANCHOR_MCP_TOKEN, bind host/port.

  3. Implement /healthz for Docker and tunnel diagnostics.

  4. Implement a small Anchor API client with typed methods and no arbitrary path escape hatch.

  5. Implement anchor_list_notes, anchor_search_notes, anchor_get_note, and anchor_list_tags.

  6. Add response shaping that strips heavy fields unless explicitly requested.

  7. Implement Markdown-to-Delta conversion helpers and tests.

  8. Implement create/update with optional optimistic locking via baseVersion.

  9. Implement import batching with the known import limits.

  10. Implement attachment upload for allowed images/audio only.

  11. Add Dockerfile and Compose example including the tunnel client placeholder.

  12. Add tests with mocked Anchor responses and validation failures.

  13. Add operational docs for rotating tokens and wiring the ChatGPT tunnel client.

Open Questions

  • Exact tunnel-client image, environment variables, and auth header format.

  • Whether Anchor can be configured or patched to allow PDFs and other file types.

  • Whether note content should be accepted as Markdown and converted to Quill Delta, or whether the MCP should expose Anchor's native content format directly.

  • Whether the tunnel client can pass binary payloads well enough for attachment upload and export download.

  • Whether offset should be simulated client-side because GET /api/notes only exposes limit, not offset pagination.

Build a read-only MCP server with anchor_list_notes, anchor_search_notes, anchor_get_note, and anchor_list_tags. Deploy it privately in the Anchor stack behind the tunnel client. Add create/update/import only after the read path and authentication model are verified.

A
license - permissive license
Not graded
quality - not tested
C
maintenance

Maintenance

–Maintainers
–Response time
–Release cycle
–Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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.
    0
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A secure multi-tenant MCP proxy that exposes 81 tools for full CRUD, search, chat, podcast, and command management on the OpenNotebook API, enabling natural language interaction with notebooks, notes, sources, and more.
    GPL 3.0

View all related MCP servers

Related MCP Connectors

  • Search, read, and write your Apple Notes from ChatGPT/Claude via a local Mac agent + MCP relay.

  • Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only

  • Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/llego/anchor-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server