@elizaos/plugin-mcp 2.0.0-beta.1 → 2.0.3-beta.3

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 CHANGED
@@ -1,201 +1,25 @@
1
- # MCP Plugin for elizaOS
1
+ # @elizaos/plugin-mcp
2
2
 
3
- [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-blue.svg)](https://conventionalcommits.org)
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
- This plugin integrates the Model Context Protocol (MCP) with elizaOS, allowing agents to connect to multiple MCP servers and use their resources, prompts, and tools.
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
- ## 🔍 What is MCP?
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
- The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. It provides a standardized way to connect LLMs with the context they need.
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
- ## 🚀 Usage
15
+ ## Usage
39
16
 
40
- 1. Add the plugin to your character configuration:
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
- "puppeteer": {
211
- "type": "stdio",
212
- "command": "npx",
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
- ## 🔒 Security Considerations
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
- Please be aware that MCP servers can execute arbitrary code, so only connect to servers you trust.
45
+ ## Configuration
233
46
 
234
- ## 🔍 Troubleshooting
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
- If you encounter issues with the MCP plugin:
52
+ Transport config (see `src/types.ts`):
237
53
 
238
- 1. Check that your MCP servers are correctly configured and running
239
- 2. Ensure the commands are accessible in the elizaOS environment
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
- ## 👥 Contributing
57
+ ## Plugin surface
244
58
 
245
- Thanks for considering contributing to our project!
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
- ### How to Contribute
63
+ ## src layout
248
64
 
249
- 1. Fork the repository.
250
- 2. Create a new branch: `git checkout -b feature-branch-name`.
251
- 3. Make your changes.
252
- 4. Commit your changes using conventional commits.
253
- 5. Push to your fork and submit a pull request.
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
- ### Commit Guidelines
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
- We use [Conventional Commits](https://www.conventionalcommits.org/) for our commit messages:
92
+ ## Security
258
93
 
259
- - `test`: 💍 Adding missing tests
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
- ## 📄 License
96
+ ## License
268
97
 
269
- This plugin is released under the same license as elizaOS.
98
+ MIT.
@@ -558,15 +558,15 @@ var resourceAnalysisTemplate = `{{{mcpProvider.text}}}
558
558
 
559
559
  # Prompt
560
560
 
561
- You are a helpful assistant responding to a user's request. You've just accessed the resource "{{{uri}}}" to help answer this request.
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
- You are an intelligent assistant helping select the right resource to address a user's request.
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
- You are a helpful assistant responding to a user's request. You've just used the "{{{toolName}}}" tool from the "{{{serverName}}}" server to help answer this request.
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 (!server || server.status !== "connected") {
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: { ...state, values: { ...state.values, mcpProvider } },
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, state),
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(serverConfigs));
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(serverConfigs).map(async ([name, config]) => {
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 connection.transport.close();
2152
- await connection.client.close();
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
- const parsed = Number.parseInt(raw, 10);
2382
- if (!Number.isFinite(parsed)) {
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
- const query = url.searchParams.get("q") ?? "";
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
- if (!serverName.trim()) {
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(serverName);
2482
+ const details = await getMcpServerDetails(normalizedServerName);
2415
2483
  if (!details) {
2416
- error(res, `MCP server "${serverName}" not found`, 404);
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=2D70561DBB87DFA464756E2164756E21
2655
+ //# debugId=E8D9359237465E8664756E2164756E21