AgentStack
MCP verified Apache-2.0 Self-run

Conduit Mcp

mcp-nyo16-conduit-mcp · by nyo16

Elixir implementation of the Model Context Protocol (MCP) — build servers to expose tools, resources, and prompts to LLM applications.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add mcp-nyo16-conduit-mcp

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README — it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-nyo16-conduit-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Conduit Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

ConduitMCP

An Elixir implementation of the Model Context Protocol (MCP) specification (2025-11-25). Build MCP servers to expose tools, resources, and prompts to LLM applications like Claude Desktop, VS Code, and Cursor.

[](https://github.com/nyo16/conduitmcp/actions/workflows/ci.yml) [](https://hex.pm/packages/conduitmcp) []()

Features

  • MCP Apps — Tools can return interactive UI rendered as sandboxed iframes in host clients
  • Three Ways to Build — DSL macros, raw callbacks, or component modules — pick your level of control
  • Full MCP Spec — Tools, resources, prompts, completion, logging, subscriptions (MCP 2025-11-25 + 2025-06-18)
  • Runtime Validation — NimbleOptions-powered param validation with type coercion and custom constraints
  • Stateless Architecture — Pure functions, no processes, maximum concurrency via Bandit
  • Authentication — Bearer tokens, API keys, OAuth 2.1 (RFC 9728), custom verification
  • Rate Limiting — HTTP-level and message-level rate limiting with Hammer
  • Session Management — Pluggable session stores (ETS, Redis, PostgreSQL, Mnesia)
  • Observability — Telemetry events, optional Prometheus metrics via PromEx
  • Phoenix Ready — Drop-in integration with Phoenix routers
  • CORS & Security — Configurable origins, preflight handling, origin validation

Installation

def deps do
  [
    {:conduit_mcp, "~> 0.9"}
  ]
end

Requires Elixir ~> 1.18.

Quick Start

Define a server in one module:

defmodule MyServer do
  use ConduitMcp.Server

  tool "echo", "Echo a message" do
    param :message, :string, "Message to echo", required: true
    handle fn _conn, %{"message" => msg} -> text(msg) end
  end
end

Run it with Bandit:

# lib/my_app/application.ex
children = [
  {Bandit, plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyServer}, port: 4000}
]

Try it:

curl -s -X POST http://localhost:4000/ -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": {"name": "echo", "arguments": {"message": "hello"}}
}'

For multi-tool servers, authentication, sessions, OAuth, MCP Apps, and more, see the [guides](guides/) and [examples/](examples/).

Stability

ConduitMCP is pre-1.0. Most APIs are stable; the following are explicitly experimental and may change before 1.0:

  • ConduitMcp.Tasks and ConduitMcp.Tasks.Janitor — task lifecycle is new in MCP spec 2025-11-25 and the surface area may evolve.
  • ConduitMcp.Client — message builders for server-to-client requests (sampling, elicitation, roots). The bidirectional transport these depend on is not yet wired through Streamable HTTP.
  • ConduitMcp.Cancellation — the cooperative cancellation model may grow a more direct preemption path.

Breaking changes between minor versions will be called out in [CHANGELOG.md](CHANGELOG.md).

Three Ways to Define Servers

ConduitMCP gives you three modes. Each is a complete, independent way to build an MCP server — pick whichever fits your project.

| | DSL Mode | Manual Mode | Endpoint Mode | |--|----------|-------------|---------------| | Style | Declarative macros | Raw callbacks | Component modules | | Schema | Auto-generated | You build the maps | Auto from schema do field ... end | | Params | String-keyed maps | String-keyed maps | Atom-keyed maps | | Rate limiting | Transport option | Transport option | Declarative in use opts | | Best for | Quick setup | Maximum control | Larger servers, team projects |


1. DSL Mode

Everything in one module with compile-time macros. Schemas and validation generated automatically.

