From 40d6ce53d7d370cb32d0e0109af0cd2e4d541da5 Mon Sep 17 00:00:00 2001 From: webhead Date: Tue, 10 Jun 2025 09:39:04 -0700 Subject: [PATCH] Add MCP (Model Context Protocol) server support - Implement MCPClient for stdio and SSE transport connections - Add MCPManager for centralized MCP server management - Create MCPToolAdapter to integrate MCP tools with existing infrastructure - Add MCPResourceHandler and MCPPromptHandler for extended capabilities - Update ConfigLoader to support mcpServerConfigurations - Extend connectors to accept mcpServers array alongside tools - Add comprehensive validation for MCP configurations - Include MCP_INTEGRATION.md documentation - Update README.md with MCP features - Add example MCP configurations to template.config.json Closes #3 --- MCP_INTEGRATION.md | 182 ++++++++++++++ README.md | 7 + package-lock.json | 227 ++++++++++++++++++ package.json | 1 + src/classes/configLoader.ts | 71 +++++- src/classes/connectors/BaseConnector.ts | 20 +- .../connectors/ToolsAnthropicConnector.ts | 8 +- .../connectors/ToolsOpenAIConnector.ts | 8 +- src/classes/mcp/MCPClient.ts | 206 ++++++++++++++++ src/classes/mcp/MCPManager.ts | 102 ++++++++ src/classes/mcp/MCPPromptHandler.ts | 72 ++++++ src/classes/mcp/MCPResourceHandler.ts | 73 ++++++ src/classes/mcp/MCPToolAdapter.ts | 58 +++++ template.config.json | 26 +- 14 files changed, 1053 insertions(+), 8 deletions(-) create mode 100644 MCP_INTEGRATION.md create mode 100644 src/classes/mcp/MCPClient.ts create mode 100644 src/classes/mcp/MCPManager.ts create mode 100644 src/classes/mcp/MCPPromptHandler.ts create mode 100644 src/classes/mcp/MCPResourceHandler.ts create mode 100644 src/classes/mcp/MCPToolAdapter.ts diff --git a/MCP_INTEGRATION.md b/MCP_INTEGRATION.md new file mode 100644 index 0000000..e710d5a --- /dev/null +++ b/MCP_INTEGRATION.md @@ -0,0 +1,182 @@ +# MCP (Model Context Protocol) Integration + +This document describes how to configure and use MCP servers with the Discord AI Chat Bot. + +## Overview + +The Model Context Protocol (MCP) allows the bot to connect to external servers that provide: +- **Tools**: Additional functions that can be called by AI models +- **Resources**: Files and data that can be accessed during conversations +- **Prompts**: Reusable prompt templates + +## Configuration + +### 1. Define MCP Servers + +Add MCP server configurations to your `config.json`: + +```json +{ + "mcpServerConfigurations": { + "filesystem": { + "transportType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"] + }, + "github": { + "transportType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "your-github-token" + } + }, + "memory": { + "transportType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-memory"] + }, + "custom-server": { + "transportType": "sse", + "url": "http://localhost:3000/mcp" + } + } +} +``` + +### 2. Assign MCP Servers to Connectors + +Add MCP servers to connector configurations: + +```json +{ + "connectorConfigurations": { + "ToolsOpenAIConnector": { + "class": "classes/connectors/ToolsOpenAIConnector", + "connectionOptions": { + "url": "https://api.openai.com/v1/chat/completions", + "apiKey": "OPENAI_KEY", + "tools": ["SearxingTool"], + "mcpServers": ["filesystem", "memory", "github"] + } + } + } +} +``` + +## Transport Types + +### stdio Transport +- Used for local MCP servers that run as processes +- Requires `command` and optionally `args` and `env` +- Example servers: filesystem, GitHub, SQLite + +### SSE (Server-Sent Events) Transport +- Used for remote MCP servers accessible via HTTP +- Requires `url` pointing to the SSE endpoint +- Suitable for cloud-hosted MCP servers + +## Available MCP Servers + +### Official Servers + +1. **Filesystem** (`@modelcontextprotocol/server-filesystem`) + - Access files and directories + - Read/write operations + - Safe sandboxed access + +2. **GitHub** (`@modelcontextprotocol/server-github`) + - Repository operations + - File reading/searching + - Issue and PR management + +3. **Memory** (`@modelcontextprotocol/server-memory`) + - Key-value storage + - Persistent memory across conversations + +4. **PostgreSQL** (`@modelcontextprotocol/server-postgres`) + - Database queries + - Schema inspection + +5. **Brave Search** (`@modelcontextprotocol/server-brave-search`) + - Web search capabilities + - Requires Brave API key + +### Community Servers + +See [MCP Servers List](https://github.com/modelcontextprotocol/servers) for more options. + +## How It Works + +1. **Initialization**: When the bot starts, it connects to all configured MCP servers +2. **Tool Discovery**: The bot automatically discovers tools from each MCP server +3. **Tool Integration**: MCP tools are seamlessly integrated with existing bot tools +4. **Resource Access**: MCP resources can be accessed as attachments in responses +5. **Prompt Templates**: MCP prompts can enhance system instructions + +## Features + +### MCP Tools +- Automatically discovered and made available to AI models +- Work alongside existing bot tools (SearxingTool, WolframTool, etc.) +- Support the same interfaces as native tools + +### MCP Resources +- Files and data from MCP servers +- Automatically converted to attachments in Discord messages +- Support for text, images, and other media types + +### MCP Prompts +- Reusable prompt templates +- Can be used to enhance system instructions +- Support for parameterized prompts + +## Example Use Cases + +1. **File System Access** + ```json + "mcpServers": ["filesystem"] + ``` + AI can read/write files in allowed directories + +2. **GitHub Integration** + ```json + "mcpServers": ["github"] + ``` + AI can browse repos, read code, manage issues + +3. **Persistent Memory** + ```json + "mcpServers": ["memory"] + ``` + AI can remember information across conversations + +4. **Combined Powers** + ```json + "mcpServers": ["filesystem", "github", "memory"] + ``` + Full suite of capabilities + +## Security Considerations + +- **Sandboxing**: Filesystem server only accesses allowed directories +- **Authentication**: Use environment variables for API keys +- **Transport Security**: SSE transport should use HTTPS in production +- **Tool Permissions**: Each connector only has access to its assigned MCP servers + +## Troubleshooting + +1. **Server won't start**: Check command path and arguments +2. **Tools not appearing**: Verify server is running and discoverable +3. **Authentication errors**: Ensure API keys are set in environment +4. **Connection timeouts**: Check firewall and network settings + +## Development + +To create a custom MCP server: +1. Implement the MCP protocol specification +2. Expose via stdio or SSE transport +3. Add configuration to `mcpServerConfigurations` +4. Assign to appropriate connectors + +See [MCP Documentation](https://modelcontextprotocol.io) for protocol details. \ No newline at end of file diff --git a/README.md b/README.md index c7bf850..9f71d34 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A powerful, multi-provider Discord bot for AI interactions. Seamlessly integrate - Content moderation - Configurable system instructions - Passive listening with custom triggers + - MCP (Model Context Protocol) server integration - **Discord Integration** - Slash commands @@ -53,6 +54,12 @@ A powerful, multi-provider Discord bot for AI interactions. Seamlessly integrate - Add Reactions - Use External Emojis +- **MCP Integration** 🔌 + - Connect to MCP servers for extended capabilities + - Access external tools and resources + - Support for filesystem, GitHub, databases, and more + - See [MCP Integration Guide](./MCP_INTEGRATION.md) for details + ## Quick Start 🚀 1. **Clone the Repository** diff --git a/package-lock.json b/package-lock.json index 77c9e21..c6819b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.2.1", "license": "ISC", "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0", "@thunder04/supermap": "^3.0.2", "discord.js": "^14.19.2", "pg": "^8.12.0" @@ -146,6 +147,17 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -222,6 +234,33 @@ "npm": ">=7.0.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/discord-api-types": { "version": "0.38.1", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.1.tgz", @@ -261,6 +300,40 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -473,6 +546,33 @@ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", "dev": true }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -481,6 +581,24 @@ "node": ">= 10.x" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", @@ -520,6 +638,15 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", @@ -548,6 +675,15 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/zod": { + "version": "3.25.57", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.57.tgz", + "integrity": "sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -629,6 +765,16 @@ } } }, + "@modelcontextprotocol/sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "requires": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, "@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -685,6 +831,21 @@ "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==" }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "discord-api-types": { "version": "0.38.1", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.1.tgz", @@ -715,6 +876,31 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -875,11 +1061,42 @@ "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", "dev": true }, + "raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", @@ -906,6 +1123,11 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", @@ -916,6 +1138,11 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "zod": { + "version": "3.25.57", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.57.tgz", + "integrity": "sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==" } } } diff --git a/package.json b/package.json index d05fe67..61a2d2c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "author": "", "license": "ISC", "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0", "@thunder04/supermap": "^3.0.2", "discord.js": "^14.19.2", "pg": "^8.12.0" diff --git a/src/classes/configLoader.ts b/src/classes/configLoader.ts index 87b7b5d..3343392 100644 --- a/src/classes/configLoader.ts +++ b/src/classes/configLoader.ts @@ -2,11 +2,14 @@ import {readFileSync} from "fs"; import BaseConnector, { ConnectionOptions, GenerationOptions } from "./connectors/BaseConnector"; import BaseTool from "./tools/BaseTool"; import { join } from "path"; +import { MCPManager } from "./mcp/MCPManager"; +import { MCPServerConfig } from "./mcp/MCPClient"; export class ConfigLoader { static #config: Config; static #connectors: Record = {}; static #tools: Record = {}; + static #mcpManager = MCPManager.getInstance(); static { ConfigLoader.loadConfig(); } @@ -150,6 +153,48 @@ export class ConfigLoader { } } } + + // Validate MCP servers if provided + if (connectorConfig.connectionOptions.mcpServers !== undefined) { + if (!Array.isArray(connectorConfig.connectionOptions.mcpServers)) { + throw new Error(`connectorConfigurations.${name}.connectionOptions.mcpServers must be an array if provided`); + } + for (const serverName of connectorConfig.connectionOptions.mcpServers) { + if (typeof serverName !== "string") { + throw new Error(`connectorConfigurations.${name}.connectionOptions.mcpServers must contain only strings`); + } + if (config.mcpServerConfigurations && !config.mcpServerConfigurations[serverName]) { + throw new Error(`Connector ${name} references non-existent MCP server: ${serverName}`); + } + } + } + } + + // Validate MCP server configurations if provided + if (config.mcpServerConfigurations !== undefined) { + if (typeof config.mcpServerConfigurations !== "object" || config.mcpServerConfigurations === null) { + throw new Error("mcpServerConfigurations must be an object if provided"); + } + for (const [name, serverConfig] of Object.entries(config.mcpServerConfigurations) as [string, MCPServerConfig][]) { + if (typeof serverConfig.transportType !== "string" || !["stdio", "sse"].includes(serverConfig.transportType)) { + throw new Error(`mcpServerConfigurations.${name}.transportType must be "stdio" or "sse"`); + } + if (serverConfig.transportType === "stdio") { + if (typeof serverConfig.command !== "string") { + throw new Error(`mcpServerConfigurations.${name}.command must be a string for stdio transport`); + } + if (serverConfig.args !== undefined && !Array.isArray(serverConfig.args)) { + throw new Error(`mcpServerConfigurations.${name}.args must be an array if provided`); + } + } else if (serverConfig.transportType === "sse") { + if (typeof serverConfig.url !== "string") { + throw new Error(`mcpServerConfigurations.${name}.url must be a string for SSE transport`); + } + } + if (serverConfig.env !== undefined && (typeof serverConfig.env !== "object" || serverConfig.env === null)) { + throw new Error(`mcpServerConfigurations.${name}.env must be an object if provided`); + } + } } // Validate modelConfigurations @@ -216,7 +261,12 @@ export class ConfigLoader { } ConfigLoader.#config = config; - // Load tools first which are then used by connectors + // Load MCP servers first + this.loadMCPServers().catch(error => { + console.error("Failed to load some MCP servers:", error); + }); + + // Load tools which are then used by connectors this.loadTools(); this.loadConnectors(); console.info("Loaded config"); @@ -256,6 +306,24 @@ export class ConfigLoader { } this.#tools = tools; } + + private static async loadMCPServers() { + if (!ConfigLoader.#config.mcpServerConfigurations) { + return; + } + + for (const [serverName, serverConfig] of Object.entries(ConfigLoader.#config.mcpServerConfigurations)) { + try { + await this.#mcpManager.initializeServer(serverName, serverConfig); + } catch (error) { + console.error(`Failed to initialize MCP server ${serverName}:`, error); + } + } + } + + static getMCPToolsForConnector(mcpServerNames: string[]): BaseTool[] { + return this.#mcpManager.getToolsForServers(mcpServerNames); + } } export interface Config { @@ -274,6 +342,7 @@ export interface Config { connectorConfigurations: Record; toolConfigurations: Record; modelConfigurations: Record; + mcpServerConfigurations?: Record; } export interface HeyConfiguration { diff --git a/src/classes/connectors/BaseConnector.ts b/src/classes/connectors/BaseConnector.ts index 3dfa9a5..08bcb88 100644 --- a/src/classes/connectors/BaseConnector.ts +++ b/src/classes/connectors/BaseConnector.ts @@ -1,5 +1,6 @@ import { DiscordBotClient } from "../client"; import { UpdatesEmitter } from "../updatesEmitter"; +import { ConfigLoader } from "../configLoader"; export default abstract class BaseConnector { #connectionOptions: ConnectionOptions; @@ -8,9 +9,21 @@ export default abstract class BaseConnector { this.#connectionOptions = options; } get availableTools() { - return this.#connectionOptions.tools?.map(tool => { - return BaseConnector.client.toolInstances[tool]! - }) || [] + const tools = []; + + // Add regular tools + if (this.#connectionOptions.tools) { + tools.push(...this.#connectionOptions.tools.map(tool => { + return BaseConnector.client.toolInstances[tool]! + })); + } + + // Add MCP tools + if (this.#connectionOptions.mcpServers) { + tools.push(...ConfigLoader.getMCPToolsForConnector(this.#connectionOptions.mcpServers)); + } + + return tools; } get connectionOptions() { return this.#connectionOptions; @@ -30,6 +43,7 @@ export interface ConnectionOptions { url: string; apiKey: string; tools?: string[]; + mcpServers?: string[]; } export interface RequestOptions { diff --git a/src/classes/connectors/ToolsAnthropicConnector.ts b/src/classes/connectors/ToolsAnthropicConnector.ts index 37acfb6..c32a723 100644 --- a/src/classes/connectors/ToolsAnthropicConnector.ts +++ b/src/classes/connectors/ToolsAnthropicConnector.ts @@ -1,5 +1,6 @@ import BaseConnector, { ChatCompletionResult, ChatMessage, ChatMessageRoles, GenerationOptions, RequestOptions } from "./BaseConnector"; import { AnthropicToolDefinition } from "../tools/BaseTool"; +import { MCPResourceHandler } from "../mcp/MCPResourceHandler"; export default class ToolsAnthropicConnector extends BaseConnector { private collectedAttachments: any[] = []; @@ -11,11 +12,16 @@ export default class ToolsAnthropicConnector extends BaseConnector { this.collectedAttachments = []; // Reset attachments for new request const response = await this.executeToolCall(await this.formatMessages(messages), systemInstruction, generationOptions, requestOptions); + // Resolve any MCP resource attachments + const resolvedAttachments = this.collectedAttachments.length > 0 + ? await MCPResourceHandler.resolveResourceAttachments(this.collectedAttachments) + : []; + return { resultMessage: { role: ChatMessageRoles.ASSISTANT, content: response.content, - attachments: this.collectedAttachments + attachments: resolvedAttachments } }; } diff --git a/src/classes/connectors/ToolsOpenAIConnector.ts b/src/classes/connectors/ToolsOpenAIConnector.ts index f190ac6..ecd10b4 100644 --- a/src/classes/connectors/ToolsOpenAIConnector.ts +++ b/src/classes/connectors/ToolsOpenAIConnector.ts @@ -1,4 +1,5 @@ import BaseConnector, {ChatCompletionResult, ChatMessage, ConnectionOptions, GenerationOptions, RequestOptions} from "./BaseConnector"; +import { MCPResourceHandler } from "../mcp/MCPResourceHandler"; export default class ToolsOpenAIConnector extends BaseConnector { private collectedAttachments: any[] = []; @@ -30,10 +31,15 @@ export default class ToolsOpenAIConnector extends BaseConnector { } } + // Resolve any MCP resource attachments + const resolvedAttachments = this.collectedAttachments.length > 0 + ? await MCPResourceHandler.resolveResourceAttachments(this.collectedAttachments) + : []; + return { resultMessage: { ...response, - attachments: this.collectedAttachments, + attachments: resolvedAttachments, audio_data_string: response.audio?.data ? `data:audio/${generationOptions["audio"]?.["format"]};base64,${response.audio.data}` : undefined } }; diff --git a/src/classes/mcp/MCPClient.ts b/src/classes/mcp/MCPClient.ts new file mode 100644 index 0000000..b9edc98 --- /dev/null +++ b/src/classes/mcp/MCPClient.ts @@ -0,0 +1,206 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { + CallToolResultSchema, + ListResourcesResultSchema, + ListToolsResultSchema, + ListPromptsResultSchema, + ReadResourceResultSchema, + GetPromptResultSchema, + Tool, + Resource, + Prompt +} from "@modelcontextprotocol/sdk/types.js"; + +export interface MCPServerConfig { + command?: string; + args?: string[]; + url?: string; + transportType: "stdio" | "sse"; + env?: Record; +} + +export class MCPClient { + private client: Client; + private serverConfig: MCPServerConfig; + private connected: boolean = false; + private tools: Tool[] = []; + private resources: Resource[] = []; + private prompts: Prompt[] = []; + + constructor(serverName: string, config: MCPServerConfig) { + this.serverConfig = config; + this.client = new Client({ + name: `discord-bot-${serverName}`, + version: "1.0.0" + }, { + capabilities: { + tools: {}, + resources: {}, + prompts: {} + } + }); + } + + async connect(): Promise { + if (this.connected) return; + + let transport; + if (this.serverConfig.transportType === "stdio") { + if (!this.serverConfig.command) { + throw new Error("Command is required for stdio transport"); + } + transport = new StdioClientTransport({ + command: this.serverConfig.command, + args: this.serverConfig.args, + env: this.serverConfig.env + }); + } else if (this.serverConfig.transportType === "sse") { + if (!this.serverConfig.url) { + throw new Error("URL is required for SSE transport"); + } + transport = new SSEClientTransport(new URL(this.serverConfig.url)); + } else { + throw new Error(`Unsupported transport type: ${this.serverConfig.transportType}`); + } + + await this.client.connect(transport); + this.connected = true; + + // Discover available capabilities + await this.discoverCapabilities(); + } + + async disconnect(): Promise { + if (!this.connected) return; + await this.client.close(); + this.connected = false; + } + + private async discoverCapabilities(): Promise { + // Discover tools + try { + const toolsResult = await this.client.request( + { method: "tools/list" }, + ListToolsResultSchema + ); + this.tools = toolsResult.tools || []; + } catch (error) { + console.error("Failed to discover tools:", error); + } + + // Discover resources + try { + const resourcesResult = await this.client.request( + { method: "resources/list" }, + ListResourcesResultSchema + ); + this.resources = resourcesResult.resources || []; + } catch (error) { + console.error("Failed to discover resources:", error); + } + + // Discover prompts + try { + const promptsResult = await this.client.request( + { method: "prompts/list" }, + ListPromptsResultSchema + ); + this.prompts = promptsResult.prompts || []; + } catch (error) { + console.error("Failed to discover prompts:", error); + } + } + + async callTool(toolName: string, args: Record): Promise { + if (!this.connected) { + throw new Error("MCP client not connected"); + } + + const tool = this.tools.find(t => t.name === toolName); + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + + const result = await this.client.request( + { + method: "tools/call", + params: { + name: toolName, + arguments: args + } + }, + CallToolResultSchema + ); + + return result; + } + + async readResource(uri: string): Promise { + if (!this.connected) { + throw new Error("MCP client not connected"); + } + + const resource = this.resources.find(r => r.uri === uri); + if (!resource) { + throw new Error(`Resource ${uri} not found`); + } + + const result = await this.client.request( + { + method: "resources/read", + params: { uri } + }, + ReadResourceResultSchema + ); + + if (result.contents && result.contents.length > 0) { + const firstContent = result.contents[0]; + if (firstContent && typeof firstContent === 'object' && 'text' in firstContent) { + return String(firstContent.text); + } + } + return ""; + } + + async getPrompt(promptName: string, args: Record): Promise { + if (!this.connected) { + throw new Error("MCP client not connected"); + } + + const prompt = this.prompts.find(p => p.name === promptName); + if (!prompt) { + throw new Error(`Prompt ${promptName} not found`); + } + + const result = await this.client.request( + { + method: "prompts/get", + params: { + name: promptName, + arguments: args + } + }, + GetPromptResultSchema + ); + + return result.messages.map(m => m.content.text).join("\n"); + } + + getTools(): Tool[] { + return this.tools; + } + + getResources(): Resource[] { + return this.resources; + } + + getPrompts(): Prompt[] { + return this.prompts; + } + + isConnected(): boolean { + return this.connected; + } +} \ No newline at end of file diff --git a/src/classes/mcp/MCPManager.ts b/src/classes/mcp/MCPManager.ts new file mode 100644 index 0000000..78258e6 --- /dev/null +++ b/src/classes/mcp/MCPManager.ts @@ -0,0 +1,102 @@ +import { MCPClient, MCPServerConfig } from "./MCPClient"; +import { MCPToolAdapter } from "./MCPToolAdapter"; +import BaseTool from "../tools/BaseTool"; + +export class MCPManager { + private static instance: MCPManager; + private clients: Map = new Map(); + private toolAdapters: Map = new Map(); + + private constructor() {} + + static getInstance(): MCPManager { + if (!MCPManager.instance) { + MCPManager.instance = new MCPManager(); + } + return MCPManager.instance; + } + + async initializeServer(serverName: string, config: MCPServerConfig): Promise { + if (this.clients.has(serverName)) { + console.warn(`MCP server ${serverName} already initialized`); + return; + } + + const client = new MCPClient(serverName, config); + try { + await client.connect(); + this.clients.set(serverName, client); + + // Create tool adapters for all tools from this server + const tools = client.getTools(); + const adapters = tools.map(tool => new MCPToolAdapter(client, tool)); + this.toolAdapters.set(serverName, adapters); + + console.info(`Initialized MCP server ${serverName} with ${tools.length} tools`); + } catch (error) { + console.error(`Failed to initialize MCP server ${serverName}:`, error); + throw error; + } + } + + async disconnectServer(serverName: string): Promise { + const client = this.clients.get(serverName); + if (client) { + await client.disconnect(); + this.clients.delete(serverName); + this.toolAdapters.delete(serverName); + console.info(`Disconnected MCP server ${serverName}`); + } + } + + async disconnectAll(): Promise { + for (const [, client] of this.clients) { + await client.disconnect(); + } + this.clients.clear(); + this.toolAdapters.clear(); + } + + getClient(serverName: string): MCPClient | undefined { + return this.clients.get(serverName); + } + + getToolsForServers(serverNames: string[]): BaseTool[] { + const tools: BaseTool[] = []; + for (const serverName of serverNames) { + const adapters = this.toolAdapters.get(serverName); + if (adapters) { + tools.push(...adapters); + } + } + return tools; + } + + getAllAvailableTools(): BaseTool[] { + const tools: BaseTool[] = []; + for (const adapters of this.toolAdapters.values()) { + tools.push(...adapters); + } + return tools; + } + + async getResourceContent(serverName: string, uri: string): Promise { + const client = this.clients.get(serverName); + if (!client) { + throw new Error(`MCP server ${serverName} not found`); + } + return await client.readResource(uri); + } + + async getPrompt(serverName: string, promptName: string, args: Record): Promise { + const client = this.clients.get(serverName); + if (!client) { + throw new Error(`MCP server ${serverName} not found`); + } + return await client.getPrompt(promptName, args); + } + + getAvailableServers(): string[] { + return Array.from(this.clients.keys()); + } +} \ No newline at end of file diff --git a/src/classes/mcp/MCPPromptHandler.ts b/src/classes/mcp/MCPPromptHandler.ts new file mode 100644 index 0000000..164a945 --- /dev/null +++ b/src/classes/mcp/MCPPromptHandler.ts @@ -0,0 +1,72 @@ +import { MCPManager } from "./MCPManager"; + +export class MCPPromptHandler { + private static mcpManager = MCPManager.getInstance(); + + /** + * Gets an MCP prompt by name with arguments + * @param promptName The prompt name + * @param args Arguments for the prompt + * @returns The formatted prompt content + */ + static async getPrompt(promptName: string, args: Record = {}): Promise { + const servers = this.mcpManager.getAvailableServers(); + + for (const serverName of servers) { + try { + const promptContent = await this.mcpManager.getPrompt(serverName, promptName, args); + return promptContent; + } catch (error) { + // Try next server + continue; + } + } + + throw new Error(`Prompt ${promptName} not found in any MCP server`); + } + + /** + * Lists all available prompts across all MCP servers + */ + static async listAllPrompts(): Promise<{ serverName: string; prompts: any[] }[]> { + const servers = this.mcpManager.getAvailableServers(); + const allPrompts: { serverName: string; prompts: any[] }[] = []; + + for (const serverName of servers) { + const client = this.mcpManager.getClient(serverName); + if (client) { + const prompts = client.getPrompts(); + allPrompts.push({ serverName, prompts }); + } + } + + return allPrompts; + } + + /** + * Enhances a system instruction with MCP prompts + * @param baseInstruction The base system instruction + * @param promptNames Array of MCP prompt names to include + * @param promptArgs Arguments for the prompts + * @returns The enhanced system instruction + */ + static async enhanceSystemInstruction( + baseInstruction: string, + promptNames: string[], + promptArgs: Record> = {} + ): Promise { + let enhancedInstruction = baseInstruction; + + for (const promptName of promptNames) { + try { + const args = promptArgs[promptName] || {}; + const promptContent = await this.getPrompt(promptName, args); + enhancedInstruction += `\n\n${promptContent}`; + } catch (error) { + console.error(`Failed to get MCP prompt ${promptName}:`, error); + } + } + + return enhancedInstruction; + } +} \ No newline at end of file diff --git a/src/classes/mcp/MCPResourceHandler.ts b/src/classes/mcp/MCPResourceHandler.ts new file mode 100644 index 0000000..a4925f7 --- /dev/null +++ b/src/classes/mcp/MCPResourceHandler.ts @@ -0,0 +1,73 @@ +import { MCPManager } from "./MCPManager"; + +export class MCPResourceHandler { + private static mcpManager = MCPManager.getInstance(); + + /** + * Fetches MCP resource content by URI + * @param uri The resource URI (e.g., "file:///path/to/file.txt") + * @returns The resource content as a string + */ + static async fetchResource(uri: string): Promise { + // Try to find which server owns this resource + const servers = this.mcpManager.getAvailableServers(); + + for (const serverName of servers) { + try { + const content = await this.mcpManager.getResourceContent(serverName, uri); + return content; + } catch (error) { + // Try next server + continue; + } + } + + throw new Error(`Resource ${uri} not found in any MCP server`); + } + + /** + * Lists all available resources across all MCP servers + */ + static async listAllResources(): Promise<{ serverName: string; resources: any[] }[]> { + const servers = this.mcpManager.getAvailableServers(); + const allResources: { serverName: string; resources: any[] }[] = []; + + for (const serverName of servers) { + const client = this.mcpManager.getClient(serverName); + if (client) { + const resources = client.getResources(); + allResources.push({ serverName, resources }); + } + } + + return allResources; + } + + /** + * Converts MCP resource URIs in attachments to actual content + * @param attachments Array of attachment strings (URLs or URIs) + * @returns Array of attachments with MCP resources resolved + */ + static async resolveResourceAttachments(attachments: string[]): Promise { + const resolved: string[] = []; + + for (const attachment of attachments) { + // Check if this looks like an MCP resource URI + if (attachment.startsWith("file://") || attachment.startsWith("resource://")) { + try { + const content = await this.fetchResource(attachment); + // Convert to text attachment + const base64Content = Buffer.from(content).toString('base64'); + resolved.push(`data:text/plain;base64,${base64Content}`); + } catch (error) { + console.error(`Failed to resolve MCP resource ${attachment}:`, error); + resolved.push(attachment); // Keep original if resolution fails + } + } else { + resolved.push(attachment); + } + } + + return resolved; + } +} \ No newline at end of file diff --git a/src/classes/mcp/MCPToolAdapter.ts b/src/classes/mcp/MCPToolAdapter.ts new file mode 100644 index 0000000..1495a14 --- /dev/null +++ b/src/classes/mcp/MCPToolAdapter.ts @@ -0,0 +1,58 @@ +import BaseTool, { ToolCallData, ToolResponse } from "../tools/BaseTool"; +import { MCPClient } from "./MCPClient"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; + +export class MCPToolAdapter extends BaseTool { + private mcpClient: MCPClient; + private mcpTool: Tool; + + constructor(mcpClient: MCPClient, tool: Tool) { + super({ + name: tool.name, + description: tool.description || "", + parameters: tool.inputSchema as any + }); + this.mcpClient = mcpClient; + this.mcpTool = tool; + } + + async handleToolCall(parameters: ToolCallData): Promise { + try { + const result = await this.mcpClient.callTool(this.mcpTool.name, parameters); + + // Extract any attachments from the result + const attachments: string[] = []; + + // Handle different content types + if (result.content) { + for (const content of result.content) { + if (content.type === "image" && content.data) { + // Convert base64 image to data URL + const mimeType = content.mimeType || "image/png"; + attachments.push(`data:${mimeType};base64,${content.data}`); + } else if (content.type === "resource" && content.resource) { + // Add resource URI as attachment for later fetching + attachments.push(content.resource.uri); + } + } + } + + // Extract text content + let textResult = ""; + if (result.content) { + textResult = result.content + .filter((c: any) => c.type === "text") + .map((c: any) => c.text) + .join("\n"); + } + + return { + result: textResult || result, + attachments: attachments.length > 0 ? attachments : undefined + }; + } catch (error) { + console.error(`MCP tool call failed for ${this.mcpTool.name}:`, error); + throw new Error(`Tool execution failed: ${error instanceof Error ? error.message : "Unknown error"}`); + } + } +} \ No newline at end of file diff --git a/template.config.json b/template.config.json index b986a5a..a2d3fbf 100644 --- a/template.config.json +++ b/template.config.json @@ -71,7 +71,8 @@ "connectionOptions": { "url": "https://api.openai.com/v1/chat/completions", "apiKey": "OPENAI_KEY", - "tools": ["SearxingTool", "WolframTool"] + "tools": ["SearxingTool", "WolframTool"], + "mcpServers": ["filesystem", "memory"] } }, "ToolsAnthropicConnector": { @@ -79,7 +80,8 @@ "connectionOptions": { "url": "https://api.anthropic.com/v1/messages", "apiKey": "ANTHROPIC_KEY", - "tools": ["SearxingTool", "WolframTool"] + "tools": ["SearxingTool", "WolframTool"], + "mcpServers": ["brave-search"] } } }, @@ -91,6 +93,26 @@ "class": "classes/tools/WolframTool" } }, + "mcpServerConfigurations": { + "filesystem": { + "transportType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + }, + "brave-search": { + "transportType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-brave-search"], + "env": { + "BRAVE_API_KEY": "YOUR_BRAVE_API_KEY" + } + }, + "memory": { + "transportType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-memory"] + } + }, "modelConfigurations": { "OpenAI-MultiTool": { "connector": "ToolsOpenAIConnector",