Skip to content

laurynas-biveinis/mcp-server-lib.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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/badge.svg https://github.com/laurynas-biveinis/mcp-server-lib.el/actions/workflows/linter.yml/badge.svg https://melpa.org/packages/mcp-server-lib-badge.svg https://stable.melpa.org/packages/mcp-server-lib-badge.svg

Overview

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.

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

Installation

From MELPA:

M-x package-install RET mcp-server-lib RET

For Users

If you’re using an MCP server built with this library:

  1. Run M-x mcp-server-lib-install to install the stdio script
  2. The script will be at ~/.emacs.d/emacs-mcp-stdio.sh
  3. 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 elisp-dev-mcp for a complete example.

Client Registration

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.

API Reference

Registering Tools

(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

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:

(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 to t if tool doesn’t modify state
  • :server-id - Server identifier (optional, defaults to "default")

Registering Resources

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 Error Handling

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 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:

(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")

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-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”)

Utility Functions

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)

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:

;; At the top of your test file
(setq mcp-server-lib-ert-server-id "my-mcp-server")

Test Helper Functions

;; 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))))

JSON-RPC Error Constants

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")

Debugging

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

Customization

To install the script to a different location:

(setq mcp-server-lib-install-directory "/path/to/directory")

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

License

This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the LICENSE file for details.

Acknowledgments

About

Emacs Lisp implementation of the Model Context Protocol

Resources

License

Stars

Watchers

Forks

Packages

No packages published