使用说明
Dynamic MCP SSE Server
One Endpoint, Infinite Toolkits
A dynamic MCP (Model Context Protocol) SSE Server built with Spring Boot.
Different clients get different tool sets from the same endpoint — just change the URL parameter.
Quick Start • Architecture • API Reference • Production Deployment • Comparison
English | 中文
Client A → GET /sse?id=weather → tools: [weather_query, weather_forecast]
Client B → GET /sse?id=translate → tools: [text_translate, language_detect]
Client C → GET /sse?id=my_toolkit → tools: [your_tool_1, your_tool_2, ...]
第一个开源 Java/Spring引导 实现每会话动态MCP工具路由。 受到百度动态MCP方法的启发。
问题
今天的MCP生态系统是 默认情况下为静态每个AI客户端(Claude Desktop、Cursor、Windsurf……)都要求您在配置文件中硬编码每个MCP服务器:
{
"mcpServers": {
"weather": { "command": "npx", "args": ["weather-mcp-server"] },
"translate": { "command": "npx", "args": ["translate-mcp-server"] },
"search": { "command": "npx", "args": ["search-mcp-server"] }
}
}
这造成了真正的问题:
- 每个工具包=一个单独的服务器进程(资源浪费)
- 添加新工具包意味着编辑配置+重新启动客户端
- 无运行时工具发布——为每个新工具部署一个新服务器
- 没有每个用户/每个租户的工具隔离
- 没有集中的工具管理或治理
解决方案
动态MCP SSE服务器 将N台静态服务器替换为 一个动态服务器:
核心功能
| 特性 | 描述 |
|---|---|
| 基于URL的工具包路由 | GET /sse?id=weather 归还气象工具; ?id=translate 返回翻译工具。相同的端点,不同的工具。 |
| 运行时工具发布 | 通过REST API添加/删除工具-无需重新启动,无需重新部署。 |
| 实时更改通知 | 已连接的客户端接收 notifications/tools/list_changed 工具更新时立即生效。 |
| 工具包隔离 | 每个SSE会话都绑定到一个工具包。A. translate 会话无法调用 weather_query --服务器强制执行此操作。 |
| 可插拔存储 | ToolkitRegistry 是一个接口。默认值:内存中。在PostgreSQL、MySQL、Redis或任何后端进行交换。 |
| 零外部依赖 | 基本实现只需要Spring Boot。没有数据库,没有消息队列,没有Redis。 |
| 符合MCP协议 | 全面实施MCP SSE传输: initialize, tools/list, tools/call, ping,更改通知。 |
快速开始
先决条件
- JDK 17+
- Maven 3.6+
1.启动服务器
cd examples/dynamic-mcp-server
mvn spring-boot:run
服务器启动于 http://localhost:8080 有两个内置的示例工具包:
| 工具包ID | 工具 |
|---|---|
weather |
weather_query --查询一个城市的当前天气 |
weather_forecast --获取多日天气预报 |
|
translate |
text_translate --在语言之间翻译文本 |
language_detect --检测文本语言 |
2.从AI客户端连接
克劳德桌面 (claude_desktop_config.json):
{
"mcpServers": {
"my-weather-tools": {
"url": "http://localhost:8080/sse?id=weather"
}
}
}
光标 (.cursor/mcp.json):
{
"mcpServers": {
"my-translate-tools": {
"url": "http://localhost:8080/sse?id=translate"
}
}
}
同一台服务器,不同 id --每个客户端看到的工具完全不同。
3.使用curl进行验证
# Step 1: Check available toolkits
curl http://localhost:8080/api/toolkits
# → ["weather","translate"]
# Step 2: Open SSE connection (keep it running in a terminal)
curl -N "http://localhost:8080/sse?id=weather"
# → event:endpoint
# → data:/mcp/message?sessionId=a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Step 3: In another terminal, send MCP initialize
curl -X POST "http://localhost:8080/mcp/message?sessionId=<YOUR_SESSION_ID>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "curl-test", "version": "1.0"}
}
}'
# Step 4: Request tools list
curl -X POST "http://localhost:8080/mcp/message?sessionId=<YOUR_SESSION_ID>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# → SSE stream receives: weather_query + weather_forecast (NOT translate tools)
# Step 5: Call a tool
curl -X POST "http://localhost:8080/mcp/message?sessionId=<YOUR_SESSION_ID>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {"name": "weather_query", "arguments": {"city": "Beijing"}}
}'
# → "Weather in Beijing: Sunny, 25°C, humidity 40%"
4.运行时发布工具
# Create a new toolkit
curl -X POST http://localhost:8080/api/toolkits \
-H "Content-Type: application/json" \
-d '{"id":"search","name":"Search Toolkit","description":"Web search tools"}'
# Add a tool
curl -X POST http://localhost:8080/api/toolkits/search/tools \
-H "Content-Type: application/json" \
-d '{
"name": "web_search",
"description": "Search the web for information",
"inputSchema": "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query\"}},\"required\":[\"query\"]}"
}'
# Instantly available: GET /sse?id=search now returns web_search
# Already-connected clients receive tools/list_changed notification
建筑
请求流
动态路由的工作原理
魔法发生在 3条逻辑线:
1. GET /sse?id=weather → Session created, bound to toolkitId="weather"
2. POST tools/list → Lookup session → get toolkitId → return ONLY weather tools
3. POST tools/call → Lookup session → verify tool belongs to toolkit → execute or reject
没有复杂的路由规则。没有中间件链。只是: session会记住它的工具包,每个请求都会通过它进行过滤。
工具包隔离
实时更改通知
项目结构
src/main/java/com/alibaba/cloud/ai/examples/dynamicmcp/
│
├── DynamicMcpServerApplication.java # Spring Boot entry point
│
├── config/
│ └── SampleToolkitInitializer.java # Registers demo toolkits on startup
│ # (weather + translate)
│
├── registry/ # === Storage Layer ===
│ ├── ToolkitDefinition.java # Data model: Toolkit → List<ToolInfo>
│ │ # ToolInfo = name + description + schema + handler
│ ├── ToolkitRegistry.java # Interface: register/get/add/remove + change listener
│ │ # (implement this for DB-backed storage)
│ └── InMemoryToolkitRegistry.java # Default: ConcurrentHashMap-based implementation
│
├── session/ # === Session Layer ===
│ └── McpSessionManager.java # Maps sessionId → (toolkitId, SseEmitter)
│ # Creates, lookups, and cleans up sessions
│
├── protocol/ # === Protocol Layer ===
│ └── McpProtocolHandler.java # JSON-RPC dispatcher:
│ # initialize → server capabilities
│ # tools/list → filtered by session's toolkit
│ # tools/call → validated + executed
│ # ping → pong
│
└── controller/ # === Transport Layer ===
├── McpSseController.java # MCP SSE transport endpoints:
│ # GET /sse?id={toolkitId} → SSE stream
│ # POST /mcp/message?sessionId={id} → JSON-RPC
│
└── ToolkitAdminController.java # Management REST API:
# CRUD toolkits and tools at runtime
API 参考
MCP传输端点
GET /sse?id={toolkitId}
建立绑定到特定工具包的SSE(服务器发送事件)连接。
参数:
| 名称 | 类型 | 必填 | 描述 |
|---|---|---|---|
id |
string | 是 | 工具包标识符(例如。, weather, translate) |
答复: SSE事件流。第一个事件:
event: endpoint
data: /mcp/message?sessionId=a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
错误: 如果工具包返回SSE错误 id 不存在。
POST /mcp/message?sessionId={sessionId}
向MCP服务器发送JSON-RPC消息。响应通过SSE流传递。
参数:
| 名称 | 类型 | 必填 | 描述 |
|---|---|---|---|
sessionId |
string | 是 | 会话ID来自 endpoint 事件 |
请求正文: JSON-RPC 2.0消息
支持的方法:
| 方法 | 描述 | 响应 |
|---|---|---|
initialize |
MCP握手 | 服务器信息+功能 |
notifications/initialized |
客户端就绪信号 | (无响应--通知) |
tools/list |
列出可用工具 | 仅此会话工具包中的工具 |
tools/call |
执行工具 | 工具结果或错误 |
ping |
健康检查 | 结果为空 {} |
tools/list 响应示例 (为 ?id=weather 会话):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "weather_query",
"description": "Query current weather for a given city",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name, e.g. Beijing" }
},
"required": ["city"]
}
},
{
"name": "weather_forecast",
"description": "Get 3-day weather forecast for a given city",
"inputSchema": { "..." }
}
]
}
}
tools/call 错误示例 (从错误的工具包调用工具):
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Tool 'weather_query' not found in toolkit 'translate'"
}
}
工具包管理API
GET /api/toolkits
列出所有已注册的工具包ID。
curl http://localhost:8080/api/toolkits
# → ["weather", "translate"]
GET /api/toolkits/{id}
获取包括所有工具在内的工具包详细信息。
curl http://localhost:8080/api/toolkits/weather
{
"id": "weather",
"name": "Weather Toolkit",
"description": "Weather query and forecast tools",
"tools": [
{
"name": "weather_query",
"description": "Query current weather for a given city",
"inputSchema": "{ ... }"
},
{
"name": "weather_forecast",
"description": "Get 3-day weather forecast for a given city",
"inputSchema": "{ ... }"
}
]
}
POST /api/toolkits
创建一个新的工具包。
curl -X POST http://localhost:8080/api/toolkits \
-H "Content-Type: application/json" \
-d '{
"id": "search",
"name": "Search Toolkit",
"description": "Web search and indexing tools"
}'
# → {"id": "search", "status": "created"}
POST /api/toolkits/{id}/tools
将工具添加到现有工具包中。 触发器 tools/list_changed 通知 此工具包的所有连接会话。
curl -X POST http://localhost:8080/api/toolkits/search/tools \
-H "Content-Type: application/json" \
-d '{
"name": "web_search",
"description": "Search the web for information",
"inputSchema": "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"]}"
}'
# → {"toolkit": "search", "tool": "web_search", "status": "added"}
DELETE /api/toolkits/{id}/tools/{toolName}
从工具箱中删除工具。触发更改通知。
curl -X DELETE http://localhost:8080/api/toolkits/search/tools/web_search
# → 204 No Content
DELETE /api/toolkits/{id}
删除整个工具包。
curl -X DELETE http://localhost:8080/api/toolkits/search
# → 204 No Content
生产部署
默认值 InMemoryToolkitRegistry 非常适合演示和开发。对于生产,您需要持久存储、身份验证和横向扩展。这是完整的设计。
数据库模式(PostgreSQL)
-- ============================================================
-- Table: mcp_toolkit
-- Stores toolkit definitions (each toolkit = a group of tools)
-- ============================================================
CREATE TABLE mcp_toolkit (
id VARCHAR(64) PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
status SMALLINT DEFAULT 1, -- 0=disabled, 1=enabled
created_by VARCHAR(128), -- creator identifier
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
COMMENT ON TABLE mcp_toolkit IS 'MCP toolkit definitions - each toolkit groups related tools';
COMMENT ON COLUMN mcp_toolkit.status IS '0=disabled (hidden from SSE), 1=enabled';
-- ============================================================
-- Table: mcp_tool
-- Individual tools within a toolkit
-- ============================================================
CREATE TABLE mcp_tool (
id BIGSERIAL PRIMARY KEY,
toolkit_id VARCHAR(64) NOT NULL REFERENCES mcp_toolkit(id) ON DELETE CASCADE,
name VARCHAR(128) NOT NULL,
description TEXT NOT NULL,
input_schema JSONB NOT NULL, -- JSON Schema for tool parameters
endpoint_url VARCHAR(512), -- Backend service URL (proxy mode)
endpoint_type VARCHAR(32) DEFAULT 'internal', -- internal / http / grpc / mcp-bridge
config JSONB, -- Extra config: headers, timeout, retry, etc.
status SMALLINT DEFAULT 1, -- 0=disabled, 1=enabled
sort_order INT DEFAULT 0, -- Display order within toolkit
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (toolkit_id, name)
);
CREATE INDEX idx_mcp_tool_toolkit ON mcp_tool(toolkit_id, status);
COMMENT ON COLUMN mcp_tool.endpoint_type IS
'internal = handler in JVM; http = proxy to REST API; grpc = proxy to gRPC; mcp-bridge = proxy to another MCP server';
COMMENT ON COLUMN mcp_tool.config IS
'JSON: {"headers": {...}, "timeout_ms": 5000, "retry": 2, "auth": "bearer:xxx"}';
-- ============================================================
-- Table: mcp_access
-- API key based access control and toolkit authorization
-- ============================================================
CREATE TABLE mcp_access (
id BIGSERIAL PRIMARY KEY,
api_key VARCHAR(128) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL, -- Human-readable name (e.g., "Team Alpha Key")
toolkit_ids TEXT[] NOT NULL, -- Allowed toolkit IDs
rate_limit INT DEFAULT 100, -- Max requests per minute
expires_at TIMESTAMP, -- NULL = never expires
status SMALLINT DEFAULT 1, -- 0=revoked, 1=active
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_mcp_access_key ON mcp_access(api_key) WHERE status = 1;
COMMENT ON TABLE mcp_access IS
'API key authentication: each key grants access to specific toolkits';
-- ============================================================
-- Table: mcp_tool_call_log (Optional — Observability)
-- Audit log for tool invocations
-- ============================================================
CREATE TABLE mcp_tool_call_log (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
toolkit_id VARCHAR(64) NOT NULL,
tool_name VARCHAR(128) NOT NULL,
arguments JSONB,
result_text TEXT,
is_error BOOLEAN DEFAULT FALSE,
duration_ms INT,
called_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_tool_call_log_time ON mcp_tool_call_log(called_at DESC);
CREATE INDEX idx_tool_call_log_toolkit ON mcp_tool_call_log(toolkit_id, called_at DESC);
COMMENT ON TABLE mcp_tool_call_log IS
'Audit trail: who called what tool, when, with what arguments, and what happened';
MySQL version (click to expand)
CREATE TABLE mcp_toolkit (
id VARCHAR(64) PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
status TINYINT DEFAULT 1,
created_by VARCHAR(128),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE mcp_tool (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
toolkit_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
description TEXT NOT NULL,
input_schema JSON NOT NULL,
endpoint_url VARCHAR(512),
endpoint_type VARCHAR(32) DEFAULT 'internal',
config JSON,
status TINYINT DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_toolkit_tool (toolkit_id, name),
KEY idx_toolkit_status (toolkit_id, status),
FOREIGN KEY (toolkit_id) REFERENCES mcp_toolkit(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE mcp_access (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
api_key VARCHAR(128) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL,
toolkit_ids JSON NOT NULL, -- MySQL doesn't support TEXT[], use JSON array
rate_limit INT DEFAULT 100,
expires_at DATETIME,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE mcp_tool_call_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
toolkit_id VARCHAR(64) NOT NULL,
tool_name VARCHAR(128) NOT NULL,
arguments JSON,
result_text TEXT,
is_error TINYINT(1) DEFAULT 0,
duration_ms INT,
called_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_call_log_time (called_at DESC),
KEY idx_call_log_toolkit (toolkit_id, called_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实现结构
关键实施点
1.JdbcToolkitRegistry
替换 InMemoryToolkitRegistry 使用数据库支持的实现:
@Component
@Profile("production")
public class JdbcToolkitRegistry implements ToolkitRegistry {
// tools/list → SELECT * FROM mcp_tool WHERE toolkit_id = ? AND status = 1
// addTool → INSERT INTO mcp_tool (...) VALUES (...)
// then → NOTIFY tool_changed, 'weather' (PostgreSQL)
// or → redisTemplate.convertAndSend("tool_changed", "weather")
// ...
}
2.工具执行路由(代理模式)
这 endpoint_type 领域 mcp_tool 决定工具的执行方式:
endpoint_type = "internal"
→ Execute Java handler in JVM (same as demo mode)
endpoint_type = "http"
→ POST {endpoint_url}
Headers: from mcp_tool.config.headers
Body: tool arguments (JSON)
Response text → tool result
endpoint_type = "grpc"
→ gRPC call to {endpoint_url}
endpoint_type = "mcp-bridge"
→ Forward to another MCP server as tools/call
Enables chaining: Dynamic MCP → upstream MCP server
这意味着您可以注册实际上由以下支持的工具 任何现有的HTTP API --无需更改代码,只需在数据库中添加一行即可。
3.变更检测与通知
| 方法 | 最佳选择 | 方法 |
|---|---|---|
| PostgreSQL LISTEN/NOTIFY | 单实例或PG原生 | NOTIFY tool_changed, 'weather' 插入/更新后 |
| Redis发布/订阅 | 多实例横向扩展 | 发布到 tool_changed 频道 |
| 轮询 | 最简单,不需要基础设施 | 检查 updated_at > last_checked 每N秒 |
4.API密钥认证
添加a HandlerInterceptor 上 /sse:
GET /sse?id=weather&apiKey=sk-xxxx
│ │
│ ▼
│ SELECT toolkit_ids FROM mcp_access
│ WHERE api_key = 'sk-xxxx' AND status = 1
│ │
│ ▼
│ toolkit_ids contains 'weather'?
│ YES → proceed
│ NO → 403 Forbidden
▼
Create session
5.缓存策略
Request flow: Session → Redis Cache → PostgreSQL
tools/list: Redis GET toolkit:{id}:tools
├─ HIT → return cached
└─ MISS → SELECT FROM mcp_tool → cache with 60s TTL
tool change: Write to DB → invalidate Redis key → notify sessions
多实例水平缩放
上面的生产架构图显示了多实例部署模式:具有粘性会话的负载均衡器将客户端分布在实例A和实例B之间,Redis Pub/Sub在实例之间广播工具更改事件。
要点:
- 需要粘性会话 --SSE端点返回
sessionId,后续POST必须到达同一实例。使用负载平衡器cookie关联或会话感知路由。 - 刀具更换流程 --实例A写入DB+发布到Redis→ 实例B收到事件→ 这两个实例都会通知其本地会话。
比较
|特点| 动态MCP SSE服务器 |scitara cto/
动态mcp服务器|元工具ai/
微软/
mcp网关|
|---------|:---:|:---:|:---:|:---:|
| 语言 |Java/Spring Boot | TypeScript | TypeScript | C#/。NET|
| 路由机制 |URL参数 ?id=xxx |URL参数 ?apiKey=xxx |命名空间|工具名称路由|
| 运行时工具发布 |REST API | REST API |管理员界面| K8s CRD|
| 实时更改通知 |SSE推| SSE推|-|-|
| 会话级隔离 |每个工具包|每个用户|每个命名空间|每个服务器|
| 基础依赖关系 |无| MongoDB | PostgreSQL | Kubernetes|
| 数据库备份存储 |可插拔接口|仅限MongoDB | PostgreSQL |-|
| 代理模式 |设计(endpoint_type)|-|内置|内置|
| 复杂性 |轻量级(约9个文件)|中等|重型|重型|
| 生产就绪认证 |设计(API密钥)|内置|内置| K8s RBAC|
何时使用何种方法
- 这个项目 --您处于Java/Spring生态系统中,想要完全控制的轻量级内容,并且需要每个工具包的动态路由
- metamcp --您需要一个具有UI的全功能MCP聚合器,不介意TypeScript,并希望将许多现有的MCP服务器合并到一个
- microsoft/mcp网关 --您使用的是Kubernetes,需要基于CRD管理的基础设施级MCP路由
- scitara动态mcp服务器 --你身处TypeScript生态系统,希望与MongoDB实现按用户工具隔离
技术栈
| 组件 | 技术 |
|---|---|
| 语言 | Java 17 |
| 框架 | Spring Boot 3.5 |
| 苏格兰和南方能源公司运输 | 春季 SseEmitter |
| MCP类型 | MCP SDK 0.14.0(McpSchema 仅) |
| 协议 | SSE上的JSON-RPC 2.0(自定义实现) |
| 存储 | 可插拔(ToolkitRegistry 接口) |
| 默认存储 | ConcurrentHashMap (记忆中) |
MCP SDK使用说明: 此项目不使用SDK的内置
McpServer或HttpServletSseServerTransportProvider--他们的tools/list是全局的,不能按会话进行筛选。相反,我们直接使用Spring实现MCP SSE传输协议SseEmitter,这使我们能够完全控制每个会话的工具路由。
贡献
欢迎投稿!一些特别有价值的领域:
- [ ]
JdbcToolkitRegistry--PostgreSQL与MySQL的实现ToolkitRegistry - []API密钥身份验证拦截器
- []HTTP代理工具执行器(用于
endpoint_type = "http") - []Redis缓存层用于工具定义
- []用于多实例变更通知的Redis发布/订阅
- []用于工具包管理的管理web UI
- []MCP网桥执行器(转发到上游MCP服务器)
- []度量和可观察性(千分尺/开放遥测)
- []每个API键的速率限制
- []可流式HTTP传输(除SSE之外)