UNPKG

node-red-contrib-mcp-server

Version:

A comprehensive Node-RED wrapper for Model Context Protocol (MCP) servers providing standardized AI agent tool interfaces, server lifecycle management, real-time communication capabilities, and visual MCP server creation

356 lines (304 loc) 15.7 kB
<!-- MCP Tool Node --> <script type="text/javascript"> RED.nodes.registerType('mcp-tool', { category: 'mcp', color: '#FFA726', defaults: { name: {value: ""}, serverUrl: {value: "http://localhost:8000", required: true}, toolName: {value: "", required: true}, toolParams: {value: "{}"}, timeout: {value: 30000, validate: function(v) { return v >= 1000; }}, outputMode: {value: "result", required: true} }, inputs: 1, outputs: 1, icon: "font-awesome/fa-cog", label: function() { return this.name || this.toolName || "MCP Tool"; }, labelStyle: function() { return this.name ? "node_label_italic" : ""; }, oneditprepare: function() { var node = this; // Load available tools from server function loadAvailableTools() { var serverUrl = $("#node-input-serverUrl").val(); if (!serverUrl) { RED.notify("Server URL is required", "error"); return; } var button = $("#load-tools-btn"); var select = $("#node-input-toolName"); var originalText = button.text(); button.text("Loading...").prop('disabled', true); // Properly encode the URL for the REST endpoint var encodedUrl = encodeURIComponent(serverUrl); $.ajax({ url: "mcp-tools/" + encodedUrl, method: "GET", timeout: 10000, headers: { "Accept": "application/json" } }) .done(function(data) { // Clear existing options (except the first placeholder) select.find('option:not(:first)').remove(); if (data.tools && data.tools.length > 0) { data.tools.forEach(function(tool) { var option = $('<option></option>') .attr('value', tool.name) .text(tool.name + (tool.description ? ' - ' + tool.description : '')); select.append(option); }); RED.notify(`Loaded ${data.tools.length} tools from server`, "success"); } else { RED.notify("No tools found on server", "warning"); } }) .fail(function(xhr, status, error) { var errorMsg = "Failed to load tools"; if (xhr.responseJSON && xhr.responseJSON.error) { errorMsg += ": " + xhr.responseJSON.error; } else if (error) { errorMsg += ": " + error; } else if (status) { errorMsg += ": " + status; } // Check if it's a connection issue if (xhr.status === 0 || status === "timeout") { errorMsg = "Cannot connect to server. Ensure the MCP server is running."; } else if (xhr.status === 500) { errorMsg = "Server error. Check that the MCP server supports tools/list."; } RED.notify(errorMsg, "error"); }) .always(function() { button.text(originalText).prop('disabled', false); }); } // Load tools button $("#load-tools-btn").click(loadAvailableTools); // Auto-load tools when server URL changes $("#node-input-serverUrl").on('blur', function() { if ($(this).val()) { loadAvailableTools(); } }); // Omnispindle tool presets var omnispindleTools = [ { name: "add_todo_tool", params: '{"description": "", "project": ""}' }, { name: "get_todo_tool", params: '{"todo_id": ""}' }, { name: "list_todos_by_status_tool", params: '{"status": "pending"}' }, { name: "update_todo_tool", params: '{"todo_id": "", "updates": {}}' }, { name: "delete_todo_tool", params: '{"todo_id": ""}' }, { name: "mark_todo_complete_tool", params: '{"todo_id": ""}' }, { name: "list_project_todos_tool", params: '{"project": ""}' }, { name: "query_todos_tool", params: '{"query_or_filter": ""}' } ]; // Populate Omnispindle tools dropdown var omnispindleSelect = $("#omnispindle-tools"); omnispindleTools.forEach(function(tool) { var option = $('<option></option>') .attr('value', JSON.stringify(tool)) .text(tool.name.replace('_tool', '').replace(/_/g, ' ')); omnispindleSelect.append(option); }); // Load Omnispindle preset $("#load-omnispindle-tool-btn").click(function() { var selectedTool = $("#omnispindle-tools").val(); if (selectedTool) { var tool = JSON.parse(selectedTool); $("#node-input-serverUrl").val("http://localhost:8000"); $("#node-input-toolName").val(tool.name); $("#node-input-toolParams").val(tool.params); RED.notify(`Loaded ${tool.name} preset`, "success"); } }); // Validate JSON parameters $("#node-input-toolParams").on('blur', function() { var params = $(this).val(); try { JSON.parse(params); $(this).removeClass("input-error"); $("#params-validation").hide(); } catch (error) { $(this).addClass("input-error"); $("#params-validation").show().text("Invalid JSON: " + error.message); } }); // Format JSON button $("#format-json-btn").click(function() { var params = $("#node-input-toolParams").val(); try { var parsed = JSON.parse(params); var formatted = JSON.stringify(parsed, null, 2); $("#node-input-toolParams").val(formatted); RED.notify("JSON formatted", "success"); } catch (error) { RED.notify("Invalid JSON: " + error.message, "error"); } }); // Update output mode description function updateOutputModeDescription() { var mode = $("#node-input-outputMode").val(); var descriptions = { "result": "Extract only the result from the MCP response", "full": "Return the complete MCP response including metadata", "custom": "Keep original message and add response/result properties" }; $("#output-mode-description").text(descriptions[mode] || ""); } $("#node-input-outputMode").change(updateOutputModeDescription); updateOutputModeDescription(); // Load tools on init if server URL is set if ($("#node-input-serverUrl").val()) { setTimeout(loadAvailableTools, 500); } } }); </script> <script type="text/html" data-template-name="mcp-tool"> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <input type="text" id="node-input-name" placeholder="Optional display name"> </div> <div class="form-row"> <label for="node-input-serverUrl"><i class="fa fa-globe"></i> Server URL</label> <input type="text" id="node-input-serverUrl" placeholder="http://localhost:8000"> <div style="margin-top: 5px; font-size: 12px; color: #666;"> Base URL of the MCP server </div> </div> <hr> <h4>Tool Configuration</h4> <div class="form-row"> <label for="node-input-toolName"><i class="fa fa-cog"></i> Tool Name</label> <select id="node-input-toolName"> <option value="">Select a tool...</option> </select> <button type="button" id="load-tools-btn" class="btn btn-secondary" style="margin-top: 5px;"> <i class="fa fa-refresh"></i> Load Available Tools </button> </div> <div class="form-row"> <label for="node-input-toolParams"><i class="fa fa-code"></i> Default Parameters</label> <textarea id="node-input-toolParams" rows="4" placeholder='{"param1": "value1", "param2": "value2"}'></textarea> <div id="params-validation" style="display: none; margin-top: 5px; font-size: 12px; color: #e74c3c;"></div> <div style="margin-top: 5px; font-size: 12px; color: #666;"> JSON object with default parameters (can be overridden by input messages) </div> <button type="button" id="format-json-btn" class="btn btn-secondary" style="margin-top: 5px;"> <i class="fa fa-magic"></i> Format JSON </button> </div> <div class="form-row"> <label for="node-input-outputMode"><i class="fa fa-arrow-right"></i> Output Mode</label> <select id="node-input-outputMode"> <option value="result">Result Only</option> <option value="full">Full Response</option> <option value="custom">Custom (preserve original)</option> </select> <div id="output-mode-description" style="margin-top: 5px; font-size: 12px; color: #666;"> <!-- Description will be populated by JavaScript --> </div> </div> <div class="form-row"> <label for="node-input-timeout"><i class="fa fa-hourglass"></i> Timeout</label> <input type="number" id="node-input-timeout" min="1000" step="1000" placeholder="30000"> <span style="margin-left: 10px; font-size: 12px; color: #666;">milliseconds</span> </div> <!-- Omnispindle Presets --> <hr> <h4>Quick Setup</h4> <div class="form-row"> <label for="omnispindle-tools"><i class="fa fa-magic"></i> Omnispindle Tools</label> <select id="omnispindle-tools"> <option value="">Select a preset...</option> </select> <button type="button" id="load-omnispindle-tool-btn" class="btn btn-primary" style="margin-top: 5px;"> <i class="fa fa-download"></i> Load Selected Tool </button> </div> </script> <script type="text/html" data-help-name="mcp-tool"> <p>A Node-RED node for invoking specific Model Context Protocol (MCP) tools with predefined configurations. Simplifies calling MCP server tools with optional parameter overrides.</p> <h3>Configuration</h3> <dl class="message-properties"> <dt>Server URL <span class="property-type">string</span></dt> <dd>Base URL of the MCP server (e.g., http://localhost:8000)</dd> <dt>Tool Name <span class="property-type">string</span></dt> <dd>Name of the MCP tool to invoke</dd> <dt>Default Parameters <span class="property-type">JSON</span></dt> <dd>JSON object with default parameters for the tool</dd> <dt>Output Mode <span class="property-type">string</span></dt> <dd>How to format the output: result only, full response, or custom</dd> <dt>Timeout <span class="property-type">number</span></dt> <dd>Request timeout in milliseconds</dd> </dl> <h3>Output Modes</h3> <dl class="message-properties"> <dt>Result Only <span class="property-type">default</span></dt> <dd>Extracts only the result data from the MCP response</dd> <dt>Full Response <span class="property-type">complete</span></dt> <dd>Returns the complete MCP JSON-RPC response including metadata</dd> <dt>Custom <span class="property-type">preserve</span></dt> <dd>Keeps the original message and adds response/result properties</dd> </dl> <h3>Input Message Overrides</h3> <p>The following message properties can override default parameters:</p> <dl class="message-properties"> <dt>msg.topic <span class="property-type">string</span></dt> <dd>Override the tool name to call</dd> <dt>msg.payload <span class="property-type">object</span></dt> <dd>Parameters to merge with defaults</dd> <dt>msg.payload.method <span class="property-type">string</span></dt> <dd>Override the tool name when payload is an object</dd> <dt>msg.payload.params <span class="property-type">object</span></dt> <dd>Parameters to merge with defaults when payload is structured</dd> </dl> <h3>Omnispindle Tool Shortcuts</h3> <p>The following specific message properties are recognized for Omnispindle tools:</p> <dl class="message-properties"> <dt>msg.description <span class="property-type">string</span></dt> <dd>Todo description for add/update operations</dd> <dt>msg.project <span class="property-type">string</span></dt> <dd>Project name for todo operations</dd> <dt>msg.todo_id <span class="property-type">string</span></dt> <dd>Todo ID for update/delete/complete operations</dd> <dt>msg.status <span class="property-type">string</span></dt> <dd>Status filter for list operations</dd> <dt>msg.priority <span class="property-type">string</span></dt> <dd>Priority level for todo operations</dd> </dl> <h3>Examples</h3> <p><strong>Create Todo (using shortcuts):</strong></p> <pre><code>msg.description = "Implement new feature"; msg.project = "MyProject"; // Tool configured as "add_todo_tool" return msg;</code></pre> <p><strong>List Todos by Status:</strong></p> <pre><code>msg.payload = { status: "pending" }; // Tool configured as "list_todos_by_status_tool" return msg;</code></pre> <p><strong>Override Tool Name:</strong></p> <pre><code>msg.topic = "different_tool_name"; msg.payload = { param1: "value1" }; return msg;</code></pre> <h3>Available Omnispindle Tools</h3> <ul> <li><strong>add_todo_tool:</strong> Create new todos</li> <li><strong>list_todos_by_status_tool:</strong> List todos by status</li> <li><strong>update_todo_tool:</strong> Update existing todos</li> <li><strong>delete_todo_tool:</strong> Delete todos</li> <li><strong>mark_todo_complete_tool:</strong> Mark todos as complete</li> <li><strong>list_project_todos_tool:</strong> List todos for a project</li> <li><strong>query_todos_tool:</strong> Search/query todos</li> </ul> <h3>Dynamic Tool Loading</h3> <p>Use the "Load Available Tools" button to fetch and populate tools from the connected MCP server. This helps discover available tools dynamically.</p> <h3>Requirements</h3> <p>The target MCP server must be running and accessible. The specified tool must be available on the server.</p> </script>