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

treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New
wfgilman
I’ve cleaned up and open sourced three financial libraries I was using for my company. They are bindings for the APIs of these three comp...
New
mbuhot
Leverage Open Api 3.0 (Swagger) to document, test, validate and explore your Plug and Phoenix APIs. Generate and serve a JSON Open API ...
New
sasajuric
I’d like to announce a small library called boundaries. This is an experimental project which explores the idea of enforcing boundaries ...
New
mplatts
With HEEX released we decided to start a components library using Tailwind CSS - check it out here: Petal Components. We also have a boi...
New
Flo0807
Hello everyone! I am excited to share our heart project Backpex with you. After building several Phoenix applications, we realized that...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31494 112
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement