Dmk

Dmk

NexusMCP - MCP server library with per-session GenServer architecture

Hey everyone,

I’ve just published NexusMCP, an MCP (Model Context Protocol) server library for Elixir.

Why another MCP library?

When I needed to add MCP to a production app a few months back, I tried the existing options - primarily anubis_mcp and vancouver. I ran into bugs that were dealbreakers for production use:

  1. Session expiry didn’t work properly - timed-out sessions would linger instead of returning 404, leading to confusing client behavior
  2. Tool call responses came back as SSE streams instead of inline JSON -the Streamable HTTP spec says POST responses should return JSON directly, not force everything through SSE
  3. Zombie sessions - non-initialize requests to invalid sessions would silently create new sessions instead of returning 404

Rather than patching around these, I decided to build something from scratch that leans into OTP patterns. I’ve been running it in production for a while now and just got around to publishing it.

Since I started building, emcp was also released I believe it takes a different approach with ETS tables, but haven’t looked at it much. Would have if it was out before I started on this :grinning_face_with_smiling_eyes:.

The approach

NexusMCP uses a GenServer-per-session architecture. Each MCP client gets its own process, which gives you:

  • Parallel tool execution - tool calls run concurrently via Task.Supervisor.async_nolink, so clients can fire off multiple tool calls and they execute simultaneously without blocking each other
  • Process isolation - one session crashing doesn’t affect others, and a crashed tool call doesn’t kill the session
  • Idle timeout cleanup - sessions auto-terminate after inactivity (configurable, default 2 hours), no zombie sessions
  • Swappable session registry - defaults to Elixir’s Registry for single-node, but the behaviour is pluggable for distributed setups (Horde, :global, etc.)

DSL for defining tools

defmodule MyApp.MCP do
  use NexusMCP.Server,
    name: "my-app",
    version: "1.0.0"

  deftool "get_page", "Get a page by ID",
    params: [id: {:string!, "Page ID"}] do
    page = CMS.get_page!(params["id"])
    {:ok, Map.take(page, [:id, :title, :slug])}
  end
end

The deftool macro handles schema generation, parameter validation, and handler dispatch at compile time. There's also a wrap_tool_call/2 callback for setting up process-local context (tenant context, etc.) or rescuing common errors.

What's new in v0.2.0

Just pushed a couple of updates today:

- Tool annotations - readOnlyHint, destructiveHint, idempotentHint, etc. per the latest MCP spec
- Origin validation - optional allowed_origins on the transport for restricting which origins can connect
- Protocol version bump to 2025-06-18

deftool "delete_item", "Delete an item",
  params: [id: {:string!, "Item ID"}],
  annotations: %{destructiveHint: true, idempotentHint: true} do
  Items.delete!(params["id"])
  {:ok, %{deleted: true}}
end

Setup

# mix.exs
{:nexus_mcp, "~> 0.2.0"}

# application.ex
children = [{NexusMCP.Supervisor, []}]

# router.ex
forward "/mcp", NexusMCP.Transport,
  server: MyApp.MCP,
  allowed_origins: ["https://myapp.com"]

Minimal dependencies - just plug and jason.

https://github.com/dmkenney/nexus_mcp

Would love feedback, issues, or PRs. Cheers!

First Post!

Dmk

Dmk

A little updated to share - v0.3.0 is out, adding the other two MCP primitives so the server DSL now covers all three:

  1. tools
  2. prompts
  3. resources

Prompts (defprompt) surface user-invoked templates via prompts/list / prompts/get:

defprompt "code_review", "Ask the model to review code",
  arguments: [code: {:string!, "The code to review"}] do
  {:ok, [%{role: "user",
           content: %{type: "text", text: "Please review:\n" <> params["code"]}}]}
end

Resources (defresource, defresource_template) handle static and URI-templated context. Return values get coerced to MCP’s text/blob shape based on mime_type:

defresource_template "file:///{path}",
  name: "project_files",
  mime_type: "text/plain" do
  {:ok, File.read!(params["path"])}
end

Targeting the 2025-11-25 spec. Still TODO: resources/subscribe, list-changed notifications, completion/complete, and pagination cursors.

{:nexus_mcp, “~> 0.3.0”}

Last Post!

Dmk

Dmk

Small one this time: v0.3.1 is out, a bugfix release.

If you’re running NexusMCP across a distributed cluster with a custom SessionRegistry (e.g. a :pg-backed one), an RPC to a session living on a node that has left the cluster, say mid-deploy during a rolling restart, would exit with {{:nodedown, node}, _}. The transport wasn’t catching that, so the request process crashed instead of failing gracefully.

It now degrades to a 404 (session not found), and any other unexpected exit from the session call is logged and handled the same way rather than taking the request down with it. Single-node setups aren’t affected; this only shows up once sessions can live on a node that can disappear out from under you.

Thanks to @ryancurtin for the report and the fix. He hit it in a production ECS rolling deploy and traced it down to the exact exit reason (PR #1).

{:nexus_mcp, “~> 0.3.1”}

Hex: nexus_mcp | Hex
HexDocs: NexusMCP v0.3.1 — Documentation
Changelog: Release v0.3.1 · dmkenney/nexus_mcp · GitHub

Where Next?

Popular in Announcing Top

zorbash
I created Kitto a framework for dashboards inspired by Dashing. The distributed characteristics of Elixir and the low memory footprint...
New
mischov
import Meeseeks.CSS html = HTTPoison.get!("https://news.ycombinator.com/").body for story &lt;- Meeseeks.all(html, css("tr.athing")) do...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New
tmbb
PhoenixWS - Websockets over Phoenix Channels Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
kevinlang
Hey all, We have made an Ecto3 Adapter for SQLite3, ecto_sqlite3! We have successfully on-boarded the full suite of integration tests (...
New
trisolaran
Hi! :waving_hand: I would like to present LiveSelect, a little library that I wrote to easily add a dynamic selection input to your LV f...
198 11220 107
New
type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44532 311
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement