mcp-server-lib.el
is a library for building Model Context Protocol (MCP) servers in Emacs Lisp. It provides the infrastructure for Emacs packages to expose their functionality as tools and resources to Large Language Models.
- Simple API for registering tools (Elisp functions) and resources
- Resource templates with URI pattern matching (RFC 6570 subset)
- Handles MCP protocol communication and JSON-RPC messages
- Stdio transport via emacsclient wrapper script
- Built-in usage metrics and debugging support
- Emacs 27.1 or later
- Running Emacs daemon (for stdio transport)
- elisp-dev-mcp - Elisp development support tools
From MELPA:
M-x package-install RET mcp-server-lib RET
If you’re using an MCP server built with this library:
- Run
M-x mcp-server-lib-install
to install the stdio script - The script will be at
~/.emacs.d/emacs-mcp-stdio.sh
- Follow your MCP server’s documentation for client registration
To uninstall: M-x mcp-server-lib-uninstall
To build your own MCP server, see elisp-dev-mcp for a complete example.
Register your MCP server with a client using the stdio script:
claude mcp add -s user -t stdio your-server -- ~/.emacs.d/emacs-mcp-stdio.sh \ --init-function=your-init-func --stop-function=your-stop-func
Script options:
--init-function=NAME
- Emacs function to call on startup--stop-function=NAME
- Emacs function to call on shutdown--socket=PATH
- Custom Emacs server socket (optional)
For debugging, set EMACS_MCP_DEBUG_LOG
to a file path.
(mcp-server-lib-register-tool #'my-function
:id "tool-name"
:description "What this tool does"
:title "Display Name" ; optional
:read-only t) ; optional
;; Tool handler with parameters
(defun my-handler (location)
"Get weather for LOCATION.
MCP Parameters:
location - city, address, or coordinates"
(mcp-server-lib-with-error-handling
;; Your implementation
))
Tool handlers should return strings. Use mcp-server-lib-tool-throw
for errors or wrap with mcp-server-lib-with-error-handling
.
Optional properties:
:title
- User-friendly display name:read-only
- Set tot
if tool doesn’t modify state
The library uses a unified API for both static and templated resources. The presence of {variable}
syntax automatically determines whether a resource is static or templated:
;; Static resource (no variables)
(mcp-server-lib-register-resource "resource://uri"
(lambda () "resource content")
:name "Resource Name"
:description "What this provides" ; optional
:mime-type "text/plain") ; optional
;; Dynamic resource example
(mcp-server-lib-register-resource "buffer://current"
(lambda () (buffer-string))
:name "Current Buffer")
;; Template resource with simple variable
(mcp-server-lib-register-resource "org://{filename}"
(lambda (params)
(with-temp-buffer
(insert-file-contents (alist-get "filename" params nil nil #'string=))
(buffer-string)))
:name "Org file content"
:description "Read any org file by name")
;; Template with multiple variables
(mcp-server-lib-register-resource "org://{filename}/headline/{+path}"
(lambda (params)
(let ((file (alist-get "filename" params nil nil #'string=))
(path (alist-get "path" params nil nil #'string=)))
;; path can contain slashes with {+path}
(org-get-headline-content file path)))
:name "Org headline"
:description "Get specific headline from org file")
Static resource handlers take no arguments and return strings. Template resource handlers receive an alist of parameters extracted from the URI. Use regular error
for failures.
Supported template syntax (RFC 6570 subset):
{variable}
- Simple variable expansion{+variable}
- Reserved expansion (allows slashes)
Direct resources take precedence over templates when both match a URI.
Resource template handlers receive extracted parameters as an alist. These parameters are matched from the URI but not automatically decoded - if you’re working with file paths that might contain special characters, you’ll want to decode them:
(mcp-server-lib-register-resource "file://{path}"
(lambda (params)
(let ((path (alist-get "path" params nil nil #'string=)))
;; Decode if needed for filesystem access
(with-temp-buffer
(insert-file-contents (url-unhex-string path))
(buffer-string))))
:name "File reader")
Variable names in templates follow simple rules - stick to letters, numbers, and underscores. The URI scheme (like file://
or org://
) needs to be a valid URI scheme starting with a letter. URI schemes are case-insensitive per RFC 3986, so HTTP://example.com
will match a template registered as http://{domain}
.
When multiple templates could match the same URI, which template is selected is undefined and depends on implementation details. Avoid registering overlapping templates.
Templates can match empty values too - org://
will match org://{filename}
with an empty filename.
Literal segments in templates must match exactly - test://items/{id}
will match test://items/123
but not test://item/123
.
The implementation uses non-greedy (first-match) behavior when matching variables. For example, test://{name}.txt
matching test://file.config.txt
extracts name
“file.config”, not =name
“file.config.txt”=.
To unregister any resource (static or templated):
(mcp-server-lib-unregister-resource "org://{filename}")
(mcp-server-lib-unregister-resource "resource://uri")
When clients request the resource list, direct resources appear with a uri
field while templates show up with a uriTemplate
field. This helps clients distinguish between static resources and dynamic patterns they can use.
mcp-server-lib-name
- The name of the MCP server (“emacs-mcp-server-lib”)
mcp-server-lib-protocol-version
- The MCP protocol version supported by this server (“2025-03-26”)
For testing and debugging:
;; Create JSON-RPC requests
(mcp-server-lib-create-tools-list-request &optional id)
(mcp-server-lib-create-tools-call-request tool-name &optional id args)
(mcp-server-lib-create-resources-list-request &optional id)
(mcp-server-lib-create-resources-read-request uri &optional id)
;; Process requests and get parsed response
(mcp-server-lib-process-jsonrpc-parsed request)
;; Server management
(mcp-server-lib-start)
(mcp-server-lib-stop)
The mcp-server-lib-ert
module provides utilities for writing ERT tests for MCP servers:
;; Track metrics changes during test execution
(mcp-server-lib-ert-with-metrics-tracking
((method expected-calls expected-errors) ...)
;; Test code here
)
;; Example: Verify a method is called once with no errors
(mcp-server-lib-ert-with-metrics-tracking
(("tools/list" 1 0))
;; Code that should call tools/list once
(mcp-server-lib-process-jsonrpc-parsed
(mcp-server-lib-create-tools-list-request)))
;; Simplified syntax for verifying successful single method calls
(mcp-server-lib-ert-verify-req-success "tools/list"
(mcp-server-lib-process-jsonrpc-parsed
(mcp-server-lib-create-tools-list-request)))
;; Process a request and get the successful result
(let* ((request (mcp-server-lib-create-tools-list-request))
(tools (mcp-server-lib-ert-get-success-result "tools/list" request)))
;; tools contains the result field from the response
(should (arrayp tools)))
;; Get resource list (convenience function)
(let ((resources (mcp-server-lib-ert-get-resource-list)))
(should (= 2 (length resources)))
(should (string= "test://resource1"
(alist-get 'uri (aref resources 0)))))
;; Check error response structure
(mcp-server-lib-ert-check-error-object response -32601 "Method not found")
;; Verify resource read succeeds with expected fields
(mcp-server-lib-ert-verify-resource-read
"test://resource1"
'((uri . "test://resource1")
(mimeType . "text/plain")
(text . "test result")))
;; Run tests with MCP server
(mcp-server-lib-ert-with-server :tools nil :resources nil
;; Server is started, initialized, and will be stopped after body
(let ((response (mcp-server-lib-process-jsonrpc-parsed
(json-encode '(("jsonrpc" . "2.0")
("method" . "tools/list")
("id" . 1))))))
(should-not (alist-get 'error response))))
The library provides public constants for standard JSON-RPC 2.0 error codes:
mcp-server-lib-jsonrpc-error-parse ; -32700 Parse Error
mcp-server-lib-jsonrpc-error-invalid-request ; -32600 Invalid Request
mcp-server-lib-jsonrpc-error-method-not-found ; -32601 Method Not Found
mcp-server-lib-jsonrpc-error-invalid-params ; -32602 Invalid Params
mcp-server-lib-jsonrpc-error-internal ; -32603 Internal Error
These constants can be used when checking error responses in tests:
(mcp-server-lib-ert-check-error-object
response
mcp-server-lib-jsonrpc-error-method-not-found
"Method not found")
Enable JSON-RPC message logging:
(setq mcp-server-lib-log-io t) ; Log to *mcp-server-lib-log* buffer
View usage metrics:
M-x mcp-server-lib-show-metrics
M-x mcp-server-lib-reset-metrics
To install the script to a different location:
(setq mcp-server-lib-install-directory "/path/to/directory")
- **Script not found**: Run
M-x mcp-server-lib-install
first - **Connection errors**: Ensure Emacs daemon is running
- **Debugging**: Set
mcp-server-lib-log-io
tot
and check*mcp-server-lib-log*
buffer
This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the LICENSE file for details.
- Model Context Protocol specification
- Python MCP SDK implementation