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.
| Mode | Command | Tools | Purpose |
|---|---|---|---|
elisp-core | emacs --batch -Q -l src/emcp-stdio.el -f emcp-stdio-start | ~779 + 9 daemon | Pure 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.
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.
- Emacs 28+ (required for
json-serialize,json-parse-string) - For daemon data-layer tools: a running Emacs daemon (
emacs --daemon) - GNU Make (
gmakeon macOS)
# 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)"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.
When a running Emacs daemon is detected, the following tools become available. The batch process handles MCP protocol; the daemon holds the data.
| Tool | Description |
|---|---|
emcp-data-eval | Evaluate arbitrary Elisp in the daemon |
emcp-data-buffer-list | List all open buffers with file associations |
emcp-data-buffer-read | Read full contents of a named buffer |
emcp-data-buffer-insert | Insert text at end of a named buffer |
emcp-data-find-file | Open a file in the daemon, return buffer name |
emcp-data-org-headings | Extract org headings with level, TODO state, title, tags |
emcp-data-org-set-todo | Set the TODO state of an org heading |
emcp-data-org-table | Extract an org table as tab-separated rows |
emcp-data-org-capture | Append 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.
{
"mcpServers": {
"emacs-mcp-elisp": {
"type": "stdio",
"command": "emacs",
"args": ["--batch", "-Q",
"-l", "/path/to/emcp/src/emcp-stdio.el",
"-f", "emcp-stdio-start"]
}
}
}{
"mcpServers": {
"emacs-mcp-elisp": {
"command": "emacs",
"args": ["--batch", "-Q",
"-l", "/path/to/emcp/src/emcp-stdio.el",
"-f", "emcp-stdio-start"]
}
}
}(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))))npx @anthropic-ai/mcp-inspector emacs --batch -Q \
-l /path/to/emcp/src/emcp-stdio.el \
-f emcp-stdio-startecho '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
emacs --batch -Q -l src/emcp-stdio.el -f emcp-stdio-startSee MCP protocol sequence diagram (rendered on GitHub).
| Layer | File | Role | Contract |
|---|---|---|---|
| L0 | emacs | Running Emacs (batch or daemon) | — |
| L1 | src/emcp-stdio.el | Pure Elisp MCP server | I/O – Collection – Dispatch – Daemon |
| L2 | bin/health-check.sh | Structured health check | — |
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
| Component | Choice | Rationale |
|---|---|---|
| MCP server | Emacs Lisp (emcp-stdio.el) | Emacs IS the server; no process boundary |
| Introspection | Emacs Lisp | Only thing with live access to obarray |
| Argument escaping | prin1-to-string (Elisp) | Native serialization; no custom escaper needed |
| Protocol | JSON-RPC over stdio | MCP spec; json-serialize / json-parse-string built-in |
| Daemon IPC | emacsclient --eval | Standard Emacs daemon interface |
| Tests | ert | Native Emacs Lisp test framework |
| Build | gmake | Consistent with homelab conventions |
| Spec format | org-mode | Canonical; tangleable; Mermaid-embeddable |
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.mdSee 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
Falsifiable hypotheses tracked via cprr. See CONJECTURES.md for
full measurement data and evidence.
See conjecture state machine diagram (rendered on GitHub).
| ID | Claim | Status |
|---|---|---|
| C-001 | Claude Code uses on-demand tool indexing | prior-confirmed |
| C-002 | Arglist heuristic yields > 80% precision | open |
| C-003 | emacsclient round-trip < 50ms for string functions | open |
| C-004 | Non-ASCII survives the full Emacs round trip | open |
| C-005 | Maximalist tool count causes measurable init latency difference | open |
| C-006 | Vanilla Emacs exposes fewer functions than configured Emacs | open |
Set EMCP_TRACE=1 to enable per-call latency and init timing instrumentation.
See project arc gantt chart (rendered on GitHub).
- 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.
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.
Apache License 2.0. See LICENSE.
