├── LICENSE ├── CLAUDE.md ├── README.md └── mcp-extension.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jack Gaino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md for TypingMind MCP Extension 2 | 3 | ## Commands 4 | - No specific build/lint/test commands as this is a simple JS extension for TypingMind 5 | 6 | ## Code Style Guide 7 | - **Configuration**: Keep configuration values at top of file 8 | - **Constants**: Use UPPER_CASE for constants like `MCP_BRIDGE_URL` 9 | - **Async/Await**: Use async/await for promise handling 10 | - **Promises**: Chain properly with catch handlers 11 | - **Error Handling**: Use try/catch blocks with specific error messages 12 | - **Comments**: Keep code self-documenting with minimal comments 13 | - **Function Naming**: Use camelCase and descriptive names 14 | - **String Templates**: Use backtick strings for dynamic content 15 | - **DOM Manipulation**: Use clean, minimal DOM manipulation 16 | - **Debugging**: Console log with prefixes (e.g., "MCP Extension:") 17 | - **User Feedback**: Provide clear visual feedback for successes/errors 18 | 19 | ## Notes 20 | - This extension interfaces with MCP-Bridge to expose Model Context Protocol tools in TypingMind 21 | - Code should maintain backward compatibility with existing TypingMind extensions 22 | - All interface points with MCP-Bridge should be clearly documented -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypingMind MCP Extension 2 | 3 | A TypingMind extension that integrates with [MCP-Bridge](https://github.com/SecretiveShell/MCP-Bridge) to bring Model Context Protocol tools into TypingMind. 4 | 5 | ## Installation 6 | 7 | **Important:** It's highly recommended to download and verify this JavaScript file before using it. TypingMind only allows installation of extensions by providing a URL to a JavaScript file. 8 | 9 | 1. Download the `mcp-extension.js` file 10 | 2. Review the code to verify its safety and functionality 11 | 3. Host the file somewhere accessible (personal web server, GitHub Pages, etc.) 12 | 4. In TypingMind, add the extension by providing the URL to your hosted file 13 | 14 | ## Configuration 15 | 16 | The extension uses a configurable host and port for MCP-Bridge defined at the top of the file: 17 | ```javascript 18 | // MCP Bridge configuration 19 | const MCP_BRIDGE_URL = 'http://localhost:8000'; 20 | ``` 21 | 22 | You will need to modify this URL to match your MCP-Bridge setup before hosting the file. 23 | 24 | ## How It Works 25 | 26 | This extension: 27 | - Connects to your MCP-Bridge server 28 | - Fetches available Model Context Protocol tools 29 | - Registers them as TypingMind plugins 30 | - Tools will appear with the prefix "MCP -" in your plugins list 31 | 32 | ## Visual Feedback 33 | 34 | When the extension loads: 35 | 36 | - **Success**: A blue notification will appear at the bottom right of the screen with information about the number of tools loaded 37 | - **Error**: A red notification will appear at the bottom right if there's a connection issue or other error 38 | 39 | Notifications automatically disappear after 5 seconds. 40 | 41 | ## Debugging 42 | 43 | If you encounter issues: 44 | 45 | 1. Open your browser's developer console (F12 or right-click → Inspect → Console) 46 | 2. Look for logs prefixed with "MCP Extension" 47 | 3. Common errors: 48 | - Connection refused: Check if your MCP-Bridge is running 49 | - Invalid URL: Verify the `MCP_BRIDGE_URL` in the extension is correct 50 | - CORS errors: Your MCP-Bridge needs to allow requests from your TypingMind domain 51 | 52 | ## Limitations 53 | 54 | - No support for resources or prompts yet 55 | - Requires self-hosting the JS file 56 | 57 | ## References 58 | 59 | - [TypingMind Extensions Documentation](https://docs.typingmind.com/typing-mind-extensions) 60 | - [MCP-Bridge](https://github.com/SecretiveShell/MCP-Bridge) 61 | 62 | ## License 63 | 64 | MIT License - See LICENSE file for details -------------------------------------------------------------------------------- /mcp-extension.js: -------------------------------------------------------------------------------- 1 | // MCP Bridge configuration 2 | const MCP_BRIDGE_URL = 'http://localhost:8000'; 3 | 4 | // Utility functions for IndexedDB operations 5 | const DB_NAME = 'keyval-store'; 6 | const STORE_NAME = 'keyval'; 7 | const PLUGINS_KEY = 'TM_useInstalledPlugins'; 8 | 9 | async function getDB() { 10 | return new Promise((resolve, reject) => { 11 | const request = indexedDB.open(DB_NAME); 12 | request.onerror = () => reject(request.error); 13 | request.onsuccess = () => resolve(request.result); 14 | }); 15 | } 16 | 17 | async function getPlugins() { 18 | try { 19 | const db = await getDB(); return new Promise((resolve, reject) => { 20 | const transaction = db.transaction(STORE_NAME, 'readonly'); 21 | const store = transaction.objectStore(STORE_NAME); 22 | const request = store.get(PLUGINS_KEY); 23 | 24 | request.onerror = () => reject(request.error); 25 | request.onsuccess = () => { 26 | try { 27 | const plugins = request.result || []; 28 | resolve(plugins); 29 | } catch (e) { 30 | reject(new Error('Failed to parse plugins: ' + e.message)); 31 | } 32 | }; 33 | }); 34 | } catch (e) { 35 | console.error('Failed to read plugins:', e); 36 | return []; 37 | } 38 | } 39 | 40 | async function savePlugins(plugins) { 41 | try { 42 | const db = await getDB(); 43 | return new Promise((resolve, reject) => { 44 | const transaction = db.transaction(STORE_NAME, 'readwrite'); 45 | const store = transaction.objectStore(STORE_NAME); 46 | const request = store.put(plugins, PLUGINS_KEY); 47 | 48 | request.onerror = () => reject(request.error); 49 | request.onsuccess = () => resolve(); 50 | }); 51 | } catch (e) { 52 | console.error('Failed to save plugins:', e); 53 | throw e; 54 | } 55 | } 56 | 57 | // Notification display functions 58 | function displayToast(message, type = 'info') { 59 | console.log(`MCP Extension ${type}:`, message); 60 | 61 | // Set color based on notification type 62 | const bgColor = type === 'error' ? 'red' : '#007bff'; 63 | const prefix = type === 'error' ? 'Error' : 'Info'; 64 | 65 | // Create or update toast element 66 | const toastContainer = document.querySelector(`[data-mcp-extension-${type}]`) || 67 | (() => { 68 | const div = document.createElement('div'); 69 | div.setAttribute(`data-mcp-extension-${type}`, ''); 70 | div.style.cssText = `position:fixed;bottom:20px;right:20px;background:${bgColor};color:white;padding:10px;border-radius:5px;z-index:9999;`; 71 | document.body.appendChild(div); 72 | return div; 73 | })(); 74 | 75 | toastContainer.textContent = `MCP Extension ${prefix}: ${message}`; 76 | setTimeout(() => toastContainer.remove(), 5000); 77 | } 78 | 79 | function displayError(message) { 80 | console.error('MCP Extension Error:', message); 81 | displayToast(message, 'error'); 82 | } 83 | 84 | // Main plugin sync function 85 | async function syncMCPPlugins() { 86 | try { 87 | const response = await globalThis.fetch(`${MCP_BRIDGE_URL}/mcp/tools`); 88 | if (!response.ok) { 89 | throw new Error(`Failed to fetch MCP tools: ${response.statusText}`); 90 | } 91 | const mcpToolsData = await response.json(); 92 | await updateMCPPlugins(mcpToolsData); 93 | } catch (error) { 94 | displayError(error.message); 95 | } 96 | } 97 | 98 | // Plugin update logic 99 | async function updateMCPPlugins(mcpToolsData) { 100 | try { 101 | // Get current plugins 102 | const currentPlugins = await getPlugins(); 103 | const currentMCPPlugins = currentPlugins.filter(p => p.id?.startsWith('mcp_')); 104 | const nonMCPPlugins = currentPlugins.filter(p => !p.id?.startsWith('mcp_')); 105 | 106 | // Build new MCP plugins 107 | const newMCPPlugins = []; 108 | const categories = new Set(); 109 | 110 | for (const mcpName in mcpToolsData) { 111 | const { tools } = mcpToolsData[mcpName]; 112 | if (!Array.isArray(tools)) continue; 113 | 114 | categories.add(mcpName); 115 | 116 | for (const tool of tools) { 117 | const pluginId = `mcp_${tool.name}`; 118 | const existing = currentMCPPlugins.find(p => p.id === pluginId); 119 | 120 | const plugin = { 121 | uuid: existing?.uuid || crypto.randomUUID(), 122 | id: pluginId, 123 | emoji: "🔧", 124 | title: `MCP - ${tool.name}`, 125 | overviewMarkdown: `## ${tool.name}\n\n${tool.description}`, 126 | openaiSpec: { 127 | name: pluginId, 128 | description: tool.description, 129 | parameters: tool.inputSchema 130 | }, 131 | implementationType: "javascript", 132 | outputType: "respond_to_ai", 133 | code: `async function ${pluginId}(data) { 134 | const url = '${MCP_BRIDGE_URL}/mcp/tools/${tool.name}/call'; 135 | let body = data; 136 | if (typeof data === 'string') { 137 | const requiredParams = ${JSON.stringify(tool.inputSchema.required)}; 138 | if (requiredParams.length > 0) { 139 | body = { 140 | [requiredParams[0]]: data 141 | }; 142 | } 143 | } 144 | const response = await globalThis.fetch(url, { 145 | method: 'POST', 146 | headers: { 'Content-Type': 'application/json' }, 147 | body: JSON.stringify(body) 148 | }); 149 | if (!response.ok) throw new Error('Request failed: ' + response.statusText); 150 | return await response.json(); 151 | }` 152 | }; 153 | 154 | newMCPPlugins.push(plugin); 155 | } 156 | } 157 | 158 | // Calculate changes 159 | const added = newMCPPlugins.filter(p => !currentMCPPlugins.some(cp => cp.id === p.id)); 160 | const removed = currentMCPPlugins.filter(p => !newMCPPlugins.some(np => np.id === p.id)); 161 | const unchanged = newMCPPlugins.length - added.length; 162 | 163 | // Merge and save 164 | const updatedPlugins = [...nonMCPPlugins, ...newMCPPlugins]; 165 | await savePlugins(updatedPlugins); 166 | 167 | // Prepare notification message 168 | let message = ''; 169 | if (added.length > 0) { 170 | message += `Added ${added.length} plugin${added.length > 1 ? 's' : ''}. `; 171 | } 172 | if (removed.length > 0) { 173 | message += `Removed ${removed.length} plugin${removed.length > 1 ? 's' : ''}. `; 174 | } 175 | if (unchanged > 0) { 176 | message += `${unchanged} plugin${unchanged > 1 ? 's' : ''} unchanged. `; 177 | } 178 | message += `Total: ${newMCPPlugins.length} plugins across ${categories.size} categor${categories.size > 1 ? 'ies' : 'y'}.`; 179 | 180 | // Show toast notification 181 | displayToast(message); 182 | 183 | console.log('MCP plugins synchronized successfully', { 184 | total: updatedPlugins.length, 185 | mcp: newMCPPlugins.length, 186 | other: nonMCPPlugins.length, 187 | added: added.length, 188 | removed: removed.length, 189 | categories: Array.from(categories) 190 | }); 191 | } catch (error) { 192 | displayError(`Failed to update plugins: ${error.message}`); 193 | } 194 | } 195 | 196 | console.log('MCP Extension initializing...'); 197 | syncMCPPlugins().catch(err => { 198 | displayError(`Initialization failed: ${err.message}`); 199 | }); --------------------------------------------------------------------------------