1. 问题根源:为什么通用代理在 n8n MCP 面前会“水土不服”?

如果你最近在尝试用 Claude Desktop 连接 n8n 的 MCP 服务器,大概率会遇到一个让人抓狂的问题:连接明明建立成功了,初始化握手也完成了,但 Claude Desktop 就是“秒断”,工具列表也刷不出来。你可能会想,是不是网络问题?或者配置写错了?我一开始也是这么想的,折腾了半天,最后才发现,问题出在最根本的通信协议处理上。

简单来说,n8n 的 MCP 服务器实现,和 Claude Desktop 期望的“标准”MCP 通信模式,存在一个关键差异。这个差异,恰恰是像 supergateway 这样的通用代理服务器无法处理的。它们俩的“对话方式”根本对不上。

n8n 的 MCP 服务器采用了一种“混合双工”的通信模型。 这听起来有点技术,我打个比方你就明白了。想象一下两个人打电话:

  • 标准 MCP 协议:就像用一部普通的电话,你说我听,我说你听,用的是同一条线路(比如 stdio 或纯 SSE 流)。
  • n8n 的 MCP 实现:它用了两部电话!一部专门用来“听”(SSE 连接,用于接收服务器推送的通知和会话信息),另一部专门用来“说”(HTTP POST 请求,用于向服务器发送指令)。而且,用来“说”的那部电话的号码(HTTP 端点)还是动态生成的,会通过“听”的那部电话告诉你。

当你用 supergateway 这样的工具时,它默认认为双方都在用同一部“纯 SSE 电话”进行全双工通信。它会尝试通过 SSE 连接来发送所有消息。但 n8n 那边呢?它只用自己的“听筒”(SSE)接收通知,却等着你用另一条“话筒线”(HTTP POST)来跟它说话。结果就是,supergateway 对着 n8n 的“听筒”大喊大叫,n8n 却完全收不到指令,或者收到了也无法正确处理,最终导致连接异常关闭。

更棘手的是第二个问题:流式数据的分块传输与 JSON 解析。当 n8n 的某个工具(比如一个复杂的数据库查询或网页抓取)需要返回大量数据时,为了效率和实时性,它会通过 SSE 流“一块一块”地把数据发送出来。一个完整的 JSON 响应,可能会被拆分成多个 data: 事件分块传输。通用代理通常假设每个 data: 事件都是一个完整的、可独立解析的 JSON 对象,直接转发给 Claude Desktop。但 Claude Desktop 的 JSON 解析器可没那么“智能”,它收到一个不完整的 JSON 片段时,会立刻报错:“JSON 解析错误:未终止的字符串”,然后整个连接就崩了。

所以,核心难题有两个:一是通信模型不匹配(混合双工 vs 纯双工),二是数据完整性无法保证(流式分块 vs 完整 JSON)。要解决这个问题,我们必须放弃“通用”方案,为 n8n 和 Claude Desktop 量身定制一个能理解它们双方“语言习惯”的专属翻译官——也就是一个智能代理服务器。

2. 设计思路:打造一个“懂行”的智能数据缓冲代理

既然知道了病根,药方就好开了。我们的目标不再是简单地转发流量,而是要构建一个智能中介,它需要具备以下核心能力:

  1. 协议适配器:它能同时理解“n8n 语”和“Claude Desktop 语”。对于从 Claude Desktop 来的请求,它知道要转换成 HTTP POST 发送给 n8n 的动态消息端点;对于从 n8n SSE 流来的数据,它能正确组装并转发给 Claude Desktop。
  2. 流式数据装配工:这是攻克难题的关键。它不能像普通代理那样“来一块转一块”,而必须充当一个临时的装配车间。当检测到 n8n 开始发送一个消息事件时,它要启动一个“收集模式”,把后续收到的一个个数据块像拼图一样累积起来,直到收到明确的结束信号(比如一个空行)。
  3. 完整性校验员:在拼装过程中,它要不断尝试解析累积的缓冲区。一旦能成功解析出一个完整的 JSON 对象,就说明这份数据完整了,可以安全地转发给 Claude Desktop。如果数据块太大(比如超过预设的 200KB),它还要能安全地截断,避免内存溢出。
  4. 会话管理员:它能从 n8n 通过 SSE 发送的初始消息中,提取出那个至关重要的动态 sessionId,并用这个 sessionId 构造出正确的 HTTP POST 地址。没有这个,后续所有对话都无法进行。

