此库提供了一个简单的基于服务器发送事件(SSE)的模型上下文协议(MCP)实现。
有关模型上下文协议的更多信息,请访问: 模型上下文协议文档。
您必须实现MCPServer行为。
您只需要实现所需的回调函数(handle_ping/1和handle_initialize/2),以及任何您想要支持的功能的可选回调函数。
use MCPServer宏提供了:
参见DefaultServer以获取MCPServer行为的默认实现。
config/config.exs中添加所需配置:# 配置SSE的MIME类型
config :mime, :types, %{
"text/event-stream" => ["sse"]
}
# 配置MCP服务器
config :mcp_sse, :mcp_server, YourApp.YourMCPServer
mix.exs中添加依赖项:def deps do
[
{:mcp_sse, "~> 0.1.6"}
]
end
lib/your_app_web/router.ex):pipeline :sse do
plug :accepts, ["sse"]
end
scope "/" do
pipe_through :sse
get "/sse", SSE.ConnectionPlug, :call
post "/message", SSE.ConnectionPlug, :call
end
mix phx.server
mix new your_app --sup
config/config.exs中添加所需配置:import Config
# 配置SSE的MIME类型
config :mime, :types, %{
"text/event-stream" => ["sse"]
}
# 配置MCP服务器
config :mcp_sse, :mcp_server, YourApp.YourMCPServer
mix.exs中添加依赖项:def deps do
[
{:mcp_sse, "~> 0.1.6"},
{:plug, "~> 1.14"},
{:bandit, "~> 1.2"}
]
end
lib/your_app/router.ex):defmodule YourApp.Router do
use Plug.Router
plug Plug.Parsers,
parsers: [:urlencoded, :json],
pass: ["text/*"],
json_decoder: JSON
plug :match
plug :ensure_session_id
plug :dispatch
# 中间件确保会话ID存在
def ensure_session_id(conn, _opts) do
case get_session_id(conn) do
nil ->
# 如果没有会话ID,则生成新的会话ID
session_id = generate_session_id()
%{conn | query_params: Map.put(conn.query_params, "sessionId", session_id)}
_session_id ->
conn
end
end
# 辅助函数从查询参数中获取会话ID
defp get_session_id(conn) do
conn.query_params["sessionId"]
end
# 生成唯一的会话ID
defp generate_session_id do
Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
end
forward "/sse", to: SSE.ConnectionPlug
forward "/message", to: SSE.ConnectionPlug
match _ do
send_resp(conn, 404, "Not found")
end
end
lib/your_app/application.ex):defmodule YourApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
{Bandit, plug: YourApp.Router, port: 4000}
]
opts = [strategy: :one_for_one, name: YourApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
mix run --no-halt
MCP_SERVER_URL=localhost:4000 npx @modelcontextprotocol/inspector@latest
~/.cursor/mcp.json:{
"mcpServers": {
"your-mcp-server": {
"url": "http://localhost:4000/sse"
}
}
}
Bandit服务器可以在您的应用程序模块中通过额外选项进行配置:
# 示例:自定义端口和HTTPS
children = [
{Bandit,
plug: YourApp.Router,
port: System.get_env("PORT", "4000") |> String.to_integer(),
scheme: :https,
certfile: "priv/cert/selfsigned.pem",
keyfile: "priv/cert/selfsigned_key.pem"
}
]
您可以自定义用于SSE和消息端点的路径:
config :mcp_sse,
sse_path: "/mcp/sse", # 默认值:"/sse"
message_path: "/mcp/msg" # 默认值:"/message"
这允许您在路由器中使用自定义路径:
# Phoenix
scope "/mcp" do
pipe_through :sse
get "/sse", SSE.ConnectionPlug, :call
post "/msg", SSE.ConnectionPlug, :call
end
# Plug
forward "/mcp/sse", to: SSE.ConnectionPlug
forward "/mcp/msg", to: SSE.ConnectionPlug
SSE连接会定期发送心跳ping以防止连接超时。
您可以在config/config.exs中配置ping间隔或完全禁用它:
# 设置自定义ping间隔(以毫秒为单位)
config :mcp_sse, :sse_keepalive_timeout, 30_000 # 30秒
# 或者完全禁用ping
config :mcp_sse, :sse_keepalive_timeout, :infinity
要查看MCP服务器的实际操作:
# 我们的示例服务器
elixir dev/example_server.exs
# 您的Phoenix应用程序
mix phx.server
# 您的Plug应用程序
mix run --no-halt
elixir dev/example_client.exs
客户端脚本将:
这提供了模型上下文协议流程和服务器能力的实际演示。
// 连接到SSE端点
const sse = new EventSource('/sse');
// 处理端点消息
sse.addEventListener('endpoint', (e) => {
const messageEndpoint = e.data;
// 使用messageEndpoint进行后续的JSON-RPC请求
});
// 发送初始化请求
fetch('/message?sessionId=YOUR_SESSION_ID', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {}
}
})
});
MCP SSE服务器需要每个连接的会话ID。路由器自动:
/sse和/message端点的所有请求都有有效的会话ID在您的MCP服务器中实现工具响应时,内容必须遵循MCP规范的内容类型。 响应内容应格式化为以下类型之一:
# 文本内容
{:ok,
%{
jsonrpc: "2.0",
id: request_id,
result: %{
content: [
%{
type: "text",
text: "您的文本响应在这里"
}
]
}
}}
# 图像内容
{:ok,
%{
jsonrpc: "2.0",
id: request_id,
result: %{
content: [
%{
type: "image",
data: "base64_encoded_image_data",
mimeType: "image/png"
}
]
}
}}
# 资源引用
{:ok,
%{
jsonrpc: "2.0",
id: request_id,
result: %{
content: [
%{
type: "resource",
resource: %{
name: "resource_name",
description: "资源描述"
}
}
]
}
}}
对于如JSON这样的结构化数据,您应该将其转换为格式化的字符串:
def handle_call_tool(request_id, %{"name" => "list_companies"} = _params) do
companies = fetch_companies() # 您的数据获取逻辑
{:ok,
%{
jsonrpc: "2.0",
id: request_id,
result: %{
content: [
%{
type: "text",
text: JSON.encode!(companies, pretty: true)
}
]
}
}}
end
有关响应格式化的更多细节,请参阅MCP内容类型规范。