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
- Multi-server support
- 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
Available commands:
M-x mcp-server-lib-start
- Start the MCP serverM-x mcp-server-lib-stop
- Stop the MCP serverM-x mcp-server-lib-describe-setup
- View registered tools and resources with usage statisticsM-x mcp-server-lib-show-metrics
- View usage metricsM-x mcp-server-lib-uninstall
- Remove the stdio script
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)--server-id=ID
- Explicit server identifier (optional, will become mandatory in the future)
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
:server-id "my-server") ; optional
;; Tool handler with one parameter
(defun my-handler (location)
"Get weather for LOCATION.
MCP Parameters:
location - city, address, or coordinates"
(mcp-server-lib-with-error-handling
;; Your implementation
))
;; Tool handler with multiple parameters
(defun update-todo-state (resource-uri current-state new-state)
"Update TODO state of a task.
MCP Parameters:
resource-uri - URI of the task to update
current-state - Current TODO state (or empty string)
new-state - New TODO state to set"
(mcp-server-lib-with-error-handling
;; Direct access to parameters, no alist-get needed
(message "Updating %s from %s to %s"
resource-uri current-state new-state)))
(mcp-server-lib-register-tool #'update-todo-state
:id "update-todo"
:description "Update task TODO state"
:server-id "my-server") ; optional
Parameter descriptions in tool handler docstrings follow an indentation-based format:
- Parameter definitions use 2-4 spaces: ~ param-name - description~
- Continuation lines use 6+ spaces: ~ additional text~
- Continuation lines can span multiple lines
- All function parameters must be documented
Example with multi-line parameter descriptions:
(defun fetch-content (url timeout)
"Fetch content from a URL.
MCP Parameters:
url - web address to fetch
Supports http, https, and file protocols
Must be a valid URI
timeout - seconds to wait before giving up
Use 0 for no timeout"
(mcp-server-lib-with-error-handling
;; Implementation
))
Tools can have zero, one, or multiple parameters. When a tool has multiple
parameters, the JSON object fields from the client are automatically mapped to the
function parameters by name (converting from camelCase to kebab-case as needed).
Tools do not support &rest
parameters.
Tool handlers must return strings or nil
(which is converted to an empty string).
Other return types will cause an “Invalid Params” error.
If a tool cannot complete its operation successfully, it should use
mcp-server-lib-tool-throw
for throwing an error or the implementation should be
wrapped with mcp-server-lib-with-error-handling
.
Optional properties:
:title
- User-friendly display name:read-only
- Set tot
if tool doesn’t modify state:server-id
- Server identifier (optional, defaults to"default"
)
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
:server-id "my-server") ; optional
;; Dynamic resource example
(mcp-server-lib-register-resource "buffer://current"
(lambda () (buffer-string))
:name "Current Buffer"
:server-id "my-server") ; optional
;; 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"
:server-id "my-server") ; optional
;; 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"
:server-id "my-server") ; optional
Static resource handlers take no arguments and return strings. Template resource handlers receive an alist of parameters extracted from the URI.
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 handlers can signal specific JSON-RPC error codes to provide meaningful error information to clients:
;; Signal that client provided invalid parameters
(defun my-file-resource-handler (params)
(let ((file (alist-get "filename" params nil nil #'string=)))
(unless (file-exists-p file)
(mcp-server-lib-resource-signal-error
mcp-server-lib-jsonrpc-error-invalid-params
(format "File not found: %s" file)))
(with-temp-buffer
(insert-file-contents file)
(buffer-string))))
;; Signal an internal server error
(defun my-database-resource-handler ()
(unless (database-connected-p)
(mcp-server-lib-resource-signal-error
mcp-server-lib-jsonrpc-error-internal
"Database connection unavailable"))
(query-database))
Available error codes:
mcp-server-lib-jsonrpc-error-invalid-params
(-32602): Client provided invalid parameters, resource not foundmcp-server-lib-jsonrpc-error-internal
(-32603): Server-side processing error
It is also possible to use regular error
or signal
calls, which would return internal error (-32603).
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"
:server-id "my-server") ; optional
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}" :server-id "my-server")
(mcp-server-lib-unregister-resource "resource://uri" :server-id "my-server")
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:
Test helper functions use the dynamic variable mcp-server-lib-ert-server-id
to determine which server to operate on. Child packages testing a single server should set this once at the top of their test file:
;; At the top of your test file
(setq mcp-server-lib-ert-server-id "my-mcp-server")
;; 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)))
;; High-level tool testing helper - simplifies tool calls
;; This function combines request creation, processing, metrics verification,
;; and text extraction into a single call
(let* ((params '(("name" . "John") ("greeting" . "Hello")))
(result (mcp-server-lib-ert-call-tool "greet-user" params)))
(should (string= "Hello, John!" result)))
;; 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