-
Notifications
You must be signed in to change notification settings - Fork 4
feat(mcp): local MCP server exposing the wiki to AI clients #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
42053de
feat(mcp): local MCP server exposing the wiki to AI clients
AdminTeamCoderz ada3b18
docs(mcp): add MCP.md and link it from the README
AdminTeamCoderz 041be57
feat(mcp): add move_document tool
AdminTeamCoderz ef44655
fix(mcp): require Node 22.9 and scope the revision guarantee to conte…
AdminTeamCoderz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "mcpServers": { | ||
| "wordyme": { | ||
| "command": "pnpm", | ||
| "args": ["--filter", "@repo/mcp", "--silent", "start"] | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| # Connect Claude to your wiki | ||
|
|
||
| WordyMe ships an [MCP](https://modelcontextprotocol.io) server. Point Claude Code or | ||
| Claude Desktop at your wiki and Claude can search it, read any page, write new notes and | ||
| revise existing ones — in plain Markdown, as your own account, with every change recorded | ||
| as a restorable revision. | ||
|
|
||
| ```text | ||
| You: Read my DOCKERHUB document and create a note called "MCP Test" | ||
| with a summary table and a mermaid diagram of the release flow. | ||
|
|
||
| Claude: Created "MCP Test" in Color Workspace — open it in WordyMe: | ||
| the table renders, the diagram draws, Revisions History says "via Claude". | ||
| ``` | ||
|
|
||
| ## Why this one is different | ||
|
|
||
| **It speaks the editor's own language.** Claude writes Markdown; WordyMe's editor converts | ||
| it with the exact transformers it uses when _you_ type Markdown. Headings, lists, tables, | ||
| code blocks and even Mermaid diagram fences become real rich-text nodes — not an | ||
| approximation. Reading goes the other way through the same transformers, so Claude sees | ||
| your pages as faithful Markdown. | ||
|
|
||
| **It is you, not a second user.** WordyMe is deliberately single-user. The server signs in | ||
| with your credentials and acts on your behalf — the same delegation model every | ||
| connected app uses — so there is no "AI account" to manage and no sharing model to bolt | ||
| on. | ||
|
|
||
| **Nothing is ever lost.** Every content write — creating a page or revising one — is saved | ||
| as a new revision named **"via Claude"**. Earlier revisions stay in Revisions History and | ||
| can be restored with one click. Moving a page only changes where it sits in the tree. The | ||
| server never deletes anything. | ||
|
|
||
| **Local by design.** It runs on your machine, next to your clone of the repository. | ||
| Credentials live in a git-ignored file you control. Nothing about the WordyMe app or its | ||
| Docker image changes. | ||
|
|
||
| ## What Claude can do | ||
|
|
||
| | Tool | What it does | | ||
| | ------------------ | -------------------------------------------------------------- | | ||
| | `list_spaces` | List your Spaces | | ||
| | `list_documents` | List the documents and folders of one Space | | ||
| | `search_documents` | Full-text search across the wiki, with snippets | | ||
| | `read_document` | Return a page as Markdown | | ||
| | `create_note` | Create a note from Markdown, optionally inside a folder | | ||
| | `update_document` | Replace a page body with new Markdown, saved as a new revision | | ||
| | `move_document` | Move a page or folder into a folder, or back to the Space root | | ||
|
|
||
| Things people ask once it is connected: | ||
|
|
||
| - _"Search my wiki for everything about backups and summarise it."_ | ||
| - _"Read my meeting notes from this week and draft the follow-up email."_ | ||
| - _"Turn this rough list into a properly structured page with a table."_ | ||
| - _"Add a Mermaid diagram of the deployment flow to the Docker page."_ | ||
|
|
||
| ## Setup in three steps | ||
|
|
||
| Requires a running WordyMe and a clone of this repository. | ||
|
|
||
| 1. **Credentials** — copy the example and fill in your login: | ||
|
|
||
| ```bash | ||
| cp apps/mcp/.env.example apps/mcp/.env | ||
| ``` | ||
|
|
||
| `WORDYME_URL` is where the API answers: `http://localhost:3000` for `pnpm dev`, | ||
| `http://localhost:8080` for Docker, or your instance URL. | ||
|
|
||
| 2. **Claude Code** (desktop app or CLI) — nothing to install. The repository carries a | ||
| project-level `.mcp.json`; open the repository in Claude Code and approve the | ||
| `wordyme` server when prompted. | ||
|
|
||
| 3. **Claude Desktop** — add to `claude_desktop_config.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "wordyme": { | ||
| "command": "pnpm", | ||
| "args": ["--dir", "/path/to/WordyMe", "--filter", "@repo/mcp", "--silent", "start"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Then ask Claude to list your Spaces. | ||
|
|
||
| ## How it works | ||
|
|
||
| - **Conversion** runs a headless copy of the WordyMe editor in Node — the same node list | ||
| and the same Markdown transformers the browser uses — so output is what the editor | ||
| itself would have produced. | ||
| - **Authentication** uses the backend's existing Better Auth bearer-token support: one | ||
| sign-in at first use, an expired session re-signs transparently. | ||
| - **Writes** go through the normal document and revision APIs; MCP-created notes are | ||
| structurally identical to notes created in the editor. | ||
|
|
||
| The implementation lives in [`apps/mcp`](apps/mcp/README.md). | ||
|
|
||
| ## Honest limits | ||
|
|
||
| - Fidelity is bounded by Markdown. Standard constructs and the editor's fenced | ||
| extensions (` ```mermaid `) round-trip; exotic nodes such as sketches, scores and | ||
| stickies degrade to plain text when read. | ||
| - Nested list items need 4-space indentation. | ||
| - Claude acts with your full account; the server is meant for your own machine. | ||
|
|
||
| ## What's next | ||
|
|
||
| - **Built into the image.** An opt-in `/mcp` endpoint inside the WordyMe container, so | ||
| self-hosters connect Claude with a URL and no local setup. | ||
| - **Proper OAuth.** WordyMe as the identity provider — Claude signs in through WordyMe's | ||
| own consent screen, with revocable tokens and read-only scopes. | ||
| - **claude.ai.** With the two above, publicly reachable instances connect from the web app | ||
| as well. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Copy to .env (git-ignored). Variables already set in the environment take precedence. | ||
| # The origin the WordyMe API answers on: http://localhost:3000 for `pnpm dev`, | ||
| # http://localhost:8080 for Docker, or your instance URL. | ||
| WORDYME_URL=http://localhost:3000 | ||
| WORDYME_EMAIL=you@example.com | ||
| WORDYME_PASSWORD=your-password |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # @repo/mcp | ||
|
|
||
| A local MCP (Model Context Protocol) server that exposes a WordyMe instance to AI | ||
| clients such as Claude Code and Claude Desktop. Markdown in, Markdown out — content is | ||
| converted with the editor's own transformers, so what the AI writes is exactly what the | ||
| editor would produce if you typed the same Markdown yourself. | ||
|
|
||
| The server acts **as your account** (delegation — WordyMe stays single-user). Every content | ||
| change it makes is saved as a new revision named **"via Claude"**, restorable from | ||
| Revisions History; moving a document only changes its place in the tree. | ||
|
|
||
| ## Tools | ||
|
|
||
| | Tool | What it does | | ||
| | ------------------ | ------------------------------------------------------------- | | ||
| | `list_spaces` | List the Spaces in the wiki | | ||
| | `list_documents` | List one Space's documents and folders | | ||
| | `search_documents` | Full-text search with snippets | | ||
| | `read_document` | Return a document as Markdown | | ||
| | `create_note` | Create a note from Markdown | | ||
| | `update_document` | Replace a document body with new Markdown (new revision) | | ||
| | `move_document` | Move a document or folder into a folder, or to the Space root | | ||
|
|
||
| ## Setup | ||
|
|
||
| Requires a running WordyMe instance and its owner credentials. `WORDYME_URL` is the | ||
| origin the API answers on: | ||
|
|
||
| | How WordyMe runs | `WORDYME_URL` | | ||
| | ----------------------------- | ---------------------------------------------- | | ||
| | `pnpm dev` (backend directly) | `http://localhost:3000` | | ||
| | Docker / self-hosted | `http://localhost:8080` (or your instance URL) | | ||
|
|
||
| ### 1. Credentials | ||
|
|
||
| ```bash | ||
| cp apps/mcp/.env.example apps/mcp/.env | ||
| ``` | ||
|
|
||
| Fill in `apps/mcp/.env`. It is git-ignored and read by the server at startup; variables | ||
| already present in the environment take precedence over the file. | ||
|
|
||
| ### 2. Claude Code (desktop app or CLI) | ||
|
|
||
| Nothing to install: the repository ships a project-level `.mcp.json` that starts the | ||
| server with `pnpm --filter @repo/mcp --silent start`. Open the repository in Claude Code | ||
| and approve the project MCP server when prompted — new sessions then have the `wordyme` | ||
| tools. Check status with `/mcp` inside a session. | ||
|
|
||
| ### Claude Desktop | ||
|
|
||
| `claude_desktop_config.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "wordyme": { | ||
| "command": "pnpm", | ||
| "args": ["--dir", "/path/to/WordyMe", "--filter", "@repo/mcp", "--silent", "start"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Credentials never live in MCP client configuration or in this repository — only in | ||
| `apps/mcp/.env` (or the process environment). | ||
|
|
||
| ## Notes | ||
|
|
||
| - `pnpm smoke` runs the Markdown round-trip self-test (no WordyMe instance needed). | ||
| - Content fidelity is bounded by Markdown: standard constructs plus the editor's fenced | ||
| extensions (```mermaid diagrams) round-trip; exotic nodes (sketches, scores, stickies) | ||
| degrade to plain text. Nested list items need 4-space indentation. | ||
| - The server never deletes anything and every write is a new revision. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { config } from '@repo/eslint-config/base'; | ||
|
|
||
| export default config; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "name": "@repo/mcp", | ||
| "version": "0.0.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "engines": { | ||
| "node": ">=22.9.0" | ||
| }, | ||
| "scripts": { | ||
| "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --outfile=dist/index.js --loader:.css=empty --loader:.svg=empty --loader:.png=empty --loader:.jpg=empty --loader:.woff=empty --loader:.woff2=empty --log-level=warning", | ||
| "start": "pnpm build && node --env-file-if-exists=.env dist/index.js", | ||
| "smoke": "esbuild scripts/smoke-markdown.ts --bundle --platform=node --target=node20 --format=esm --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --outfile=dist/smoke.js --loader:.css=empty --loader:.svg=empty --loader:.png=empty --loader:.jpg=empty --loader:.woff=empty --loader:.woff2=empty --log-level=warning && node dist/smoke.js", | ||
| "lint": "eslint .", | ||
| "check-types": "tsc --noEmit" | ||
| }, | ||
| "dependencies": { | ||
| "@happy-dom/global-registrator": "^20.11.2", | ||
| "@lexical/markdown": "^0.41.0", | ||
| "@modelcontextprotocol/sdk": "^1.30.0", | ||
| "@repo/backend": "workspace:*", | ||
| "@repo/editor": "workspace:*", | ||
| "@repo/lib": "workspace:*", | ||
| "@repo/sdk": "workspace:*", | ||
| "axios": "^1.19.0", | ||
| "lexical": "^0.41.0", | ||
| "zod": "^3.25.76" | ||
| }, | ||
| "devDependencies": { | ||
| "@repo/eslint-config": "workspace:*", | ||
| "@repo/typescript-config": "workspace:*", | ||
| "@types/node": "^26.2.0", | ||
| "esbuild": "^0.25.12", | ||
| "eslint": "^10.8.0", | ||
| "typescript": "^6.0.3" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| /** | ||
| * SPDX-FileCopyrightText: 2026 TeamCoderz Ltd <legal@teamcoderz.org> | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| import { fromMarkdown, toMarkdown } from '../src/markdown.js'; | ||
|
|
||
| const sample = `# Smoke Test | ||
|
|
||
| Some **bold** and *italic* text with a [link](https://wordy.me). | ||
|
|
||
| ## A list | ||
|
|
||
| - one | ||
| - two | ||
| - nested | ||
|
|
||
| ## A table | ||
|
|
||
| | Col A | Col B | | ||
| | ----- | ----- | | ||
| | 1 | 2 | | ||
|
|
||
| ## Code | ||
|
|
||
| \`\`\`ts | ||
| const x: number = 1; | ||
| \`\`\` | ||
|
|
||
| \`\`\`mermaid | ||
| graph TD; A-->B; | ||
| \`\`\` | ||
| `; | ||
|
|
||
| const state = fromMarkdown(sample); | ||
| const roundTripped = toMarkdown(state); | ||
|
|
||
| const rootTypes = state.root.children.map((node) => node.type); | ||
| if (rootTypes[0] !== 'page-setup' || rootTypes[1] !== 'page') { | ||
| throw new Error(`Expected the editor's page scaffold, got root children: ${rootTypes}`); | ||
| } | ||
| if (!roundTripped.includes('## A table') || !roundTripped.includes('```mermaid')) { | ||
| throw new Error('Round-trip lost block structure'); | ||
| } | ||
|
|
||
| console.log('--- root structure ---'); | ||
| console.log(rootTypes.join(' > ')); | ||
| console.log('--- markdown round-trip ---'); | ||
| console.log(roundTripped); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.