Skip to content

feat: MCP server with personal MCP tokens - #309

Open
neSpecc wants to merge 4 commits into
mainfrom
feat/mcp-server
Open

neSpecc wants to merge 4 commits into
mainfrom
feat/mcp-server

Conversation

@neSpecc

@neSpecc neSpecc commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a remote MCP endpoint so MCP clients (Claude Code, Cursor, claude.ai custom connectors) can read and edit NoteX notes on behalf of a user. Auth is a personal MCP token passed in the Authorization header, no OAuth for now.

The full plan, decisions and deviations are in docs/mcp-plan.md.

MCP tokens

  • New mcp_tokens table (migration 0036): token hash (sha256), 4-char preview, read/write permission, label, last usage time, tool call count.
  • GET / POST / DELETE /user/mcp-tokens. The plain ntx_… token is returned only once on creation; users can revoke only their own tokens.
  • userIdResolver ignores ntx_… tokens, so other routes do not accept them.

/mcp endpoint

  • POST /mcp serves MCP over Streamable HTTP in stateless mode with JSON responses (@modelcontextprotocol/sdk). GET and DELETE return 405.
  • Tools: list_notes (recent / created / nested in a parent), get_note (by URL or id, including custom hostnames), create_note (root or inside a parent), update_note (replace / append, optional expectedUpdatedAt conflict check). No delete, no search.
  • Content is passed as Editor.js blocks; the server fills block ids and builds tools[] from the note and user tools.
  • Tools call existing routes through fastify.inject() with the token owner's access token, so route policies and validation apply unchanged.
  • read tokens see only list_notes and get_note.