defmodule MyApp.MCPServer do
  use ConduitMcp.Server

  tool "greet", "Greet someone" do
    param :name, :string, "Person's name", required: true
    param :style, :string, "Greeting style", enum: ["formal", "casual"]

    handle fn _conn, params ->
      name = params["name"]
      style = params["style"] || "casual"
      greeting = if style == "formal", do: "Good day", else: "Hey"
      text("#{greeting}, #{name}!")
    end
  end

  prompt "code_review", "Code review assistant" do
    arg :code, :string, "Code to review", required: true
    arg :language, :string, "Language", default: "elixir"

    get fn _conn, args ->
      [
        system("You are a code reviewer"),
        user("Review this #{args["language"]} code:\n#{args["code"]}")
      ]
    end
  end

  resource "user://{id}" do
    description "User profile"
    mime_type "application/json"

    read fn _conn, params, _opts ->
      user = MyApp.Users.get!(params["id"])
      json(user)
    end
  end
end

Response helpers (auto-imported): text/1, json/1, image/1, audio/2, error/1, raw/1, raw_resource/2, system/1, user/1, assistant/1 — see [Responses](#responses) for details and custom response patterns.


2. Manual Mode

Full control. You implement callbacks directly with raw JSON Schema maps. No compile-time magic.

defmodule MyApp.MCPServer do
  use ConduitMcp.Server, dsl: false

  @tools [
    %{
      "name" => "greet",
      "description" => "Greet someone",
      "inputSchema" => %{
        "type" => "object",
        "properties" => %{"name" => %{"type" => "string"}},
        "required" => ["name"]
      }
    }
  ]

  @impl true
  def handle_list_tools(_conn), do: {:ok, %{"tools" => @tools}}

  @impl true
  def handle_call_tool(_conn, "greet", %{"name" => name}) do
    {:ok, %{"content" => [%{"type" => "text", "text" => "Hello, #{name}!"}]}}
  end
end

3. Endpoint + Component Mode

Each tool, resource, or prompt is its own module. An Endpoint aggregates them with declarative config for rate limiting, auth, and server metadata.

# Each tool is its own module
defmodule MyApp.Echo do
  use ConduitMcp.Component, type: :tool, description: "Echoes text back"

  schema do
    field :text, :string, "The text to echo", required: true, max_length: 500
  end

  @impl true
  def execute(%{text: text}, _conn) do
    text(text)
  end
end

defmodule MyApp.ReadUser do
  use ConduitMcp.Component,
    type: :resource,
    uri: "user://{id}",
    description: "User by ID",
    mime_type: "application/json"

  @impl true
  def execute(%{id: id}, _conn) do
    user = MyApp.Users.get!(id)
    {:ok, %{"contents" => [%{
      "uri" => "user://#{id}",
      "mimeType" => "application/json",
      "text" => JSON.encode!(user)
    }]}}
  end
end

# Endpoint aggregates components
defmodule MyApp.MCPServer do
  use ConduitMcp.Endpoint,
    name: "My App",
    version: "1.0.0",
    rate_limit: [backend: MyApp.RateLimiter, limit: 60, scale: 60_000],
    message_rate_limit: [backend: MyApp.RateLimiter, limit: 50, scale: 300_000]

  component MyApp.Echo
  component MyApp.ReadUser
end

Endpoint config is auto-extracted by transports — no duplication needed:

{Bandit,
 plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyApp.MCPServer},
 port: 4001}

See the [Endpoint Mode Guide](guides/endpoint_mode.md) for full details on components, schema DSL, and options.


Running Your Server

Standalone with Bandit

# lib/my_app/application.ex
def start(_type, _args) do
  children = [
    {Bandit,
     plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyApp.MCPServer},
     port: 4001}
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end

Phoenix Integration

# lib/my_app_web/router.ex
scope "/mcp" do
  forward "/", ConduitMcp.Transport.StreamableHTTP,
    server_module: MyApp.MCPServer,
    auth: [strategy: :bearer_token, token: System.get_env("MCP_AUTH_TOKEN")]
end

Transports

| Transport | Module | Description | |-----------|--------|-------------| | StreamableHTTP | ConduitMcp.Transport.StreamableHTTP | Recommended. Single POST / endpoint for bidirectional communication | | SSE | ConduitMcp.Transport.SSE | Legacy. GET /sse for streaming, POST /message for requests |

Both transports support authentication, rate limiting, CORS, and session management.

Responses

All tool/resource/prompt handlers return {:ok, map()} or {:error, map()}. Helper macros are imported automatically in DSL and Endpoint modes.

Tool Response Helpers

| Helper | What it returns | Use case | |--------|----------------|----------| | text("hello") | {:ok, %{"content" => [%{"type" => "text", "text" => "hello"}]}} | Plain text responses | | json(%{a: 1}) | {:ok, %{"content" => [%{"type" => "text", "text" => "{\"a\":1}"}]}} | Structured data (Jason-encoded) | | image(base64_data) | {:ok, %{"content" => [%{"type" => "image", "data" => ...}]}} | Images (base64) | | audio(data, "audio/wav") | {:ok, %{"content" => [%{"type" => "audio", "data" => ..., "mimeType" => ...}]}} | Audio clips | | error("fail") | {:error, %{"code" => -32000, "message" => "fail"}} | Error with default code | | error("fail", -32602) | {:error, %{"code" => -32602, "message" => "fail"}} | Error with custom code | | raw(any_map) | {:ok, any_map} | Bypass MCP wrapping entirely | | raw_resource(html, "text/html") | {:ok, %{"contents" => [%{"mimeType" => ..., "text" => ...}]}} | Resource content with MIME type |

Prompt Message Helpers

| Helper | Returns | |--------|---------| | system("You are a reviewer") | %{"role" => "system", "content" => %{"type" => "text", "text" => ...}} | | user("Review this code") | %{"role" => "user", "content" => %{"type" => "text", "text" => ...}} | | assistant("Here is my review") | %{"role" => "assistant", "content" => %{"type" => "text", "text" => ...}} |

Multi-Content Responses

Use texts/1 to return multiple text items in a single response:

{:ok, %{"content" => texts(["Line 1", "Line 2", "Line 3"])}}
# => {:ok, %{"content" => [%{"type" => "text", "text" => "Line 1"}, ...]}}

Raw / Fully Custom Responses

For maximum control, skip the helpers entirely and return the map yourself:

def execute(_params, _conn) do
  {:ok, %{
    "content" => [
      %{"type" => "text", "text" => "Here is the chart:"},
      %{"type" => "image", "data" => base64_png, "mimeType" => "image/png"},
      %{"type" => "text", "text" => "Analysis complete."}
    ]
  }}
end

The raw/1 helper is a shortcut for returning any map without MCP content wrapping — useful for debugging or non-standard responses:

raw(%{"custom_key" => "custom_value", "nested" => %{"data" => [1, 2, 3]}})
# => {:ok, %{"custom_key" => "custom_value", "nested" => %{"data" => [1, 2, 3]}}}

> Note: raw/1 bypasses the MCP content structure. Clients expecting standard "content" arrays won't parse it correctly. Use it for debugging or custom integrations.

Error Codes

Standard JSON-RPC 2.0 error codes used by the protocol:

| Code | Meaning | |------|---------| | -32700 | Parse error | | -32600 | Invalid request | | -32601 | Method not found | | -32602 | Invalid params | | -32603 | Internal error | | -32000 | Tool/server error (default for error/1) | | -32002 | Resource not found |

Parameter Validation

All three modes support runtime validation via NimbleOptions. DSL and Endpoint modes generate validation schemas automatically. Manual mode can opt in via __validation_schema_for_tool__/1.

Constraints

| Constraint | Types | Example | |------------|-------|---------| | required: true | All | required: true | | min: N / max: N | number, integer | min: 0, max: 100 | | min_length: N / max_length: N | string | min_length: 3, max_length: 255 | | enum: [...] | All | enum: ["red", "green", "blue"] | | default: value | All | default: "guest" | | validator: fun | All | validator: &valid_email?/1 |

Type Coercion

Enabled by default. Automatic conversion: "25"25, "true"true, "85.5"85.5.

Configuration

config :conduit_mcp, :validation,
  runtime_validation: true,
  strict_mode: true,
  type_coercion: true,
  log_validation_errors: false

Authentication

Configure in transport options or Endpoint use opts:

# Bearer token
auth: [strategy: :bearer_token, token: "your-secret-token"]

# API key
auth: [strategy: :api_key, api_key: "your-key", header: "x-api-key"]

# Custom verification
auth: [strategy: :function, verify: fn token ->
  case MyApp.Auth.verify(token) do
    {:ok, user} -> {:ok, user}
    _ -> {:error, "Invalid token"}
  end
end]

# OAuth 2.1 (RFC 9728)
auth: [strategy: :oauth, issuer: "https://auth.example.com", audience: "my-app"]

Authenticated user is available via conn.assigns[:current_user] in all callbacks.

Rate Limiting

Two layers using Hammer (optional dependency):

# Setup: add {:hammer, "~> 7.2"} to deps, then:
defmodule MyApp.RateLimiter do
  use Hammer, backend: :ets
end

HTTP rate limiting — limits raw connections:

rate_limit: [backend: MyApp.RateLimiter, limit: 100, scale: 60_000]

Message rate limiting — limits MCP method calls (tool calls, reads, prompts):

message_rate_limit: [
  backend: MyApp.RateLimiter,
  limit: 50,
  scale: 300_000,
  excluded_methods: ["initialize", "ping"]
]

Both support per-user keying via :key_func. Returns HTTP 429 with Retry-After header.

Session Management

StreamableHTTP supports server-side sessions with pluggable stores:

session: [store: ConduitMcp.Session.EtsStore]  # Default
session: [store: MyApp.RedisSessionStore]       # Custom store
session: false                                   # Disable

See guides: [Multi-Node Sessions](guides/multinodesessions.md)

Telemetry

Events emitted for monitoring:

| Event | Description | |-------|-------------| | [:conduit_mcp, :request, :stop] | All MCP requests | | [:conduit_mcp, :tool, :execute] | Tool executions | | [:conduit_mcp, :resource, :read] | Resource reads | | [:conduit_mcp, :prompt, :get] | Prompt retrievals | | [:conduit_mcp, :rate_limit, :check] | HTTP rate limit checks | | [:conduit_mcp, :message_rate_limit, :check] | Message rate limit checks | | [:conduit_mcp, :auth, :verify] | Authentication attempts |

Optional Prometheus metrics via ConduitMcp.PromEx — see module docs.

Client Configuration

VS Code / Cursor

{
  "mcpServers": {
    "my-app": {
      "url": "http://localhost:4001/",
      "headers": {
        "Authorization": "Bearer your-token"
      }
    }
  }
}

Claude Desktop

{
  "mcpServers": {
    "my-app": {
      "command": "elixir",
      "args": ["/path/to/your/server.exs"]
    }
  }
}

MCP Spec Coverage

ConduitMCP implements the full MCP specification:

| Feature | Status | Spec Version | |---------|--------|-------------| | Tools (list, call) | Supported | 2025-06-18 | | Resources (list, read, subscribe) | Supported | 2025-06-18 | | Prompts (list, get) | Supported | 2025-06-18 | | Completion | Supported | 2025-06-18 | | Logging | Supported | 2025-06-18 | | Protocol negotiation | Supported | 2025-11-25 | | Session management | Supported | 2025-11-25 | | OAuth 2.1 (RFC 9728) | Supported | 2025-11-25 | | StreamableHTTP transport | Supported | 2025-11-25 | | SSE transport (legacy) | Supported | 2025-06-18 | | MCP Apps (ext-apps) | Supported | Extension |

Guides

  • [Choosing a Mode](guides/choosingamode.md) — DSL vs Manual vs Endpoint comparison
  • [Endpoint Mode](guides/endpoint_mode.md) — Component modules, schema DSL, full walkthrough
  • [Authentication](guides/authentication.md) — All auth strategies in detail
  • [Rate Limiting](guides/rate_limiting.md) — HTTP and message rate limiting
  • [Multi-Node Sessions](guides/multinodesessions.md) — Redis, PostgreSQL, Mnesia session stores
  • [Oban Tasks

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.