About Lib.el
Lib.el is an MCP server published by laurynas-biveinis in the AI category: emacs Lisp implementation of the Model Context Protocol.
The repository has 49 stars and 12 forks, with the last commit 17 days ago. Recent commit activity is the strongest signal that reported issues will get attention.
Install
git clone https://github.com/laurynas-biveinis/mcp-server-lib.elThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Ask AI
Ask AI about Lib.el
Powered by Claude · Grounded in docs
Security checks
Static checks derived from the repository 5 days ago. Heuristics on the source, not an audit.
- ✓README presentThe repository ships documentation.
- ✓License declaredUsable under a stated license.
- !Tests presentNo tests found. Behaviour changes may go unnoticed between versions.
- !Dependencies pinnedNo lockfile. Installs can pull different code over time.
- !Scoped permissionsDoesn't declare a permission scope. Assume it can do anything its process can.
1 check has not been run yet.
Releases
README
#+TITLE: mcp-server-lib.el - Model Context Protocol Server Library for Emacs Lisp
[[https://github.com/laurynas-biveinis/mcp-server-lib.el/actions/workflows/elisp-test.yml][https://github.com/laurynas-biveinis/mcp-server-lib.el/actions/workflows/elisp-test.yml/badge.svg]] [[https://github.com/laurynas-biveinis/mcp-server-lib.el/actions/workflows/linter.yml][https://github.com/laurynas-biveinis/mcp-server-lib.el/actions/workflows/linter.yml/badge.svg]] [[https://melpa.org/#/mcp-server-lib][https://melpa.org/packages/mcp-server-lib-badge.svg]] [[https://stable.melpa.org/#/mcp-server-lib][file:https://stable.melpa.org/packages/mcp-server-lib-badge.svg]]
- Overview
=mcp-server-lib.el= is a library for building [[https://modelcontextprotocol.io/][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.
- Features
- 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
- Requirements
- Emacs 27.1 or later
- Running Emacs daemon (for stdio transport)
- MCP servers built on this library
- [[https://github.com/laurynas-biveinis/elisp-dev-mcp][elisp-dev-mcp]] - Elisp development support tools
- [[https://github.com/laurynas-biveinis/org-mcp][org-mcp]] - read and write Org content
- Installation
From [[https://melpa.org/#/mcp-server-lib][MELPA]] or [[https://stable.melpa.org/#/mcp-server-lib][MELPA Stable]]:
=M-x package-install RET mcp-server-lib RET=
- For Users
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 server
- =M-x mcp-server-lib-stop= - Stop the MCP server
- =M-x mcp-server-lib-describe-setup= - View registered tools and resources with usage statistics
- =M-x mcp-server-lib-show-metrics= - View usage metrics
- =M-x mcp-server-lib-uninstall= - Remove the stdio script
- For Developers
To build your own MCP server, see [[https://github.com/laurynas-biveinis/elisp-dev-mcp][elisp-dev-mcp]] for a complete example.
** Client Registration
Register your MCP server with a client using the stdio script:
#+BEGIN_EXAMPLE
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
#+END_EXAMPLE
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.
** API Reference
*** Registration
Use =mcp-server-lib-register-server= to declare a server's metadata, tools, and resources in a single call. Within one call, duplicate tool =:id=s or resource URIs are rejected.
#+begin_src elisp (mcp-server-lib-register-server :id "my-server" ; optional, defaults to "default" :name "My Server" ; optional, defaults to :id :version "1.2.0" ; optional, defaults to "1.0.0" :instructions "Cite source resource URIs when quoting their contents." ; optional :tools (list (list #'my-list-files :id "list-files" :description "Lists all files" :title "List Files" ; optional :read-only t) ; optional (list #'update-todo-state :id "update-todo" :description "Update task TODO state")) :resources (list (list "resource://uri" (lambda () "resource content") :name "Resource Name" :description "What this provides" ; optional :mime-type "text/plain") ; optional ;; Template URIs (containing {var}) are detected automatically (list "org://{filename}" (lambda (params) (with-temp-buffer (insert-file-contents (alist-get "filename" params nil nil #'string=)) (buffer-string))) :name "Org file content"))) #+end_src
Each entry is registered with reference counting, so two bundled calls with overlapping entries increment ref-counts and need two =mcp-server-lib-unregister-server= calls to fully tear down.
Every =mcp-server-lib-register-server= call creates or bumps a per-server metadata record, so each successful call must be paired with =mcp-server-lib-unregister-server= for clean teardown -- including calls that supply no =:tools=, =:resources=, or =:instructions=, and calls that rely on the default ="default"= =:id=.
The optional =:name= (defaulting to =:id=) and =:version= (defaulting to ="1.0.0"=) are reported as =serverInfo.name= and =serverInfo.version= in the server's =initialize= response. These describe the server implementation; =:version= is the server's own version, distinct from the MCP =protocolVersion=. The =:id=, in contrast, is an internal routing key that selects the server's tools and resources and is never sent to the client. A server configured only through the obsolete =mcp-server-lib-register-tool= / =mcp-server-lib-register-resource= API has no metadata record, so its =serverInfo= reports the =server-id= as the name and the default version.
The =:instructions= string is emitted in the server's =initialize= response (per the MCP protocol spec), giving the client's LLM guidance on how to use the server's tools and resources.
**** Updating an existing tool or resource
Re-registration under an existing key keeps the original spec; the new handler and properties are discarded and only the ref-count is bumped. This first-writer-wins policy keeps overlapping tools/resources stable for as long as any caller still holds a reference. To update a handler or properties, tear down with =mcp-server-lib-unregister-server= first and then re-register. =:name=, =:version=, and =:instructions= are exceptions (last-writer-wins) because they live on the server record with no per-key ref-count: a re-registration that supplies one of these keys overwrites the stored value, while omitting it leaves the previous value intact.
*** Tool Handlers
Tool handler functions:
#+begin_src elisp ;; Tool handler with no parameters (defun my-list-files () "List files in the current directory." (mcp-server-lib-with-error-handling (mapconcat #'identity (directory-files ".") "\n")))
;; 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))) #+end_src
**** MCP Parameters Format
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:
#+begin_src elisp (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 )) #+end_src
These docstring descriptions are the single source for the tool's =inputSchema=; do not repeat them in the tool =:description= (see below).
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. JSON field names must match the Lisp parameter names exactly. 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=.
Tool entry properties accepted in =:tools=:
- =:id= (required) - String identifier for the tool (e.g., ="list-files"=)
- =:description= (required) - String describing what the tool does and when to use it. Describe the tool as a whole; do not restate individual parameters here. Per-parameter descriptions belong solely in the handler's =MCP Parameters:= docstring block, from which they are extracted into the tool's =inputSchema=. The client receives both, so repeating parameter text in =:description= is redundant and risks drift.
- =:title= - User-friendly display name
- =:read-only= - Set to =t= if tool doesn't modify state
- =:param-schemas= - Alist mapping parameter names to JSON schema fragments; see [[*Parameter Types][Parameter Types]]
Unknown or duplicate property keys signal an error at registration time.
**** Parameter Types
Every parameter is published in the tool's =inputSchema= as ={"type": "string"}=. A parameter that takes another JSON type declares it in =:param-schemas=, an alist mapping the parameter name to a schema fragment that replaces the default:
#+begin_src elisp (list #'my-search :id "search" :description "Search tasks" :param-schemas '(("after" (type . ["string" "null"])) ("fields" (type . "array") (items . ((type . "string")))) ("filter" (type . "object")))) #+end_src
Clients that validate arguments against the schema, such as Claude Code, then send =null=, arrays and objects as such rather than as their JSON text. The fragment is encoded with =json-encode=, so JSON arrays are written as vectors.
- The description still comes from the =MCP Parameters:= docstring block; a fragment that sets =description= is rejected.
- Each name must be a handler parameter and may appear once.
- Registration rejects a fragment =json-encode= cannot encode, and a =type= that is neither a JSON Schema type name nor a vector of distinct ones.
- =required= is still derived from the handler's arglist, so declaring a parameter's type does not change whether it is optional.
- Declaring a type does not change how arguments reach the handler: JSON =null= arrives as =nil=, an array as a vector and an object as an alist.
*** Resource Handlers
The library uses a unified API for both static and templated resources. The presence of ={variable}= syntax in a URI automatically determines whether the entry is a static resource or a template.
Resource handlers go in the =:resources= list of =mcp-server-lib-register-server=; each entry is =(URI HANDLER &rest PROPERTIES)= so the handler is the second element.
#+begin_src elisp ;; Static resource (no variables): handler takes no arguments (lambda () "resource content")
;; Template resource with simple variable: handler takes params alist (lambda (params) (with-temp-buffer (insert-file-contents (alist-get "filename" params nil nil #'string=)) (buffer-string)))
;; Template with multiple variables (including {+var} for reserved expansion) (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))) #+end_src
Resource entry properties accepted in =:resources=:
- The first list element is the =URI= (required string)
- The second list element is the =handler= function (required)
- =:name= (required) - Human-readable name
- =:description= - Description of the resource
- =:mime-type= - MIME type (default: ="text/plain"=)
Unknown or duplicate property keys signal an error at registration time.
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 Error Handling
Resource handlers can signal specific JSON-RPC error codes to provide meaningful error information to clients:
#+begin_src elisp ;; 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)) #+end_src
Available error codes:
- =mcp-server-lib-jsonrpc-error-invalid-params= (-32602): Client provided invalid parameters, resource not found
- =mcp-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).
*** Working with Resource Templates
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:
#+begin_src elisp (mcp-server-lib-register-server :id "my-server" :resources (list (list "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"))) #+end_src
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"=.
*** Unregistration
To tear down everything a package registered with a given server-id (for example, in a =-disable= function), call =mcp-server-lib-unregister-server=. This is the symmetric counterpart of =mcp-server-lib-register-server=:
#+begin_src elisp (defun my-mcp-disable () (mcp-server-lib-unregister-server "my-server")) #+end_src
Each registered entry — tools, resources, templates, and the per-server metadata record holding =:name=, =:version=, and optional =:instructions= — has its reference count decremented exactly once. Entries whose count was one are fully removed; entries with a higher count (e.g. from nested test fixtures) remain at the reduced count. =:name=, =:version=, and =:instructions= are never reverted by =mcp-server-lib-unregister-server=; only =mcp-server-lib-register-server= updates them. Calling =mcp-server-lib-unregister-server= on an unknown server-id is a silent no-op. Pass ="default"= explicitly to operate on the default server.
*** Deprecated APIs
The individual registration / unregistration functions are obsolete as of 0.3.0 in favor of the bundled =mcp-server-lib-register-server= / =mcp-server-lib-unregister-server= form:
- =mcp-server-lib-register-tool=
- =mcp-server-lib-register-resource=
- =mcp-server-lib-unregister-tool=
- =mcp-server-lib-unregister-resource=
The migration is per-fixture, not per-call: there is no per-key replacement for =mcp-server-lib-unregister-tool= or =mcp-server-lib-unregister-resource=. Pair each bundled =mcp-server-lib-register-server= with one =mcp-server-lib-unregister-server= (which tears down every entry the bundled call registered).
The deprecated functions continue to work; references to them produce byte-compile warnings.
The two API styles can be mixed during migration. =mcp-server-lib-unregister-server= tears down per-kind entries regardless of how they were registered, so it also clears tools and resources registered via the obsolete =mcp-server-lib-register-tool= / =mcp-server-lib-register-resource=. The per-server metadata record, however, is only created by =mcp-server-lib-register-server=; the per-key =mcp-server-lib-unregister-tool= / =mcp-server-lib-unregister-resource= do not touch it, so a per-key legacy unregister after a bundled register leaves the record (including any =:instructions=) in place until =mcp-server-lib-unregister-server= clears it.
*** Resource Lists
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.
*** Constants
=mcp-server-lib-protocol-version= - The MCP protocol version supported by this server ("2025-03-26")
=mcp-server-lib-default-server-version= - Default =serverInfo.version= reported when a server registers without =:version= ("1.0.0")
*** Utility Functions
For testing and debugging:
#+begin_src elisp ;; 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-id)
;; Server management (mcp-server-lib-start) (mcp-server-lib-stop)
;; Query whether a server-id has a registered metadata record (mcp-server-lib-server-registered-p server-id)
;; Path where mcp-server-lib-install places emacs-mcp-stdio.sh (mcp-server-lib-installed-script-path) #+end_src
*** Test Utilities
The =mcp-server-lib-ert= module provides utilities for writing ERT tests for MCP servers:
**** Server Context Variable
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:
#+begin_src elisp ;; At the top of your test file (setq mcp-server-lib-ert-server-id "my-mcp-server") #+end_src
The constant =mcp-server-lib-ert-default-version= (="1.0.0-test"=) is the =:version= that test helpers register a server with when none is given, and the =serverInfo.version= they expect in the =initialize= result, so registration and assertion agree by default.
**** Test Helper Functions
#+begin_src elisp ;; 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) "example server"))
;; 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) "example server"))
;; 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")))
;; Assert the structure of an initialize result (mcp-server-lib-ert-assert-initialize-result init-result tools resources :instructions "How to drive this server" :name "My Server" :version "1.2.0")
;; 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))) "example server"))) (should-not (alist-get 'error response))))
;; Same, against a non-default serverInfo (mcp-server-lib-ert-with-server :tools t :name "My Server" :version "1.2.0" ...) #+end_src
=mcp-server-lib-ert-assert-initialize-result= takes the initialize result, a boolean for the expected tools capability, and a boolean for the expected resources capability. =:instructions= asserts the field equals that string, or that it is absent when omitted; =:name= defaults to =mcp-server-lib-ert-server-id= and =:version= to =mcp-server-lib-ert-default-version=.
=mcp-server-lib-ert-with-server= accepts the same three keywords and asserts them against the result of the initialize handshake it performs. If nothing is registered under =mcp-server-lib-ert-server-id= it registers a minimal server with those values and tears it down afterwards; a server the test registered itself is left untouched. Unknown keywords, or a trailing keyword with no value, signal an error at macro-expansion time.
*** JSON-RPC Error Constants
The library provides public constants for standard JSON-RPC 2.0 error codes:
#+begin_src elisp
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
#+end_src
These constants can be used when checking error responses in tests:
#+begin_src elisp (mcp-server-lib-ert-check-error-object response mcp-server-lib-jsonrpc-error-method-not-found "Method not found") #+end_src
*** Debugging
Enable JSON-RPC message logging:
#+begin_src elisp (setq mcp-server-lib-log-io t) ; Log to mcp-server-lib-log buffer #+end_src
View usage metrics:
#+begin_src elisp M-x mcp-server-lib-show-metrics M-x mcp-server-lib-reset-metrics #+end_src
** Customization
To install the script to a different location:
#+begin_src elisp (setq mcp-server-lib-install-directory "/path/to/directory") #+end_src
- Troubleshooting
- 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= to =t= and check =mcp-server-lib-log= buffer
- Similar packages
- https://github.com/utsahi/mcp-server.el
- https://github.com/rhblind/emacs-mcp-server
- https://codeberg.org/jlouisbiz/rcd-mcp-emacs-documentation
- https://codeberg.org/martenlienen/emcp
- License
This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the LICENSE file for details.
- Acknowledgments
- [[https://modelcontextprotocol.io/][Model Context Protocol]] specification
- [[https://github.com/modelcontextprotocol/python-sdk][Python MCP SDK]] implementation
README fetched from the repository 5 days ago. The original is authoritative.