# Phoenix Integration Guide
ExMCP provides seamless integration with Phoenix applications through the `ExMCP.HttpPlug` module, which implements the standard Plug behavior. This allows you to easily add MCP (Model Context Protocol) server capabilities to your existing Phoenix applications.
## Quick Setup
### 1. Add ExMCP to Your Phoenix Project
```elixir
# In mix.exs
defp deps do
[
{:ex_mcp, "~> 1.0.0-rc.0"},
# ... your other dependencies
]
end
```
### 2. Create an MCP Handler
Create a handler module that implements your MCP server logic:
```elixir
# lib/my_app/mcp_handler.ex
defmodule MyApp.MCPHandler do
use ExMCP.Server.Handler
@impl true
def init(_args), do: {:ok, %{}}
@impl true
def handle_initialize(_params, state) do
{:ok, %{
name: Application.get_env(:my_app, :app_name, "my-phoenix-app"),
version: Application.spec(:my_app, :vsn) |> to_string(),
capabilities: %{
tools: %{},
resources: %{}
}
}, state}
end
@impl true
def handle_list_tools(state) do
tools = [
%{
name: "get_user_count",
description: "Get the total number of registered users",
input_schema: %{
type: "object",
properties: %{}
}
},
%{
name: "search_posts",
description: "Search blog posts",
input_schema: %{
type: "object",
properties: %{
query: %{type: "string", description: "Search query"},
limit: %{type: "integer", minimum: 1, maximum: 50, default: 10}
},
required: ["query"]
}
}
]
{:ok, tools, state}
end
@impl true
def handle_call_tool("get_user_count", _args, state) do
count = MyApp.Accounts.count_users()
result = [
%{
type: "text",
text: "Total registered users: #{count}"
}
]
{:ok, result, state}
end
def handle_call_tool("search_posts", args, state) do
query = Map.get(args, "query")
limit = Map.get(args, "limit", 10)
posts = MyApp.Blog.search_posts(query, limit: limit)
results = Enum.map(posts, fn post ->
%{
type: "text",
text: "**#{post.title}**\n#{post.excerpt}\nPublished: #{post.published_at}"
}
end)
{:ok, results, state}
end
def handle_call_tool(tool_name, _args, state) do
error = %{
code: -32601,
message: "Unknown tool: #{tool_name}"
}
{:error, error, state}
end
# Implement other required callbacks
@impl true
def handle_list_resources(state), do: {:ok, [], state}
@impl true
def handle_read_resource(_uri, state) do
error = %{code: -32601, message: "Resources not implemented"}
{:error, error, state}
end
@impl true
def handle_list_prompts(state), do: {:ok, [], state}
@impl true
def handle_get_prompt(_name, _args, state) do
error = %{code: -32601, message: "Prompts not implemented"}
{:error, error, state}
end
end
```
### 3. Add to Your Phoenix Router
```elixir
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# ... your existing pipelines
pipeline :mcp do
plug :accepts, ["json"]
# Add authentication if needed:
# plug MyAppWeb.Plugs.Authenticate
end
# ... your existing routes
scope "/api" do
pipe_through :mcp
# Mount MCP server at /api/mcp
forward "/mcp", ExMCP.HttpPlug,
handler: MyApp.MCPHandler,
server_info: %{
name: "my-phoenix-app",
version: "1.0.0"
},
sse_enabled: true, # Enable Server-Sent Events for real-time communication
cors_enabled: true # Enable CORS for web clients
end
end
```
### 4. Test Your Integration
Start your Phoenix server and test the MCP endpoint:
```bash
# Start Phoenix server
mix phx.server
# Test with curl
curl -X POST http://localhost:4000/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
# Expected response:
# {
# "jsonrpc": "2.0",
# "id": 1,
# "result": {
# "tools": [
# {
# "name": "get_user_count",
# "description": "Get the total number of registered users",
# "input_schema": {"type": "object", "properties": {}}
# },
# {
# "name": "search_posts",
# "description": "Search blog posts",
# "input_schema": {
# "type": "object",
# "properties": {
# "query": {"type": "string", "description": "Search query"},
# "limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 10}
# },
# "required": ["query"]
# }
# }
# ]
# }
# }
```
## Advanced Configuration
### Authentication & Authorization
Integrate MCP with your existing Phoenix authentication:
```elixir
# lib/my_app_web/plugs/mcp_auth.ex
defmodule MyAppWeb.Plugs.MCPAuth do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
case MyApp.Auth.verify_token(token) do
{:ok, user} ->
assign(conn, :current_user, user)
{:error, _reason} ->
conn
|> put_status(401)
|> Phoenix.Controller.json(%{error: "Invalid token"})
|> halt()
end
_ ->
conn
|> put_status(401)
|> Phoenix.Controller.json(%{error: "Authorization required"})
|> halt()
end
end
end
# In your router:
pipeline :mcp_authenticated do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.MCPAuth
end
scope "/api" do
pipe_through :mcp_authenticated
forward "/mcp", ExMCP.HttpPlug,
handler: MyApp.AuthenticatedMCPHandler,
server_info: %{name: "secure-app", version: "1.0.0"}
end
```
### Accessing Request Context
Access the current user and other Phoenix context in your MCP handler:
```elixir
defmodule MyApp.AuthenticatedMCPHandler do
use ExMCP.Server.Handler
@impl true
def handle_call_tool("get_my_posts", _args, state) do
# Access current user from the request context
user = get_current_user(state)
posts = MyApp.Blog.list_user_posts(user.id)
results = Enum.map(posts, fn post ->
%{type: "text", text: "#{post.title}: #{post.excerpt}"}
end)
{:ok, results, state}
end
# Helper to extract user from state (you'll need to modify the HttpPlug to pass this)
defp get_current_user(state) do
# This would require modifying ExMCP.HttpPlug to pass request context
# For now, you can access it via Process.get/1 if set by a plug
Process.get(:current_user)
end
end
```
### Server-Sent Events (SSE)
ExMCP supports real-time communication via SSE. Clients can connect to the SSE endpoint for live updates:
```javascript
// JavaScript client example
const eventSource = new EventSource('http://localhost:4000/api/mcp/sse');
eventSource.onmessage = function(event) {
const response = JSON.parse(event.data);
console.log('Received MCP response:', response);
};
// Send MCP requests via regular HTTP POST
fetch('http://localhost:4000/api/mcp', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {name: 'get_user_count', arguments: {}}
})
});
// Response will arrive via SSE connection
```
### Resource Integration
Expose your Phoenix application data as MCP resources:
```elixir
@impl true
def handle_list_resources(state) do
resources = [
%{
uri: "phoenix://users",
name: "User List",
description: "List of all registered users",
mimeType: "application/json"
},
%{
uri: "phoenix://posts/recent",
name: "Recent Posts",
description: "Most recent blog posts",
mimeType: "application/json"
}
]
{:ok, resources, state}
end
@impl true
def handle_read_resource("phoenix://users", state) do
users = MyApp.Accounts.list_users()
data = Enum.map(users, fn user ->
%{id: user.id, email: user.email, name: user.name}
end)
content = [%{
type: "text",
text: Jason.encode!(data, pretty: true),
mimeType: "application/json"
}]
{:ok, content, state}
end
def handle_read_resource("phoenix://posts/recent", state) do
posts = MyApp.Blog.list_recent_posts(limit: 10)
content = [%{
type: "text",
text: Jason.encode!(posts, pretty: true),
mimeType: "application/json"
}]
{:ok, content, state}
end
```
## Production Considerations
### Performance
- **Connection Pooling**: Use a connection pool for database operations in your MCP handlers
- **Caching**: Cache frequently requested data to reduce database load
- **Rate Limiting**: Implement rate limiting for MCP endpoints if needed
```elixir
# Rate limiting example with Hammer
pipeline :mcp_rate_limited do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.RateLimit, bucket_name: "mcp_api"
end
```
### Security
- **Input Validation**: Always validate tool arguments and resource URIs
- **Authorization**: Check user permissions before executing tools or reading resources
- **Audit Logging**: Log all MCP operations for security auditing
```elixir
@impl true
def handle_call_tool(tool_name, args, state) do
# Log the operation
Logger.info("MCP tool called", tool: tool_name, args: args, user: get_current_user_id())
# Validate permissions
case check_tool_permission(tool_name, get_current_user()) do
:ok ->
# Execute tool
do_call_tool(tool_name, args, state)
{:error, reason} ->
error = %{code: -32000, message: "Permission denied: #{reason}"}
{:error, error, state}
end
end
```
### Monitoring
Monitor your MCP endpoints like any other Phoenix endpoint:
```elixir
# Add Telemetry events for MCP operations
defmodule MyApp.MCPTelemetry do
def track_tool_call(tool_name, duration, success) do
:telemetry.execute(
[:my_app, :mcp, :tool_call],
%{duration: duration},
%{tool: tool_name, success: success}
)
end
end
# In your handler:
@impl true
def handle_call_tool(tool_name, args, state) do
start_time = System.monotonic_time()
result = do_call_tool(tool_name, args, state)
duration = System.monotonic_time() - start_time
success = case result do
{:ok, _, _} -> true
_ -> false
end
MyApp.MCPTelemetry.track_tool_call(tool_name, duration, success)
result
end
```
## Examples and Use Cases
### E-commerce Integration
```elixir
# Expose product search and order management
@impl true
def handle_list_tools(state) do
tools = [
%{
name: "search_products",
description: "Search for products in the catalog",
input_schema: %{
type: "object",
properties: %{
query: %{type: "string"},
category: %{type: "string"},
price_max: %{type: "number"}
}
}
},
%{
name: "get_order_status",
description: "Get the status of an order",
input_schema: %{
type: "object",
properties: %{
order_id: %{type: "string"}
},
required: ["order_id"]
}
}
]
{:ok, tools, state}
end
```
### Analytics Dashboard
```elixir
# Expose analytics data as MCP tools
@impl true
def handle_call_tool("get_dashboard_metrics", args, state) do
timeframe = Map.get(args, "timeframe", "last_7_days")
metrics = MyApp.Analytics.get_metrics(timeframe)
result = [%{
type: "text",
text: """
📊 Dashboard Metrics (#{timeframe}):
👥 Active Users: #{metrics.active_users}
📈 Page Views: #{metrics.page_views}
💰 Revenue: $#{metrics.revenue}
📊 Conversion Rate: #{metrics.conversion_rate}%
"""
}]
{:ok, result, state}
end
```
## Troubleshooting
### Common Issues
1. **CORS Errors**: Ensure `cors_enabled: true` in your HttpPlug configuration
2. **Authentication Issues**: Verify your authentication pipeline is working correctly
3. **SSE Connection Drops**: Check your load balancer timeout settings
4. **JSON Parsing Errors**: Validate your tool arguments schema
### Debug Mode
Enable debug logging to troubleshoot issues:
```elixir
# In config/dev.exs
config :logger, level: :debug
# In your handler:
require Logger
@impl true
def handle_call_tool(tool_name, args, state) do
Logger.debug("MCP tool call: #{tool_name} with args: #{inspect(args)}")
result = do_call_tool(tool_name, args, state)
Logger.debug("MCP tool result: #{inspect(result)}")
result
end
```
## Next Steps
1. **Read the [ExMCP Documentation](https://hexdocs.pm/ex_mcp)** for complete API reference
2. **Check out [Examples](https://github.com/azmaveth/ex_mcp/tree/master/examples)** for more implementation patterns
3. **Review [Security Guide](SECURITY.md)** for production deployment best practices
4. **Join the Community** - contribute to the project or ask questions in Issues
---
**Ready to make your Phoenix app AI-ready?** Start with the Quick Setup above and begin exposing your application's capabilities to AI models through the standardized MCP protocol.