Urchin.Server behaviour (Urchin v0.4.0)

Copy Markdown View Source

Behaviour and DSL for authoring MCP servers.

There are two ways to define a server:

DSL

defmodule Demo do
  use Urchin.Server, name: "demo", version: "1.0.0"

  tool "echo",
    description: "Echo the message back",
    input_schema: %{
      "type" => "object",
      "properties" => %{"message" => %{"type" => "string"}},
      "required" => ["message"]
    } do
    {:ok, [Urchin.Content.text(args["message"])]}
  end

  resource "config://app", name: "config", mime_type: "application/json" do
    {:ok, [Urchin.Content.text_resource("config://app", ~s({"ok":true}))]}
  end

  prompt "greet", arguments: [%{name: "name", required: true}] do
    {:ok, [Urchin.Prompt.user_message(Urchin.Content.text("Hello " <> args["name"]))]}
  end
end

Inside a tool/prompt block the bindings args (the decoded arguments map) and ctx (an Urchin.Context) are available. Inside a resource/resource_template block only ctx is available; for templates ctx.uri and ctx.params are set.

Capabilities are derived automatically from the declared features.

Duplicate literal tool names declared via the DSL are rejected at compile time (non-literal names cannot be compared statically and are not checked). Urchin enforces no tool-name pattern (the MCP schema imposes none); servers should still follow the MCP naming recommendations (a conservative charset, a length bound, no whitespace).

Behaviour

Implement the callbacks directly for full control or stateful servers. All callbacks except server_info/0 are optional; a feature is considered supported only when its callbacks are implemented (or declared via the DSL).

Handler return values

  • list_* callbacks: {:ok, items} or {:ok, items, next_cursor}
  • call_tool/3: {:ok, content}, {:ok, content, opts} (with :structured_content / :is_error), an Urchin.Result.CallTool struct, or {:error, reason}
  • read_resource/2: {:ok, contents} or {:error, reason}
  • get_prompt/3: {:ok, messages} or {:ok, messages, description}

For every callback, a returned or raised Urchin.Error becomes that JSON-RPC error. A call_tool/3 handler's {:error, binary} is surfaced as a CallToolResult with isError: true so the model can self-correct, as is a tool that raises any other exception. For the other callbacks an {:error, binary} becomes a JSON-RPC internal error and any other raised exception becomes an internal error.

Summary

Functions

Declares a prompt. The do block receives args and ctx bindings and must return a get_prompt/3 result.

Declares a static resource. The do block receives ctx (with ctx.uri set) and must return a read_resource/2 result. Defaults :name to the URI.

Declares a resource template (RFC 6570). The do block receives ctx with ctx.uri and ctx.params (extracted template variables). Defaults :name to the template.

Declares a tool. The do block receives args and ctx bindings and must return a call_tool/3 result.

Types

call_result()

@type call_result() ::
  {:ok, [map()]}
  | {:ok, [map()], keyword()}
  | {:ok, Urchin.Result.CallTool.t()}
  | {:error, Urchin.Error.t() | String.t()}

cursor()

@type cursor() :: String.t() | nil

list_result(item)

@type list_result(item) ::
  {:ok, [item]}
  | {:ok, [item], cursor()}
  | {:error, Urchin.Error.t() | String.t()}

Callbacks

call_tool(name, args, t)

(optional)
@callback call_tool(name :: String.t(), args :: map(), Urchin.Context.t()) ::
  call_result()

capabilities()

(optional)
@callback capabilities() :: map()

complete(ref, argument, completion_context, t)

(optional)
@callback complete(
  ref :: map(),
  argument :: map(),
  completion_context :: map(),
  Urchin.Context.t()
) :: {:ok, map()} | {:error, Urchin.Error.t() | String.t()}

get_prompt(name, args, t)

(optional)
@callback get_prompt(name :: String.t(), args :: map(), Urchin.Context.t()) ::
  {:ok, [map()]}
  | {:ok, [map()], String.t() | nil}
  | {:error, Urchin.Error.t() | String.t()}

init(arg)

(optional)
@callback init(arg :: term()) :: {:ok, term()} | {:error, term()}

instructions()

(optional)
@callback instructions() :: String.t() | nil

list_prompts(cursor, t)

(optional)
@callback list_prompts(cursor(), Urchin.Context.t()) :: list_result(Urchin.Prompt.t())

list_resource_templates(cursor, t)

(optional)
@callback list_resource_templates(cursor(), Urchin.Context.t()) ::
  list_result(Urchin.ResourceTemplate.t())

list_resources(cursor, t)

(optional)
@callback list_resources(cursor(), Urchin.Context.t()) :: list_result(Urchin.Resource.t())

list_tools(cursor, t)

(optional)
@callback list_tools(cursor(), Urchin.Context.t()) :: list_result(Urchin.Tool.t())

read_resource(uri, t)

(optional)
@callback read_resource(uri :: String.t(), Urchin.Context.t()) ::
  {:ok, [map()]} | {:error, Urchin.Error.t() | String.t()}

server_info()

@callback server_info() :: map()

set_log_level(level, t)

(optional)
@callback set_log_level(level :: String.t(), Urchin.Context.t()) :: :ok | {:error, term()}

subscribe_resource(uri, t)

(optional)
@callback subscribe_resource(uri :: String.t(), Urchin.Context.t()) ::
  :ok | {:error, term()}

unsubscribe_resource(uri, t)

(optional)
@callback unsubscribe_resource(uri :: String.t(), Urchin.Context.t()) ::
  :ok | {:error, term()}

Functions

prompt(name, opts \\ [], list)

(macro)

Declares a prompt. The do block receives args and ctx bindings and must return a get_prompt/3 result.

resource(uri, opts \\ [], list)

(macro)

Declares a static resource. The do block receives ctx (with ctx.uri set) and must return a read_resource/2 result. Defaults :name to the URI.

resource_template(uri_template, opts \\ [], list)

(macro)

Declares a resource template (RFC 6570). The do block receives ctx with ctx.uri and ctx.params (extracted template variables). Defaults :name to the template.

tool(name, opts \\ [], list)

(macro)

Declares a tool. The do block receives args and ctx bindings and must return a call_tool/3 result.

Options beyond the Urchin.Tool fields:

  • :scopes - OAuth scopes the caller must hold (checked against ctx.auth) before the handler runs. The call fails with an error when the scopes are missing, including when the request carries no authorization.