MCP Hub
Back to servers

org-mcp

An Emacs-integrated MCP server that provides structured read/write access to Org-mode files, allowing AI assistants to manage tasks, outlines, and notes.

Stars
30
Forks
1
Tools
11
Updated
Nov 12, 2025
Validated
Jan 11, 2026

#+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.

Reviews

No reviews yet

Sign in to write a review