Other changes

  • PATCH /note/:notePublicId accepts an optional expectedUpdatedAt: the note is updated only if its version still matches (checked in the same UPDATE), otherwise 409. update_note always passes it, so concurrent updates can't overwrite each other. Note history is written after a successful update.
  • zod bumped to ^3.25.0 (required by the SDK).
  • New config httpApi.mcp.webUrl (default https://notex.so) for links to notes.

Deploy notes

  • Run migrations (0036-mcp-tokens@add-mcp-tokens-table.sql).
  • Set httpApi.mcp.webUrl if the web app is not on https://notex.so.

Related

Test plan

  • yarn typecheck, yarn eslint src
  • yarn vitest run: 193 passed, incl. new mcpTokens.test.ts (10), mcp.test.ts (19) and a PATCH version conflict test
  • Migration applied on a clean Postgres
  • /mcp over real HTTP with curl: initialize, tools/list, create_note, list_notes, 405 on GET, 401 for a revoked token, read token sees two tools
  • Try with Claude Code / Cursor against a stage server

🤖 Generated with Claude Code

neSpecc and others added 2 commits October 21, 2026 02:56
Add a remote MCP endpoint so MCP clients (Claude Code, Cursor, claude.ai)
can read and edit NoteX notes on behalf of a user.

- mcp_tokens table, McpTokenService and /user/mcp-tokens routes to create,
  list and revoke personal tokens. Only the sha256 hash is stored; the
  plain token is returned once on creation. Tokens have read or write
  permission and track last usage and tool call count.
- POST /mcp serves MCP over Streamable HTTP in stateless mode with
  `Authorization: Bearer ntx_…`. Tools list_notes, get_note, create_note
  and update_note call existing routes through fastify.inject() with the
  token owner's access token, so route policies and validation apply.
  Read tokens see only list_notes and get_note.
- userIdResolver ignores MCP tokens, so other routes don't accept them.
- Add @modelcontextprotocol/sdk and bump zod to ^3.25 required by it.
- docs/mcp-plan.md tracks the implementation plan and deviations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 3 (MCP tokens section in notes.web settings) is done on
feat/mcp-tokens in notes.web. Record its deviations and the manual
checks of the endpoint over real HTTP and of the settings page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 87.5% (🎯 80%)
⬆️ +1.69 pp
10922 / 12481
🟢 Statements 87.5% (🎯 80%)
⬆️ +1.69 pp
10922 / 12481
🟢 Functions 83.12% (🎯 80%)
⬆️ +3.52 pp
340 / 409
🟢 Branches 86.09% (🎯 80%)
⬆️ +1.20 pp
607 / 705
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/domain/index.ts 100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
src/domain/entities/NoteUpdateConflictError.ts 100% 100% 100% 100%
src/domain/entities/mcpToken.ts 100% 100% 100% 100%
src/domain/service/mcpToken.ts 100% 100% 100% 100%
src/domain/service/note.ts 96.1%
⬇️ -0.11 pp
82.19%
⬆️ +0.77 pp
100%
🟰 ±0 pp
96.1%
⬇️ -0.11 pp
109-110, 142-143, 154-155, 183-185, 229-230, 331-332, 435-436, 452-453, 494-495, 544-545, 586-587
src/infrastructure/config/index.ts 99.06%
⬆️ +0.06 pp
50%
🟰 ±0 pp
100%
🟰 ±0 pp
99.06%
⬆️ +0.06 pp
204-205
src/presentation/http/http-api.ts 96.43%
⬆️ +0.14 pp
88.88%
🟰 ±0 pp
93.75%
🟰 ±0 pp
96.43%
⬆️ +0.14 pp
111-119, 130-131, 346-347, 371-372
src/presentation/http/middlewares/common/userIdResolver.ts 100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
src/presentation/http/router/mcpTokens.ts 100% 100% 100% 100%
src/presentation/http/router/note.ts 97.66%
⬇️ -0.30 pp
79.48%
⬆️ +0.07 pp
100%
🟰 ±0 pp
97.66%
⬇️ -0.30 pp
157-158, 412-414, 652-653, 669-675, 736-737, 793-794, 845-846
src/presentation/http/schema/McpToken.ts 100% 100% 100% 100%
src/presentation/mcp/content.ts 100% 86.66% 100% 100%
src/presentation/mcp/index.ts 88.54% 91.66% 100% 88.54% 96-110
src/presentation/mcp/noteRef.ts 83.56% 90.9% 100% 83.56% 62-73
src/presentation/mcp/notexApi.ts 95.78% 81.81% 100% 95.78% 85-88
src/presentation/mcp/server.ts 100% 100% 100% 100%
src/presentation/mcp/types.ts 100% 100% 100% 100%
src/presentation/mcp/tools/createNote.ts 100% 100% 100% 100%
src/presentation/mcp/tools/getNote.ts 100% 100% 100% 100%
src/presentation/mcp/tools/listNotes.ts 86.32% 75% 100% 86.32% 35, 37-45, 95-100
src/presentation/mcp/tools/updateNote.ts 96.34% 92.85% 100% 96.34% 72-74
src/repository/index.ts 100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
src/repository/mcpToken.repository.ts 100% 100% 100% 100%
src/repository/note.repository.ts 100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
src/repository/storage/mcpToken.storage.ts 100% 100% 100% 100%
src/repository/storage/postgres/orm/sequelize/mcpToken.ts 100% 100% 100% 100%
src/repository/storage/postgres/orm/sequelize/note.ts 97.78%
⬆️ +0.49 pp
82.14%
⬆️ +6.14 pp
100%
🟰 ±0 pp
97.78%
⬆️ +0.49 pp
243-244, 247-248, 305-306, 352-353, 445-446
Generated in workflow #941 for commit 6379282 by the Vitest Coverage Report Action

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Concurrent updates can lose edits, and successful MCP responses omit configured CORS headers.

2 open issues
What changed in this PR

Adds a remote MCP endpoint to NoteX’s existing API, allowing clients to read and edit notes using personal tokens.

Changes:

  • Adds token creation, revocation, permissions, storage, and usage tracking.
  • Implements four MCP tools through existing note routes.
  • Adds integration tests, configuration, dependencies, and deployment documentation.
File Description
yarn.lock Locks MCP dependencies and updated Zod.
src/​repository/​storage/​postgres/​orm/​sequelize/​mcpToken.ts Implements token persistence and usage tracking.
src/​repository/​storage/​mcpToken.storage.ts Exports token storage.
src/​repository/​mcpToken.repository.ts Provides token repository operations.
src/​repository/​index.ts Initializes token storage and repository.
src/​presentation/​mcp/​types.ts Defines tool context and response helpers.
src/​presentation/​mcp/​tools/​updateNote.ts Adds replace and append operations.
src/​presentation/​mcp/​tools/​listNotes.ts Lists recent, created, and nested notes.
src/​presentation/​mcp/​tools/​getNote.ts Returns note content and metadata.
src/​presentation/​mcp/​tools/​createNote.ts Creates root or nested notes.
src/​presentation/​mcp/​server.ts Registers tools by token permission.
src/​presentation/​mcp/​notexApi.ts Calls existing authenticated API routes.
src/​presentation/​mcp/​noteRef.ts Resolves note references and builds links.
src/​presentation/​mcp/​mcp.test.ts Tests authentication and MCP tools.
src/​presentation/​mcp/​index.ts Implements authenticated MCP transport.
src/​presentation/​mcp/​content.ts Handles block IDs, tools, and titles.
src/​presentation/​http/​schema/​McpToken.ts Defines token validation and serialization.
src/​presentation/​http/​router/​mcpTokens.ts Adds token management endpoints.
src/​presentation/​http/​router/​mcpTokens.test.ts Tests token lifecycle and ownership.
src/​presentation/​http/​middlewares/​common/​userIdResolver.ts Separates MCP tokens from access tokens.
src/​presentation/​http/​http-api.ts Registers MCP routes and token schema.
src/​infrastructure/​config/​index.ts Defines MCP web URL configuration.
src/​domain/​service/​mcpToken.ts Issues, hashes, resolves, and revokes tokens.
src/​domain/​index.ts Initializes the token service.
src/​domain/​entities/​mcpToken.ts Defines token entities and public fields.
package.json Adds MCP SDK and updates Zod.
migrations/​tenant/​0036-mcp-tokens@add-mcp-tokens-table.sql Creates token table, constraints, and indexes.
docs/​mcp-plan.md Documents implementation and deployment decisions.
app-config.yaml Sets the local web URL.

Review effort: Balanced

Comment thread src/presentation/mcp/tools/updateNote.ts
Comment thread src/presentation/mcp/index.ts
neSpecc and others added 2 commits October 21, 2026 03:24
- PATCH /note/:notePublicId accepts an optional expectedUpdatedAt. The
  note is updated only if updated_at still matches, checked in the same
  UPDATE query (millisecond precision), otherwise the route returns 409.
  Note history is written after a successful update.
- update_note always passes the version it read, so concurrent replaces
  and appends get a conflict instead of silently losing changes.
- /mcp copies headers set by Fastify hooks to the raw response, so CORS
  headers survive reply.hijack().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants