conduit_mcp

mcp
Guvenlik Denetimi
Uyari
Health Uyari
  • License — License: Apache-2.0
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Uyari
  • process.env — Environment variable access in examples/phoenix_mcp/assets/js/app.js
  • fs module — File system access in examples/phoenix_mcp/assets/vendor/heroicons.js
Permissions Gecti
  • Permissions — No dangerous permissions requested
Purpose
This tool is an Elixir library that implements the Model Context Protocol (MCP). It allows developers to build servers that securely expose tools, resources, and prompts to LLM applications like Claude Desktop or Cursor.

Security Assessment
Overall Risk: Low. The core library does not request dangerous permissions, execute shell commands, or contain hardcoded secrets. The automated scan flagged environment variable and file system access, but a manual review confirms these warnings are false positives for the core package. They strictly originate from the `examples/phoenix_mcp/` directory—a frontend demo utilizing standard JavaScript browser APIs. The underlying Elixir architecture is intentionally stateless and features built-in security controls, including robust authentication (Bearer tokens, OAuth 2.1), CORS validation, and rate limiting.

Quality Assessment
The project is actively maintained, with its most recent push occurring today. It is covered by CI automation, reports over 500 passing tests, and uses the standard Apache-2.0 license. The only notable drawback is its low community visibility; it currently has only 5 GitHub stars. This indicates that while the code appears well-engineered, it has not yet undergone widespread peer review or battle-testing in large-scale production environments.

Verdict
Safe to use, though the early-stage community footprint means you should perform your own standard due diligence before deploying it widely.
SUMMARY

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

README.md

ConduitMCP

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.

CI
Tests
Version
MCP Spec

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.0"}
  ]
end

Requires Elixir ~> 1.18.

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 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" => Jason.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 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

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

Documentation

Examples

License

Apache License 2.0

Yorumlar (0)

Sonuc bulunamadi