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
HTML
<!-- 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>