├── images ├── logo.jpg └── banner.jpg ├── .npmrc.template ├── .npmignore ├── .github ├── ISSUE_TEMPLATE.md ├── workflows │ ├── ci.yml │ └── release.yml ├── PULL_REQUEST_TEMPLATE.md └── pull_request_template.md ├── src ├── templates │ ├── errorAnalysisPrompt.ts │ ├── feedbackTemplate.ts │ ├── resourceAnalysisTemplate.ts │ ├── toolReasoningTemplate.ts │ ├── resourceSelectionTemplate.ts │ └── toolSelectionTemplate.ts ├── provider.ts ├── index.ts ├── utils │ ├── json.ts │ ├── error.ts │ ├── validation.ts │ ├── mcp.ts │ └── processing.ts ├── types.ts ├── actions │ ├── callToolAction.ts │ └── readResourceAction.ts └── service.ts ├── biome.json ├── tsconfig.json ├── package.json ├── .gitignore └── README.md /images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fleek-platform/eliza-plugin-mcp/HEAD/images/logo.jpg -------------------------------------------------------------------------------- /.npmrc.template: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fleek-platform/eliza-plugin-mcp/HEAD/images/banner.jpg -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tsconfig.json 3 | jest.config.js 4 | eslint.config.js 5 | commintlint.config.js 6 | .prettierrc 7 | .prettierignore 8 | .husky/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: CI 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Check out code 12 | uses: actions/checkout@v4 13 | 14 | - name: Set up Bun 15 | uses: oven-sh/setup-bun@v2 16 | with: 17 | bun-version: 1.2.5 18 | 19 | - name: Install dependencies 20 | run: bun install 21 | 22 | - name: Check linter and formatter 23 | run: bun run ci 24 | -------------------------------------------------------------------------------- /src/templates/errorAnalysisPrompt.ts: -------------------------------------------------------------------------------- 1 | export const errorAnalysisPrompt = ` 2 | {{{mcpProvider.text}}} 3 | 4 | {{{recentMessages}}} 5 | 6 | # Prompt 7 | 8 | You're an assistant helping a user, but there was an error accessing the resource you tried to use. 9 | 10 | User request: "{{{userMessage}}}" 11 | Error message: {{{error}}} 12 | 13 | Create a helpful response that: 14 | 1. Acknowledges the issue in user-friendly terms 15 | 2. Offers alternative approaches to help if possible 16 | 3. Doesn't expose technical error details unless they're truly helpful 17 | 4. Maintains a helpful, conversational tone 18 | 19 | Your response: 20 | `; 21 | -------------------------------------------------------------------------------- /src/provider.ts: -------------------------------------------------------------------------------- 1 | import type { IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; 2 | import type { McpService } from "./service"; 3 | import { MCP_SERVICE_NAME } from "./types"; 4 | 5 | export const provider: Provider = { 6 | name: "MCP", 7 | description: "Information about connected MCP servers, tools, and resources", 8 | 9 | get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => { 10 | const mcpService = runtime.getService(MCP_SERVICE_NAME); 11 | if (!mcpService) { 12 | return { 13 | values: { mcp: {} }, 14 | data: { mcp: {} }, 15 | text: "No MCP servers are available.", 16 | }; 17 | } 18 | 19 | return mcpService.getProviderData(); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "correctness": { 11 | "noUnusedVariables": "error" 12 | }, 13 | "a11y": { 14 | "useButtonType": "off" 15 | }, 16 | "style": { 17 | "noCommaOperator": "off" 18 | } 19 | } 20 | }, 21 | "formatter": { 22 | "enabled": true, 23 | "indentWidth": 2, 24 | "indentStyle": "space", 25 | "lineWidth": 100 26 | }, 27 | "javascript": { 28 | "formatter": { 29 | "quoteStyle": "double", 30 | "trailingCommas": "es5", 31 | "semicolons": "always" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { type IAgentRuntime, type Plugin, logger } from "@elizaos/core"; 2 | import { callToolAction } from "./actions/callToolAction"; 3 | import { readResourceAction } from "./actions/readResourceAction"; 4 | import { provider } from "./provider"; 5 | import { McpService } from "./service"; 6 | 7 | const mcpPlugin: Plugin = { 8 | name: "mcp", 9 | description: "Plugin for connecting to MCP (Model Context Protocol) servers", 10 | 11 | init: async (_config: Record, _runtime: IAgentRuntime) => { 12 | logger.info("Initializing MCP plugin..."); 13 | }, 14 | 15 | services: [McpService], 16 | actions: [callToolAction, readResourceAction], 17 | providers: [provider], 18 | }; 19 | 20 | export type { McpService }; 21 | 22 | export default mcpPlugin; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": [ 5 | "ESNext", 6 | "DOM" 7 | ], 8 | "target": "ESNext", 9 | "module": "ESNext", 10 | "moduleDetection": "force", 11 | "jsx": "react-jsx", 12 | "allowJs": false, 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": false, 17 | "emitDeclarationOnly": true, 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": false, 23 | "noPropertyAccessFromIndexSignature": false, 24 | "declaration": true, 25 | "outDir": "./dist", 26 | "rootDir": "./src" 27 | } 28 | } -------------------------------------------------------------------------------- /src/templates/feedbackTemplate.ts: -------------------------------------------------------------------------------- 1 | export const feedbackTemplate = ` 2 | {{{mcpProvider.text}}} 3 | 4 | {{{recentMessages}}} 5 | 6 | # Prompt 7 | 8 | You previously attempted to parse a JSON selection but encountered an error. You need to fix the issues and provide a valid JSON response. 9 | 10 | PREVIOUS RESPONSE: 11 | {{{originalResponse}} 12 | 13 | ERROR: 14 | {{{errorMessage}} 15 | 16 | Available {{{itemType}}}s: 17 | {{{itemsDescription}} 18 | 19 | User request: "{{{userMessage}}}" 20 | 21 | CORRECTED INSTRUCTIONS: 22 | 1. Create a valid JSON object that selects the most appropriate {{{itemType}}} for the task 23 | 2. Make sure to use proper JSON syntax with double quotes for keys and string values 24 | 3. Ensure all values exactly match the available {{{itemType}}}s (names are case-sensitive!) 25 | 4. Do not include any markdown formatting, explanations, or non-JSON content 26 | 5. Do not use placeholders - all values should be concrete and usable 27 | 28 | YOUR CORRECTED VALID JSON RESPONSE: 29 | `; 30 | -------------------------------------------------------------------------------- /src/templates/resourceAnalysisTemplate.ts: -------------------------------------------------------------------------------- 1 | export const resourceAnalysisTemplate = ` 2 | {{{mcpProvider.text}}} 3 | 4 | {{{recentMessages}}} 5 | 6 | # Prompt 7 | 8 | You are a helpful assistant responding to a user's request. You've just accessed the resource "{{{uri}}}" to help answer this request. 9 | 10 | Original user request: "{{{userMessage}}}" 11 | 12 | Resource metadata: 13 | {{{resourceMeta}} 14 | 15 | Resource content: 16 | {{{resourceContent}} 17 | 18 | Instructions: 19 | 1. Analyze how well the resource's content addresses the user's specific question or need 20 | 2. Identify the most relevant information from the resource 21 | 3. Create a natural, conversational response that incorporates this information 22 | 4. If the resource content is insufficient, acknowledge its limitations and explain what you can determine 23 | 5. Do not start with phrases like "According to the resource" or "Here's what I found" - instead, integrate the information naturally 24 | 6. Maintain your helpful, intelligent assistant personality while presenting the information 25 | 26 | Your response (written as if directly to the user): 27 | `; 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why? 2 | 3 | Clear and short explanation here. 4 | 5 | ## How? 6 | 7 | - Done A (replace with a breakdown of the steps) 8 | - Done B 9 | - Done C 10 | 11 | ## Tickets? 12 | 13 | - [Ticket 1](the-ticket-url-here) 14 | - [Ticket 2](the-ticket-url-here) 15 | - [Ticket 3](the-ticket-url-here) 16 | 17 | ## Contribution checklist? 18 | 19 | - [ ] The commit messages are detailed 20 | - [ ] The `build` command runs locally 21 | - [ ] Assets or static content are linked and stored in the project 22 | - [ ] Document filename is named after the slug 23 | - [ ] You've reviewed spelling using a grammar checker 24 | - [ ] For documentation, guides or references, you've tested the commands and steps 25 | - [ ] You've done enough research before writing 26 | 27 | ## Security checklist? 28 | 29 | - [ ] Sensitive data has been identified and is being protected properly 30 | - [ ] Injection has been prevented (parameterized queries, no eval or system calls) 31 | - [ ] The Components are escaping output (to prevent XSS) 32 | 33 | ## References? 34 | 35 | Optionally, provide references such as links 36 | 37 | ## Preview? 38 | 39 | Optionally, provide the preview url here 40 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Why? 2 | 3 | Clear and short explanation here. 4 | 5 | ## How? 6 | 7 | - Done A (replace with a breakdown of the steps) 8 | - Done B 9 | - Done C 10 | 11 | ## Tickets? 12 | 13 | - [Ticket 1](the-ticket-url-here) 14 | - [Ticket 2](the-ticket-url-here) 15 | - [Ticket 3](the-ticket-url-here) 16 | 17 | ## Contribution checklist? 18 | 19 | - [ ] The commit messages are detailed 20 | - [ ] The `build` command runs locally 21 | - [ ] Assets or static content are linked and stored in the project 22 | - [ ] Document filename is named after the slug 23 | - [ ] You've reviewed spelling using a grammar checker 24 | - [ ] For documentation, guides or references, you've tested the commands and steps 25 | - [ ] You've done enough research before writing 26 | 27 | ## Security checklist? 28 | 29 | - [ ] Sensitive data has been identified and is being protected properly 30 | - [ ] Injection has been prevented (parameterized queries, no eval or system calls) 31 | - [ ] The Components are escaping output (to prevent XSS) 32 | 33 | ## References? 34 | 35 | Optionally, provide references such as links 36 | 37 | ## Preview? 38 | 39 | Optionally, provide the preview url here 40 | -------------------------------------------------------------------------------- /src/utils/json.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv"; 2 | import JSON5 from "json5"; 3 | 4 | export function parseJSON(input: string): T { 5 | const cleanedInput = input.replace(/^```(?:json)?\s*|\s*```$/g, "").trim(); 6 | return JSON5.parse(cleanedInput); 7 | } 8 | 9 | const ajv = new Ajv({ 10 | allErrors: true, 11 | strict: false, 12 | }); 13 | 14 | export function validateJsonSchema( 15 | data: unknown, 16 | schema: Record 17 | ): { success: true; data: T } | { success: false; error: string } { 18 | try { 19 | const validate = ajv.compile(schema); 20 | const valid = validate(data); 21 | 22 | if (!valid) { 23 | const errors = (validate.errors || []).map((err) => { 24 | const path = err.instancePath ? `${err.instancePath.replace(/^\//, "")}` : "value"; 25 | return `${path}: ${err.message}`; 26 | }); 27 | 28 | return { success: false, error: errors.join(", ") }; 29 | } 30 | 31 | return { success: true, data: data as T }; 32 | } catch (error) { 33 | return { 34 | success: false, 35 | error: `Schema validation error: ${error instanceof Error ? error.message : String(error)}`, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | env: 13 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Bun 20 | uses: oven-sh/setup-bun@v2 21 | with: 22 | bun-version: 1.2.5 23 | 24 | - name: Install dependencies 25 | run: bun install 26 | 27 | - name: Build package 28 | run: bun run build 29 | 30 | - name: Update package name for publishing 31 | run: | 32 | jq '.name = "@fleek-platform/eliza-plugin-mcp"' package.json > package.json.tmp 33 | mv package.json.tmp package.json 34 | 35 | - name: Set NPM profile for publishing packages 36 | uses: franzbischoff/replace_envs@v1 37 | env: 38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | with: 40 | from_file: '.npmrc.template' 41 | to_file: '.npmrc' 42 | commit: 'false' 43 | 44 | - name: Publish package 45 | run: bun publish --access=public -------------------------------------------------------------------------------- /src/templates/toolReasoningTemplate.ts: -------------------------------------------------------------------------------- 1 | export const toolReasoningTemplate = ` 2 | {{{mcpProvider.text}}} 3 | 4 | {{{recentMessages}}} 5 | 6 | # Prompt 7 | 8 | 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. 9 | 10 | Original user request: "{{{userMessage}}}" 11 | 12 | Tool response: 13 | {{{toolOutput}}} 14 | 15 | {{#if hasAttachments}} 16 | The tool also returned images or other media that will be shared with the user. 17 | {{/if}} 18 | 19 | Instructions: 20 | 1. Analyze how well the tool's response addresses the user's specific question or need 21 | 2. Identify the most relevant information from the tool's output 22 | 3. Create a natural, conversational response that incorporates this information 23 | 4. If the tool's response is insufficient, acknowledge its limitations and explain what you can determine 24 | 5. Do not start with phrases like "I used the X tool" or "Here's what I found" - instead, integrate the information naturally 25 | 6. Maintain your helpful, intelligent assistant personality while presenting the information 26 | 27 | Your response (written as if directly to the user): 28 | `; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fleek-platform/eliza-plugin-mcp", 3 | "description": "ElizaOS plugin to integrate with MCP (Model Context Protocol) servers", 4 | "module": "dist/index.js", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "version": "0.0.8", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/fleek-platform/eliza-plugin-mcp.git" 13 | }, 14 | "tags": [ 15 | "mcp", 16 | "model", 17 | "context", 18 | "protocol", 19 | "elizaos-plugins" 20 | ], 21 | "scripts": { 22 | "build": "bun build ./src/index.ts --outdir ./dist --target node --minify --treeshake", 23 | "types": "bunx tsc --emitDeclarationOnly --outDir ./dist", 24 | "check:write": "bunx @biomejs/biome check --write ./src", 25 | "check": "bunx @biomejs/biome check ./src", 26 | "ci": "bunx @biomejs/biome ci ./src", 27 | "version:patch": "bunx bumpp patch --tag -y", 28 | "version:minor": "bunx bumpp minor --tag -y", 29 | "version:major": "bunx bumpp major --tag -y", 30 | "release:patch": "bun run version:patch && git push --follow-tags", 31 | "release:minor": "bun run version:minor && git push --follow-tags", 32 | "release:major": "bun run version:major && git push --follow-tags" 33 | }, 34 | "devDependencies": { 35 | "@biomejs/biome": "1.9.4", 36 | "@types/bun": "1.2.5", 37 | "bumpp": "10.1.0", 38 | "typescript": "^5.0.0" 39 | }, 40 | "dependencies": { 41 | "@elizaos/core": "1.0.0-beta.7", 42 | "@modelcontextprotocol/sdk": "^1.7.0", 43 | "ajv": "^8.17.1", 44 | "json5": "^2.2.3" 45 | }, 46 | "agentConfig": { 47 | "pluginType": "elizaos:plugin:1.0.0", 48 | "pluginParameters": {} 49 | } 50 | } -------------------------------------------------------------------------------- /src/templates/resourceSelectionTemplate.ts: -------------------------------------------------------------------------------- 1 | export const resourceSelectionTemplate = ` 2 | {{{mcpProvider.text}}} 3 | 4 | {{{recentMessages}}} 5 | 6 | # Prompt 7 | 8 | You are an intelligent assistant helping select the right resource to address a user's request. 9 | 10 | CRITICAL INSTRUCTIONS: 11 | 1. You MUST specify both a valid serverName AND uri from the list above 12 | 2. The serverName value should match EXACTLY the server name shown in parentheses (Server: X) 13 | CORRECT: "serverName": "github" (if the server is called "github") 14 | WRONG: "serverName": "GitHub" or "Github" or any other variation 15 | 3. The uri value should match EXACTLY the resource uri listed 16 | CORRECT: "uri": "weather://San Francisco/current" (if that's the exact uri) 17 | WRONG: "uri": "weather://sanfrancisco/current" or any variation 18 | 4. Identify the user's information need from the conversation context 19 | 5. Select the most appropriate resource based on its description and the request 20 | 6. If no resource seems appropriate, output {"noResourceAvailable": true} 21 | 22 | !!! YOUR RESPONSE MUST BE A VALID JSON OBJECT ONLY !!! 23 | 24 | STRICT FORMAT REQUIREMENTS: 25 | - NO code block formatting (NO backticks or \`\`\`) 26 | - NO comments (NO // or /* */) 27 | - NO placeholders like "replace with...", "example", "your...", "actual", etc. 28 | - Every parameter value must be a concrete, usable value (not instructions to replace) 29 | - Use proper JSON syntax with double quotes for strings 30 | - NO explanatory text before or after the JSON object 31 | 32 | EXAMPLE RESPONSE: 33 | { 34 | "serverName": "weather-server", 35 | "uri": "weather://San Francisco/current", 36 | "reasoning": "Based on the conversation, the user is asking about current weather in San Francisco. This resource provides up-to-date weather information for that city." 37 | } 38 | 39 | REMEMBER: Your response will be parsed directly as JSON. If it fails to parse, the operation will fail completely! 40 | `; 41 | -------------------------------------------------------------------------------- /src/templates/toolSelectionTemplate.ts: -------------------------------------------------------------------------------- 1 | export const toolSelectionTemplate = ` 2 | {{{mcpProvider.text}}} 3 | 4 | {{{recentMessages}}} 5 | 6 | # Prompt 7 | 8 | You are an intelligent assistant helping select the right tool to address a user's request. 9 | 10 | Choose from the available MCP tools to address the user's request. 11 | 12 | CRITICAL INSTRUCTIONS: 13 | 1. You MUST specify both a valid serverName AND toolName from the list above 14 | 2. The serverName value should match EXACTLY the server name shown in parentheses (Server: X) 15 | CORRECT: "serverName": "github" (if the server is called "github") 16 | WRONG: "serverName": "GitHub" or "Github" or any other variation 17 | 3. The toolName value should match EXACTLY the tool name listed 18 | CORRECT: "toolName": "get_file_contents" (if that's the exact tool name) 19 | WRONG: "toolName": "getFileContents" or "get-file-contents" or any variation 20 | 4. Identify the user's core information need or task 21 | 5. Select the most appropriate tool based on its capabilities and the request 22 | 6. For each required parameter, EXTRACT ACTUAL VALUES FROM THE CONVERSATION CONTEXT 23 | DO NOT use placeholder values like "octocat" or "Hello-World" unless explicitly mentioned by the user 24 | 7. If no tool seems appropriate, output {"noToolAvailable": true} 25 | 26 | !!! YOUR RESPONSE MUST BE A VALID JSON OBJECT ONLY !!! 27 | 28 | STRICT FORMAT REQUIREMENTS: 29 | - NO code block formatting (NO backticks or \`\`\`) 30 | - NO comments (NO // or /* */) 31 | - NO placeholders like "replace with...", "example", "your...", "actual", etc. 32 | - Every parameter value must be a concrete, usable value (not instructions to replace) 33 | - Use proper JSON syntax with double quotes for strings 34 | - Use proper types: strings in quotes, numbers without quotes, booleans as true/false 35 | - NO explanatory text before or after the JSON object 36 | 37 | EXAMPLE FOR GITHUB FILE REQUEST: 38 | { 39 | "serverName": "github", 40 | "toolName": "get_file_contents", 41 | "arguments": { 42 | "owner": "facebook", // EXTRACT THIS FROM CONVERSATION, NOT "octocat" 43 | "repo": "react", // EXTRACT THIS FROM CONVERSATION, NOT "Hello-World" 44 | "path": "README.md", 45 | "branch": "main" 46 | }, 47 | "reasoning": "The user wants to see the README from the facebook/react repository based on our conversation." 48 | } 49 | 50 | REMEMBER: Your response will be parsed directly as JSON. If it fails to parse, the operation will fail completely. 51 | `; 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | # Bun 178 | .bun 179 | bun.lockb -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type HandlerCallback, 3 | type IAgentRuntime, 4 | type Memory, 5 | ModelType, 6 | composePromptFromState, 7 | logger, 8 | } from "@elizaos/core"; 9 | import type { State } from "@elizaos/core"; 10 | import { errorAnalysisPrompt } from "../templates/errorAnalysisPrompt"; 11 | import type { McpProvider } from "../types"; 12 | 13 | export async function handleMcpError( 14 | state: State, 15 | mcpProvider: McpProvider, 16 | error: unknown, 17 | runtime: IAgentRuntime, 18 | message: Memory, 19 | type: "tool" | "resource", 20 | callback?: HandlerCallback 21 | ): Promise { 22 | const errorMessage = error instanceof Error ? error.message : String(error); 23 | 24 | logger.error(`Error executing MCP ${type}: ${errorMessage}`, error); 25 | 26 | if (callback) { 27 | const enhancedState: State = { 28 | ...state, 29 | values: { 30 | ...state.values, 31 | mcpProvider, 32 | userMessage: message.content.text || "", 33 | error: errorMessage, 34 | }, 35 | }; 36 | 37 | const prompt = composePromptFromState({ 38 | state: enhancedState, 39 | template: errorAnalysisPrompt, 40 | }); 41 | 42 | try { 43 | const errorResponse = await runtime.useModel(ModelType.TEXT_SMALL, { 44 | prompt, 45 | }); 46 | 47 | await callback({ 48 | thought: `Error calling MCP ${type}: ${errorMessage}. Providing a helpful response to the user.`, 49 | text: errorResponse, 50 | actions: ["REPLY"], 51 | }); 52 | } catch (modelError) { 53 | logger.error( 54 | "Failed to generate error response:", 55 | modelError instanceof Error ? modelError.message : String(modelError) 56 | ); 57 | 58 | await callback({ 59 | thought: `Error calling MCP ${type} and failed to generate a custom response. Providing a generic fallback response.`, 60 | text: `I'm sorry, I wasn't able to get the information you requested. There seems to be an issue with the ${type} right now. Is there something else I can help you with?`, 61 | actions: ["REPLY"], 62 | }); 63 | } 64 | } 65 | 66 | return false; 67 | } 68 | 69 | export class McpError extends Error { 70 | constructor( 71 | message: string, 72 | public readonly code: string = "UNKNOWN" 73 | ) { 74 | super(message); 75 | this.name = "McpError"; 76 | } 77 | 78 | static connectionError(serverName: string, details?: string): McpError { 79 | return new McpError( 80 | `Failed to connect to server '${serverName}'${details ? `: ${details}` : ""}`, 81 | "CONNECTION_ERROR" 82 | ); 83 | } 84 | 85 | static toolNotFound(toolName: string, serverName: string): McpError { 86 | return new McpError(`Tool '${toolName}' not found on server '${serverName}'`, "TOOL_NOT_FOUND"); 87 | } 88 | 89 | static resourceNotFound(uri: string, serverName: string): McpError { 90 | return new McpError( 91 | `Resource '${uri}' not found on server '${serverName}'`, 92 | "RESOURCE_NOT_FOUND" 93 | ); 94 | } 95 | 96 | static validationError(details: string): McpError { 97 | return new McpError(`Validation error: ${details}`, "VALIDATION_ERROR"); 98 | } 99 | 100 | static serverError(serverName: string, details?: string): McpError { 101 | return new McpError( 102 | `Server error from '${serverName}'${details ? `: ${details}` : ""}`, 103 | "SERVER_ERROR" 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import type { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; 3 | import type { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 4 | import type { 5 | EmbeddedResource, 6 | ImageContent, 7 | Resource, 8 | ResourceTemplate, 9 | TextContent, 10 | Tool, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | 13 | export const MCP_SERVICE_NAME = "mcp"; 14 | export const DEFAULT_MCP_TIMEOUT_SECONDS = 60000; 15 | export const MIN_MCP_TIMEOUT_SECONDS = 1; 16 | export const DEFAULT_MAX_RETRIES = 2; 17 | 18 | export type StdioMcpServerConfig = { 19 | type: "stdio"; 20 | command?: string; 21 | args?: string[]; 22 | env?: Record; 23 | cwd?: string; 24 | timeoutInMillis?: number; 25 | }; 26 | 27 | export type SseMcpServerConfig = { 28 | type: "sse"; 29 | url: string; 30 | timeout?: number; 31 | }; 32 | 33 | export type McpServerConfig = StdioMcpServerConfig | SseMcpServerConfig; 34 | 35 | export type McpSettings = { 36 | servers: Record; 37 | maxRetries?: number; 38 | }; 39 | 40 | export type McpServerStatus = "connecting" | "connected" | "disconnected"; 41 | 42 | export interface McpServer { 43 | name: string; 44 | status: McpServerStatus; 45 | config: string; 46 | error?: string; 47 | disabled?: boolean; 48 | tools?: Tool[]; 49 | resources?: Resource[]; 50 | resourceTemplates?: ResourceTemplate[]; 51 | } 52 | 53 | export interface McpConnection { 54 | server: McpServer; 55 | client: Client; 56 | transport: StdioClientTransport | SSEClientTransport; 57 | } 58 | 59 | export interface McpToolResult { 60 | content: Array; 61 | isError?: boolean; 62 | } 63 | 64 | export interface McpToolCallResponse { 65 | content: Array; 66 | isError?: boolean; 67 | } 68 | 69 | export interface McpResourceResponse { 70 | contents: Array<{ 71 | uri: string; 72 | mimeType?: string; 73 | text?: string; 74 | blob?: string; 75 | }>; 76 | } 77 | 78 | export interface McpToolInfo { 79 | description: string; 80 | inputSchema?: { 81 | properties?: Record; 82 | required?: string[]; 83 | [key: string]: unknown; 84 | }; 85 | } 86 | 87 | export interface McpResourceInfo { 88 | name: string; 89 | description: string; 90 | mimeType?: string; 91 | } 92 | 93 | export interface McpServerInfo { 94 | status: string; 95 | tools: Record; 96 | resources: Record; 97 | } 98 | 99 | export type McpProvider = { 100 | values: { mcp: McpProviderData }; 101 | data: { mcp: McpProviderData }; 102 | text: string; 103 | }; 104 | 105 | export interface McpProviderData { 106 | [serverName: string]: McpServerInfo; 107 | } 108 | 109 | export const ToolSelectionSchema = { 110 | type: "object", 111 | required: ["serverName", "toolName", "arguments"], 112 | properties: { 113 | serverName: { 114 | type: "string", 115 | minLength: 1, 116 | errorMessage: "serverName must not be empty", 117 | }, 118 | toolName: { 119 | type: "string", 120 | minLength: 1, 121 | errorMessage: "toolName must not be empty", 122 | }, 123 | arguments: { 124 | type: "object", 125 | }, 126 | reasoning: { 127 | type: "string", 128 | }, 129 | noToolAvailable: { 130 | type: "boolean", 131 | }, 132 | }, 133 | }; 134 | 135 | export const ResourceSelectionSchema = { 136 | type: "object", 137 | required: ["serverName", "uri"], 138 | properties: { 139 | serverName: { 140 | type: "string", 141 | minLength: 1, 142 | errorMessage: "serverName must not be empty", 143 | }, 144 | uri: { 145 | type: "string", 146 | minLength: 1, 147 | errorMessage: "uri must not be empty", 148 | }, 149 | reasoning: { 150 | type: "string", 151 | }, 152 | noResourceAvailable: { 153 | type: "boolean", 154 | }, 155 | }, 156 | }; 157 | -------------------------------------------------------------------------------- /src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | import type { State } from "@elizaos/core"; 2 | import { type McpProviderData, ResourceSelectionSchema, ToolSelectionSchema } from "../types"; 3 | import { validateJsonSchema } from "./json"; 4 | 5 | export interface ToolSelection { 6 | serverName: string; 7 | toolName: string; 8 | arguments: Record; 9 | reasoning?: string; 10 | noToolAvailable?: boolean; 11 | } 12 | 13 | export interface ResourceSelection { 14 | serverName: string; 15 | uri: string; 16 | reasoning?: string; 17 | noResourceAvailable?: boolean; 18 | } 19 | 20 | export function validateToolSelection( 21 | selection: unknown, 22 | composedState: State 23 | ): { success: true; data: ToolSelection } | { success: false; error: string } { 24 | const basicResult = validateJsonSchema(selection, ToolSelectionSchema); 25 | if (!basicResult.success) { 26 | return { success: false, error: basicResult.error }; 27 | } 28 | 29 | const data = basicResult.data; 30 | 31 | if (data.noToolAvailable) { 32 | return { success: true, data }; 33 | } 34 | 35 | const mcpData = composedState.values.mcp || {}; 36 | const serverInfo = mcpData[data.serverName]; 37 | 38 | if (!serverInfo || serverInfo.status !== "connected") { 39 | return { 40 | success: false, 41 | error: `Server '${data.serverName}' not found or not connected`, 42 | }; 43 | } 44 | 45 | const toolInfo = serverInfo.tools?.[data.toolName]; 46 | if (!toolInfo) { 47 | return { 48 | success: false, 49 | error: `Tool '${data.toolName}' not found on server '${data.serverName}'`, 50 | }; 51 | } 52 | 53 | if (toolInfo.inputSchema) { 54 | const validationResult = validateJsonSchema( 55 | data.arguments, 56 | toolInfo.inputSchema as Record 57 | ); 58 | 59 | if (!validationResult.success) { 60 | return { 61 | success: false, 62 | error: `Invalid arguments: ${validationResult.error}`, 63 | }; 64 | } 65 | } 66 | 67 | return { success: true, data }; 68 | } 69 | 70 | export function validateResourceSelection( 71 | selection: unknown 72 | ): { success: true; data: ResourceSelection } | { success: false; error: string } { 73 | return validateJsonSchema(selection, ResourceSelectionSchema); 74 | } 75 | 76 | export function createToolSelectionFeedbackPrompt( 77 | originalResponse: string, 78 | errorMessage: string, 79 | composedState: State, 80 | userMessage: string 81 | ): string { 82 | let toolsDescription = ""; 83 | 84 | for (const [serverName, server] of Object.entries(composedState.values.mcp || {}) as [ 85 | string, 86 | McpProviderData[string], 87 | ][]) { 88 | if (server.status !== "connected") continue; 89 | 90 | for (const [toolName, tool] of Object.entries(server.tools || {}) as [ 91 | string, 92 | { description?: string }, 93 | ][]) { 94 | toolsDescription += `Tool: ${toolName} (Server: ${serverName})\n`; 95 | toolsDescription += `Description: ${tool.description || "No description available"}\n\n`; 96 | } 97 | } 98 | 99 | return createFeedbackPrompt( 100 | originalResponse, 101 | errorMessage, 102 | "tool", 103 | toolsDescription, 104 | userMessage 105 | ); 106 | } 107 | 108 | export function createResourceSelectionFeedbackPrompt( 109 | originalResponse: string, 110 | errorMessage: string, 111 | composedState: State, 112 | userMessage: string 113 | ): string { 114 | let resourcesDescription = ""; 115 | 116 | for (const [serverName, server] of Object.entries(composedState.values.mcp || {}) as [ 117 | string, 118 | McpProviderData[string], 119 | ][]) { 120 | if (server.status !== "connected") continue; 121 | 122 | for (const [uri, resource] of Object.entries(server.resources || {}) as [ 123 | string, 124 | { description?: string; name?: string }, 125 | ][]) { 126 | resourcesDescription += `Resource: ${uri} (Server: ${serverName})\n`; 127 | resourcesDescription += `Name: ${resource.name || "No name available"}\n`; 128 | resourcesDescription += `Description: ${ 129 | resource.description || "No description available" 130 | }\n\n`; 131 | } 132 | } 133 | 134 | return createFeedbackPrompt( 135 | originalResponse, 136 | errorMessage, 137 | "resource", 138 | resourcesDescription, 139 | userMessage 140 | ); 141 | } 142 | 143 | function createFeedbackPrompt( 144 | originalResponse: string, 145 | errorMessage: string, 146 | itemType: string, 147 | itemsDescription: string, 148 | userMessage: string 149 | ): string { 150 | return `Error parsing JSON: ${errorMessage} 151 | 152 | Your original response: 153 | ${originalResponse} 154 | 155 | Please try again with valid JSON for ${itemType} selection. 156 | Available ${itemType}s: 157 | ${itemsDescription} 158 | 159 | User request: ${userMessage}`; 160 | } 161 | -------------------------------------------------------------------------------- /src/actions/callToolAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Action, 3 | type HandlerCallback, 4 | type IAgentRuntime, 5 | type Memory, 6 | ModelType, 7 | type State, 8 | logger, 9 | } from "@elizaos/core"; 10 | import type { McpService } from "../service"; 11 | import { toolSelectionTemplate } from "../templates/toolSelectionTemplate"; 12 | import { MCP_SERVICE_NAME } from "../types"; 13 | import { handleMcpError } from "../utils/error"; 14 | import { withModelRetry } from "../utils/mcp"; 15 | import { handleToolResponse, processToolResult } from "../utils/processing"; 16 | import { createToolSelectionFeedbackPrompt, validateToolSelection } from "../utils/validation"; 17 | import type { ToolSelection } from "../utils/validation"; 18 | 19 | function createToolSelectionPrompt( 20 | state: State, 21 | mcpProvider: { values: { mcp: unknown }; data: { mcp: unknown }; text: string } 22 | ): string { 23 | return composePromptFromState({ 24 | state: { 25 | ...state, 26 | values: { 27 | ...state.values, 28 | mcpProvider, 29 | }, 30 | }, 31 | template: toolSelectionTemplate, 32 | }); 33 | } 34 | 35 | import { composePromptFromState } from "@elizaos/core"; 36 | 37 | export const callToolAction: Action = { 38 | name: "CALL_TOOL", 39 | similes: [ 40 | "CALL_MCP_TOOL", 41 | "USE_TOOL", 42 | "USE_MCP_TOOL", 43 | "EXECUTE_TOOL", 44 | "EXECUTE_MCP_TOOL", 45 | "RUN_TOOL", 46 | "RUN_MCP_TOOL", 47 | "INVOKE_TOOL", 48 | "INVOKE_MCP_TOOL", 49 | ], 50 | description: "Calls a tool from an MCP server to perform a specific task", 51 | 52 | validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State): Promise => { 53 | const mcpService = runtime.getService(MCP_SERVICE_NAME); 54 | if (!mcpService) return false; 55 | 56 | const servers = mcpService.getServers(); 57 | return ( 58 | servers.length > 0 && 59 | servers.some( 60 | (server) => server.status === "connected" && server.tools && server.tools.length > 0 61 | ) 62 | ); 63 | }, 64 | 65 | handler: async ( 66 | runtime: IAgentRuntime, 67 | message: Memory, 68 | _state?: State, 69 | _options?: { [key: string]: unknown }, 70 | callback?: HandlerCallback 71 | ): Promise => { 72 | const composedState = await runtime.composeState(message, ["RECENT_MESSAGES", "MCP"]); 73 | 74 | const mcpService = runtime.getService(MCP_SERVICE_NAME); 75 | if (!mcpService) { 76 | throw new Error("MCP service not available"); 77 | } 78 | 79 | const mcpProvider = mcpService.getProviderData(); 80 | 81 | try { 82 | const toolSelectionPrompt = createToolSelectionPrompt(composedState, mcpProvider); 83 | 84 | logger.info(`Tool selection prompt: ${toolSelectionPrompt}`); 85 | 86 | const toolSelection = await runtime.useModel(ModelType.TEXT_SMALL, { 87 | prompt: toolSelectionPrompt, 88 | }); 89 | 90 | const parsedSelection = await withModelRetry( 91 | toolSelection, 92 | runtime, 93 | (data) => validateToolSelection(data, composedState), 94 | message, 95 | composedState, 96 | (originalResponse, errorMessage, state, userMessage) => 97 | createToolSelectionFeedbackPrompt(originalResponse, errorMessage, state, userMessage), 98 | callback, 99 | "I'm having trouble figuring out the best way to help with your request. Could you provide more details about what you're looking for?" 100 | ); 101 | 102 | if (!parsedSelection || parsedSelection.noToolAvailable) { 103 | if (callback && parsedSelection?.noToolAvailable) { 104 | await callback({ 105 | text: "I don't have a specific tool that can help with that request. Let me try to assist you directly instead.", 106 | thought: 107 | "No appropriate MCP tool available for this request. Falling back to direct assistance.", 108 | actions: ["REPLY"], 109 | }); 110 | } 111 | return true; 112 | } 113 | 114 | const { serverName, toolName, arguments: toolArguments, reasoning } = parsedSelection; 115 | 116 | logger.debug(`Selected tool "${toolName}" on server "${serverName}" because: ${reasoning}`); 117 | 118 | const result = await mcpService.callTool(serverName, toolName, toolArguments); 119 | logger.debug( 120 | `Called tool ${toolName} on server ${serverName} with arguments ${JSON.stringify(toolArguments)}` 121 | ); 122 | 123 | const { toolOutput, hasAttachments, attachments } = processToolResult( 124 | result, 125 | serverName, 126 | toolName, 127 | runtime, 128 | message.entityId 129 | ); 130 | 131 | await handleToolResponse( 132 | runtime, 133 | message, 134 | serverName, 135 | toolName, 136 | toolArguments, 137 | toolOutput, 138 | hasAttachments, 139 | attachments, 140 | composedState, 141 | mcpProvider, 142 | callback 143 | ); 144 | 145 | return true; 146 | } catch (error) { 147 | return handleMcpError(composedState, mcpProvider, error, runtime, message, "tool", callback); 148 | } 149 | }, 150 | 151 | examples: [ 152 | [ 153 | { 154 | name: "{{user}}", 155 | content: { 156 | text: "Can you search for information about climate change?", 157 | }, 158 | }, 159 | { 160 | name: "{{assistant}}", 161 | content: { 162 | text: "I'll help you with that request. Let me access the right tool...", 163 | actions: ["CALL_MCP_TOOL"], 164 | }, 165 | }, 166 | { 167 | name: "{{assistant}}", 168 | content: { 169 | text: "I found the following information about climate change:\n\nClimate change refers to long-term shifts in temperatures and weather patterns. These shifts may be natural, but since the 1800s, human activities have been the main driver of climate change, primarily due to the burning of fossil fuels like coal, oil, and gas, which produces heat-trapping gases.", 170 | actions: ["CALL_MCP_TOOL"], 171 | }, 172 | }, 173 | ], 174 | ], 175 | }; 176 | -------------------------------------------------------------------------------- /src/utils/mcp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type HandlerCallback, 3 | type IAgentRuntime, 4 | type Memory, 5 | ModelType, 6 | type State, 7 | logger, 8 | } from "@elizaos/core"; 9 | import { 10 | DEFAULT_MAX_RETRIES, 11 | type McpProvider, 12 | type McpProviderData, 13 | type McpResourceInfo, 14 | type McpServer, 15 | type McpToolInfo, 16 | } from "../types"; 17 | import { parseJSON } from "./json"; 18 | 19 | export async function withModelRetry( 20 | initialInput: string, 21 | runtime: IAgentRuntime, 22 | validationFn: (data: unknown) => { success: true; data: T } | { success: false; error: string }, 23 | message: Memory, 24 | composedState: State, 25 | createFeedbackPromptFn: ( 26 | originalResponse: string, 27 | errorMessage: string, 28 | composedState: State, 29 | userMessage: string 30 | ) => string, 31 | callback?: HandlerCallback, 32 | failureMsg?: string, 33 | retryCount = 0 34 | ): Promise { 35 | const maxRetries = getMaxRetries(runtime); 36 | 37 | try { 38 | logger.info("Raw response:", initialInput); 39 | 40 | const parsedJson = parseJSON(initialInput); 41 | logger.info("Parsed response:", parsedJson); 42 | 43 | const validationResult = validationFn(parsedJson); 44 | 45 | if (!validationResult.success) { 46 | throw new Error(validationResult.error); 47 | } 48 | 49 | return validationResult.data; 50 | } catch (parseError) { 51 | const errorMessage = parseError instanceof Error ? parseError.message : "Unknown parsing error"; 52 | 53 | logger.error("Failed to parse response:", errorMessage); 54 | 55 | if (retryCount < maxRetries) { 56 | logger.info(`Retrying (attempt ${retryCount + 1}/${maxRetries})`); 57 | 58 | const feedbackPrompt = createFeedbackPromptFn( 59 | initialInput, 60 | errorMessage, 61 | composedState, 62 | message.content.text || "" 63 | ); 64 | 65 | const retrySelection = await runtime.useModel(ModelType.TEXT_SMALL, { 66 | prompt: feedbackPrompt, 67 | }); 68 | 69 | return withModelRetry( 70 | retrySelection, 71 | runtime, 72 | validationFn, 73 | message, 74 | composedState, 75 | createFeedbackPromptFn, 76 | callback, 77 | failureMsg, 78 | retryCount + 1 79 | ); 80 | } 81 | 82 | if (callback && failureMsg) { 83 | await callback({ 84 | text: failureMsg, 85 | thought: 86 | "Failed to parse response after multiple retries. Requesting clarification from user.", 87 | actions: ["REPLY"], 88 | }); 89 | } 90 | return null; 91 | } 92 | } 93 | 94 | export function getMaxRetries(runtime: IAgentRuntime): number { 95 | try { 96 | const settings = runtime.getSetting("mcp"); 97 | if (settings && "maxRetries" in settings && settings.maxRetries !== undefined) { 98 | const configValue = Number(settings.maxRetries); 99 | if (!Number.isNaN(configValue) && configValue >= 0) { 100 | logger.info(`Using configured selection retries: ${configValue}`); 101 | return configValue; 102 | } 103 | } 104 | } catch (error) { 105 | logger.debug( 106 | "Error reading selection retries config:", 107 | error instanceof Error ? error.message : String(error) 108 | ); 109 | } 110 | 111 | return DEFAULT_MAX_RETRIES; 112 | } 113 | 114 | export async function handleNoSelectionAvailable( 115 | selection: T & { noToolAvailable?: boolean; noResourceAvailable?: boolean }, 116 | callback?: HandlerCallback, 117 | message = "I don't have a specific item that can help with that request. Let me try to assist you directly instead." 118 | ): Promise { 119 | if (selection.noToolAvailable || selection.noResourceAvailable) { 120 | if (callback) { 121 | await callback({ 122 | text: message, 123 | thought: 124 | "No appropriate MCP item available for this request. Falling back to direct assistance.", 125 | actions: ["REPLY"], 126 | }); 127 | } 128 | return true; 129 | } 130 | return false; 131 | } 132 | 133 | export async function createMcpMemory( 134 | runtime: IAgentRuntime, 135 | message: Memory, 136 | type: string, 137 | serverName: string, 138 | content: string, 139 | metadata: Record 140 | ): Promise { 141 | const memory = await runtime.addEmbeddingToMemory({ 142 | entityId: message.entityId, 143 | agentId: runtime.agentId, 144 | roomId: message.roomId, 145 | content: { 146 | text: `Used the "${type}" from "${serverName}" server. 147 | Content: ${content}`, 148 | metadata: { 149 | ...metadata, 150 | serverName, 151 | }, 152 | }, 153 | }); 154 | 155 | await runtime.createMemory(memory, type === "resource" ? "resources" : "tools", true); 156 | } 157 | 158 | export function buildMcpProviderData(servers: McpServer[]): McpProvider { 159 | const mcpData: McpProviderData = {}; 160 | let textContent = ""; 161 | 162 | if (servers.length === 0) { 163 | return { 164 | values: { mcp: {} }, 165 | data: { mcp: {} }, 166 | text: "No MCP servers are currently connected.", 167 | }; 168 | } 169 | 170 | for (const server of servers) { 171 | mcpData[server.name] = { 172 | status: server.status, 173 | tools: {} as Record, 174 | resources: {} as Record, 175 | }; 176 | 177 | textContent += `## Server: ${server.name} (${server.status})\n\n`; 178 | 179 | if (server.tools && server.tools.length > 0) { 180 | textContent += "### Tools:\n\n"; 181 | 182 | for (const tool of server.tools) { 183 | mcpData[server.name].tools[tool.name] = { 184 | description: tool.description || "No description available", 185 | inputSchema: tool.inputSchema || {}, 186 | }; 187 | 188 | textContent += `- **${tool.name}**: ${tool.description || "No description available"}\n`; 189 | if (tool.inputSchema?.properties) { 190 | textContent += ` Parameters: ${JSON.stringify(tool.inputSchema.properties)}\n`; 191 | } 192 | } 193 | textContent += "\n"; 194 | } 195 | 196 | if (server.resources && server.resources.length > 0) { 197 | textContent += "### Resources:\n\n"; 198 | 199 | for (const resource of server.resources) { 200 | mcpData[server.name].resources[resource.uri] = { 201 | name: resource.name, 202 | description: resource.description || "No description available", 203 | mimeType: resource.mimeType, 204 | }; 205 | 206 | textContent += `- **${resource.name}** (${resource.uri}): ${ 207 | resource.description || "No description available" 208 | }\n`; 209 | } 210 | textContent += "\n"; 211 | } 212 | } 213 | 214 | return { 215 | values: { mcp: mcpData }, 216 | data: { mcp: mcpData }, 217 | text: `# MCP Configuration\n\n${textContent}`, 218 | }; 219 | } 220 | -------------------------------------------------------------------------------- /src/actions/readResourceAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Action, 3 | type HandlerCallback, 4 | type IAgentRuntime, 5 | type Memory, 6 | ModelType, 7 | type State, 8 | composePromptFromState, 9 | logger, 10 | } from "@elizaos/core"; 11 | import type { McpService } from "../service"; 12 | import { resourceSelectionTemplate } from "../templates/resourceSelectionTemplate"; 13 | import { MCP_SERVICE_NAME } from "../types"; 14 | import { handleMcpError } from "../utils/error"; 15 | import { withModelRetry } from "../utils/mcp"; 16 | import { 17 | handleResourceAnalysis, 18 | processResourceResult, 19 | sendInitialResponse, 20 | } from "../utils/processing"; 21 | import { 22 | createResourceSelectionFeedbackPrompt, 23 | validateResourceSelection, 24 | } from "../utils/validation"; 25 | import type { ResourceSelection } from "../utils/validation"; 26 | 27 | function createResourceSelectionPrompt(composedState: State, userMessage: string): string { 28 | const mcpData = composedState.values.mcp || {}; 29 | const serverNames = Object.keys(mcpData); 30 | 31 | let resourcesDescription = ""; 32 | for (const serverName of serverNames) { 33 | const server = mcpData[serverName]; 34 | if (server.status !== "connected") continue; 35 | 36 | const resourceUris = Object.keys(server.resources || {}); 37 | for (const uri of resourceUris) { 38 | const resource = server.resources[uri]; 39 | resourcesDescription += `Resource: ${uri} (Server: ${serverName})\n`; 40 | resourcesDescription += `Name: ${resource.name || "No name available"}\n`; 41 | resourcesDescription += `Description: ${ 42 | resource.description || "No description available" 43 | }\n`; 44 | resourcesDescription += `MIME Type: ${resource.mimeType || "Not specified"}\n\n`; 45 | } 46 | } 47 | 48 | const enhancedState: State = { 49 | ...composedState, 50 | values: { 51 | ...composedState.values, 52 | resourcesDescription, 53 | userMessage, 54 | }, 55 | }; 56 | 57 | return composePromptFromState({ 58 | state: enhancedState, 59 | template: resourceSelectionTemplate, 60 | }); 61 | } 62 | 63 | export const readResourceAction: Action = { 64 | name: "READ_RESOURCE", 65 | similes: [ 66 | "READ_MCP_RESOURCE", 67 | "GET_RESOURCE", 68 | "GET_MCP_RESOURCE", 69 | "FETCH_RESOURCE", 70 | "FETCH_MCP_RESOURCE", 71 | "ACCESS_RESOURCE", 72 | "ACCESS_MCP_RESOURCE", 73 | ], 74 | description: "Reads a resource from an MCP server", 75 | 76 | validate: async (runtime: IAgentRuntime, _message: Memory, _state?: State): Promise => { 77 | const mcpService = runtime.getService(MCP_SERVICE_NAME); 78 | if (!mcpService) return false; 79 | 80 | const servers = mcpService.getServers(); 81 | return ( 82 | servers.length > 0 && 83 | servers.some( 84 | (server) => server.status === "connected" && server.resources && server.resources.length > 0 85 | ) 86 | ); 87 | }, 88 | 89 | handler: async ( 90 | runtime: IAgentRuntime, 91 | message: Memory, 92 | _state?: State, 93 | _options?: { [key: string]: unknown }, 94 | callback?: HandlerCallback 95 | ): Promise => { 96 | const composedState = await runtime.composeState(message, ["RECENT_MESSAGES", "MCP"]); 97 | 98 | const mcpService = runtime.getService(MCP_SERVICE_NAME); 99 | if (!mcpService) { 100 | throw new Error("MCP service not available"); 101 | } 102 | 103 | const mcpProvider = mcpService.getProviderData(); 104 | 105 | try { 106 | await sendInitialResponse(callback); 107 | 108 | const resourceSelectionPrompt = createResourceSelectionPrompt( 109 | composedState, 110 | message.content.text || "" 111 | ); 112 | 113 | const resourceSelection = await runtime.useModel(ModelType.TEXT_SMALL, { 114 | prompt: resourceSelectionPrompt, 115 | }); 116 | 117 | const parsedSelection = await withModelRetry( 118 | resourceSelection, 119 | runtime, 120 | (data) => validateResourceSelection(data), 121 | message, 122 | composedState, 123 | (originalResponse, errorMessage, state, userMessage) => 124 | createResourceSelectionFeedbackPrompt(originalResponse, errorMessage, state, userMessage), 125 | callback, 126 | "I'm having trouble figuring out where to find the information you're looking for. Could you provide more details about what you need?" 127 | ); 128 | 129 | if (!parsedSelection || parsedSelection.noResourceAvailable) { 130 | if (callback && parsedSelection?.noResourceAvailable) { 131 | await callback({ 132 | text: "I don't have a specific resource that contains the information you're looking for. Let me try to assist you directly instead.", 133 | thought: 134 | "No appropriate MCP resource available for this request. Falling back to direct assistance.", 135 | actions: ["REPLY"], 136 | }); 137 | } 138 | return true; 139 | } 140 | 141 | const { serverName, uri, reasoning } = parsedSelection; 142 | 143 | logger.debug(`Selected resource "${uri}" on server "${serverName}" because: ${reasoning}`); 144 | 145 | const result = await mcpService.readResource(serverName, uri); 146 | logger.debug(`Read resource ${uri} from server ${serverName}`); 147 | 148 | const { resourceContent, resourceMeta } = processResourceResult(result, uri); 149 | 150 | await handleResourceAnalysis( 151 | runtime, 152 | message, 153 | uri, 154 | serverName, 155 | resourceContent, 156 | resourceMeta, 157 | callback 158 | ); 159 | 160 | return true; 161 | } catch (error) { 162 | return handleMcpError( 163 | composedState, 164 | mcpProvider, 165 | error, 166 | runtime, 167 | message, 168 | "resource", 169 | callback 170 | ); 171 | } 172 | }, 173 | 174 | examples: [ 175 | [ 176 | { 177 | name: "{{user}}", 178 | content: { 179 | text: "Can you get the documentation about installing ElizaOS?", 180 | }, 181 | }, 182 | { 183 | name: "{{assistant}}", 184 | content: { 185 | text: `I'll retrieve that information for you. Let me access the resource...`, 186 | actions: ["READ_MCP_RESOURCE"], 187 | }, 188 | }, 189 | { 190 | name: "{{assistant}}", 191 | content: { 192 | text: `ElizaOS installation is straightforward. You'll need Node.js 23+ and Git installed. For Windows users, WSL 2 is required. The quickest way to get started is by cloning the ElizaOS starter repository with \`git clone https://github.com/elizaos/eliza-starter.git\`, then run \`cd eliza-starter && cp .env.example .env && bun i && bun run build && bun start\`. This will set up a development environment with the core features enabled. After starting, you can access the web interface at http://localhost:3000 to interact with your agent.`, 193 | actions: ["READ_MCP_RESOURCE"], 194 | }, 195 | }, 196 | ], 197 | ], 198 | }; 199 | -------------------------------------------------------------------------------- /src/utils/processing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Content, 3 | type HandlerCallback, 4 | type IAgentRuntime, 5 | type Media, 6 | type Memory, 7 | ModelType, 8 | createUniqueUuid, 9 | logger, 10 | } from "@elizaos/core"; 11 | import { type State, composePromptFromState } from "@elizaos/core"; 12 | import { resourceAnalysisTemplate } from "../templates/resourceAnalysisTemplate"; 13 | import { toolReasoningTemplate } from "../templates/toolReasoningTemplate"; 14 | import { createMcpMemory } from "./mcp"; 15 | 16 | export function processResourceResult( 17 | result: { 18 | contents: Array<{ 19 | uri: string; 20 | mimeType?: string; 21 | text?: string; 22 | blob?: string; 23 | }>; 24 | }, 25 | uri: string 26 | ): { resourceContent: string; resourceMeta: string } { 27 | let resourceContent = ""; 28 | let resourceMeta = ""; 29 | 30 | for (const content of result.contents) { 31 | if (content.text) { 32 | resourceContent += content.text; 33 | } else if (content.blob) { 34 | resourceContent += `[Binary data - ${content.mimeType || "unknown type"}]`; 35 | } 36 | 37 | resourceMeta += `Resource: ${content.uri || uri}\n`; 38 | if (content.mimeType) { 39 | resourceMeta += `Type: ${content.mimeType}\n`; 40 | } 41 | } 42 | 43 | return { resourceContent, resourceMeta }; 44 | } 45 | 46 | export function processToolResult( 47 | result: { 48 | content: Array<{ 49 | type: string; 50 | text?: string; 51 | mimeType?: string; 52 | data?: string; 53 | resource?: { 54 | uri: string; 55 | text?: string; 56 | blob?: string; 57 | }; 58 | }>; 59 | isError?: boolean; 60 | }, 61 | serverName: string, 62 | toolName: string, 63 | runtime: IAgentRuntime, 64 | messageEntityId: string 65 | ): { toolOutput: string; hasAttachments: boolean; attachments: Media[] } { 66 | let toolOutput = ""; 67 | let hasAttachments = false; 68 | const attachments: Media[] = []; 69 | 70 | for (const content of result.content) { 71 | if (content.type === "text") { 72 | toolOutput += content.text; 73 | } else if (content.type === "image") { 74 | hasAttachments = true; 75 | attachments.push({ 76 | contentType: content.mimeType, 77 | url: `data:${content.mimeType};base64,${content.data}`, 78 | id: createUniqueUuid(runtime, messageEntityId), 79 | title: "Generated image", 80 | source: `${serverName}/${toolName}`, 81 | description: "Tool-generated image", 82 | text: "Generated image", 83 | }); 84 | } else if (content.type === "resource") { 85 | const resource = content.resource; 86 | if (resource && "text" in resource) { 87 | toolOutput += `\n\nResource (${resource.uri}):\n${resource.text}`; 88 | } else if (resource && "blob" in resource) { 89 | toolOutput += `\n\nResource (${resource.uri}): [Binary data]`; 90 | } 91 | } 92 | } 93 | 94 | return { toolOutput, hasAttachments, attachments }; 95 | } 96 | 97 | export async function handleResourceAnalysis( 98 | runtime: IAgentRuntime, 99 | message: Memory, 100 | uri: string, 101 | serverName: string, 102 | resourceContent: string, 103 | resourceMeta: string, 104 | callback?: HandlerCallback 105 | ): Promise { 106 | await createMcpMemory(runtime, message, "resource", serverName, resourceContent, { 107 | uri, 108 | isResourceAccess: true, 109 | }); 110 | 111 | const analysisPrompt = createAnalysisPrompt( 112 | uri, 113 | message.content.text || "", 114 | resourceContent, 115 | resourceMeta 116 | ); 117 | 118 | const analyzedResponse = await runtime.useModel(ModelType.TEXT_SMALL, { 119 | prompt: analysisPrompt, 120 | }); 121 | 122 | if (callback) { 123 | await callback({ 124 | text: analyzedResponse, 125 | thought: `I analyzed the content from the ${uri} resource on ${serverName} and crafted a thoughtful response that addresses the user's request while maintaining my conversational style.`, 126 | actions: ["READ_MCP_RESOURCE"], 127 | }); 128 | } 129 | } 130 | 131 | export async function handleToolResponse( 132 | runtime: IAgentRuntime, 133 | message: Memory, 134 | serverName: string, 135 | toolName: string, 136 | toolArgs: Record, 137 | toolOutput: string, 138 | hasAttachments: boolean, 139 | attachments: Media[], 140 | state: State, 141 | mcpProvider: { 142 | values: { mcp: unknown }; 143 | data: { mcp: unknown }; 144 | text: string; 145 | }, 146 | callback?: HandlerCallback 147 | ): Promise { 148 | await createMcpMemory(runtime, message, "tool", serverName, toolOutput, { 149 | toolName, 150 | arguments: toolArgs, 151 | isToolCall: true, 152 | }); 153 | 154 | const reasoningPrompt = createReasoningPrompt( 155 | state, 156 | mcpProvider, 157 | toolName, 158 | serverName, 159 | message.content.text || "", 160 | toolOutput, 161 | hasAttachments 162 | ); 163 | 164 | logger.info("reasoning prompt: ", reasoningPrompt); 165 | 166 | const reasonedResponse = await runtime.useModel(ModelType.TEXT_SMALL, { 167 | prompt: reasoningPrompt, 168 | }); 169 | 170 | if (callback) { 171 | await callback({ 172 | text: reasonedResponse, 173 | thought: `I analyzed the output from the ${toolName} tool on ${serverName} and crafted a thoughtful response that addresses the user's request while maintaining my conversational style.`, 174 | actions: ["CALL_MCP_TOOL"], 175 | attachments: hasAttachments ? attachments : undefined, 176 | }); 177 | } 178 | } 179 | 180 | export async function sendInitialResponse(callback?: HandlerCallback): Promise { 181 | if (callback) { 182 | const responseContent: Content = { 183 | thought: 184 | "The user is asking for information that can be found in an MCP resource. I will retrieve and analyze the appropriate resource.", 185 | text: "I'll retrieve that information for you. Let me access the resource...", 186 | actions: ["READ_MCP_RESOURCE"], 187 | }; 188 | await callback(responseContent); 189 | } 190 | } 191 | 192 | function createAnalysisPrompt( 193 | uri: string, 194 | userMessage: string, 195 | resourceContent: string, 196 | resourceMeta: string 197 | ): string { 198 | const enhancedState: State = { 199 | data: {}, 200 | text: "", 201 | values: { 202 | uri, 203 | userMessage, 204 | resourceContent, 205 | resourceMeta, 206 | }, 207 | }; 208 | 209 | return composePromptFromState({ 210 | state: enhancedState, 211 | template: resourceAnalysisTemplate, 212 | }); 213 | } 214 | 215 | function createReasoningPrompt( 216 | state: State, 217 | mcpProvider: { 218 | values: { mcp: unknown }; 219 | data: { mcp: unknown }; 220 | text: string; 221 | }, 222 | toolName: string, 223 | serverName: string, 224 | userMessage: string, 225 | toolOutput: string, 226 | hasAttachments: boolean 227 | ): string { 228 | const enhancedState: State = { 229 | ...state, 230 | values: { 231 | ...state.values, 232 | mcpProvider, 233 | toolName, 234 | serverName, 235 | userMessage, 236 | toolOutput, 237 | hasAttachments, 238 | }, 239 | }; 240 | 241 | return composePromptFromState({ 242 | state: enhancedState, 243 | template: toolReasoningTemplate, 244 | }); 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Plugin for ElizaOS 2 | 3 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-blue.svg)](https://conventionalcommits.org) 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. 6 | 7 | ## 🔍 What is MCP? 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 | - **Prompts**: Templated messages and workflows 15 | - **Tools**: Functions for the agent to execute 16 | 17 | ## 📦 Installation 18 | 19 | Install the plugin in your ElizaOS project: 20 | 21 | - **npm** 22 | 23 | ```bash 24 | npm install @fleek-platform/eliza-plugin-mcp 25 | ``` 26 | 27 | - **pnpm** 28 | 29 | ```bash 30 | pnpm install @fleek-platform/eliza-plugin-mcp 31 | ``` 32 | 33 | - **yarn** 34 | 35 | ```bash 36 | yarn add @fleek-platform/eliza-plugin-mcp 37 | ``` 38 | 39 | - **bun** 40 | 41 | ```bash 42 | bun add @fleek-platform/eliza-plugin-mcp 43 | ``` 44 | 45 | ## 🚀 Usage 46 | 47 | 1. Add the plugin to your character configuration: 48 | 49 | ```json 50 | { 51 | "name": "Your Character", 52 | "plugins": ["@fleek-platform/eliza-plugin-mcp"], 53 | "settings": { 54 | "mcp": { 55 | "servers": { 56 | "github": { 57 | "type": "stdio", 58 | "name": "Code Server", 59 | "command": "npx", 60 | "args": ["-y", "@modelcontextprotocol/server-github"] 61 | } 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | ## ⚙️ Configuration Options 69 | 70 | MCP supports two types of servers: "stdio" and "sse". Each type has its own configuration options. 71 | 72 | ### Common Options 73 | 74 | | Option | Type | Description | 75 | | ---------- | ------- | ----------------------------------------------- | 76 | | `type` | string | The type of MCP server: "stdio" or "sse" | 77 | | `name` | string | The display name of the server | 78 | | `timeout` | number | Timeout in seconds for tool calls (default: 60) | 79 | | `disabled` | boolean | Whether the server is disabled | 80 | 81 | ### stdio Server Options 82 | 83 | | Option | Type | Description | 84 | | --------- | -------- | ------------------------------------------------- | 85 | | `command` | string | The command to run the MCP server | 86 | | `args` | string[] | Command-line arguments for the server | 87 | | `env` | object | Environment variables to pass to the server | 88 | | `cwd` | string | _Optional_ Working directory to run the server in | 89 | 90 | ### sse Server Options 91 | 92 | | Option | Type | Description | 93 | | --------- | ------ | -------------------------------------- | 94 | | `url` | string | The URL of the SSE endpoint | 95 | 96 | ## 🛠️ Using MCP Capabilities 97 | 98 | Once configured, the plugin automatically exposes MCP servers' capabilities to your agent: 99 | 100 | ### Context Providers 101 | 102 | The plugin includes three providers that add MCP capabilities to the agent's context: 103 | 104 | 1. `MCP_SERVERS`: Lists available servers and their tools, resources and prompts 105 | 106 | ## 🔄 Plugin Flow 107 | 108 | The following diagram illustrates the MCP plugin's flow for tool selection and execution: 109 | 110 | ```mermaid 111 | graph TD 112 | %% Starting point - User request 113 | start[User Request] --> action[CALL_TOOL Action] 114 | 115 | %% MCP Server Validation 116 | action --> check{MCP Servers Available?} 117 | check -->|No| fail[Return No Tools Available] 118 | 119 | %% Tool Selection Flow 120 | check -->|Yes| state[Get MCP Provider Data] 121 | state --> prompt[Create Tool Selection Prompt] 122 | 123 | %% First Model Use - Tool Selection 124 | prompt --> model1[Use Language Model for Tool Selection] 125 | model1 --> parse[Parse Selection] 126 | parse --> retry{Valid Selection?} 127 | 128 | %% Second Model Use - Retry Selection 129 | retry -->|No| feedback[Generate Feedback] 130 | feedback --> model2[Use Language Model for Retry] 131 | model2 --> parse 132 | 133 | %% Tool Selection Result 134 | retry -->|Yes| toolAvailable{Tool Available?} 135 | toolAvailable -->|No| fallback[Fallback Response] 136 | 137 | %% Tool Execution Flow 138 | toolAvailable -->|Yes| callTool[Call MCP Tool] 139 | callTool --> processResult[Process Tool Result] 140 | 141 | %% Memory Creation 142 | processResult --> createMemory[Create Memory Record] 143 | createMemory --> reasoningPrompt[Create Reasoning Prompt] 144 | 145 | %% Third Model Use - Response Generation 146 | reasoningPrompt --> model3[Use Language Model for Response] 147 | model3 --> respondToUser[Send Response to User] 148 | 149 | %% Styling 150 | classDef model fill:#f9f,stroke:#333,stroke-width:2px; 151 | classDef decision fill:#bbf,stroke:#333,stroke-width:2px; 152 | classDef output fill:#bfb,stroke:#333,stroke-width:2px; 153 | 154 | class model1,model2,model3 model; 155 | class check,retry,toolAvailable decision; 156 | class respondToUser,fallback output; 157 | ``` 158 | 159 | ## 📋 Example: Setting Up Multiple MCP Servers 160 | 161 | Here's a complete example configuration with multiple MCP servers of both types: 162 | 163 | ```json 164 | { 165 | "name": "Developer Assistant", 166 | "plugins": ["@elizaos/plugin-mcp", "other-plugins"], 167 | "settings": { 168 | "mcp": { 169 | "servers": { 170 | "github": { 171 | "command": "npx", 172 | "args": ["-y", "@modelcontextprotocol/server-github"], 173 | "env": { 174 | "GITHUB_PERSONAL_ACCESS_TOKEN": "" 175 | } 176 | }, 177 | "puppeteer": { 178 | "command": "npx", 179 | "args": ["-y", "@modelcontextprotocol/server-puppeteer"] 180 | }, 181 | "google-maps": { 182 | "command": "npx", 183 | "args": ["-y", "@modelcontextprotocol/server-google-maps"], 184 | "env": { 185 | "GOOGLE_MAPS_API_KEY": "" 186 | } 187 | } 188 | }, 189 | "maxRetries": 2 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | ## 🔒 Security Considerations 196 | 197 | Please be aware that MCP servers can execute arbitrary code, so only connect to servers you trust. 198 | 199 | ## 🔍 Troubleshooting 200 | 201 | If you encounter issues with the MCP plugin: 202 | 203 | 1. Check that your MCP servers are correctly configured and running 204 | 2. Ensure the commands are accessible in the ElizaOS environment 205 | 3. Review the logs for connection errors 206 | 4. Verify that the plugin is properly loaded in your character configuration 207 | 208 | ## 👥 Contributing 209 | 210 | Thanks for considering contributing to our project! 211 | 212 | ### How to Contribute 213 | 214 | 1. Fork the repository. 215 | 2. Create a new branch: `git checkout -b feature-branch-name`. 216 | 3. Make your changes. 217 | 4. Commit your changes using conventional commits. 218 | 5. Push to your fork and submit a pull request. 219 | 220 | ### Commit Guidelines 221 | 222 | We use [Conventional Commits](https://www.conventionalcommits.org/) for our commit messages: 223 | 224 | - `test`: 💍 Adding missing tests 225 | - `feat`: 🎸 A new feature 226 | - `fix`: 🐛 A bug fix 227 | - `chore`: 🤖 Build process or auxiliary tool changes 228 | - `docs`: ✏️ Documentation only changes 229 | - `refactor`: 💡 A code change that neither fixes a bug or adds a feature 230 | - `style`: 💄 Markup, white-space, formatting, missing semi-colons... 231 | 232 | ## 📄 License 233 | 234 | This plugin is released under the same license as ElizaOS. -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import { type IAgentRuntime, Service, logger } from "@elizaos/core"; 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 3 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; 4 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 5 | import type { 6 | CallToolResult, 7 | Resource, 8 | ResourceTemplate, 9 | Tool, 10 | } from "@modelcontextprotocol/sdk/types.js"; 11 | import { 12 | DEFAULT_MCP_TIMEOUT_SECONDS, 13 | MCP_SERVICE_NAME, 14 | type McpConnection, 15 | type McpProvider, 16 | type McpResourceResponse, 17 | type McpServer, 18 | type McpServerConfig, 19 | type McpSettings, 20 | type SseMcpServerConfig, 21 | type StdioMcpServerConfig, 22 | } from "./types"; 23 | import { buildMcpProviderData } from "./utils/mcp"; 24 | 25 | export class McpService extends Service { 26 | static serviceType: string = MCP_SERVICE_NAME; 27 | capabilityDescription = "Enables the agent to interact with MCP (Model Context Protocol) servers"; 28 | 29 | private connections: McpConnection[] = []; 30 | private mcpProvider: McpProvider = { 31 | values: { mcp: {} }, 32 | data: { mcp: {} }, 33 | text: "", 34 | }; 35 | 36 | constructor(runtime: IAgentRuntime) { 37 | super(runtime); 38 | this.initializeMcpServers(); 39 | } 40 | 41 | static async start(runtime: IAgentRuntime): Promise { 42 | const service = new McpService(runtime); 43 | return service; 44 | } 45 | 46 | async stop(): Promise { 47 | for (const connection of this.connections) { 48 | try { 49 | await this.deleteConnection(connection.server.name); 50 | } catch (error) { 51 | logger.error( 52 | `Failed to close connection for ${connection.server.name}:`, 53 | error instanceof Error ? error.message : String(error) 54 | ); 55 | } 56 | } 57 | this.connections = []; 58 | } 59 | 60 | private async initializeMcpServers(): Promise { 61 | try { 62 | const mcpSettings = this.getMcpSettings(); 63 | 64 | if (!mcpSettings || !mcpSettings.servers) { 65 | logger.info("No MCP servers configured."); 66 | return; 67 | } 68 | 69 | await this.updateServerConnections(mcpSettings.servers); 70 | 71 | const servers = this.getServers(); 72 | 73 | this.mcpProvider = buildMcpProviderData(servers); 74 | } catch (error) { 75 | logger.error( 76 | "Failed to initialize MCP servers:", 77 | error instanceof Error ? error.message : String(error) 78 | ); 79 | } 80 | } 81 | 82 | private getMcpSettings(): McpSettings | undefined { 83 | return this.runtime.getSetting("mcp") as McpSettings; 84 | } 85 | 86 | private async updateServerConnections( 87 | serverConfigs: Record 88 | ): Promise { 89 | const currentNames = new Set(this.connections.map((conn) => conn.server.name)); 90 | const newNames = new Set(Object.keys(serverConfigs)); 91 | 92 | for (const name of currentNames) { 93 | if (!newNames.has(name)) { 94 | await this.deleteConnection(name); 95 | logger.info(`Deleted MCP server: ${name}`); 96 | } 97 | } 98 | 99 | for (const [name, config] of Object.entries(serverConfigs)) { 100 | const currentConnection = this.connections.find((conn) => conn.server.name === name); 101 | 102 | if (!currentConnection) { 103 | try { 104 | await this.connectToServer(name, config); 105 | } catch (error) { 106 | logger.error( 107 | `Failed to connect to new MCP server ${name}:`, 108 | error instanceof Error ? error.message : String(error) 109 | ); 110 | } 111 | } else if (JSON.stringify(config) !== currentConnection.server.config) { 112 | try { 113 | await this.deleteConnection(name); 114 | await this.connectToServer(name, config); 115 | logger.info(`Reconnected MCP server with updated config: ${name}`); 116 | } catch (error) { 117 | logger.error( 118 | `Failed to reconnect MCP server ${name}:`, 119 | error instanceof Error ? error.message : String(error) 120 | ); 121 | } 122 | } 123 | } 124 | } 125 | 126 | private async buildStdioClientTransport(name: string, config: StdioMcpServerConfig) { 127 | if (!config.command) { 128 | throw new Error(`Missing command for stdio MCP server ${name}`); 129 | } 130 | 131 | return new StdioClientTransport({ 132 | command: config.command, 133 | args: config.args, 134 | env: { 135 | ...config.env, 136 | ...(process.env.PATH ? { PATH: process.env.PATH } : {}), 137 | }, 138 | stderr: "pipe", 139 | cwd: config.cwd, 140 | }); 141 | } 142 | 143 | private async buildSseClientTransport(name: string, config: SseMcpServerConfig) { 144 | if (!config.url) { 145 | throw new Error(`Missing URL for SSE MCP server ${name}`); 146 | } 147 | 148 | return new SSEClientTransport(new URL(config.url)); 149 | } 150 | 151 | private async connectToServer(name: string, config: McpServerConfig): Promise { 152 | this.connections = this.connections.filter((conn) => conn.server.name !== name); 153 | 154 | try { 155 | const client = new Client( 156 | { 157 | name: "ElizaOS", 158 | version: "1.0.0", 159 | }, 160 | { 161 | capabilities: {}, 162 | } 163 | ); 164 | 165 | const transport: StdioClientTransport | SSEClientTransport = 166 | config.type === "stdio" 167 | ? await this.buildStdioClientTransport(name, config) 168 | : await this.buildSseClientTransport(name, config); 169 | 170 | const connection: McpConnection = { 171 | server: { 172 | name, 173 | config: JSON.stringify(config), 174 | status: "connecting", 175 | }, 176 | client, 177 | transport, 178 | }; 179 | 180 | this.connections.push(connection); 181 | 182 | transport.onerror = async (error) => { 183 | logger.error(`Transport error for "${name}":`, error); 184 | connection.server.status = "disconnected"; 185 | this.appendErrorMessage(connection, error.message); 186 | }; 187 | 188 | transport.onclose = async () => { 189 | connection.server.status = "disconnected"; 190 | }; 191 | 192 | await client.connect(transport); 193 | 194 | connection.server = { 195 | status: "connected", 196 | name, 197 | config: JSON.stringify(config), 198 | error: "", 199 | tools: await this.fetchToolsList(name), 200 | resources: await this.fetchResourcesList(name), 201 | resourceTemplates: await this.fetchResourceTemplatesList(name), 202 | }; 203 | 204 | logger.info(`Successfully connected to MCP server: ${name}`); 205 | } catch (error) { 206 | const connection = await this.getServerConnection(name); 207 | if (connection) { 208 | connection.server.status = "disconnected"; 209 | this.appendErrorMessage(connection, error instanceof Error ? error.message : String(error)); 210 | } 211 | throw error; 212 | } 213 | } 214 | 215 | private appendErrorMessage(connection: McpConnection, error: string) { 216 | const newError = connection.server.error ? `${connection.server.error}\n${error}` : error; 217 | connection.server.error = newError; 218 | } 219 | 220 | async deleteConnection(name: string): Promise { 221 | const connection = this.getServerConnection(name); 222 | if (connection) { 223 | try { 224 | await connection.transport.close(); 225 | await connection.client.close(); 226 | } catch (error) { 227 | logger.error( 228 | `Failed to close transport for ${name}:`, 229 | error instanceof Error ? error.message : String(error) 230 | ); 231 | } 232 | this.connections = this.connections.filter((conn) => conn.server.name !== name); 233 | } 234 | } 235 | 236 | private getServerConnection(serverName: string): McpConnection | undefined { 237 | return this.connections.find((conn) => conn.server.name === serverName); 238 | } 239 | 240 | private async fetchToolsList(serverName: string): Promise { 241 | try { 242 | const connection = this.getServerConnection(serverName); 243 | if (!connection) { 244 | return []; 245 | } 246 | 247 | const response = await connection.client.listTools(); 248 | 249 | const tools = (response?.tools || []).map((tool) => ({ 250 | ...tool, 251 | })); 252 | 253 | logger.info(`Fetched ${tools.length} tools for ${serverName}`); 254 | for (const tool of tools) { 255 | logger.info(`[${serverName}] ${tool.name}: ${tool.description}`); 256 | } 257 | 258 | return tools; 259 | } catch (error) { 260 | logger.error( 261 | `Failed to fetch tools for ${serverName}:`, 262 | error instanceof Error ? error.message : String(error) 263 | ); 264 | return []; 265 | } 266 | } 267 | 268 | private async fetchResourcesList(serverName: string): Promise { 269 | try { 270 | const connection = this.getServerConnection(serverName); 271 | if (!connection) { 272 | return []; 273 | } 274 | 275 | const response = await connection.client.listResources(); 276 | return response?.resources || []; 277 | } catch (error) { 278 | logger.warn( 279 | `No resources found for ${serverName}:`, 280 | error instanceof Error ? error.message : String(error) 281 | ); 282 | return []; 283 | } 284 | } 285 | 286 | private async fetchResourceTemplatesList(serverName: string): Promise { 287 | try { 288 | const connection = this.getServerConnection(serverName); 289 | if (!connection) { 290 | return []; 291 | } 292 | 293 | const response = await connection.client.listResourceTemplates(); 294 | return response?.resourceTemplates || []; 295 | } catch (error) { 296 | logger.warn( 297 | `No resource templates found for ${serverName}:`, 298 | error instanceof Error ? error.message : String(error) 299 | ); 300 | return []; 301 | } 302 | } 303 | 304 | public getServers(): McpServer[] { 305 | return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server); 306 | } 307 | 308 | public getProviderData(): McpProvider { 309 | return this.mcpProvider; 310 | } 311 | 312 | public async callTool( 313 | serverName: string, 314 | toolName: string, 315 | toolArguments?: Record 316 | ): Promise { 317 | const connection = this.connections.find((conn) => conn.server.name === serverName); 318 | if (!connection) { 319 | throw new Error(`No connection found for server: ${serverName}`); 320 | } 321 | 322 | if (connection.server.disabled) { 323 | throw new Error(`Server "${serverName}" is disabled`); 324 | } 325 | 326 | let timeout = DEFAULT_MCP_TIMEOUT_SECONDS; 327 | try { 328 | const config = JSON.parse(connection.server.config); 329 | timeout = config.timeoutInMillis || DEFAULT_MCP_TIMEOUT_SECONDS; 330 | } catch (error) { 331 | logger.error( 332 | `Failed to parse timeout configuration for server ${serverName}:`, 333 | error instanceof Error ? error.message : String(error) 334 | ); 335 | } 336 | 337 | const result = await connection.client.callTool( 338 | { name: toolName, arguments: toolArguments }, 339 | undefined, 340 | { timeout } 341 | ); 342 | 343 | if (!result.content) { 344 | throw new Error("Invalid tool result: missing content array"); 345 | } 346 | 347 | return result as CallToolResult; 348 | } 349 | 350 | public async readResource(serverName: string, uri: string): Promise { 351 | const connection = this.connections.find((conn) => conn.server.name === serverName); 352 | if (!connection) { 353 | throw new Error(`No connection found for server: ${serverName}`); 354 | } 355 | 356 | if (connection.server.disabled) { 357 | throw new Error(`Server "${serverName}" is disabled`); 358 | } 359 | 360 | return await connection.client.readResource({ uri }); 361 | } 362 | 363 | public async restartConnection(serverName: string): Promise { 364 | const connection = this.connections.find((conn) => conn.server.name === serverName); 365 | const config = connection?.server.config; 366 | if (config) { 367 | logger.info(`Restarting ${serverName} MCP server...`); 368 | connection.server.status = "connecting"; 369 | connection.server.error = ""; 370 | 371 | try { 372 | await this.deleteConnection(serverName); 373 | 374 | await this.connectToServer(serverName, JSON.parse(config)); 375 | logger.info(`${serverName} MCP server connected`); 376 | } catch (error) { 377 | logger.error( 378 | `Failed to restart connection for ${serverName}:`, 379 | error instanceof Error ? error.message : String(error) 380 | ); 381 | throw new Error(`Failed to connect to ${serverName} MCP server`); 382 | } 383 | } 384 | } 385 | } 386 | --------------------------------------------------------------------------------