Anchor MCP

Anchor MCP

A lightweight MCP sidecar exposing safe, read-only Anchor note tools to ChatGPT via a tunnel client in the same Docker stack. It currently supports listing, searching, reading notes, tags, and attachment metadata without exposing Anchor's private API or database.

Category
Visit Server

README

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.

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.

Recommended First Milestone

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured