# MCPKit
[](https://hex.pm/packages/mcp_kit)
[](https://github.com/mcostasilva/mcp_kit/actions/workflows/ci.yml)
[](https://hexdocs.pm/mcp_kit)
[](https://github.com/mcostasilva/mcp_kit/blob/master/LICENSE)
`MCPKit` is a reusable MCP server runtime for Phoenix applications.
It gives host applications a small set of host-facing contracts for building
policy-aware MCP servers over Streamable HTTP, with support for tools, prompts,
resources, completions, persistent sessions, and a host-started runtime for
active sessions.
## MCP Support Matrix
| Area | Status | Notes |
| --- | --- | --- |
| Streamable HTTP transport (`POST`) | ✅ | Supported |
| SSE session stream (`GET`) | ✅ | Supported for active sessions |
| Session termination (`DELETE`) | ✅ | Supported |
| Lifecycle (`initialize`, `ping`, `notifications/initialized`) | ✅ | Supported |
| Tools (`tools/list`, `tools/call`) | ✅ | Supported |
| Prompts (`prompts/list`, `prompts/get`) | ✅ | Supported |
| Resources (`resources/list`, `resources/read`, `resources/templates/list`) | ✅ | Supported |
| Utilities (`completion/complete`, `notifications/cancelled`) | ✅ | Supported |
| Request-time policy enforcement | ✅ | Via `MCPKit.Policy` |
| Resource subscriptions | ❌ | Not implemented |
| Resource update notifications | ❌ | Not implemented |
| Logging capability | ❌ | Not implemented |
| Progress notifications | ❌ | Not implemented |
| List-changed notifications | ❌ | Not implemented |
| Public host API for runtime-initiated client requests | ❌ | Internal plumbing exists, but no public host API yet |
## Installation
```elixir
def deps do
[
{:mcp_kit, "~> 0.2.4"}
]
end
```
## Quickstart
### 1. Define the host contract
```elixir
defmodule MyApp.MCP.Definition do
@behaviour MCPKit.Definition
@impl true
def server_info do
%{"name" => "my-app", "version" => "0.1.0"}
end
@impl true
def session_store, do: MyApp.MCP.SessionStore
@impl true
def policy, do: MyApp.MCP.Policy
end
```
`MCPKit` owns the MCP transport version and advertises its built-in current
protocol version automatically. Host applications do not configure this.
### 2. Implement persistent session storage
```elixir
defmodule MyApp.MCP.SessionStore do
@behaviour MCPKit.SessionStore
@impl true
def create_session(attrs) do
{:ok,
%{
id: Ecto.UUID.generate(),
initialized: false,
protocol_version: attrs.protocol_version,
client_info: attrs.client_info,
client_capabilities: attrs.client_capabilities
}}
end
@impl true
def fetch_session(session_id) do
# Load from your database or cache.
{:error, :not_found}
end
@impl true
def touch_session(session), do: {:ok, session}
@impl true
def mark_initialized(session), do: {:ok, %{session | initialized: true}}
@impl true
def delete_session(_session_id), do: {:error, :not_found}
end
```
### 3. Start the runtime
```elixir
children = [
{MCPKit.Runtime, definition: MyApp.MCP.Definition}
]
```
### 4. Add a policy
```elixir
defmodule MyApp.MCP.Policy do
@behaviour MCPKit.Policy
@impl true
def authorize(_action, _context), do: :allow
end
```
### 5. Define a tool
```elixir
defmodule MyApp.MCP.Tools.Ping do
use MCPKit.Tool
alias MCPKit.Response
schema do
field :message, :string, required: true
end
@impl true
def execute(arguments, context) do
{:reply, Response.tool() |> Response.structured(arguments), context}
end
end
```
### 6. Mount the MCP route
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
import MCPKit.Router
mcp_scope "/mcp", MyApp.MCP do
tool "ping", Tools.Ping
end
end
```
`mcp_scope/2` infers the definition module as `MyApp.MCP.Definition` and the
runtime name as `MyApp.MCP.Runtime`. Override them with `definition:` and
`runtime:` if needed.
Inside `mcp_scope`, module references follow normal Phoenix scope aliasing, so
`Tools.Ping` resolves as `MyApp.MCP.Tools.Ping` and remains visible to editor
tooling.
## Host Contracts
The host application is responsible for:
- `MCPKit.Definition`: protocol version, server info, session store, and optional policy
- `MCPKit.SessionStore`: durable session lifecycle
- `MCPKit.Runtime`: active in-memory session coordination
- `MCPKit.Policy`: request-time authorization and visibility decisions
See the guides for fuller examples:
- [Getting Started](guides/getting-started.md)
- [Policy and Tiered Access](guides/policy.md)
- [Supported MCP Surface](guides/supported-mcp-surface.md)
## Router DSL
Use `MCPKit.Router.mcp_scope/2` or `mcp_scope/3` inside a Phoenix router scope.
```elixir
mcp_scope "/mcp", MyApp.MCP do
tool "project_create", Tools.ProjectCreate
prompt "draft_release_notes", Prompts.DraftReleaseNotes
resource "project", Resources.Project
end
```
Inside `mcp_scope`, you can declare:
- `tool/2`
- `prompt/2`
- `resource/2`
## Policy
`MCPKit.Policy` is evaluated on every request. It supports these decisions:
- `:allow`
- `{:deny, :not_found}`
- `{:deny, :forbidden}`
Lists are filtered item-by-item, while direct calls, prompt gets, resource reads,
and completions are enforced separately.
`completion/complete` is also policy-aware and only returns suggestions for
visible prompt arguments and resource templates.
## Authoring APIs
### Tools
Tool modules use `MCPKit.Tool` to declare input schemas and return
`MCPKit.Response` values.
### Prompts
Prompt modules use `MCPKit.Prompt` to declare argument schemas, render MCP
messages, and optionally implement `complete/3` for argument suggestions.
### Resources
Resource modules use `MCPKit.Resource` to declare concrete resource entries,
URI templates, URI reads, and optionally implement `complete/3` for template
variable suggestions.
## Supported MCP Surface
Currently implemented methods:
- `initialize`
- `ping`
- `completion/complete`
- `prompts/list`
- `prompts/get`
- `resources/list`
- `resources/read`
- `resources/templates/list`
- `tools/list`
- `tools/call`
- `notifications/initialized`
- `notifications/cancelled`
- `DELETE` session termination
- `GET` SSE session stream
For one canonical compatibility summary, see
[Supported MCP Surface](guides/supported-mcp-surface.md).
## Publishing Checklist
- GitHub Actions publishes tagged releases automatically from `v*` tags
- configure the `HEX_API_KEY` repository secret before pushing a release tag
- update version in `mix.exs`
- confirm package links and source URL
- run `mix test`
- run `mix docs`
- run `mix hex.build`