Skip to content

jwalsh/emcp

Repository files navigation

emcp

https://img.shields.io/github/actions/workflow/status/jwalsh/emcp/test.yml.svg?branch=main&style=flat-square&logo=github&label=tests https://img.shields.io/badge/license-Apache--2.0-blue.svg?style=flat-square https://img.shields.io/github/issues/jwalsh/emcp.svg?style=flat-square https://img.shields.io/badge/emacs-%E2%89%A528-7F5AB6.svg?style=flat-square&logo=gnuemacs&logoColor=white

docs/creative/banner-lambda-readme.png

What

An MCP server that exposes the full Emacs obarray — every text-consuming function — as MCP tools, introspected at runtime from the live process.

The server is a pure Elisp implementation (src/emcp-stdio.el): Emacs IS the MCP server. No external dependencies, no manifest file, no subprocess shim. The obarray is the tool registry. funcall is the dispatch. When a running Emacs daemon is detected, nine additional data-layer tools provide live access to buffers, org files, and arbitrary eval.

ModeCommandToolsPurpose
elisp-coreemacs --batch -Q -l src/emcp-stdio.el -f emcp-stdio-start~779 + 9 daemonPure Elisp MCP server

The maximalist mode is the point: demonstrating by construction that naive “enumerate all tools” MCP server design saturates any agent context window. The critique only works if the thing works.

Axiom

The MCP server does not know what Emacs can do. Emacs tells it.

No function list is hardcoded. All tool definitions derive from runtime obarray introspection via a live Emacs process. If you are hardcoding function names anywhere except tests, you have violated the axiom.

The obarray walk happens in the same process that serves the MCP protocol. There is no serialized manifest at all.

Prerequisites

  • Emacs 28+ (required for json-serialize, json-parse-string)
  • For daemon data-layer tools: a running Emacs daemon (emacs --daemon)
  • GNU Make (gmake on macOS)

Quick Start

# No setup required — Emacs is the only dependency.

# Start the MCP server (core mode, vanilla Emacs)
emacs --batch -Q -l src/emcp-stdio.el -f emcp-stdio-start

# Optionally start an Emacs daemon first for data-layer tools
emacs --daemon
emacs --batch -Q -l src/emcp-stdio.el -f emcp-stdio-start
# → "emacs-mcp-elisp: daemon detected — data layer tools enabled"
# → "emacs-mcp-elisp: 788 tools (779 local + 9 daemon)"

Elisp Server

The pure Elisp server (src/emcp-stdio.el) reads newline-delimited JSON-RPC from stdin and writes responses to stdout. It speaks the MCP protocol directly — no manifest file, no subprocess shim.

At startup it walks the obarray and builds MCP tool definitions for every function whose arglist matches the text-consumer heuristic (parameters named string, text, buffer, object, etc.). If a running Emacs daemon is reachable via emacsclient, nine additional daemon data-layer tools are registered.

Daemon Data-Layer Tools

When a running Emacs daemon is detected, the following tools become available. The batch process handles MCP protocol; the daemon holds the data.

ToolDescription
emcp-data-evalEvaluate arbitrary Elisp in the daemon
emcp-data-buffer-listList all open buffers with file associations
emcp-data-buffer-readRead full contents of a named buffer
emcp-data-buffer-insertInsert text at end of a named buffer
emcp-data-find-fileOpen a file in the daemon, return buffer name
emcp-data-org-headingsExtract org headings with level, TODO state, title, tags
emcp-data-org-set-todoSet the TODO state of an org heading
emcp-data-org-tableExtract an org table as tab-separated rows
emcp-data-org-captureAppend a new org entry under a heading

All daemon tools accept arguments via the args array (matching the MCP tool schema). The emcp-data-eval tool takes a single sexp string; the others construct the sexp from structured arguments.

Usage

Claude Code (.mcp.json)

{
  "mcpServers": {
    "emacs-mcp-elisp": {
      "type": "stdio",
      "command": "emacs",
      "args": ["--batch", "-Q",
               "-l", "/path/to/emcp/src/emcp-stdio.el",
               "-f", "emcp-stdio-start"]
    }
  }
}

Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "emacs-mcp-elisp": {
      "command": "emacs",
      "args": ["--batch", "-Q",
               "-l", "/path/to/emcp/src/emcp-stdio.el",
               "-f", "emcp-stdio-start"]
    }
  }
}

Emacs with mcp.el