这个代理的工作流程,可以想象成一个高效的物流分拣中心:

  • 入口(来自 Claude Desktop):收到标准 MCP 格式的 JSON-RPC 请求(如 tools/listtools/call)。
  • 处理:检查当前是否已获得 n8n 的动态消息端点。如果已获得,则直接通过 HTTP POST 将请求原样发送至该端点;如果未获得,则暂存请求,等待端点就绪。
  • 出口(来自 n8n SSE):持续监听 SSE 流。收到事件和数据。
    • 如果是包含 /mcp/.../messages?sessionId=... 的端点信息,立刻提取并存储,用于后续的 HTTP 通信。
    • 如果是 event: message,开启数据收集模式。
    • 后续的 data: { ... 块被累积到缓冲区。
    • 遇到空行,尝试解析缓冲区。成功则组装成一个完整的 MCP 响应(如 tools/list 的结果)发送给 Claude Desktop;失败则根据策略(继续等待、截断、报错)处理。

这样,无论 n8n 返回的数据有多大、被分成了多少块,到了 Claude Desktop 那里,收到的永远是一个个完整、规范、可直接解析的 JSON 响应。连接稳定性问题自然迎刃而解。

3. 实战代码:一步步实现流式数据解析代理

理论讲完了,我们直接上干货。下面这个 n8n-streaming-proxy.js 就是我经过多次调试和优化后的完整解决方案。你可以把它保存到一个本地文件,比如 D:\claude-mcp\n8n-proxy.js

#!/usr/bin/env node

const https = require('https');
const { EventEmitter } = require('events');

/**
 * 专为 n8n MCP 服务器设计的流式数据解析代理
 * 核心功能:处理 SSE 流式分块传输,累积数据直到形成完整 JSON 响应
 */
class N8nStreamingMCPProxy extends EventEmitter {
    constructor(baseUrl, pathPrefix) {
        super();
        // n8n 云实例的基础 URL,例如 https://your-app.app.n8n.cloud
        this.baseUrl = baseUrl;
        // MCP 服务器路径前缀,即工作流ID,从 Claude Desktop 配置中传入
        this.pathPrefix = pathPrefix;

        // 会话状态
        this.sessionId = null;
        // 动态生成的消息发送端点
        this.messageEndpoint = null;
        this.isConnected = false;

        // 用于关联请求和响应的映射表
        this.pendingRequests = new Map();

        // 核心:流式数据缓冲区
        this.sseBuffer = ''; // 原始 SSE 数据行缓冲区
        this.messageBuffer = ''; // 当前正在收集的 JSON 消息缓冲区
        this.isCollectingMessage = false; // 是否处于消息收集模式
        this.currentMessageId = null;
        // 安全限制:防止过大的响应耗尽内存
        this.maxResponseSize = 200 * 1024; // 200KB
    }

    /**
     * 启动代理
     */
    async start() {
        console.error('[n8n-proxy] 正在启动流式 MCP 代理...');
        // 1. 连接 n8n 的 SSE 端点
        await this.connectSSE();
        // 2. 设置标准输入处理器,以接收来自 Claude Desktop 的请求
        this.setupStdinHandler();
        // 3. 稍等片刻,待连接稳定后发送初始化请求
        setTimeout(() => this.sendInitialize(), 1000);
    }

    /**
     * 连接到 n8n 的 SSE 事件流
     */
    connectSSE() {
        return new Promise((resolve, reject) => {
            const sseUrl = `${this.baseUrl}/mcp/${this.pathPrefix}/sse`;
            console.error(`[n8n-proxy] 正在连接至 SSE: ${sseUrl}`);

            const req = https.get(sseUrl, {
                headers: {
                    'Accept': 'text/event-stream',
                    'Cache-Control': 'no-cache',
                    'Connection': 'keep-alive'
                }
            }, (res) => {
                if (res.statusCode !== 200) {
                    reject(new Error(`SSE 连接失败,状态码: ${res.statusCode}`));
                    return;
                }
                console.error(`[n8n-proxy] SSE 连接已建立`);
                this.sseConnection = res;

                // 关键:处理流式数据块
                res.on('data', (chunk) => {
                    this.processSSEChunk(chunk.toString());
                });

                res.on('end', () => {
                    console.error('[n8n-proxy] SSE 连接已结束');
                    this.isConnected = false;
                });

                resolve();
            });

            req.on('error', (err) => {
                console.error(`[n8n-proxy] SSE 连接错误: ${err.message}`);
                reject(err);
            });
        });
    }

    /**
     * 处理原始的 SSE 数据块,可能包含不完整的行
     * @param {string} chunk - 从网络接收到的数据块
     */
    processSSEChunk(chunk) {
        // 将新数据块追加到缓冲区
        this.sseBuffer += chunk;
        // 按换行符分割,处理完整的行,最后一行可能不完整,留回缓冲区
        const lines = this.sseBuffer.split('\n');
        this.sseBuffer = lines.pop() || ''; // 剩余的不完整行放回缓冲区

        for (const line of lines) {
            this.processSSELine(line);
        }
    }

    /**
     * 处理单条完整的 SSE 行
     * @param {string} line
     */
    processSSELine(line) {
        // 忽略注释行
        if (line.startsWith(':')) return;

        if (line.startsWith('event: ')) {
            // 事件类型,例如 'event: message' 表示一个消息开始
            const eventType = line.substring(7).trim();
            if (eventType === 'message') {
                // 进入消息收集模式
                this.isCollectingMessage = true;
                this.messageBuffer = '';
                console.error('[n8n-proxy] 开始收集新的消息数据');
            }
        } else if (line.startsWith('data: ')) {
            // 数据行
            const data = line.substring(6).trim();
            if (data.startsWith('/mcp/')) {
                // 这是动态消息端点信息,例如:/mcp/xxx/messages?sessionId=yyy
                this.messageEndpoint = `${this.baseUrl}${data}`;
                this.extractSessionId(data);
                this.isConnected = true;
                console.error(`[n8n-proxy] 获取到消息端点: ${this.messageEndpoint}`);
            } else if (data.startsWith('{') || this.isCollectingMessage) {
                // 如果是 JSON 数据开头,或者已经在收集消息,则进行累积
                this.collectJSONData(data);
            }
        } else if (line.trim() === '') {
            // 空行表示一个事件结束
            if (this.isCollectingMessage && this.messageBuffer) {
                console.error('[n8n-proxy] 收到消息结束标志(空行),尝试解析最终消息');
                this.finalizeMessage();
            }
        }
    }

    /**
     * 累积并尝试解析 JSON 数据
     * @param {string} data - 可能不完整的 JSON 数据片段
     */
    collectJSONData(data) {
        this.messageBuffer += data;

        // 安全检查:防止缓冲区无限增长
        if (this.messageBuffer.length > this.maxResponseSize) {
            console.error('[n8n-proxy] 消息数据过大,进行截断处理');
            this.handleLargeMessage();
            return;
        }

        // 尝试解析当前缓冲区,看是否已形成一个完整的 JSON
        try {
            const message = JSON.parse(this.messageBuffer);
            // 解析成功!说明数据已完整
            this.processCompleteMessage(message);
            // 重置状态,准备接收下一条消息
            this.messageBuffer = '';
            this.isCollectingMessage = false;
        } catch (error) {
            // JSON 解析失败,说明数据还不完整,这是正常情况,继续等待下一个数据块
            // 可以在这里添加更精细的日志,但生产环境建议静默
            // console.error(`[n8n-proxy] 数据不完整,继续累积: ${error.message}`);
        }
    }

    /**
     * 最终化消息处理(当收到空行时调用)
     */
    finalizeMessage() {
        if (!this.messageBuffer.trim()) {
            this.isCollectingMessage = false;
            return;
        }

        try {
            const message = JSON.parse(this.messageBuffer);
            this.processCompleteMessage(message);
        } catch (error) {
            // 即使收到结束信号,数据仍无法解析,可能是损坏的消息
            console.error(`[n8n-proxy] 最终消息解析失败: ${error.message}`);
            console.error(`[n8n-proxy] 损坏的数据片段: ${this.messageBuffer.substring(0, 200)}...`);
            this.handleCorruptedMessage();
        } finally {
            // 无论成功与否,都重置缓冲区
            this.messageBuffer = '';
            this.isCollectingMessage = false;
        }
    }

    /**
     * 处理过大的消息
     */
    handleLargeMessage() {
        // 发送一个错误响应给客户端,避免内存问题
        const errorResponse = {
            jsonrpc: '2.0',
            id: this.currentMessageId || null,
            error: {
                code: -32603,
                message: 'Internal error: Response too large'
            }
        };
        this.sendToClient(errorResponse);
        this.messageBuffer = '';
        this.isCollectingMessage = false;
    }

    /**
     * 处理损坏的消息
     */
    handleCorruptedMessage() {
        // 可以选择发送一个错误,或者静默丢弃,取决于你的需求
        // 这里我们发送一个解析错误
        const errorResponse = {
            jsonrpc: '2.0',
            id: this.currentMessageId || null,
            error: {
                code: -32700,
                message: 'Parse error: Invalid JSON received from server'
            }
        };
        this.sendToClient(errorResponse);
    }

    /**
     * 处理一个完整的、已解析的 MCP 消息
     * @param {object} message - 完整的 JSON-RPC 消息对象
     */
    processCompleteMessage(message) {
        console.error(`[n8n-proxy] 收到完整消息 (id: ${message.id})`);
        // 如果是某个请求的响应,从 pendingRequests 中移除
        if (message.id !== undefined && this.pendingRequests.has(message.id)) {
            const reqInfo = this.pendingRequests.get(message.id);
            console.error(`[n8n-proxy] 请求 [${reqInfo.method}] 的响应已收到`);
            this.pendingRequests.delete(message.id);
        }
        // 转发给 Claude Desktop
        this.sendToClient(message);
    }

    /**
     * 将消息发送给 Claude Desktop (标准输出)
     * @param {object} message
     */
    sendToClient(message) {
        try {
            const output = JSON.stringify(message) + '\n';
            process.stdout.write(output);
        } catch (error) {
            console.error(`[n8n-proxy] 发送消息到客户端时出错: ${error.message}`);
        }
    }

    /**
     * 从端点 URL 中提取 sessionId
     * @param {string} endpoint
     */
    extractSessionId(endpoint) {
        const match = endpoint.match(/sessionId=([^&]+)/);
        if (match) {
            this.sessionId = match[1];
            console.error(`[n8n-proxy] 会话 ID: ${this.sessionId}`);
        }
    }

    /**
     * 发送消息到 n8n 服务器 (HTTP POST)
     * @param {object} message - JSON-RPC 请求对象
     */
    sendMessageToN8n(message) {
        // 如果消息端点还未就绪,延迟重试
        if (!this.messageEndpoint) {
            console.error('[n8n-proxy] 消息端点尚未就绪,延迟发送...');
            setTimeout(() => this.sendMessageToN8n(message), 1000);
            return;
        }

        // 记录发出的请求,以便匹配响应
        if (message.id !== undefined && message.method) {
            this.pendingRequests.set(message.id, {
                method: message.method,
                timestamp: Date.now()
            });
        }

        const postData = JSON.stringify(message);
        const url = new URL(this.messageEndpoint);

        const options = {
            hostname: url.hostname,
            port: url.port || 443,
            path: url.pathname + url.search,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(postData)
            }
        };

        const req = https.request(options, (res) => {
            let responseData = '';
            res.on('data', (chunk) => { responseData += chunk; });
            res.on('end', () => {
                if (res.statusCode !== 200) {
                    console.error(`[n8n-proxy] HTTP 错误状态: ${res.statusCode}, 响应: ${responseData}`);
                }
            });
        });

        req.on('error', (error) => {
            console.error(`[n8n-proxy] 发送 HTTP 请求失败: ${error.message}`);
            // 如果请求失败,向客户端返回一个错误响应
            if (message.id !== undefined) {
                const errorResponse = {
                    jsonrpc: '2.0',
                    id: message.id,
                    error: {
                        code: -32603,
                        message: `Internal error: ${error.message}`
                    }
                };
                this.sendToClient(errorResponse);
                this.pendingRequests.delete(message.id);
            }
        });

        req.write(postData);
        req.end();
    }

    /**
     * 发送初始化请求给 n8n 服务器
     */
    sendInitialize() {
        if (!this.isConnected) {
            setTimeout(() => this.sendInitialize(), 1000);
            return;
        }

        const initMessage = {
            jsonrpc: '2.0',
            id: 0,
            method: 'initialize',
            params: {
                // 注意:这里使用 Claude Desktop 兼容的协议版本
                protocolVersion: '2024-11-05',
                capabilities: {},
                clientInfo: {
                    name: 'claude-desktop',
                    version: '1.0.0'
                }
            }
        };
        console.error('[n8n-proxy] 发送初始化请求...');
        this.sendMessageToN8n(initMessage);
    }

    /**
     * 设置标准输入监听,接收来自 Claude Desktop 的请求
     */
    setupStdinHandler() {
        process.stdin.on('data', (data) => {
            try {
                const input = data.toString().trim();
                if (!input) return;

                const message = JSON.parse(input);
                console.error(`[n8n-proxy] 收到客户端请求: ${message.method || 'unknown'} (id: ${message.id})`);
                this.sendMessageToN8n(message);

            } catch (error) {
                console.error(`[n8n-proxy] 解析标准输入 JSON 时出错: ${error.message}`);
            }
        });

        process.stdin.on('end', () => {
            console.error('[n8n-proxy] 标准输入结束,退出中...');
            process.exit(0);
        });
    }
}

// 优雅地处理进程退出
process.on('SIGINT', () => {
    console.error('[n8n-proxy] 收到 SIGINT 信号,正在退出...');
    process.exit(0);
});
process.on('SIGTERM', () => {
    console.error('[n8n-proxy] 收到 SIGTERM 信号,正在退出...');
    process.exit(0);
});

// --- 脚本主入口 ---
const baseUrl = 'https://your-app.app.n8n.cloud'; // 请替换为你的 n8n 云实例地址
const pathPrefix = process.argv[2]; // 从命令行参数获取工作流ID

if (!pathPrefix) {
    console.error('用法: node n8n-streaming-proxy.js <n8n_workflow_id>');
    console.error('示例: node n8n-streaming-proxy.js 461a5dfc-c300-4b3c-915e-a752558b3905');
    process.exit(1);
}

const proxy = new N8nStreamingMCPProxy(baseUrl, pathPrefix);
proxy.start().catch((error) => {
    console.error(`代理启动失败: ${error.message}`);
    process.exit(1);
});

代码核心要点解析:

  1. collectJSONData 方法:这是流式解析的核心。它不断累积 data: 事件中的字符串到 messageBuffer,并反复尝试JSON.parse() 去解析。只要解析失败(抛出异常),就说明数据还不完整,继续等待下一块。一旦解析成功,立刻将完整的消息对象转发出去,并清空缓冲区。这种方法简单而鲁棒。
  2. 双缓冲区设计sseBuffer 用于处理网络 TCP 流可能造成的行分割不完整问题,确保我们总是处理完整的 SSE 行(以 \n 结尾)。messageBuffer 则专门用于累积属于同一个消息的多个 JSON 数据块。
  3. 协议版本处理:在 sendInitialize 方法中,我们硬编码了 protocolVersion: '2024-11-05'。这是为了主动适配 Claude Desktop 客户端。n8n 服务器可能支持更新的版本,但使用客户端兼容的版本可以避免握手失败。
  4. 错误恢复机制:包含了处理过大消息 (handleLargeMessage) 和损坏消息 (handleCorruptedMessage) 的逻辑,防止代理进程因意外数据而崩溃。

4. 配置与测试:让 Claude Desktop 无缝连接

代码准备好了,接下来就是配置和验证。整个过程比想象中简单。

4.1 配置 Claude Desktop

首先,找到你的 Claude Desktop 配置文件。通常在以下位置:

  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

用文本编辑器打开它,在 mcpServers 部分添加我们的代理。记得修改两处:一是将 baseUrl 变量中的 your-app.app.n8n.cloud 替换成你真实的 n8n 云地址;二是将代理脚本的路径 C:\\path\\to\\ 换成你实际存放 n8n-streaming-proxy.js 的路径。

{
  "mcpServers": {
    "n8n-my-workflow": {
      "command": "node",
      "args": [
        "C:\\Users\\YourName\\projects\\claude-mcp\\n8n-streaming-proxy.js",
        "461a5dfc-c300-4b3c-915e-a752558b3905"
      ]
    },
    // 你可以配置多个 n8n 工作流!
    "n8n-another-tool": {
      "command": "node",
      "args": [
        "C:\\Users\\YourName\\projects\\claude-mcp\\n8n-streaming-proxy.js",
        "another_workflow_id"
      ]
    }
  }
}

配置说明

  • command: 使用 node 来执行我们的 JavaScript 代理。
  • args: 第一个参数是代理脚本的绝对路径,第二个参数是你的 n8n 工作流 ID(在 n8n 云界面中,MCP 服务器触发节点的配置里可以找到)。

保存配置文件,然后完全重启 Claude Desktop(包括系统托盘里的后台进程)。这是必须的,因为 MCP 服务器配置只在启动时加载。

4.2 验证与测试

重启后,如何知道代理工作正常呢?这里给你几个验证方法:

方法一:观察 Claude Desktop 启动日志 启动 Claude Desktop 时,留意它的日志窗口(如果有的话)。你应该能看到类似 Starting MCP server: n8n-my-workflow 的信息,并且没有立即报错退出。如果代理脚本有 console.error 输出(像我们的代码那样),这些信息可能会出现在系统控制台,取决于你的启动方式。

方法二:使用简单的测试脚本 创建一个独立的测试文件 test-proxy.js,模拟 Claude Desktop 的行为:

#!/usr/bin/env node
const { spawn } = require('child_process');
const path = require('path');

// 你的工作流 ID
const WORKFLOW_ID = '461a5dfc-c300-4b3c-915e-a752558b3905';
const PROXY_SCRIPT = path.join(__dirname, 'n8n-streaming-proxy.js');

console.log(`🚀 启动测试代理,工作流ID: ${WORKFLOW_ID}`);

const proxy = spawn('node', [PROXY_SCRIPT, WORKFLOW_ID], {
    stdio: ['pipe', 'pipe', 'inherit'] // 将 stderr 输出到控制台,方便看日志
});

let receivedInitialized = false;
let receivedToolsList = false;

proxy.stdout.on('data', (data) => {
    const lines = data.toString().trim().split('\n').filter(l => l);
    for (const line of lines) {
        try {
            const msg = JSON.parse(line);
            console.log(`📨 收到服务器消息:`, JSON.stringify(msg, null, 2));

            if (msg.result && msg.result.capabilities) {
                console.log('✅ 初始化握手成功!');
                receivedInitialized = true;
                // 握手成功后,请求工具列表
                setTimeout(() => {
                    const toolsReq = { jsonrpc: '2.0', id: 100, method: 'tools/list' };
                    console.log(`📤 发送 tools/list 请求...`);
                    proxy.stdin.write(JSON.stringify(toolsReq) + '\n');
                }, 500);
            }

            if (msg.result && msg.result.tools) {
                console.log(`✅ 成功获取工具列表,共 ${msg.result.tools.length} 个工具`);
                console.log(`工具名称: ${msg.result.tools.map(t => t.name).join(', ')}`);
                receivedToolsList = true;
                // 测试完成,优雅退出
                setTimeout(() => {
                    console.log('✅ 所有测试通过!代理工作正常。');
                    proxy.kill('SIGTERM');
                    process.exit(0);
                }, 1000);
            }

            if (msg.error) {
                console.error(`❌ 收到错误响应:`, msg.error);
                proxy.kill('SIGTERM');
                process.exit(1);
            }

        } catch (e) {
            console.error('❌ 解析响应失败:', e.message, '原始数据:', line.substring(0, 200));
        }
    }
});

proxy.on('close', (code) => {
    console.log(`代理进程退出,代码: ${code}`);
    process.exit(code);
});

// 设置超时,防止卡死
setTimeout(() => {
    if (!receivedInitialized || !receivedToolsList) {
        console.error('❌ 测试超时,代理可能未正常工作。');
        proxy.kill('SIGTERM');
        process.exit(1);
    }
}, 15000);

运行这个测试脚本 (node test-proxy.js),如果一切顺利,你会看到一连串的成功日志,最终确认工具列表已获取。这证明你的代理已经成功桥接了 Claude Desktop 和 n8n。

方法三:在 Claude Desktop 中直接使用 这是最终的验收测试。重启 Claude Desktop 后,在聊天框中尝试使用你 n8n 工作流暴露的工具。比如,如果你的工具叫 search_business,你可以直接对 Claude 说:“请使用 search_business 工具查询一下某某公司”。如果 Claude 能正确调用并返回结果,那么恭喜你,大功告成!

4.3 常见问题与排错指南

即使按照步骤操作,也可能遇到一些小坑。这里是我踩过并总结出来的:

  • 工具列表不显示或无法使用:这通常是 Claude Desktop 的缓存问题。彻底关闭 Claude Desktop(确保任务管理器里没有相关进程),等待十几秒再重新打开。90% 的此类问题都能这样解决。
  • 连接立即断开:首先检查代理脚本的 baseUrl 是否正确,以及你的网络能否访问这个 n8n 云地址。其次,打开代理脚本,在开头添加 console.error 打印更多信息,查看错误输出。最常见的原因是 n8n 工作流没有正确激活 MCP 服务器触发节点。
  • “spawn node ENOENT” 错误:这说明系统找不到 node 命令。请确保 Node.js 已正确安装并添加到系统 PATH 环境变量中。在 Windows 上,有时需要指定 node.exe 的完整路径,例如 "command": "C:\\Program Files\\nodejs\\node.exe"
  • 代理脚本权限问题(Linux/macOS):记得给脚本添加可执行权限:chmod +x n8n-streaming-proxy.js
  • n8n 工作流本身的问题:登录你的 n8n 云界面,确认包含 MCP 服务器触发节点的工作流是已激活状态(不是测试模式)。检查该节点的配置,确保 “Server URL” 和 “Path” 是正确的,并且有后续的工具节点与之连接。

这个自定义代理方案的优势在于,它完全掌控了 Claude Desktop 和 n8n 之间的对话逻辑。你可以在其中添加更复杂的逻辑,比如请求重试、响应缓存、监控指标收集,或者适配其他有特殊协议的服务。它不再是一个黑盒,而是一个你可以完全调试和优化的透明桥梁。

更多推荐