@elizaos/plugin-mcp 2.0.0-beta.1 → 2.0.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -226
- package/dist/cjs/index.cjs +106 -28
- package/dist/cjs/index.js.map +11 -11
- package/dist/node/actions/mcp.d.ts.map +1 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +106 -28
- package/dist/node/index.js.map +11 -11
- package/dist/node/prompts.d.ts +8 -8
- package/dist/node/prompts.d.ts.map +1 -1
- package/dist/node/routes-mcp.d.ts.map +1 -1
- package/dist/node/service.d.ts +1 -0
- package/dist/node/service.d.ts.map +1 -1
- package/dist/node/types.d.ts +0 -4
- package/dist/node/types.d.ts.map +1 -1
- package/dist/node/utils/schemas.d.ts +0 -4
- package/dist/node/utils/schemas.d.ts.map +1 -1
- package/dist/node/utils/selection.d.ts.map +1 -1
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,201 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @elizaos/plugin-mcp
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
elizaOS plugin that connects an Eliza agent to external [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers and exposes their tools and resources as agent capabilities.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The plugin starts `McpService`, which connects to one or more MCP servers (stdio, SSE, or streamable-HTTP), discovers their tools and resources, and surfaces them through a single `MCP` action and an `MCP` provider. It is consumed by an elizaOS agent: add it to the character `plugins` array and configure servers under `settings.mcp.servers`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Node-only. `index.browser.ts` is a browser-unavailable entry because the MCP SDK's stdio/SSE transports require Node APIs (`eliza.platforms` is `["node"]`).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
This plugin allows your elizaOS agents to access multiple MCP servers simultaneously, each providing different capabilities:
|
|
12
|
-
|
|
13
|
-
- **Resources**: Context and data for the agent to reference
|
|
14
|
-
- **Tools**: Functions for the agent to execute
|
|
15
|
-
|
|
16
|
-
## 📦 Installation
|
|
17
|
-
|
|
18
|
-
Install the plugin in your elizaOS project:
|
|
19
|
-
|
|
20
|
-
- **npm**
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @elizaos/plugin-mcp
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
- **yarn**
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
yarn add @elizaos/plugin-mcp
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
- **bun**
|
|
9
|
+
## Install
|
|
33
10
|
|
|
34
11
|
```bash
|
|
35
|
-
bun add @elizaos/plugin-mcp
|
|
12
|
+
bun add @elizaos/plugin-mcp # or: npm install / yarn add
|
|
36
13
|
```
|
|
37
14
|
|
|
38
|
-
##
|
|
15
|
+
## Usage
|
|
39
16
|
|
|
40
|
-
|
|
17
|
+
Add the plugin and declare servers in your character file:
|
|
41
18
|
|
|
42
19
|
```json
|
|
43
20
|
{
|
|
44
21
|
"name": "Your Character",
|
|
45
22
|
"plugins": ["@elizaos/plugin-mcp"],
|
|
46
|
-
"settings": {
|
|
47
|
-
"mcp": {
|
|
48
|
-
"servers": {
|
|
49
|
-
"github": {
|
|
50
|
-
"type": "stdio",
|
|
51
|
-
"command": "npx",
|
|
52
|
-
"args": ["-y", "@modelcontextprotocol/server-github"]
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## ⚙️ Configuration Options
|
|
61
|
-
|
|
62
|
-
MCP supports multiple transport types for connecting to servers. Each type has its own configuration options.
|
|
63
|
-
|
|
64
|
-
### Transport Types
|
|
65
|
-
|
|
66
|
-
- **`streamable-http`** or **`http`** - Streamable HTTP transport (recommended)
|
|
67
|
-
- **`sse`** - Server-Sent Events transport
|
|
68
|
-
- **`stdio`** - Process-based transport using standard input/output
|
|
69
|
-
|
|
70
|
-
### HTTP Transport Options (streamable-http, http, sse)
|
|
71
|
-
|
|
72
|
-
| Option | Type | Description |
|
|
73
|
-
| --------- | ------ | --------------------------------------------------- |
|
|
74
|
-
| `type` | string | Transport type: "streamable-http", "http", or "sse" |
|
|
75
|
-
| `url` | string | The URL of the HTTP/SSE endpoint |
|
|
76
|
-
| `timeout` | number | _Optional_ Timeout for connections |
|
|
77
|
-
|
|
78
|
-
### stdio Transport Options
|
|
79
|
-
|
|
80
|
-
| Option | Type | Description |
|
|
81
|
-
| ----------------- | -------- | ------------------------------------------------------ |
|
|
82
|
-
| `type` | string | Must be "stdio" |
|
|
83
|
-
| `command` | string | _Optional_ The command to run the MCP server |
|
|
84
|
-
| `args` | string[] | _Optional_ Command-line arguments for the server |
|
|
85
|
-
| `env` | object | _Optional_ Environment variables to pass to the server |
|
|
86
|
-
| `cwd` | string | _Optional_ Working directory to run the server in |
|
|
87
|
-
| `timeoutInMillis` | number | _Optional_ Timeout in milliseconds for tool calls |
|
|
88
|
-
|
|
89
|
-
### Example Configuration
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"mcp": {
|
|
94
|
-
"servers": {
|
|
95
|
-
"my-http-server": {
|
|
96
|
-
"type": "streamable-http",
|
|
97
|
-
"url": "https://example.com/mcp"
|
|
98
|
-
},
|
|
99
|
-
"my-local-server": {
|
|
100
|
-
"type": "http",
|
|
101
|
-
"url": "http://localhost:3000",
|
|
102
|
-
"timeout": 30
|
|
103
|
-
},
|
|
104
|
-
"my-sse-server": {
|
|
105
|
-
"type": "sse",
|
|
106
|
-
"url": "http://localhost:8080"
|
|
107
|
-
},
|
|
108
|
-
"my-stdio-server": {
|
|
109
|
-
"type": "stdio",
|
|
110
|
-
"command": "mcp-server",
|
|
111
|
-
"args": ["--config", "config.json"],
|
|
112
|
-
"cwd": "/path/to/server",
|
|
113
|
-
"timeoutInMillis": 60000
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
"maxRetries": 3
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 🛠️ Using MCP Capabilities
|
|
122
|
-
|
|
123
|
-
Once configured, the plugin automatically exposes MCP servers' capabilities to your agent:
|
|
124
|
-
|
|
125
|
-
### Context Provider
|
|
126
|
-
|
|
127
|
-
The plugin includes one provider that adds MCP capabilities to the agent's context:
|
|
128
|
-
|
|
129
|
-
1. **`MCP`**: Lists available servers and their tools and resources
|
|
130
|
-
|
|
131
|
-
### Actions
|
|
132
|
-
|
|
133
|
-
The plugin provides two actions for interacting with MCP servers:
|
|
134
|
-
|
|
135
|
-
1. **`CALL_MCP_TOOL`**: Executes tools from connected MCP servers
|
|
136
|
-
2. **`READ_MCP_RESOURCE`**: Accesses resources from connected MCP servers
|
|
137
|
-
|
|
138
|
-
## 🔄 Plugin Flow
|
|
139
|
-
|
|
140
|
-
The following diagram illustrates the MCP plugin's flow for tool selection and execution:
|
|
141
|
-
|
|
142
|
-
```mermaid
|
|
143
|
-
graph TD
|
|
144
|
-
%% Starting point - User request
|
|
145
|
-
start[User Request] --> action[CALL_MCP_TOOL Action]
|
|
146
|
-
|
|
147
|
-
%% MCP Server Validation
|
|
148
|
-
action --> check{MCP Servers Available?}
|
|
149
|
-
check -->|No| fail[Return No Tools Available]
|
|
150
|
-
|
|
151
|
-
%% Tool Selection Flow
|
|
152
|
-
check -->|Yes| state[Get MCP Provider Data]
|
|
153
|
-
state --> prompt[Create Tool Selection Prompt]
|
|
154
|
-
|
|
155
|
-
%% First Model Use - Tool Selection
|
|
156
|
-
prompt --> model1[Use Language Model for Tool Selection]
|
|
157
|
-
model1 --> parse[Parse Selection]
|
|
158
|
-
parse --> retry{Valid Selection?}
|
|
159
|
-
|
|
160
|
-
%% Second Model Use - Retry Selection
|
|
161
|
-
retry -->|No| feedback[Generate Feedback]
|
|
162
|
-
feedback --> model2[Use Language Model for Retry]
|
|
163
|
-
model2 --> parse
|
|
164
|
-
|
|
165
|
-
%% Tool Selection Result
|
|
166
|
-
retry -->|Yes| toolAvailable{Tool Available?}
|
|
167
|
-
toolAvailable -->|No| fallback[Fallback Response]
|
|
168
|
-
|
|
169
|
-
%% Tool Execution Flow
|
|
170
|
-
toolAvailable -->|Yes| callTool[Call MCP Tool]
|
|
171
|
-
callTool --> processResult[Process Tool Result]
|
|
172
|
-
|
|
173
|
-
%% Memory Creation
|
|
174
|
-
processResult --> createMemory[Create Memory Record]
|
|
175
|
-
createMemory --> reasoningPrompt[Create Reasoning Prompt]
|
|
176
|
-
|
|
177
|
-
%% Third Model Use - Response Generation
|
|
178
|
-
reasoningPrompt --> model3[Use Language Model for Response]
|
|
179
|
-
model3 --> respondToUser[Send Response to User]
|
|
180
|
-
|
|
181
|
-
%% Styling
|
|
182
|
-
classDef model fill:#f9f,stroke:#333,stroke-width:2px;
|
|
183
|
-
classDef decision fill:#bbf,stroke:#333,stroke-width:2px;
|
|
184
|
-
classDef output fill:#bfb,stroke:#333,stroke-width:2px;
|
|
185
|
-
|
|
186
|
-
class model1,model2,model3 model;
|
|
187
|
-
class check,retry,toolAvailable decision;
|
|
188
|
-
class respondToUser,fallback output;
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## 📋 Example: Setting Up Multiple MCP Servers
|
|
192
|
-
|
|
193
|
-
Here's a complete example configuration with multiple MCP servers of both types:
|
|
194
|
-
|
|
195
|
-
```json
|
|
196
|
-
{
|
|
197
|
-
"name": "Developer Assistant",
|
|
198
|
-
"plugins": ["@elizaos/plugin-mcp", "other-plugins"],
|
|
199
23
|
"settings": {
|
|
200
24
|
"mcp": {
|
|
201
25
|
"servers": {
|
|
@@ -203,22 +27,11 @@ Here's a complete example configuration with multiple MCP servers of both types:
|
|
|
203
27
|
"type": "stdio",
|
|
204
28
|
"command": "npx",
|
|
205
29
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
206
|
-
"env": {
|
|
207
|
-
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
|
208
|
-
}
|
|
30
|
+
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>" }
|
|
209
31
|
},
|
|
210
|
-
"
|
|
211
|
-
"type": "
|
|
212
|
-
"
|
|
213
|
-
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
|
|
214
|
-
},
|
|
215
|
-
"google-maps": {
|
|
216
|
-
"type": "stdio",
|
|
217
|
-
"command": "npx",
|
|
218
|
-
"args": ["-y", "@modelcontextprotocol/server-google-maps"],
|
|
219
|
-
"env": {
|
|
220
|
-
"GOOGLE_MAPS_API_KEY": "<YOUR_API_KEY>"
|
|
221
|
-
}
|
|
32
|
+
"my-http-server": {
|
|
33
|
+
"type": "streamable-http",
|
|
34
|
+
"url": "https://example.com/mcp"
|
|
222
35
|
}
|
|
223
36
|
},
|
|
224
37
|
"maxRetries": 2
|
|
@@ -227,43 +40,59 @@ Here's a complete example configuration with multiple MCP servers of both types:
|
|
|
227
40
|
}
|
|
228
41
|
```
|
|
229
42
|
|
|
230
|
-
|
|
43
|
+
Config lives entirely in `settings.mcp`, not in environment variables. The host `PATH` is forwarded to stdio child processes automatically. Every server config is validated by `@elizaos/security/mcp-server-config` (`validateMcpServerConfig`) before connect/spawn; configs that fail validation are skipped and logged at error level.
|
|
231
44
|
|
|
232
|
-
|
|
45
|
+
## Configuration
|
|
233
46
|
|
|
234
|
-
|
|
47
|
+
| Key | Type | Default | Description |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `mcp.servers` | `Record<string, McpServerConfig>` | — | Map of server name → transport config |
|
|
50
|
+
| `mcp.maxRetries` | `number` | `2` | Max reconnect attempts per server |
|
|
235
51
|
|
|
236
|
-
|
|
52
|
+
Transport config (see `src/types.ts`):
|
|
237
53
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
3. Review the logs for connection errors
|
|
241
|
-
4. Verify that the plugin is properly loaded in your character configuration
|
|
54
|
+
- **stdio** — `{ type: "stdio", command, args?, env?, cwd?, timeoutInMillis? }`
|
|
55
|
+
- **HTTP/SSE** — `{ type: "streamable-http" | "http" | "sse", url, timeout? }`
|
|
242
56
|
|
|
243
|
-
##
|
|
57
|
+
## Plugin surface
|
|
244
58
|
|
|
245
|
-
|
|
59
|
+
- **Action `MCP`** — single entry point for all MCP operations. `action=call_tool` invokes a server tool, `action=read_resource` reads a server resource (`search_actions` / `list_connections` are cloud-runtime-only). Similes include `CALL_MCP_TOOL`, `READ_MCP_RESOURCE`, `USE_TOOL`.
|
|
60
|
+
- **Provider `MCP`** — injects a summary of connected servers, their status, tools, and resources into agent context.
|
|
61
|
+
- **`handleMcpRoutes`** (exported) — HTTP handler for `/api/mcp/*` (config CRUD, marketplace search, runtime status), wired up by the host server, not by the plugin object. The `McpRouteContext` type is also exported.
|
|
246
62
|
|
|
247
|
-
|
|
63
|
+
## src layout
|
|
248
64
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
65
|
+
```
|
|
66
|
+
src/
|
|
67
|
+
index.ts Plugin object — registers McpService, MCP action, MCP provider
|
|
68
|
+
types.ts Shared types + config guards (McpSettings, McpServerConfig, …)
|
|
69
|
+
service.ts McpService — connection lifecycle, tool calls, resource reads, ping/reconnect
|
|
70
|
+
provider.ts MCP provider — connected-server summary for agent state
|
|
71
|
+
routes-mcp.ts handleMcpRoutes — /api/mcp/config, /api/mcp/status, marketplace
|
|
72
|
+
mcp-marketplace.ts Client for registry.modelcontextprotocol.io (search + details)
|
|
73
|
+
prompts.ts Handlebars-style prompt templates
|
|
74
|
+
actions/mcp.ts mcpAction handler — op routing
|
|
75
|
+
templates/ Thin re-export shims over prompts.ts
|
|
76
|
+
utils/ Selection, validation, processing, error, and JSON helpers
|
|
77
|
+
tool-compatibility/ Per-provider tool-schema fixup (Anthropic/OpenAI/Google)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
254
81
|
|
|
255
|
-
|
|
82
|
+
```bash
|
|
83
|
+
bun run build # bun run build.ts → dist/ (ESM + CJS + .d.ts)
|
|
84
|
+
bun run dev # hot-rebuild with bun --hot
|
|
85
|
+
bun run test # vitest run
|
|
86
|
+
bun run typecheck # tsgo --noEmit
|
|
87
|
+
bun run lint # biome check --write --unsafe
|
|
88
|
+
bun run format # biome format --write
|
|
89
|
+
bun run clean # rm -rf dist .turbo
|
|
90
|
+
```
|
|
256
91
|
|
|
257
|
-
|
|
92
|
+
## Security
|
|
258
93
|
|
|
259
|
-
|
|
260
|
-
- `feat`: 🎸 A new feature
|
|
261
|
-
- `fix`: 🐛 A bug fix
|
|
262
|
-
- `chore`: 🤖 Build process or auxiliary tool changes
|
|
263
|
-
- `docs`: ✏️ Documentation only changes
|
|
264
|
-
- `refactor`: 💡 A code change that neither fixes a bug or adds a feature
|
|
265
|
-
- `style`: 💄 Markup, white-space, formatting, missing semi-colons...
|
|
94
|
+
MCP servers can execute arbitrary code, so only connect to servers you trust. Spawn/connect of every configured server is gated on `validateMcpServerConfig` from `@elizaos/security/mcp-server-config`.
|
|
266
95
|
|
|
267
|
-
##
|
|
96
|
+
## License
|
|
268
97
|
|
|
269
|
-
|
|
98
|
+
MIT.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -558,15 +558,15 @@ var resourceAnalysisTemplate = `{{{mcpProvider.text}}}
|
|
|
558
558
|
|
|
559
559
|
# Prompt
|
|
560
560
|
|
|
561
|
-
|
|
561
|
+
Respond to the user's request using the resource "{{{uri}}}".
|
|
562
562
|
|
|
563
563
|
Original user request: "{{{userMessage}}}"
|
|
564
564
|
|
|
565
565
|
Resource metadata:
|
|
566
|
-
{{{resourceMeta}}
|
|
566
|
+
{{{resourceMeta}}}
|
|
567
567
|
|
|
568
568
|
Resource content:
|
|
569
|
-
{{{resourceContent}}
|
|
569
|
+
{{{resourceContent}}}
|
|
570
570
|
|
|
571
571
|
Instructions:
|
|
572
572
|
1. Analyze how well the resource's content addresses the user's specific question or need
|
|
@@ -583,7 +583,7 @@ var resourceSelectionTemplate = `{{{mcpProvider.text}}}
|
|
|
583
583
|
|
|
584
584
|
# Prompt
|
|
585
585
|
|
|
586
|
-
|
|
586
|
+
Select the right resource to address the user's request.
|
|
587
587
|
|
|
588
588
|
CRITICAL INSTRUCTIONS:
|
|
589
589
|
1. You MUST specify both a valid serverName AND uri from the list above
|
|
@@ -626,7 +626,7 @@ var toolReasoningTemplate = `{{{mcpProvider.text}}}
|
|
|
626
626
|
|
|
627
627
|
# Prompt
|
|
628
628
|
|
|
629
|
-
|
|
629
|
+
Synthesize the result from the "{{{toolName}}}" tool into a response to the user's request.
|
|
630
630
|
|
|
631
631
|
Original user request: "{{{userMessage}}}"
|
|
632
632
|
|
|
@@ -773,13 +773,11 @@ var ResourceSelectionSchema = {
|
|
|
773
773
|
properties: {
|
|
774
774
|
serverName: {
|
|
775
775
|
type: "string",
|
|
776
|
-
minLength: 1
|
|
777
|
-
errorMessage: "serverName must not be empty"
|
|
776
|
+
minLength: 1
|
|
778
777
|
},
|
|
779
778
|
uri: {
|
|
780
779
|
type: "string",
|
|
781
|
-
minLength: 1
|
|
782
|
-
errorMessage: "uri must not be empty"
|
|
780
|
+
minLength: 1
|
|
783
781
|
},
|
|
784
782
|
reasoning: {
|
|
785
783
|
type: "string"
|
|
@@ -1171,13 +1169,11 @@ var toolSelectionNameSchema = {
|
|
|
1171
1169
|
properties: {
|
|
1172
1170
|
serverName: {
|
|
1173
1171
|
type: "string",
|
|
1174
|
-
minLength: 1
|
|
1175
|
-
errorMessage: "serverName must not be empty"
|
|
1172
|
+
minLength: 1
|
|
1176
1173
|
},
|
|
1177
1174
|
toolName: {
|
|
1178
1175
|
type: "string",
|
|
1179
|
-
minLength: 1
|
|
1180
|
-
errorMessage: "toolName must not be empty"
|
|
1176
|
+
minLength: 1
|
|
1181
1177
|
},
|
|
1182
1178
|
reasoning: {
|
|
1183
1179
|
type: "string"
|
|
@@ -1223,7 +1219,7 @@ function validateToolSelectionName(parsed, state) {
|
|
|
1223
1219
|
const data = basicResult.data;
|
|
1224
1220
|
const mcpData = state.values.mcp ?? {};
|
|
1225
1221
|
const server = mcpData[data.serverName];
|
|
1226
|
-
if (
|
|
1222
|
+
if (server?.status !== "connected") {
|
|
1227
1223
|
return {
|
|
1228
1224
|
success: false,
|
|
1229
1225
|
error: `Server "${data.serverName}" not found or not connected`
|
|
@@ -1394,8 +1390,16 @@ async function createToolSelectionName({
|
|
|
1394
1390
|
callback,
|
|
1395
1391
|
mcpProvider
|
|
1396
1392
|
}) {
|
|
1393
|
+
const stateWithMcp = {
|
|
1394
|
+
...state,
|
|
1395
|
+
values: {
|
|
1396
|
+
...state.values,
|
|
1397
|
+
mcp: state.values.mcp ?? mcpProvider.data.mcp,
|
|
1398
|
+
mcpProvider
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1397
1401
|
const toolSelectionPrompt = import_core4.composePromptFromState({
|
|
1398
|
-
state:
|
|
1402
|
+
state: stateWithMcp,
|
|
1399
1403
|
template: toolSelectionNameTemplate
|
|
1400
1404
|
});
|
|
1401
1405
|
const toolSelectionName = await runtime.useModel(import_core4.ModelType.TEXT_LARGE, {
|
|
@@ -1404,10 +1408,10 @@ async function createToolSelectionName({
|
|
|
1404
1408
|
return await withModelRetry({
|
|
1405
1409
|
runtime,
|
|
1406
1410
|
message,
|
|
1407
|
-
state,
|
|
1411
|
+
state: stateWithMcp,
|
|
1408
1412
|
callback,
|
|
1409
1413
|
input: toolSelectionName,
|
|
1410
|
-
validationFn: (parsed) => validateToolSelectionName(parsed,
|
|
1414
|
+
validationFn: (parsed) => validateToolSelectionName(parsed, stateWithMcp),
|
|
1411
1415
|
createFeedbackPromptFn: (originalResponse, errorMessage, composedState, userMessage) => createToolSelectionFeedbackPrompt(typeof originalResponse === "string" ? originalResponse : JSON.stringify(originalResponse), errorMessage, composedState, userMessage),
|
|
1412
1416
|
failureMsg: "I'm having trouble figuring out the best way to help with your request."
|
|
1413
1417
|
});
|
|
@@ -1894,6 +1898,7 @@ var provider = {
|
|
|
1894
1898
|
|
|
1895
1899
|
// src/service.ts
|
|
1896
1900
|
var import_core6 = require("@elizaos/core");
|
|
1901
|
+
var import_mcp_server_config = require("@elizaos/security/mcp-server-config");
|
|
1897
1902
|
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
1898
1903
|
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
1899
1904
|
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
@@ -1994,15 +1999,28 @@ class McpService extends import_core6.Service {
|
|
|
1994
1999
|
}
|
|
1995
2000
|
return;
|
|
1996
2001
|
}
|
|
2002
|
+
async filterValidatedServerConfigs(serverConfigs) {
|
|
2003
|
+
const validated = {};
|
|
2004
|
+
for (const [name, config] of Object.entries(serverConfigs)) {
|
|
2005
|
+
const rejection = await import_mcp_server_config.validateMcpServerConfig(config);
|
|
2006
|
+
if (rejection) {
|
|
2007
|
+
import_core6.logger.error({ server: name, rejection }, "Skipping MCP server with invalid or unsafe config");
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
validated[name] = config;
|
|
2011
|
+
}
|
|
2012
|
+
return validated;
|
|
2013
|
+
}
|
|
1997
2014
|
async updateServerConnections(serverConfigs) {
|
|
2015
|
+
const safeConfigs = await this.filterValidatedServerConfigs(serverConfigs);
|
|
1998
2016
|
const currentNames = new Set(this.connections.keys());
|
|
1999
|
-
const newNames = new Set(Object.keys(
|
|
2017
|
+
const newNames = new Set(Object.keys(safeConfigs));
|
|
2000
2018
|
for (const name of currentNames) {
|
|
2001
2019
|
if (!newNames.has(name)) {
|
|
2002
2020
|
await this.deleteConnection(name);
|
|
2003
2021
|
}
|
|
2004
2022
|
}
|
|
2005
|
-
const connectionPromises = Object.entries(
|
|
2023
|
+
const connectionPromises = Object.entries(safeConfigs).map(async ([name, config]) => {
|
|
2006
2024
|
const currentConnection = this.connections.get(name);
|
|
2007
2025
|
if (!currentConnection) {
|
|
2008
2026
|
await this.initializeConnection(name, config);
|
|
@@ -2148,9 +2166,16 @@ class McpService extends import_core6.Service {
|
|
|
2148
2166
|
async deleteConnection(name) {
|
|
2149
2167
|
const connection = this.connections.get(name);
|
|
2150
2168
|
if (connection) {
|
|
2151
|
-
await
|
|
2152
|
-
|
|
2169
|
+
const closeResults = await Promise.allSettled([
|
|
2170
|
+
connection.transport.close(),
|
|
2171
|
+
connection.client.close()
|
|
2172
|
+
]);
|
|
2153
2173
|
this.connections.delete(name);
|
|
2174
|
+
for (const result of closeResults) {
|
|
2175
|
+
if (result.status === "rejected") {
|
|
2176
|
+
import_core6.logger.warn({ error: result.reason, serverName: name }, `Failed to close MCP connection resource for "${name}"`);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2154
2179
|
}
|
|
2155
2180
|
const state = this.connectionStates.get(name);
|
|
2156
2181
|
if (state) {
|
|
@@ -2168,6 +2193,17 @@ class McpService extends import_core6.Service {
|
|
|
2168
2193
|
if (!config.command) {
|
|
2169
2194
|
throw new Error(`Missing command for stdio MCP server ${name}`);
|
|
2170
2195
|
}
|
|
2196
|
+
const rejection = await import_mcp_server_config.validateMcpServerConfig({
|
|
2197
|
+
type: "stdio",
|
|
2198
|
+
command: config.command,
|
|
2199
|
+
args: config.args,
|
|
2200
|
+
env: config.env,
|
|
2201
|
+
cwd: config.cwd,
|
|
2202
|
+
timeoutInMillis: config.timeoutInMillis
|
|
2203
|
+
});
|
|
2204
|
+
if (rejection) {
|
|
2205
|
+
throw new Error(`MCP stdio server "${name}" rejected at spawn: ${rejection}`);
|
|
2206
|
+
}
|
|
2171
2207
|
return new import_stdio.StdioClientTransport({
|
|
2172
2208
|
command: config.command,
|
|
2173
2209
|
args: config.args ? [...config.args] : undefined,
|
|
@@ -2183,6 +2219,13 @@ class McpService extends import_core6.Service {
|
|
|
2183
2219
|
if (!config.url) {
|
|
2184
2220
|
throw new Error(`Missing URL for HTTP MCP server ${name}`);
|
|
2185
2221
|
}
|
|
2222
|
+
const rejection = await import_mcp_server_config.validateMcpServerConfig({
|
|
2223
|
+
type: config.type,
|
|
2224
|
+
url: config.url
|
|
2225
|
+
});
|
|
2226
|
+
if (rejection) {
|
|
2227
|
+
throw new Error(`MCP remote server "${name}" rejected at connect: ${rejection}`);
|
|
2228
|
+
}
|
|
2186
2229
|
return new import_sse.SSEClientTransport(new URL(config.url));
|
|
2187
2230
|
}
|
|
2188
2231
|
appendErrorMessage(connection, error) {
|
|
@@ -2374,12 +2417,17 @@ async function getMcpServerDetails(name) {
|
|
|
2374
2417
|
}
|
|
2375
2418
|
|
|
2376
2419
|
// src/routes-mcp.ts
|
|
2420
|
+
var MCP_MARKETPLACE_QUERY_MAX_LENGTH = 200;
|
|
2421
|
+
var MCP_MARKETPLACE_SERVER_NAME_MAX_LENGTH = 200;
|
|
2377
2422
|
function parseClampedInteger(value, options = {}) {
|
|
2378
2423
|
const raw = value == null ? "" : value.trim();
|
|
2379
2424
|
if (!raw)
|
|
2380
2425
|
return Number.isFinite(options.fallback) ? options.fallback : undefined;
|
|
2381
|
-
|
|
2382
|
-
|
|
2426
|
+
if (!/^[+-]?\d+$/.test(raw)) {
|
|
2427
|
+
return Number.isFinite(options.fallback) ? options.fallback : undefined;
|
|
2428
|
+
}
|
|
2429
|
+
const parsed = Number(raw);
|
|
2430
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
2383
2431
|
return Number.isFinite(options.fallback) ? options.fallback : undefined;
|
|
2384
2432
|
}
|
|
2385
2433
|
if (options.min !== undefined && parsed < options.min)
|
|
@@ -2388,10 +2436,23 @@ function parseClampedInteger(value, options = {}) {
|
|
|
2388
2436
|
return options.max;
|
|
2389
2437
|
return parsed;
|
|
2390
2438
|
}
|
|
2439
|
+
function normalizeBoundedString(value, maxLength, label) {
|
|
2440
|
+
const normalized = value.trim();
|
|
2441
|
+
if (normalized.length > maxLength) {
|
|
2442
|
+
throw new RangeError(`${label} must be ${maxLength} characters or fewer`);
|
|
2443
|
+
}
|
|
2444
|
+
return normalized;
|
|
2445
|
+
}
|
|
2391
2446
|
async function handleMcpRoutes(ctx) {
|
|
2392
2447
|
const { req, res, method, pathname, url, state, json, error, readJsonBody } = ctx;
|
|
2393
2448
|
if (method === "GET" && pathname === "/api/mcp/marketplace/search") {
|
|
2394
|
-
|
|
2449
|
+
let query;
|
|
2450
|
+
try {
|
|
2451
|
+
query = normalizeBoundedString(url.searchParams.get("q") ?? "", MCP_MARKETPLACE_QUERY_MAX_LENGTH, "Marketplace search query");
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
error(res, err instanceof Error ? err.message : String(err), 400);
|
|
2454
|
+
return true;
|
|
2455
|
+
}
|
|
2395
2456
|
const limitStr = url.searchParams.get("limit");
|
|
2396
2457
|
const limit = limitStr ? parseClampedInteger(limitStr, { min: 1, max: 50, fallback: 30 }) : 30;
|
|
2397
2458
|
try {
|
|
@@ -2406,14 +2467,21 @@ async function handleMcpRoutes(ctx) {
|
|
|
2406
2467
|
const serverName = ctx.decodePathComponent(pathname.slice("/api/mcp/marketplace/details/".length), res, "server name");
|
|
2407
2468
|
if (serverName === null)
|
|
2408
2469
|
return true;
|
|
2409
|
-
|
|
2470
|
+
let normalizedServerName;
|
|
2471
|
+
try {
|
|
2472
|
+
normalizedServerName = normalizeBoundedString(serverName, MCP_MARKETPLACE_SERVER_NAME_MAX_LENGTH, "Server name");
|
|
2473
|
+
} catch (err) {
|
|
2474
|
+
error(res, err instanceof Error ? err.message : String(err), 400);
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
if (!normalizedServerName) {
|
|
2410
2478
|
error(res, "Server name is required", 400);
|
|
2411
2479
|
return true;
|
|
2412
2480
|
}
|
|
2413
2481
|
try {
|
|
2414
|
-
const details = await getMcpServerDetails(
|
|
2482
|
+
const details = await getMcpServerDetails(normalizedServerName);
|
|
2415
2483
|
if (!details) {
|
|
2416
|
-
error(res, `MCP server "${
|
|
2484
|
+
error(res, `MCP server "${normalizedServerName}" not found`, 404);
|
|
2417
2485
|
return true;
|
|
2418
2486
|
}
|
|
2419
2487
|
json(res, { ok: true, server: details });
|
|
@@ -2501,6 +2569,12 @@ async function handleMcpRoutes(ctx) {
|
|
|
2501
2569
|
error(res, "servers must be a JSON object", 400);
|
|
2502
2570
|
return true;
|
|
2503
2571
|
}
|
|
2572
|
+
for (const serverName of Object.keys(body.servers)) {
|
|
2573
|
+
if (ctx.isBlockedObjectKey(serverName)) {
|
|
2574
|
+
error(res, 'Invalid server name: "__proto__", "constructor", and "prototype" are reserved', 400);
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2504
2578
|
const mcpRejection = await ctx.resolveMcpServersRejection(body.servers);
|
|
2505
2579
|
if (mcpRejection) {
|
|
2506
2580
|
error(res, mcpRejection, 400);
|
|
@@ -2568,10 +2642,14 @@ var mcpPlugin = {
|
|
|
2568
2642
|
init: async (_config, _runtime) => {
|
|
2569
2643
|
import_core8.logger.info("Initializing MCP plugin...");
|
|
2570
2644
|
},
|
|
2645
|
+
async dispose(runtime) {
|
|
2646
|
+
const svc = runtime.getService(McpService.serviceType);
|
|
2647
|
+
await svc?.stop();
|
|
2648
|
+
},
|
|
2571
2649
|
services: [McpService],
|
|
2572
2650
|
actions: [...import_core8.promoteSubactionsToActions(withMcpContext(mcpAction))],
|
|
2573
2651
|
providers: [provider]
|
|
2574
2652
|
};
|
|
2575
2653
|
var src_default = mcpPlugin;
|
|
2576
2654
|
|
|
2577
|
-
//# debugId=
|
|
2655
|
+
//# debugId=E8D9359237465E8664756E2164756E21
|