node-red-contrib-processcube-mcp
Version:
MCP nodes for ProcessCube
185 lines (150 loc) • 6.49 kB
JavaScript
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { isInitializeRequest } = require('@modelcontextprotocol/sdk/types.js');
const { InMemoryEventStore } = require('@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js');
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
const { randomUUID } = require('crypto');
const { z } = require('zod');
const EventEmitter = require('node:events');
module.exports = function (RED) {
function MCPToolInput(config) {
RED.nodes.createNode(this, config);
var node = this;
node.config = config;
node.eventEmitter = new EventEmitter();
}
RED.nodes.registerType('mcp-tool-input', MCPToolInput);
async function getServer() {
const server = new McpServer(
{ name: "ProcessCube LowCode", version: "1.0.0"
}, {
capabilities: {
tools: {
}
}
});
RED.nodes.eachNode(function(n) {
if (n.type === "mcp-tool-input") {
const node = RED.nodes.getNode(n.id);
server.tool(
node.config.name,
node.config.description,
{
data: z.any().describe('Daten vom Benutzer, welche das Tool benötigt werden. Gib sie immer als Objekt (z.B. { argument1: wert1, argument2: wert2}) an.')
},
async ({data})=> {
return new Promise((resolve, reject) => {
const msgId = RED.util.generateId();
node.eventEmitter.once(`finish-${msgId}`, (result) => {
resolve({
content: [{
type: "text",
text: result
}]
})
});
node.eventEmitter.once(`error-${msgId}`, (result) => {
resolve({
content: [{
type: "text",
text: `Error: ${result}`
}]
})
});
const payload = isStrictObject(data) ? data : JSON.parse(data);
let msg = {
payload: payload,
_msgid: msgId,
mcpToolInputNodeId: node.id
}
node.send(msg);
});
}
);
}
});
return server;
}
const transports = {};
RED.httpNode.all("/mcp", async function (req, res) {
console.log(`Received ${req.method} request to /mcp`);
try {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'];
let transport;
if (sessionId && transports[sessionId]) {
// Check if the transport is of the correct type
const existingTransport = transports[sessionId];
if (existingTransport instanceof StreamableHTTPServerTransport) {
// Reuse existing transport
transport = existingTransport;
} else {
// Transport exists but is not a StreamableHTTPServerTransport (could be SSEServerTransport)
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: Session exists but uses a different transport protocol',
},
id: null,
});
return;
}
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: (sessionId) => {
// Store the transport by session ID when session is initialized
console.log(`StreamableHTTP session initialized with ID: ${sessionId}`);
transports[sessionId] = transport;
}
});
console.log(`Transport: ${transport}`);
// Set up onclose handler to clean up transport when closed
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(`Transport closed for session ${sid}, removing from transports map`);
delete transports[sid];
}
};
console.log('Vor dem Server')
const server = await getServer();
// Connect the transport to the MCP server
await server.connect(transport);
console.log('Nach dem Server')
} else {
// Invalid request - no session ID or not initialization request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
console.log(`Vor dem handleRequest: ${JSON.stringify(req.body)}`);
// Handle the request with the transport
await transport.handleRequest(req, res, req.body);
console.log('Nach dem handleRequest')
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
function isStrictObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof String);
}
};