Content
<p align="center">
<h1 align="center">Dynamic MCP SSE Server</h1>
<p align="center">
<strong>One Endpoint, Infinite Toolkits</strong>
<br />
A dynamic <a href="https://modelcontextprotocol.io/">MCP (Model Context Protocol)</a> SSE Server built with Spring Boot.
<br />
Different clients get different tool sets from the <strong>same endpoint</strong> — just change the URL parameter.
</p>
<p align="center">
<a href="#quick-start">Quick Start</a> •
<a href="#architecture">Architecture</a> •
<a href="#api-reference">API Reference</a> •
<a href="#production-deployment">Production Deployment</a> •
<a href="#comparison">Comparison</a>
</p>
<p align="center">
English | <a href="README_CN.md">中文</a>
</p>
</p>
---
```
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, ...]
```
> First open-source **Java / Spring Boot** implementation of per-session dynamic MCP tool routing.
> Inspired by Baidu's dynamic MCP approach.
---
## The Problem
Today's MCP ecosystem is **static by default**. Every AI client (Claude Desktop, Cursor, Windsurf...) requires you to hardcode each MCP Server in a config file:
```json
{
"mcpServers": {
"weather": { "command": "npx", "args": ["weather-mcp-server"] },
"translate": { "command": "npx", "args": ["translate-mcp-server"] },
"search": { "command": "npx", "args": ["search-mcp-server"] }
}
}
```
**This creates real problems:**
- Every toolkit = a separate server process (resource waste)
- Adding a new toolkit means editing config + restarting the client
- No runtime tool publishing — deploy a new server for every new tool
- No per-user / per-tenant tool isolation
- No centralized tool management or governance
## The Solution
**Dynamic MCP SSE Server** replaces N static servers with **one dynamic server**:
<p align="center">
<img src="docs/images/01-static-vs-dynamic.png" alt="Static vs Dynamic MCP" width="800" />
</p>
### Core Features
| Feature | Description |
|---------|-------------|
| **URL-based toolkit routing** | `GET /sse?id=weather` returns weather tools; `?id=translate` returns translate tools. Same endpoint, different tools. |
| **Runtime tool publishing** | Add/remove tools via REST API — no restart, no redeploy. |
| **Live change notifications** | Connected clients receive `notifications/tools/list_changed` instantly when tools are updated. |
| **Toolkit isolation** | Each SSE session is bound to one toolkit. A `translate` session cannot call `weather_query` — the server enforces this. |
| **Pluggable storage** | `ToolkitRegistry` is an interface. Default: in-memory. Swap in PostgreSQL, MySQL, Redis, or any backend. |
| **Zero external dependencies** | The base implementation needs nothing beyond Spring Boot. No database, no message queue, no Redis. |
| **MCP protocol compliant** | Full implementation of MCP SSE transport: `initialize`, `tools/list`, `tools/call`, `ping`, change notifications. |
---
## Quick Start
### Prerequisites
- JDK 17+
- Maven 3.6+
### 1. Start the Server
```bash
cd examples/dynamic-mcp-server
mvn spring-boot:run
```
Server starts on `http://localhost:8080` with two built-in sample toolkits:
| Toolkit ID | Tools |
|-----------|-------|
| `weather` | `weather_query` — Query current weather for a city |
| | `weather_forecast` — Get multi-day weather forecast |
| `translate` | `text_translate` — Translate text between languages |
| | `language_detect` — Detect text language |
### 2. Connect from AI Clients
**Claude Desktop** (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"my-weather-tools": {
"url": "http://localhost:8080/sse?id=weather"
}
}
}
```
**Cursor** (`.cursor/mcp.json`):
```json
{
"mcpServers": {
"my-translate-tools": {
"url": "http://localhost:8080/sse?id=translate"
}
}
}
```
Same server, different `id` — each client sees completely different tools.
### 3. Verify with curl
```bash
# 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. Publish Tools at Runtime
```bash
# 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
```
---
## Architecture
### Request Flow
<p align="center">
<img src="docs/images/02-architecture.png" alt="Architecture & Request Flow" width="800" />
</p>
### How Dynamic Routing Works
The magic happens in **3 lines of logic**:
```
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
```
No complex routing rules. No middleware chains. Just: **session remembers its toolkit, every request is filtered through it.**
### Toolkit Isolation
<p align="center">
<img src="docs/images/03-toolkit-isolation.png" alt="Toolkit Isolation" width="800" />
</p>
### Live Change Notifications
<p align="center">
<img src="docs/images/04-live-notification.png" alt="Live Change Notifications" width="800" />
</p>
### Project Structure
```
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 Reference
### MCP Transport Endpoints
#### `GET /sse?id={toolkitId}`
Establish an SSE (Server-Sent Events) connection bound to a specific toolkit.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | string | Yes | Toolkit identifier (e.g., `weather`, `translate`) |
**Response:** SSE event stream. First event:
```
event: endpoint
data: /mcp/message?sessionId=a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
**Error:** Returns SSE error if toolkit `id` doesn't exist.
---
#### `POST /mcp/message?sessionId={sessionId}`
Send JSON-RPC messages to the MCP server. Responses are delivered via the SSE stream.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `sessionId` | string | Yes | Session ID from the `endpoint` event |
**Request body:** JSON-RPC 2.0 message
**Supported methods:**
| Method | Description | Response |
|--------|-------------|----------|
| `initialize` | MCP handshake | Server info + capabilities |
| `notifications/initialized` | Client ready signal | *(no response — notification)* |
| `tools/list` | List available tools | **Only tools in this session's toolkit** |
| `tools/call` | Execute a tool | Tool result or error |
| `ping` | Health check | Empty result `{}` |
**`tools/list` response example** (for `?id=weather` session):
```json
{
"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` error example** (calling a tool from wrong toolkit):
```json
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Tool 'weather_query' not found in toolkit 'translate'"
}
}
```
---
### Toolkit Management API
#### `GET /api/toolkits`
List all registered toolkit IDs.
```bash
curl http://localhost:8080/api/toolkits
# → ["weather", "translate"]
```
#### `GET /api/toolkits/{id}`
Get toolkit details including all tools.
```bash
curl http://localhost:8080/api/toolkits/weather
```
```json
{
"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`
Create a new toolkit.
```bash
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`
Add a tool to an existing toolkit. **Triggers `tools/list_changed` notification** to all connected sessions of this toolkit.
```bash
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}`
Remove a tool from a toolkit. Triggers change notification.
```bash
curl -X DELETE http://localhost:8080/api/toolkits/search/tools/web_search
# → 204 No Content
```
#### `DELETE /api/toolkits/{id}`
Delete an entire toolkit.
```bash
curl -X DELETE http://localhost:8080/api/toolkits/search
# → 204 No Content
```
---
## Production Deployment
The default `InMemoryToolkitRegistry` is great for demos and development. For production, you need persistent storage, authentication, and horizontal scaling. Here's the complete design.
### Database Schema (PostgreSQL)
```sql
-- ============================================================
-- 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';
```
<details>
<summary><strong>MySQL version</strong> (click to expand)</summary>
```sql
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;
```
</details>
### Implementation Architecture
<p align="center">
<img src="docs/images/05-production-architecture.png" alt="Production Deployment Architecture" width="800" />
</p>
### Key Implementation Points
#### 1. JdbcToolkitRegistry
Replace `InMemoryToolkitRegistry` with a database-backed implementation:
```java
@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. Tool Execution Routing (Proxy Mode)
The `endpoint_type` field in `mcp_tool` determines how tools are executed:
```
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
```
This means you can register tools that are actually backed by **any existing HTTP API** — no code changes needed, just add a row to the database.
#### 3. Change Detection & Notification
| Approach | Best For | How |
|----------|----------|-----|
| PostgreSQL LISTEN/NOTIFY | Single-instance or PG-native | `NOTIFY tool_changed, 'weather'` after INSERT/UPDATE |
| Redis Pub/Sub | Multi-instance horizontal scaling | Publish to `tool_changed` channel |
| Polling | Simplest, no infra needed | Check `updated_at > last_checked` every N seconds |
#### 4. API Key Authentication
Add a `HandlerInterceptor` on `/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. Caching Strategy
```
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
```
### Multi-Instance Horizontal Scaling
The production architecture diagram above shows the multi-instance deployment pattern: Load Balancer with sticky sessions distributes clients across Instance A and Instance B, with Redis Pub/Sub broadcasting tool change events between instances.
**Key points:**
- **Sticky sessions required** — SSE endpoint returns `sessionId`, subsequent POSTs must reach the same instance. Use load balancer cookie affinity or session-aware routing.
- **Tool change flow** — Instance A writes to DB + publishes to Redis → Instance B receives the event → Both instances notify their local sessions.
---
## Comparison
| Feature | **Dynamic MCP SSE Server** | scitara-cto/<br>dynamic-mcp-server | metatool-ai/<br>metamcp | microsoft/<br>mcp-gateway |
|---------|:---:|:---:|:---:|:---:|
| **Language** | Java / Spring Boot | TypeScript | TypeScript | C# / .NET |
| **Routing mechanism** | URL param `?id=xxx` | URL param `?apiKey=xxx` | Namespace | Tool-name routing |
| **Runtime tool publish** | REST API | REST API | Admin UI | K8s CRD |
| **Live change notification** | SSE push | SSE push | - | - |
| **Session-level isolation** | Per-toolkit | Per-user | Per-namespace | Per-server |
| **Base dependencies** | None | MongoDB | PostgreSQL | Kubernetes |
| **DB-backed storage** | Pluggable interface | MongoDB only | PostgreSQL | - |
| **Proxy mode** | Designed (endpoint_type) | - | Built-in | Built-in |
| **Complexity** | Lightweight (~9 files) | Medium | Heavy | Heavy |
| **Production-ready auth** | Designed (API key) | Built-in | Built-in | K8s RBAC |
### When to Use What
- **This project** — You're in the Java/Spring ecosystem, want something lightweight that you fully control, and need per-toolkit dynamic routing
- **metamcp** — You need a full-featured MCP aggregator with UI, don't mind TypeScript, and want to merge many existing MCP servers into one
- **microsoft/mcp-gateway** — You're on Kubernetes and need infrastructure-level MCP routing with CRD-based management
- **scitara dynamic-mcp-server** — You're in the TypeScript ecosystem and want per-user tool isolation with MongoDB
---
## Tech Stack
| Component | Technology |
|-----------|------------|
| Language | Java 17 |
| Framework | Spring Boot 3.5 |
| SSE Transport | Spring `SseEmitter` |
| MCP Types | MCP SDK 0.14.0 (`McpSchema` only) |
| Protocol | JSON-RPC 2.0 over SSE (custom implementation) |
| Storage | Pluggable (`ToolkitRegistry` interface) |
| Default Storage | `ConcurrentHashMap` (in-memory) |
> **Note on MCP SDK usage:** This project does NOT use the SDK's built-in `McpServer` or `HttpServletSseServerTransportProvider` — their `tools/list` is global and cannot be filtered per session. Instead, we implement the MCP SSE transport protocol directly with Spring's `SseEmitter`, which gives us full control over per-session tool routing.
---
## Contributing
Contributions are welcome! Some areas that would be especially valuable:
- [ ] `JdbcToolkitRegistry` — PostgreSQL/MySQL implementation of `ToolkitRegistry`
- [ ] API key authentication interceptor
- [ ] HTTP proxy tool executor (for `endpoint_type = "http"`)
- [ ] Redis cache layer for tool definitions
- [ ] Redis Pub/Sub for multi-instance change notification
- [ ] Admin web UI for toolkit management
- [ ] MCP bridge executor (forward to upstream MCP servers)
- [ ] Metrics and observability (Micrometer/OpenTelemetry)
- [ ] Rate limiting per API key
- [ ] Streamable HTTP transport (in addition to SSE)
---
## License
[MIT License](LICENSE)
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
servers
Model Context Protocol Servers
servers
Model Context Protocol Servers
Time
A Model Context Protocol server for time and timezone conversions.