#+TITLE: org-mcp
[[https://github.com/laurynas-biveinis/org-mcp/actions/workflows/elisp-test.yml][https://github.com/laurynas-biveinis/org-mcp/actions/workflows/elisp-test.yml/badge.svg]] [[https://github.com/laurynas-biveinis/org-mcp/actions/workflows/super-linter.yml][https://github.com/laurynas-biveinis/org-mcp/actions/workflows/super-linter.yml/badge.svg]]
- Overview
org-mcp is an Emacs package that implements a Model Context Protocol (MCP) server for Org-mode. It enables AI assistants and other MCP clients to interact with your Org files through a structured API.
- Installation
On Emacs 29.1 or later:
#+begin_src emacs-lisp (package-vc-install "https://github.com/laurynas-biveinis/org-mcp.git") #+end_src
Eventually I plan to publish this package on MELPA.
- Usage
WARNING: some of the tools in this package give LLMs WRITE access to your Org files, and, once in a thousand invocations, LLMs will try to delete everything, because they are like that. Backups and automatic versioning on every change are strongly advised.
** Configuring allowed files
Once you read and internalized the warning above, set the allowed Org file set, using absolute paths:
#+begin_src emacs-lisp (setq org-mcp-allowed-files '("/path/to/foo.org" "/path/to/bar.org")) #+end_src
** Registering with an MCP Client
After =mcp-server-lib= has been properly installed (including =M-x mcp-server-lib-install=), register =org-mcp= with your MCP client:
#+begin_src bash claude mcp add -s user -t stdio org-mcp -- ~/.emacs.d/emacs-mcp-stdio.sh --server-id=org-mcp --init-function=org-mcp-enable --stop-function=org-mcp-disable #+end_src
Before using the MCP server, you must start it in Emacs with =M-x mcp-server-lib-start=. Stop it with =M-x mcp-server-lib-stop= when done.
** Available MCP Resources
Note: File paths in URIs use minimal encoding (only =#= characters are encoded). Avoid using =%= characters in Org file names.
*** org://{filename}
- Description: Access the raw content of an allowed Org file
- URI Pattern: =org://{filename}= where filename is the absolute path to the file
- Configuration: Files must be explicitly allowed via =org-mcp-allowed-files= using absolute paths
- Returns: Plain text content of the Org file
*** org-outline://{filename}
- Description: Get the hierarchical structure of an Org file
- URI Pattern: =org-outline://{filename}= where filename is the absolute path to the file
- Configuration: Files must be explicitly allowed via =org-mcp-allowed-files= using absolute paths
- Returns: JSON representation of the document structure with headings and their levels
Example: #+begin_example
Access via MCP:
URI: org-outline:///home/user/org/projects.org Returns: JSON structure like: { "headings": [ { "title": "Project Alpha", "level": 1, "children": [ {"title": "Requirements", "level": 2, "children": []}, {"title": "Implementation", "level": 2, "children": []} ] } ] } #+end_example
*** org-headline://{filename}#{path}
- Description: Access the content of a specific headline by its path
- URI Pattern: =org-headline://{filename}#{path}= where:
- =filename= is the absolute path (with # encoded as %23)
- =path= is URL-encoded headline titles separated by =/=
- Headlines containing # must be encoded as %23 in the path
- Configuration: Files must be explicitly allowed via =org-mcp-allowed-files= using absolute paths
- Returns: Plain text content of the specified headline section including all subheadings
Example: #+begin_example
Access a headline:
URI: org-headline:///home/user/org/projects.org#Project%20Alpha/Requirements Returns: Content of "Requirements" under "Project Alpha"
Headline with # character (must be encoded as %23):
URI: org-headline:///home/user/org/projects.org#Issue%20%2342 Returns: Content of "Issue #42" headline
Access entire file (no fragment):
URI: org-headline:///home/user/org/projects.org Returns: Full content of the file
File with # in the name (must be encoded as %23):
URI: org-headline:///home/user/org/file%231.org#Headline Returns: Content of "Headline" from file#1.org
Both file and headline with # (all encoded):
URI: org-headline:///home/user/org/file%231.org#Task%20%235 Returns: Content of "Task #5" from file#1.org #+end_example
Encoding limitations: File paths use minimal encoding (only =#= → =%23=) for readability. Files with =%= characters in their names should be avoided, as they may cause decoding issues. For such files, rename them or use =org-id://= URIs instead. Headline paths use full URL encoding.
*** org-id URI Format
- Description: Access Org node content by its unique ID property
- URI Pattern: =org-id://{uuid}= where uuid is the value of an ID property
- Configuration: The file containing the ID must be in =org-mcp-allowed-files=
- Returns: Plain text content of the headline with the specified ID, including all subheadings
Example: #+begin_example
Org file with ID property:
,* Project Meeting Notes :PROPERTIES: :ID: 550e8400-e29b-41d4-a716-446655440000 :END: Meeting content here... #+end_example
Access via MCP:
- URI: =org-id://550e8400-e29b-41d4-a716-446655440000=
- Returns: Content of "Project Meeting Notes" section
** Available MCP Tools
Note: All write tools will create Org IDs for any touched nodes that did not have them originally. The IDs will be returned in the tool response.
*** org-get-todo-config
- Description: Get TODO keyword configuration for understanding task states
- Parameters: None
- Returns: JSON object with =sequences= and =semantics=
Example response: #+begin_src json { "sequences": [ { "type": "sequence", "keywords": ["TODO", "NEXT", "|", "DONE", "CANCELLED"] } ], "semantics": [ {"state": "TODO", "isFinal": false, "sequenceType": "sequence"}, {"state": "NEXT", "isFinal": false, "sequenceType": "sequence"}, {"state": "DONE", "isFinal": true, "sequenceType": "sequence"}, {"state": "CANCELLED", "isFinal": true, "sequenceType": "sequence"} ] } #+end_src
*** org-get-tag-config
- Description: Get tag configuration as literal Elisp variable values
- Parameters: None
- Returns: JSON object with literal Elisp strings for all tag-related variables
Example return value: #+begin_src json { "org-use-tag-inheritance": "t", "org-tags-exclude-from-inheritance": "("urgent")", "org-tag-alist": "(("work" . 119) ("urgent" . 117) (:startgroup) ("@office" . 111) ("@home" . 104) ("@errand" . 101) (:endgroup) (:startgrouptag) ("project") (:grouptags) ("proj_a") ("proj_b") (:endgrouptag))", "org-tag-persistent-alist": "nil" } #+end_src
*** org-get-allowed-files
- Description: Get the list of Org files accessible through the org-mcp server
- Parameters: None
- Returns: JSON object with =files= array containing absolute paths of allowed Org files
Use cases:
- Discovery: "What Org files can I access through MCP?"
- URI Construction: "I need to build an org-headline:// URI - what's the exact path?"
- Access Troubleshooting: "Why is my file access failing?"
- Configuration Verification: "Did my org-mcp-allowed-files setting work correctly?"
Example response: #+begin_src json { "files": [ "/home/user/org/tasks.org", "/home/user/org/projects.org", "/home/user/notes/daily.org" ] } #+end_src
Empty configuration returns: #+begin_src json { "files": [] } #+end_src
*** org-update-todo-state
- Description: Update the TODO state of a specific headline
- Parameters:
- =uri= (string, required): URI of the headline (supports =org-headline://= or =org-id://=)
- =currentState= (string, required): Current TODO state (empty string "" for no state) - must match actual state
- =newState= (string, required): New TODO state (must be valid in org-todo-keywords)
- Returns: Success status with previous and new states, and ID-based URI of the updated headline
Example: #+begin_src json
Request:
{ "uri": "org-headline:///home/user/org/projects.org/Project%20Alpha", "currentState": "TODO", "newState": "IN-PROGRESS" }
Success response:
{ "success": true, "previousState": "TODO", "newState": "IN-PROGRESS", "uri": "org-id://554A22F6-E29F-4759-8AD2-E7CA225C6397" }
State mismatch error:
{ "error": "State mismatch: expected TODO, found IN-PROGRESS" } #+end_src
*** org-rename-headline
- Description: Rename the title of an existing headline while preserving its TODO state, tags, and properties
- Parameters:
- =uri= (string, required): URI of the headline (supports =org-headline://= or =org-id://=)
- =currentTitle= (string, required): Current headline title (without TODO state or tags) - must match actual title
- =newTitle= (string, required): New headline title (without TODO state or tags)
- Returns: Success status with previous and new titles
Example: #+begin_src json
Request:
{ "uri": "org-headline:///home/user/org/projects.org/Original%20Task", "currentTitle": "Original Task", "newTitle": "Updated Task Name" }
Success response:
{ "success": true, "previousTitle": "Original Task", "newTitle": "Updated Task Name", "uri": "org-id://550e8400-e29b-41d4-a716-446655440002" }
Title mismatch error:
{ "error": "Title mismatch: expected 'Original Task', found 'Different Task'" } #+end_src
*** org-add-todo
- Description: Add a new TODO item to an Org file
- Parameters:
- =title= (string, required): The headline text
- =todoState= (string, required): TODO state from =org-todo-keywords=
- =tags= (string or array, required): Tags to add (e.g., "urgent" or ["work", "urgent"])
- =body= (string, optional): Body text content to add under the heading
- =parentUri= (string, required): URI of parent item. Use =org-headline://filename.org/= for top-level items in a file
- =afterUri= (string, optional): URI of sibling to insert after. If not given, append as last child of parent
- Returns: Object with success status, new item URI, file name, and title
Example: #+begin_src json
Request:
{ "title": "Implement new feature", "todoState": "TODO", "tags": ["work", "urgent"], "body": "This feature needs to be completed by end of week.", "parentUri": "org-headline:///home/user/org/projects.org/" }
Success response:
{ "success": true, "uri": "org-id://550e8400-e29b-41d4-a716-446655440001", "file": "projects.org", "title": "Implement new feature" } #+end_src
*** org-edit-body
- Description: Edit body content of an Org node using partial string replacement
- Parameters:
- =resourceUri= (string, required): URI of the node to edit (supports =org-headline://= or =org-id://=)
- =oldBody= (string, required): Substring to search for within the node's body (must be unique unless replaceAll is true). Use empty string "" to add content to an empty node
- =newBody= (string, required): Replacement text
- =replaceAll= (boolean, optional): Replace all occurrences (default: false)
- Returns: Success status with ID-based URI of the updated node
- Special behavior: When =oldBody= is an empty string (""), the tool will only work if the node has no body content, allowing you to add initial content to empty nodes
Example: #+begin_src json
Request:
{ "resourceUri": "org-id://abc-123", "oldBody": "This is a placeholder.", "newBody": "Implementation started - using Strategy pattern." }
Success response:
{ "success": true, "uri": "org-id://abc-123" }
Adding content to empty node:
{ "resourceUri": "org-id://new-task", "oldBody": "", "newBody": "Initial task description." } #+end_src
** Workaround Tools Duplicating Resource Templates
Note: The following tools are temporary workarounds that duplicate the resource template functionality as tools. They exist because Claude Code currently doesn't discover resource templates.
*** org-read-file
- Description: Read complete raw content of an Org file
- Parameters:
- =file= (string, required): Absolute path to an Org file
- Returns: Plain text content of the entire Org file
- Configuration: File must be in =org-mcp-allowed-files=
*** org-read-outline
- Description: Get hierarchical structure of an Org file as JSON outline
- Parameters:
- =file= (string, required): Absolute path to an Org file
- Returns: JSON object with hierarchical outline structure
- Configuration: File must be in =org-mcp-allowed-files=
*** org-read-headline
- Description: Read specific Org headline by hierarchical path
- Parameters:
- =file= (string, required): Absolute path to an Org file
- =headlinePath= (string, required): Non-empty slash-separated path to headline. Only slashes within headline titles must be URL-encoded as =%2F= to distinguish them from path separators. Other characters (spaces, =#=, etc.) do not need encoding. To read entire files, use =org-read-file= instead
- Returns: Plain text content of the headline and its subtree
- Configuration: File must be in =org-mcp-allowed-files=
*** org-read-by-id
- Description: Read Org headline by its unique ID property
- Parameters:
- =uuid= (string, required): UUID from headline's ID property
- Returns: Plain text content of the headline and its subtree
- Configuration: File containing the ID must be in =org-mcp-allowed-files=
- Note: More stable than path-based access since IDs don't change when headlines are renamed or moved
- License
This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the LICENSE file for details.