NexusMCP.Server behaviour (NexusMCP v0.5.0)

Copy Markdown View Source

Behaviour for defining an MCP server.

Supports the three MCP server primitives:

  • Tools — model-controlled functions (deftool)
  • Prompts — user-controlled message templates (defprompt)
  • Resources — application-controlled context (defresource, defresource_template)

Usage

Tools (DSL style)

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, :body])}
  end
end

Prompts

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 this code:\n" <> params["code"]}}
  ]}
end

Resources

defresource "config://app",
  name: "app_config",
  description: "Application configuration",
  mime_type: "application/json" do
  {:ok, Jason.encode!(MyApp.config())}
end

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

Options

  • :name - Server name (required)
  • :version - Server version (required)
  • :idle_timeout - Session idle timeout in ms (default: 7_200_000 / 2 hours)
  • :hibernate_after - Hibernate a session once it has been quiet for this many ms (default: 60_000). Set to :infinity to disable hibernation.

Session memory

A session process keeps the heap it grew while handling requests, and the BEAM does not shrink it again on its own. A session that returned a few large tool results therefore keeps holding that heap for as long as it lives, even while completely idle - with many concurrent sessions that dominates memory use.

Hibernating collapses the heap to a minimum and the process re-grows it on the next message. Doing that after every call costs real time (roughly 2x on a trivial call), so it is debounced: each request re-arms the timer, an active session never hibernates, and only one that has gone quiet pays for it. Set hibernate_after: :infinity to opt out.

Summary

Callbacks

Handle a prompts/get call. Receives the prompt name, arguments, and session.

Handle a resources/read call. Receives the URI, the params captured from the URI template (empty map for static resources), and the session.

Handle a tool call. Receives the tool name, params, and session state.

How long a session must be quiet before it hibernates, in milliseconds.

Returns the idle timeout in milliseconds.

Per-session initialization. Called when a new session is created. Receives the session map and should return {:ok, session} or {:error, reason}.

Returns the list of prompt definitions.

Returns the list of resource template definitions.

Returns the list of static resource definitions.

Returns server info (name, version). Generated by use NexusMCP.Server.

Returns the list of tool definitions.

Wraps every tool call execution. Runs in the Task process before the handler. Override to set up process-local state (e.g. tenant context) or rescue errors.

Types

session()

@type session() :: %{session_id: String.t(), assigns: map()}

Callbacks

handle_prompt_get(name, arguments, session)

@callback handle_prompt_get(name :: String.t(), arguments :: map(), session :: session()) ::
  {:ok, [map()]} | {:error, String.t() | :not_found}

Handle a prompts/get call. Receives the prompt name, arguments, and session.

Should return {:ok, messages} where messages is a list of %{role: "user" | "assistant", content: %{...}} maps.

handle_resource_read(uri, params, session)

@callback handle_resource_read(uri :: String.t(), params :: map(), session :: session()) ::
  {:ok, String.t() | map()} | {:error, String.t() | :not_found}

Handle a resources/read call. Receives the URI, the params captured from the URI template (empty map for static resources), and the session.

Should return {:ok, content} where content is one of:

  • A string — wrapped as text if the resource's mimeType is textual, otherwise base64-encoded as blob
  • %{text: string} or %{blob: base64} — passed through

Return {:error, :not_found} for unknown URIs.

handle_tool_call(name, params, session)

@callback handle_tool_call(name :: String.t(), params :: map(), session :: session()) ::
  {:ok, term()} | {:error, String.t()}

Handle a tool call. Receives the tool name, params, and session state.

hibernate_after()

(optional)
@callback hibernate_after() :: non_neg_integer() | :infinity

How long a session must be quiet before it hibernates, in milliseconds.

:infinity disables hibernation.

Optional. use NexusMCP.Server defines it from the :hibernate_after option; a module implementing this behaviour by hand may leave it out, in which case sessions fall back to the 60s default.

idle_timeout()

@callback idle_timeout() :: non_neg_integer()

Returns the idle timeout in milliseconds.

init(session)

@callback init(session :: session()) :: {:ok, session()} | {:error, String.t()}

Per-session initialization. Called when a new session is created. Receives the session map and should return {:ok, session} or {:error, reason}.

prompts()

@callback prompts() :: [map()]

Returns the list of prompt definitions.

resource_templates()

@callback resource_templates() :: [map()]

Returns the list of resource template definitions.

resources()

@callback resources() :: [map()]

Returns the list of static resource definitions.

server_info()

@callback server_info() :: %{name: String.t(), version: String.t()}

Returns server info (name, version). Generated by use NexusMCP.Server.

tools()

@callback tools() :: [map()]

Returns the list of tool definitions.

wrap_tool_call(session, fun)

@callback wrap_tool_call(
  session :: session(),
  fun :: (-> {:ok, term()} | {:error, String.t()})
) ::
  {:ok, term()} | {:error, String.t()}

Wraps every tool call execution. Runs in the Task process before the handler. Override to set up process-local state (e.g. tenant context) or rescue errors.

Default implementation just calls fun.().