(mcp-connect-server
 "emacs-mcp-elisp" "emacs"
 '("--batch" "-Q" "-l" "/path/to/emcp/src/emcp-stdio.el"
   "-f" "emcp-stdio-start")
 :initial-callback (lambda (conn) (message "emacs-mcp-elisp connected"))
 :tools-callback (lambda (conn tools) (message "%d tools" (length tools))))

MCP Inspector

npx @anthropic-ai/mcp-inspector emacs --batch -Q \
  -l /path/to/emcp/src/emcp-stdio.el \
  -f emcp-stdio-start

JSON-RPC (raw)

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
  emacs --batch -Q -l src/emcp-stdio.el -f emcp-stdio-start

Architecture

See MCP protocol sequence diagram (rendered on GitHub).

Component Layers

LayerFileRoleContract
L0emacsRunning Emacs (batch or daemon)
L1src/emcp-stdio.elPure Elisp MCP serverI/OCollectionDispatchDaemon
L2bin/health-check.shStructured health check

Security Boundary

The Elisp server uses prin1-to-string for argument escaping and format "%S" for sexp construction. Emacs’s own serialization handles the security boundary natively. Edge cases handled:

  • Unbalanced parentheses
  • Embedded quotes
  • Null bytes
  • Shell metacharacters
  • Newlines

Stack Preferences

ComponentChoiceRationale
MCP serverEmacs Lisp (emcp-stdio.el)Emacs IS the server; no process boundary
IntrospectionEmacs LispOnly thing with live access to obarray
Argument escapingprin1-to-string (Elisp)Native serialization; no custom escaper needed
ProtocolJSON-RPC over stdioMCP spec; json-serialize / json-parse-string built-in
Daemon IPCemacsclient --evalStandard Emacs daemon interface
TestsertNative Emacs Lisp test framework
BuildgmakeConsistent with homelab conventions
Spec formatorg-modeCanonical; tangleable; Mermaid-embeddable

Build Pipeline

The project uses a sentinel-gated Makefile pipeline for bootstrapping:

gmake status        # show pipeline + artifact status
gmake graph         # show dependency DAG
gmake work          # run full pipeline (bootstrap -> implement)
gmake -j3 parallel  # run independent phases concurrently
gmake test          # run all tests
gmake health        # run environment health check
gmake sync          # export README.org -> README.md

Pipeline DAG

See pipeline DAG diagram (rendered on GitHub).

The full build pipeline stages:

spec.org
  -> bootstrap
    -> generate-claude-md
      -> review-prompt
        |-> wire-backlog ------+
        |-> setup-memory ------+  (gmake -j3 parallel)
        +-> health-check ------+
                              |
                    verify-bootstrap
                              |
                          decompose
                              |
                            work

Conjectures

Falsifiable hypotheses tracked via cprr. See CONJECTURES.md for full measurement data and evidence.

Conjecture State Machine

See conjecture state machine diagram (rendered on GitHub).

Conjecture Status

IDClaimStatus
C-001Claude Code uses on-demand tool indexingprior-confirmed
C-002Arglist heuristic yields > 80% precisionopen
C-003emacsclient round-trip < 50ms for string functionsopen
C-004Non-ASCII survives the full Emacs round tripopen
C-005Maximalist tool count causes measurable init latency differenceopen
C-006Vanilla Emacs exposes fewer functions than configured Emacsopen

Set EMCP_TRACE=1 to enable per-call latency and init timing instrumentation.

Project Arc

See project arc gantt chart (rendered on GitHub).

Anti-Goals

  • Curated function subset (c.f. emacs-mcp-curated): curation is the thesis being argued against. A hand-picked list makes the artifact a utility, not a demonstration.
  • General Emacs IDE integration (c.f. emacs-lsp, eglot, copilot.el): those optimize for editor-to-language-server protocol. This project uses MCP as the protocol boundary.
  • AI assistant inside Emacs (c.f. gptel, ellama): those embed the agent in the editor. This project exposes the editor to the agent. Inverted control flow.
  • Config-dependent wrapper: the introspector must work against a vanilla emacs -Q. Requiring the user’s init.el breaks reproducibility.
  • External language dependency: this is a pure Elisp project. Emacs Lisp owns introspection, dispatch, and protocol. There is no external language runtime in the stack.

Contributing

See CLAUDE.md for the full project specification and coding constraints. See AGENTS.md for agent coordination protocol. See spec.org for the canonical project specification.

License

Apache License 2.0. See LICENSE.

About

Every text-consuming function in Emacs, as an MCP tool

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors