{"id":33902239,"url":"https://github.com/nyo16/conduit_mcp","last_synced_at":"2026-04-18T18:01:50.495Z","repository":{"id":325998066,"uuid":"1072643657","full_name":"nyo16/conduit_mcp","owner":"nyo16","description":"Elixir implementation of the Model Context Protocol (MCP) — build servers to expose tools, resources, and prompts to LLM applications.","archived":false,"fork":false,"pushed_at":"2026-04-03T21:59:27.000Z","size":1477,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-03T23:27:04.772Z","etag":null,"topics":["ai","elixir","llm","mcp","model-context-protocol-server"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nyo16.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-09T02:40:22.000Z","updated_at":"2026-04-03T21:48:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nyo16/conduit_mcp","commit_stats":null,"previous_names":["nyo16/conduit_mcp"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/nyo16/conduit_mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nyo16%2Fconduit_mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nyo16%2Fconduit_mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nyo16%2Fconduit_mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nyo16%2Fconduit_mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nyo16","download_url":"https://codeload.github.com/nyo16/conduit_mcp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nyo16%2Fconduit_mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31978808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T17:30:12.329Z","status":"ssl_error","status_checked_at":"2026-04-18T17:29:59.069Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","elixir","llm","mcp","model-context-protocol-server"],"created_at":"2025-12-12T00:15:33.437Z","updated_at":"2026-04-18T18:01:50.471Z","avatar_url":"https://github.com/nyo16.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"![ConduitMCP](images/header.jpeg)\n\n# ConduitMCP\n\nAn Elixir implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) specification (2025-11-25). Build MCP servers to expose tools, resources, and prompts to LLM applications like Claude Desktop, VS Code, and Cursor.\n\n[![CI](https://github.com/nyo16/conduit_mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nyo16/conduit_mcp/actions/workflows/ci.yml)\n[![Tests](https://img.shields.io/badge/tests-526%20passing-brightgreen)]()\n[![Version](https://img.shields.io/badge/version-0.9.0-blue)]()\n[![MCP Spec](https://img.shields.io/badge/MCP-2025--11--25-purple)]()\n\n## Features\n\n- **MCP Apps** — Tools can return interactive UI rendered as sandboxed iframes in host clients\n- **Three Ways to Build** — DSL macros, raw callbacks, or component modules — pick your level of control\n- **Full MCP Spec** — Tools, resources, prompts, completion, logging, subscriptions (MCP 2025-11-25 + 2025-06-18)\n- **Runtime Validation** — NimbleOptions-powered param validation with type coercion and custom constraints\n- **Stateless Architecture** — Pure functions, no processes, maximum concurrency via Bandit\n- **Authentication** — Bearer tokens, API keys, OAuth 2.1 (RFC 9728), custom verification\n- **Rate Limiting** — HTTP-level and message-level rate limiting with Hammer\n- **Session Management** — Pluggable session stores (ETS, Redis, PostgreSQL, Mnesia)\n- **Observability** — Telemetry events, optional Prometheus metrics via PromEx\n- **Phoenix Ready** — Drop-in integration with Phoenix routers\n- **CORS \u0026 Security** — Configurable origins, preflight handling, origin validation\n\n## Installation\n\n```elixir\ndef deps do\n  [\n    {:conduit_mcp, \"~\u003e 0.9.0\"}\n  ]\nend\n```\n\nRequires Elixir ~\u003e 1.18.\n\n## Three Ways to Define Servers\n\nConduitMCP gives you three modes. Each is a complete, independent way to build an MCP server — pick whichever fits your project.\n\n| | DSL Mode | Manual Mode | Endpoint Mode |\n|--|----------|-------------|---------------|\n| **Style** | Declarative macros | Raw callbacks | Component modules |\n| **Schema** | Auto-generated | You build the maps | Auto from `schema do field ... end` |\n| **Params** | String-keyed maps | String-keyed maps | Atom-keyed maps |\n| **Rate limiting** | Transport option | Transport option | Declarative in `use` opts |\n| **Best for** | Quick setup | Maximum control | Larger servers, team projects |\n\n---\n\n### 1. DSL Mode\n\nEverything in one module with compile-time macros. Schemas and validation generated automatically.\n\n```elixir\ndefmodule MyApp.MCPServer do\n  use ConduitMcp.Server\n\n  tool \"greet\", \"Greet someone\" do\n    param :name, :string, \"Person's name\", required: true\n    param :style, :string, \"Greeting style\", enum: [\"formal\", \"casual\"]\n\n    handle fn _conn, params -\u003e\n      name = params[\"name\"]\n      style = params[\"style\"] || \"casual\"\n      greeting = if style == \"formal\", do: \"Good day\", else: \"Hey\"\n      text(\"#{greeting}, #{name}!\")\n    end\n  end\n\n  prompt \"code_review\", \"Code review assistant\" do\n    arg :code, :string, \"Code to review\", required: true\n    arg :language, :string, \"Language\", default: \"elixir\"\n\n    get fn _conn, args -\u003e\n      [\n        system(\"You are a code reviewer\"),\n        user(\"Review this #{args[\"language\"]} code:\\n#{args[\"code\"]}\")\n      ]\n    end\n  end\n\n  resource \"user://{id}\" do\n    description \"User profile\"\n    mime_type \"application/json\"\n\n    read fn _conn, params, _opts -\u003e\n      user = MyApp.Users.get!(params[\"id\"])\n      json(user)\n    end\n  end\nend\n```\n\n**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.\n\n---\n\n### 2. Manual Mode\n\nFull control. You implement callbacks directly with raw JSON Schema maps. No compile-time magic.\n\n```elixir\ndefmodule MyApp.MCPServer do\n  use ConduitMcp.Server, dsl: false\n\n  @tools [\n    %{\n      \"name\" =\u003e \"greet\",\n      \"description\" =\u003e \"Greet someone\",\n      \"inputSchema\" =\u003e %{\n        \"type\" =\u003e \"object\",\n        \"properties\" =\u003e %{\"name\" =\u003e %{\"type\" =\u003e \"string\"}},\n        \"required\" =\u003e [\"name\"]\n      }\n    }\n  ]\n\n  @impl true\n  def handle_list_tools(_conn), do: {:ok, %{\"tools\" =\u003e @tools}}\n\n  @impl true\n  def handle_call_tool(_conn, \"greet\", %{\"name\" =\u003e name}) do\n    {:ok, %{\"content\" =\u003e [%{\"type\" =\u003e \"text\", \"text\" =\u003e \"Hello, #{name}!\"}]}}\n  end\nend\n```\n\n---\n\n### 3. Endpoint + Component Mode\n\nEach tool, resource, or prompt is its own module. An Endpoint aggregates them with declarative config for rate limiting, auth, and server metadata.\n\n```elixir\n# Each tool is its own module\ndefmodule MyApp.Echo do\n  use ConduitMcp.Component, type: :tool, description: \"Echoes text back\"\n\n  schema do\n    field :text, :string, \"The text to echo\", required: true, max_length: 500\n  end\n\n  @impl true\n  def execute(%{text: text}, _conn) do\n    text(text)\n  end\nend\n\ndefmodule MyApp.ReadUser do\n  use ConduitMcp.Component,\n    type: :resource,\n    uri: \"user://{id}\",\n    description: \"User by ID\",\n    mime_type: \"application/json\"\n\n  @impl true\n  def execute(%{id: id}, _conn) do\n    user = MyApp.Users.get!(id)\n    {:ok, %{\"contents\" =\u003e [%{\n      \"uri\" =\u003e \"user://#{id}\",\n      \"mimeType\" =\u003e \"application/json\",\n      \"text\" =\u003e Jason.encode!(user)\n    }]}}\n  end\nend\n\n# Endpoint aggregates components\ndefmodule MyApp.MCPServer do\n  use ConduitMcp.Endpoint,\n    name: \"My App\",\n    version: \"1.0.0\",\n    rate_limit: [backend: MyApp.RateLimiter, limit: 60, scale: 60_000],\n    message_rate_limit: [backend: MyApp.RateLimiter, limit: 50, scale: 300_000]\n\n  component MyApp.Echo\n  component MyApp.ReadUser\nend\n```\n\nEndpoint config is auto-extracted by transports — no duplication needed:\n\n```elixir\n{Bandit,\n plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyApp.MCPServer},\n port: 4001}\n```\n\nSee the [Endpoint Mode Guide](guides/endpoint_mode.md) for full details on components, schema DSL, and options.\n\n---\n\n## Running Your Server\n\n### Standalone with Bandit\n\n```elixir\n# lib/my_app/application.ex\ndef start(_type, _args) do\n  children = [\n    {Bandit,\n     plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyApp.MCPServer},\n     port: 4001}\n  ]\n\n  Supervisor.start_link(children, strategy: :one_for_one)\nend\n```\n\n### Phoenix Integration\n\n```elixir\n# lib/my_app_web/router.ex\nscope \"/mcp\" do\n  forward \"/\", ConduitMcp.Transport.StreamableHTTP,\n    server_module: MyApp.MCPServer,\n    auth: [strategy: :bearer_token, token: System.get_env(\"MCP_AUTH_TOKEN\")]\nend\n```\n\n### Transports\n\n| Transport | Module | Description |\n|-----------|--------|-------------|\n| **StreamableHTTP** | `ConduitMcp.Transport.StreamableHTTP` | Recommended. Single `POST /` endpoint for bidirectional communication |\n| **SSE** | `ConduitMcp.Transport.SSE` | Legacy. `GET /sse` for streaming, `POST /message` for requests |\n\nBoth transports support authentication, rate limiting, CORS, and session management.\n\n## Responses\n\nAll tool/resource/prompt handlers return `{:ok, map()}` or `{:error, map()}`. Helper macros are imported automatically in DSL and Endpoint modes.\n\n### Tool Response Helpers\n\n| Helper | What it returns | Use case |\n|--------|----------------|----------|\n| `text(\"hello\")` | `{:ok, %{\"content\" =\u003e [%{\"type\" =\u003e \"text\", \"text\" =\u003e \"hello\"}]}}` | Plain text responses |\n| `json(%{a: 1})` | `{:ok, %{\"content\" =\u003e [%{\"type\" =\u003e \"text\", \"text\" =\u003e \"{\\\"a\\\":1}\"}]}}` | Structured data (Jason-encoded) |\n| `image(base64_data)` | `{:ok, %{\"content\" =\u003e [%{\"type\" =\u003e \"image\", \"data\" =\u003e ...}]}}` | Images (base64) |\n| `audio(data, \"audio/wav\")` | `{:ok, %{\"content\" =\u003e [%{\"type\" =\u003e \"audio\", \"data\" =\u003e ..., \"mimeType\" =\u003e ...}]}}` | Audio clips |\n| `error(\"fail\")` | `{:error, %{\"code\" =\u003e -32000, \"message\" =\u003e \"fail\"}}` | Error with default code |\n| `error(\"fail\", -32602)` | `{:error, %{\"code\" =\u003e -32602, \"message\" =\u003e \"fail\"}}` | Error with custom code |\n| `raw(any_map)` | `{:ok, any_map}` | Bypass MCP wrapping entirely |\n| `raw_resource(html, \"text/html\")` | `{:ok, %{\"contents\" =\u003e [%{\"mimeType\" =\u003e ..., \"text\" =\u003e ...}]}}` | Resource content with MIME type |\n\n### Prompt Message Helpers\n\n| Helper | Returns |\n|--------|---------|\n| `system(\"You are a reviewer\")` | `%{\"role\" =\u003e \"system\", \"content\" =\u003e %{\"type\" =\u003e \"text\", \"text\" =\u003e ...}}` |\n| `user(\"Review this code\")` | `%{\"role\" =\u003e \"user\", \"content\" =\u003e %{\"type\" =\u003e \"text\", \"text\" =\u003e ...}}` |\n| `assistant(\"Here is my review\")` | `%{\"role\" =\u003e \"assistant\", \"content\" =\u003e %{\"type\" =\u003e \"text\", \"text\" =\u003e ...}}` |\n\n### Multi-Content Responses\n\nUse `texts/1` to return multiple text items in a single response:\n\n```elixir\n{:ok, %{\"content\" =\u003e texts([\"Line 1\", \"Line 2\", \"Line 3\"])}}\n# =\u003e {:ok, %{\"content\" =\u003e [%{\"type\" =\u003e \"text\", \"text\" =\u003e \"Line 1\"}, ...]}}\n```\n\n### Raw / Fully Custom Responses\n\nFor maximum control, skip the helpers entirely and return the map yourself:\n\n```elixir\ndef execute(_params, _conn) do\n  {:ok, %{\n    \"content\" =\u003e [\n      %{\"type\" =\u003e \"text\", \"text\" =\u003e \"Here is the chart:\"},\n      %{\"type\" =\u003e \"image\", \"data\" =\u003e base64_png, \"mimeType\" =\u003e \"image/png\"},\n      %{\"type\" =\u003e \"text\", \"text\" =\u003e \"Analysis complete.\"}\n    ]\n  }}\nend\n```\n\nThe `raw/1` helper is a shortcut for returning any map without MCP content wrapping — useful for debugging or non-standard responses:\n\n```elixir\nraw(%{\"custom_key\" =\u003e \"custom_value\", \"nested\" =\u003e %{\"data\" =\u003e [1, 2, 3]}})\n# =\u003e {:ok, %{\"custom_key\" =\u003e \"custom_value\", \"nested\" =\u003e %{\"data\" =\u003e [1, 2, 3]}}}\n```\n\n\u003e **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.\n\n### Error Codes\n\nStandard JSON-RPC 2.0 error codes used by the protocol:\n\n| Code | Meaning |\n|------|---------|\n| `-32700` | Parse error |\n| `-32600` | Invalid request |\n| `-32601` | Method not found |\n| `-32602` | Invalid params |\n| `-32603` | Internal error |\n| `-32000` | Tool/server error (default for `error/1`) |\n| `-32002` | Resource not found |\n\n## Parameter Validation\n\nAll three modes support runtime validation via [NimbleOptions](https://hexdocs.pm/nimble_options). DSL and Endpoint modes generate validation schemas automatically. Manual mode can opt in via `__validation_schema_for_tool__/1`.\n\n### Constraints\n\n| Constraint | Types | Example |\n|------------|-------|---------|\n| `required: true` | All | `required: true` |\n| `min: N` / `max: N` | number, integer | `min: 0, max: 100` |\n| `min_length: N` / `max_length: N` | string | `min_length: 3, max_length: 255` |\n| `enum: [...]` | All | `enum: [\"red\", \"green\", \"blue\"]` |\n| `default: value` | All | `default: \"guest\"` |\n| `validator: fun` | All | `validator: \u0026valid_email?/1` |\n\n### Type Coercion\n\nEnabled by default. Automatic conversion: `\"25\"` → `25`, `\"true\"` → `true`, `\"85.5\"` → `85.5`.\n\n### Configuration\n\n```elixir\nconfig :conduit_mcp, :validation,\n  runtime_validation: true,\n  strict_mode: true,\n  type_coercion: true,\n  log_validation_errors: false\n```\n\n## Authentication\n\nConfigure in transport options or Endpoint `use` opts:\n\n```elixir\n# Bearer token\nauth: [strategy: :bearer_token, token: \"your-secret-token\"]\n\n# API key\nauth: [strategy: :api_key, api_key: \"your-key\", header: \"x-api-key\"]\n\n# Custom verification\nauth: [strategy: :function, verify: fn token -\u003e\n  case MyApp.Auth.verify(token) do\n    {:ok, user} -\u003e {:ok, user}\n    _ -\u003e {:error, \"Invalid token\"}\n  end\nend]\n\n# OAuth 2.1 (RFC 9728)\nauth: [strategy: :oauth, issuer: \"https://auth.example.com\", audience: \"my-app\"]\n```\n\nAuthenticated user is available via `conn.assigns[:current_user]` in all callbacks.\n\n## Rate Limiting\n\nTwo layers using [Hammer](https://hex.pm/packages/hammer) (optional dependency):\n\n```elixir\n# Setup: add {:hammer, \"~\u003e 7.2\"} to deps, then:\ndefmodule MyApp.RateLimiter do\n  use Hammer, backend: :ets\nend\n```\n\n**HTTP rate limiting** — limits raw connections:\n\n```elixir\nrate_limit: [backend: MyApp.RateLimiter, limit: 100, scale: 60_000]\n```\n\n**Message rate limiting** — limits MCP method calls (tool calls, reads, prompts):\n\n```elixir\nmessage_rate_limit: [\n  backend: MyApp.RateLimiter,\n  limit: 50,\n  scale: 300_000,\n  excluded_methods: [\"initialize\", \"ping\"]\n]\n```\n\nBoth support per-user keying via `:key_func`. Returns HTTP 429 with `Retry-After` header.\n\n## Session Management\n\nStreamableHTTP supports server-side sessions with pluggable stores:\n\n```elixir\nsession: [store: ConduitMcp.Session.EtsStore]  # Default\nsession: [store: MyApp.RedisSessionStore]       # Custom store\nsession: false                                   # Disable\n```\n\nSee guides: [Multi-Node Sessions](guides/multi_node_sessions.md)\n\n## Telemetry\n\nEvents emitted for monitoring:\n\n| Event | Description |\n|-------|-------------|\n| `[:conduit_mcp, :request, :stop]` | All MCP requests |\n| `[:conduit_mcp, :tool, :execute]` | Tool executions |\n| `[:conduit_mcp, :resource, :read]` | Resource reads |\n| `[:conduit_mcp, :prompt, :get]` | Prompt retrievals |\n| `[:conduit_mcp, :rate_limit, :check]` | HTTP rate limit checks |\n| `[:conduit_mcp, :message_rate_limit, :check]` | Message rate limit checks |\n| `[:conduit_mcp, :auth, :verify]` | Authentication attempts |\n\nOptional Prometheus metrics via `ConduitMcp.PromEx` — see module docs.\n\n## Client Configuration\n\n### VS Code / Cursor\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"url\": \"http://localhost:4001/\",\n      \"headers\": {\n        \"Authorization\": \"Bearer your-token\"\n      }\n    }\n  }\n}\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"elixir\",\n      \"args\": [\"/path/to/your/server.exs\"]\n    }\n  }\n}\n```\n\n## MCP Spec Coverage\n\nConduitMCP implements the full [MCP specification](https://modelcontextprotocol.io/specification/):\n\n| Feature | Status | Spec Version |\n|---------|--------|-------------|\n| Tools (list, call) | Supported | 2025-06-18 |\n| Resources (list, read, subscribe) | Supported | 2025-06-18 |\n| Prompts (list, get) | Supported | 2025-06-18 |\n| Completion | Supported | 2025-06-18 |\n| Logging | Supported | 2025-06-18 |\n| Protocol negotiation | Supported | 2025-11-25 |\n| Session management | Supported | 2025-11-25 |\n| OAuth 2.1 (RFC 9728) | Supported | 2025-11-25 |\n| StreamableHTTP transport | Supported | 2025-11-25 |\n| SSE transport (legacy) | Supported | 2025-06-18 |\n| MCP Apps (ext-apps) | Supported | Extension |\n\n## Guides\n\n- [Choosing a Mode](guides/choosing_a_mode.md) — DSL vs Manual vs Endpoint comparison\n- [Endpoint Mode](guides/endpoint_mode.md) — Component modules, schema DSL, full walkthrough\n- [Authentication](guides/authentication.md) — All auth strategies in detail\n- [Rate Limiting](guides/rate_limiting.md) — HTTP and message rate limiting\n- [Multi-Node Sessions](guides/multi_node_sessions.md) — Redis, PostgreSQL, Mnesia session stores\n- [Oban Tasks](guides/oban_tasks.md) — Long-running tasks with Oban\n- [MCP Apps](guides/mcp_apps.md) — Interactive UI from MCP tools\n\n## Documentation\n\n- [API Documentation](https://hexdocs.pm/conduit_mcp)\n- [Changelog](CHANGELOG.md)\n- [MCP Specification](https://modelcontextprotocol.io/specification/)\n\n## Examples\n\n- [Simple Server Example](https://github.com/nyo16/conduit_mcp/tree/master/examples/simple_tools_server)\n- [Phoenix Integration](https://github.com/nyo16/conduit_mcp/tree/master/examples/phoenix_mcp)\n- [MCP Apps Demo](https://github.com/nyo16/conduit_mcp/tree/master/examples/mcp_apps_demo)\n\n## License\n\nApache License 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnyo16%2Fconduit_mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnyo16%2Fconduit_mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnyo16%2Fconduit_mcp/lists"}