mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(MCP Server Trigger Node): Add MCP Server Trigger node to expose tools to MCP clients (#14403)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -32,6 +32,8 @@ export interface FrontendSettings {
|
|||||||
endpointForm: string;
|
endpointForm: string;
|
||||||
endpointFormTest: string;
|
endpointFormTest: string;
|
||||||
endpointFormWaiting: string;
|
endpointFormWaiting: string;
|
||||||
|
endpointMcp: string;
|
||||||
|
endpointMcpTest: string;
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
endpointWebhookWaiting: string;
|
endpointWebhookWaiting: string;
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ export class EndpointsConfig {
|
|||||||
@Env('N8N_ENDPOINT_WEBHOOK_WAIT')
|
@Env('N8N_ENDPOINT_WEBHOOK_WAIT')
|
||||||
webhookWaiting: string = 'webhook-waiting';
|
webhookWaiting: string = 'webhook-waiting';
|
||||||
|
|
||||||
|
/** Path segment for MCP endpoints. */
|
||||||
|
@Env('N8N_ENDPOINT_MCP')
|
||||||
|
mcp: string = 'mcp';
|
||||||
|
|
||||||
|
/** Path segment for test MCP endpoints. */
|
||||||
|
@Env('N8N_ENDPOINT_MCP_TEST')
|
||||||
|
mcpTest: string = 'mcp-test';
|
||||||
|
|
||||||
/** Whether to disable n8n's UI (frontend). */
|
/** Whether to disable n8n's UI (frontend). */
|
||||||
@Env('N8N_DISABLE_UI')
|
@Env('N8N_DISABLE_UI')
|
||||||
disableUi: boolean = false;
|
disableUi: boolean = false;
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ describe('GlobalConfig', () => {
|
|||||||
form: 'form',
|
form: 'form',
|
||||||
formTest: 'form-test',
|
formTest: 'form-test',
|
||||||
formWaiting: 'form-waiting',
|
formWaiting: 'form-waiting',
|
||||||
|
mcp: 'mcp',
|
||||||
|
mcpTest: 'mcp-test',
|
||||||
payloadSizeMax: 16,
|
payloadSizeMax: 16,
|
||||||
formDataFileSizeMax: 200,
|
formDataFileSizeMax: 200,
|
||||||
rest: 'rest',
|
rest: 'rest',
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
|
||||||
|
export type CompressionResponse = Response & {
|
||||||
|
/**
|
||||||
|
* `flush()` is defined in the compression middleware.
|
||||||
|
* This is necessary because the compression middleware sometimes waits
|
||||||
|
* for a certain amount of data before sending the data to the client
|
||||||
|
*/
|
||||||
|
flush: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FlushingSSEServerTransport extends SSEServerTransport {
|
||||||
|
constructor(
|
||||||
|
_endpoint: string,
|
||||||
|
private response: CompressionResponse,
|
||||||
|
) {
|
||||||
|
super(_endpoint, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(message: JSONRPCMessage): Promise<void> {
|
||||||
|
await super.send(message);
|
||||||
|
this.response.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
195
packages/@n8n/nodes-langchain/nodes/Mcp/McpServer.ts
Normal file
195
packages/@n8n/nodes-langchain/nodes/Mcp/McpServer.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import type { Tool } from '@langchain/core/tools';
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
||||||
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import {
|
||||||
|
JSONRPCMessageSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
CallToolRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import type * as express from 'express';
|
||||||
|
import { OperationalError, type Logger } from 'n8n-workflow';
|
||||||
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||||
|
|
||||||
|
import { FlushingSSEServerTransport } from './FlushingSSEServerTransport';
|
||||||
|
import type { CompressionResponse } from './FlushingSSEServerTransport';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the JSONRPC message and checks whether the method used was a tool
|
||||||
|
* call. This is necessary in order to not have executions for listing tools
|
||||||
|
* and other commands sent by the MCP client
|
||||||
|
*/
|
||||||
|
function wasToolCall(body: string) {
|
||||||
|
try {
|
||||||
|
const message: unknown = JSON.parse(body);
|
||||||
|
const parsedMessage: JSONRPCMessage = JSONRPCMessageSchema.parse(message);
|
||||||
|
return (
|
||||||
|
'method' in parsedMessage &&
|
||||||
|
'id' in parsedMessage &&
|
||||||
|
parsedMessage?.method === CallToolRequestSchema.shape.method.value
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class McpServer {
|
||||||
|
servers: { [sessionId: string]: Server } = {};
|
||||||
|
|
||||||
|
transports: { [sessionId: string]: FlushingSSEServerTransport } = {};
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
private tools: { [sessionId: string]: Tool[] } = {};
|
||||||
|
|
||||||
|
private resolveFunctions: { [sessionId: string]: CallableFunction } = {};
|
||||||
|
|
||||||
|
constructor(logger: Logger) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.logger.debug('MCP Server created');
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectTransport(postUrl: string, resp: CompressionResponse): Promise<void> {
|
||||||
|
const transport = new FlushingSSEServerTransport(postUrl, resp);
|
||||||
|
const server = this.setUpServer();
|
||||||
|
const { sessionId } = transport;
|
||||||
|
this.transports[sessionId] = transport;
|
||||||
|
this.servers[sessionId] = server;
|
||||||
|
|
||||||
|
resp.on('close', async () => {
|
||||||
|
this.logger.debug(`Deleting transport for ${sessionId}`);
|
||||||
|
delete this.tools[sessionId];
|
||||||
|
delete this.resolveFunctions[sessionId];
|
||||||
|
delete this.transports[sessionId];
|
||||||
|
delete this.servers[sessionId];
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
// Make sure we flush the compression middleware, so that it's not waiting for more content to be added to the buffer
|
||||||
|
if (resp.flush) {
|
||||||
|
resp.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePostMessage(req: express.Request, resp: CompressionResponse, connectedTools: Tool[]) {
|
||||||
|
const sessionId = req.query.sessionId as string;
|
||||||
|
const transport = this.transports[sessionId];
|
||||||
|
this.tools[sessionId] = connectedTools;
|
||||||
|
if (transport) {
|
||||||
|
// We need to add a promise here because the `handlePostMessage` will send something to the
|
||||||
|
// MCP Server, that will run in a different context. This means that the return will happen
|
||||||
|
// almost immediately, and will lead to marking the sub-node as "running" in the final execution
|
||||||
|
await new Promise(async (resolve) => {
|
||||||
|
this.resolveFunctions[sessionId] = resolve;
|
||||||
|
await transport.handlePostMessage(req, resp, req.rawBody.toString());
|
||||||
|
});
|
||||||
|
delete this.resolveFunctions[sessionId];
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`No transport found for session ${sessionId}`);
|
||||||
|
resp.status(401).send('No transport found for sessionId');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.flush) {
|
||||||
|
resp.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.tools[sessionId]; // Clean up to avoid keeping all tools in memory
|
||||||
|
|
||||||
|
return wasToolCall(req.rawBody.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpServer(): Server {
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'n8n-mcp-server',
|
||||||
|
version: '0.1.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async (_, extra: RequestHandlerExtra) => {
|
||||||
|
if (!extra.sessionId) {
|
||||||
|
throw new OperationalError('Require a sessionId for the listing of tools');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools: this.tools[extra.sessionId].map((tool) => {
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: zodToJsonSchema(tool.schema),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra: RequestHandlerExtra) => {
|
||||||
|
if (!request.params?.name || !request.params?.arguments) {
|
||||||
|
throw new OperationalError('Require a name and arguments for the tool call');
|
||||||
|
}
|
||||||
|
if (!extra.sessionId) {
|
||||||
|
throw new OperationalError('Require a sessionId for the tool call');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
|
||||||
|
(tool) => tool.name === request.params.name,
|
||||||
|
);
|
||||||
|
if (!requestedTool) {
|
||||||
|
throw new OperationalError('Tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestedTool.invoke(request.params.arguments);
|
||||||
|
|
||||||
|
this.resolveFunctions[extra.sessionId]();
|
||||||
|
|
||||||
|
this.logger.debug(`Got request for ${requestedTool.name}, and executed it.`);
|
||||||
|
|
||||||
|
// TODO: Refactor this to no longer use the legacy tool result, but
|
||||||
|
return { toolResult: result };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error while executing Tool ${requestedTool.name}: ${error}`);
|
||||||
|
return { isError: true, content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.onclose = () => {
|
||||||
|
this.logger.debug('Closing MCP Server');
|
||||||
|
};
|
||||||
|
server.onerror = (error: unknown) => {
|
||||||
|
this.logger.error(`MCP Error: ${error}`);
|
||||||
|
};
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This singleton is shared across the instance, making sure we only have one server to worry about.
|
||||||
|
* It needs to stay in memory to keep track of the long-lived connections.
|
||||||
|
* It requires a logger at first creation to set everything up.
|
||||||
|
*/
|
||||||
|
export class McpServerSingleton {
|
||||||
|
static #instance: McpServerSingleton;
|
||||||
|
|
||||||
|
private _serverData: McpServer;
|
||||||
|
|
||||||
|
private constructor(logger: Logger) {
|
||||||
|
this._serverData = new McpServer(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
static instance(logger: Logger): McpServer {
|
||||||
|
if (!McpServerSingleton.#instance) {
|
||||||
|
McpServerSingleton.#instance = new McpServerSingleton(logger);
|
||||||
|
logger.debug('Created singleton for MCP Servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
return McpServerSingleton.#instance.serverData;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverData() {
|
||||||
|
return this._serverData;
|
||||||
|
}
|
||||||
|
}
|
||||||
168
packages/@n8n/nodes-langchain/nodes/Mcp/McpTrigger.node.ts
Normal file
168
packages/@n8n/nodes-langchain/nodes/Mcp/McpTrigger.node.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { WebhookAuthorizationError } from 'n8n-nodes-base/dist/nodes/Webhook/error';
|
||||||
|
import { validateWebhookAuthentication } from 'n8n-nodes-base/dist/nodes/Webhook/utils';
|
||||||
|
import type { INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
|
||||||
|
import { NodeConnectionTypes, Node } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { getConnectedTools } from '@utils/helpers';
|
||||||
|
|
||||||
|
import type { CompressionResponse } from './FlushingSSEServerTransport';
|
||||||
|
import { McpServerSingleton } from './McpServer';
|
||||||
|
import type { McpServer } from './McpServer';
|
||||||
|
|
||||||
|
const MCP_SSE_SETUP_PATH = 'sse';
|
||||||
|
const MCP_SSE_MESSAGES_PATH = 'messages';
|
||||||
|
|
||||||
|
export class McpTrigger extends Node {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'MCP Server Trigger',
|
||||||
|
name: 'mcpTrigger',
|
||||||
|
icon: {
|
||||||
|
light: 'file:mcp.svg',
|
||||||
|
dark: 'file:mcp.dark.svg',
|
||||||
|
},
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Expose n8n tools as an MCP Server endpoint',
|
||||||
|
activationMessage: 'You can now connect your MCP Clients to the SSE URL.',
|
||||||
|
defaults: {
|
||||||
|
name: 'MCP Server Trigger',
|
||||||
|
},
|
||||||
|
triggerPanel: {
|
||||||
|
header: 'Listen for MCP events',
|
||||||
|
executionsHelp: {
|
||||||
|
inactive:
|
||||||
|
"This trigger has two modes: test and production.<br /><br /><b>Use test mode while you build your workflow</b>. Click the 'test step' button, then make an MCP request to the test URL. The executions will show up in the editor.<br /><br /><b>Use production mode to run your workflow automatically</b>. <a data-key='activate'>Activate</a> the workflow, then make requests to the production URL. These executions will show up in the <a data-key='executions'>executions list</a>, but not the editor.",
|
||||||
|
active:
|
||||||
|
"This trigger has two modes: test and production.<br /><br /><b>Use test mode while you build your workflow</b>. Click the 'test step' button, then make an MCP request to the test URL. The executions will show up in the editor.<br /><br /><b>Use production mode to run your workflow automatically</b>. Since your workflow is activated, you can make requests to the production URL. These executions will show up in the <a data-key='executions'>executions list</a>, but not the editor.",
|
||||||
|
},
|
||||||
|
activationHint:
|
||||||
|
'Once you’ve finished building your workflow, run it without having to click this button by using the production URL.',
|
||||||
|
},
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: NodeConnectionTypes.AiTool,
|
||||||
|
displayName: 'Tools',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
|
||||||
|
name: 'httpBearerAuth',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
authentication: ['bearerAuth'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'httpHeaderAuth',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
authentication: ['headerAuth'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'httpCustomAuth',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
authentication: ['customAuth'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Authentication',
|
||||||
|
name: 'authentication',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'None', value: 'none' },
|
||||||
|
{ name: 'Bearer Auth', value: 'bearerAuth' },
|
||||||
|
{ name: 'Header Auth', value: 'headerAuth' },
|
||||||
|
{ name: 'Custom Auth', value: 'customAuth' },
|
||||||
|
],
|
||||||
|
default: 'none',
|
||||||
|
description: 'The way to authenticate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Path',
|
||||||
|
name: 'path',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'webhook',
|
||||||
|
required: true,
|
||||||
|
description: 'The base path for this MCP server',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webhooks: [
|
||||||
|
{
|
||||||
|
name: 'setup',
|
||||||
|
httpMethod: 'GET',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
isFullPath: true,
|
||||||
|
path: `={{$parameter["path"]}}/${MCP_SSE_SETUP_PATH}`,
|
||||||
|
nodeType: 'mcp',
|
||||||
|
ndvHideMethod: true,
|
||||||
|
ndvHideUrl: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
isFullPath: true,
|
||||||
|
path: `={{$parameter["path"]}}/${MCP_SSE_MESSAGES_PATH}`,
|
||||||
|
nodeType: 'mcp',
|
||||||
|
ndvHideMethod: true,
|
||||||
|
ndvHideUrl: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
async webhook(context: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||||
|
const webhookName = context.getWebhookName();
|
||||||
|
const req = context.getRequestObject();
|
||||||
|
const resp = context.getResponseObject() as unknown as CompressionResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateWebhookAuthentication(context, 'authentication');
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof WebhookAuthorizationError) {
|
||||||
|
resp.writeHead(error.responseCode);
|
||||||
|
resp.end(error.message);
|
||||||
|
return { noWebhookResponse: true };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcpServer: McpServer = McpServerSingleton.instance(context.logger);
|
||||||
|
|
||||||
|
if (webhookName === 'setup') {
|
||||||
|
// Sets up the transport and opens the long-lived connection. This resp
|
||||||
|
// will stay streaming, and is the channel that sends the events
|
||||||
|
const postUrl = req.path.replace(
|
||||||
|
new RegExp(`/${MCP_SSE_SETUP_PATH}$`),
|
||||||
|
`/${MCP_SSE_MESSAGES_PATH}`,
|
||||||
|
);
|
||||||
|
await mcpServer.connectTransport(postUrl, resp);
|
||||||
|
|
||||||
|
return { noWebhookResponse: true };
|
||||||
|
} else if (webhookName === 'default') {
|
||||||
|
// This is the command-channel, and is actually executing the tools. This
|
||||||
|
// sends the response back through the long-lived connection setup in the
|
||||||
|
// 'setup' call
|
||||||
|
const connectedTools = await getConnectedTools(context, true);
|
||||||
|
|
||||||
|
const wasToolCall = await mcpServer.handlePostMessage(req, resp, connectedTools);
|
||||||
|
|
||||||
|
if (wasToolCall) return { noWebhookResponse: true, workflowData: [[{ json: {} }]] };
|
||||||
|
return { noWebhookResponse: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { workflowData: [[{ json: {} }]] };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import { FlushingSSEServerTransport } from '../FlushingSSEServerTransport';
|
||||||
|
import type { CompressionResponse } from '../FlushingSSEServerTransport';
|
||||||
|
|
||||||
|
describe('FlushingSSEServerTransport', () => {
|
||||||
|
const mockResponse = mock<CompressionResponse>();
|
||||||
|
let transport: FlushingSSEServerTransport;
|
||||||
|
const endpoint = '/test/endpoint';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
mockResponse.status.mockReturnThis();
|
||||||
|
transport = new FlushingSSEServerTransport(endpoint, mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call flush after sending a message', async () => {
|
||||||
|
// Create a sample JSONRPC message
|
||||||
|
const message: JSONRPCMessage = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: '123',
|
||||||
|
result: { success: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send a message through the transport
|
||||||
|
await transport.start();
|
||||||
|
await transport.send(message);
|
||||||
|
|
||||||
|
expect(mockResponse.writeHead).toHaveBeenCalledWith(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
});
|
||||||
|
expect(mockResponse.write).toHaveBeenCalledWith(
|
||||||
|
// @ts-expect-error `_sessionId` is private
|
||||||
|
`event: endpoint\ndata: /test/endpoint?sessionId=${transport._sessionId}\n\n`,
|
||||||
|
);
|
||||||
|
expect(mockResponse.write).toHaveBeenCalledWith(
|
||||||
|
`event: message\ndata: ${JSON.stringify(message)}\n\n`,
|
||||||
|
);
|
||||||
|
expect(mockResponse.flush).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import type { Tool } from '@langchain/core/tools';
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import type { Request } from 'express';
|
||||||
|
import { captor, mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import type { CompressionResponse } from '../FlushingSSEServerTransport';
|
||||||
|
import { FlushingSSEServerTransport } from '../FlushingSSEServerTransport';
|
||||||
|
import { McpServer } from '../McpServer';
|
||||||
|
|
||||||
|
const sessionId = 'mock-session-id';
|
||||||
|
const mockServer = mock<Server>();
|
||||||
|
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => {
|
||||||
|
return {
|
||||||
|
Server: jest.fn().mockImplementation(() => mockServer),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockTransport = mock<FlushingSSEServerTransport>({ sessionId });
|
||||||
|
jest.mock('../FlushingSSEServerTransport', () => {
|
||||||
|
return {
|
||||||
|
FlushingSSEServerTransport: jest.fn().mockImplementation(() => mockTransport),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('McpServer', () => {
|
||||||
|
const mockRequest = mock<Request>({ query: { sessionId }, path: '/sse' });
|
||||||
|
const mockResponse = mock<CompressionResponse>();
|
||||||
|
const mockTool = mock<Tool>({ name: 'mockTool' });
|
||||||
|
|
||||||
|
let mcpServer: McpServer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockResponse.status.mockReturnThis();
|
||||||
|
|
||||||
|
mcpServer = new McpServer(mock());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('connectTransport', () => {
|
||||||
|
const postUrl = '/post-url';
|
||||||
|
|
||||||
|
it('should set up a transport and server', async () => {
|
||||||
|
await mcpServer.connectTransport(postUrl, mockResponse);
|
||||||
|
|
||||||
|
// Check that FlushingSSEServerTransport was initialized with correct params
|
||||||
|
expect(FlushingSSEServerTransport).toHaveBeenCalledWith(postUrl, mockResponse);
|
||||||
|
|
||||||
|
// Check that Server was initialized
|
||||||
|
expect(Server).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Check that transport and server are stored
|
||||||
|
expect(mcpServer.transports[sessionId]).toBeDefined();
|
||||||
|
expect(mcpServer.servers[sessionId]).toBeDefined();
|
||||||
|
|
||||||
|
// Check that connect was called on the server
|
||||||
|
expect(mcpServer.servers[sessionId].connect).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Check that flush was called if available
|
||||||
|
expect(mockResponse.flush).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set up close handler that cleans up resources', async () => {
|
||||||
|
await mcpServer.connectTransport(postUrl, mockResponse);
|
||||||
|
|
||||||
|
// Get the close callback and execute it
|
||||||
|
const closeCallbackCaptor = captor<() => Promise<void>>();
|
||||||
|
expect(mockResponse.on).toHaveBeenCalledWith('close', closeCallbackCaptor);
|
||||||
|
await closeCallbackCaptor.value();
|
||||||
|
|
||||||
|
// Check that resources were cleaned up
|
||||||
|
expect(mcpServer.transports[sessionId]).toBeUndefined();
|
||||||
|
expect(mcpServer.servers[sessionId]).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handlePostMessage', () => {
|
||||||
|
it('should call transport.handlePostMessage when transport exists', async () => {
|
||||||
|
mockTransport.handlePostMessage.mockImplementation(async () => {
|
||||||
|
// @ts-expect-error private property `resolveFunctions`
|
||||||
|
mcpServer.resolveFunctions[sessionId]();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the transport directly
|
||||||
|
mcpServer.transports[sessionId] = mockTransport;
|
||||||
|
|
||||||
|
mockRequest.rawBody = Buffer.from(
|
||||||
|
JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'tools/call',
|
||||||
|
id: 123,
|
||||||
|
params: { name: 'mockTool' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the method
|
||||||
|
const result = await mcpServer.handlePostMessage(mockRequest, mockResponse, [mockTool]);
|
||||||
|
|
||||||
|
// Verify that transport's handlePostMessage was called
|
||||||
|
expect(mockTransport.handlePostMessage).toHaveBeenCalledWith(
|
||||||
|
mockRequest,
|
||||||
|
mockResponse,
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that we check if it was a tool call
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
// Verify flush was called
|
||||||
|
expect(mockResponse.flush).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 when transport does not exist', async () => {
|
||||||
|
// Call without setting up transport
|
||||||
|
await mcpServer.handlePostMessage(mockRequest, mockResponse, [mockTool]);
|
||||||
|
|
||||||
|
// Verify error status was set
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(mockResponse.send).toHaveBeenCalledWith(expect.stringContaining('No transport found'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import type { Tool } from '@langchain/core/tools';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { IWebhookFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { McpServer } from '../McpServer';
|
||||||
|
import { McpTrigger } from '../McpTrigger.node';
|
||||||
|
|
||||||
|
const mockTool = mock<Tool>({ name: 'mockTool' });
|
||||||
|
jest.mock('@utils/helpers', () => ({
|
||||||
|
getConnectedTools: jest.fn().mockImplementation(() => [mockTool]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockServer = mock<McpServer>();
|
||||||
|
jest.mock('../McpServer', () => ({
|
||||||
|
McpServerSingleton: {
|
||||||
|
instance: jest.fn().mockImplementation(() => mockServer),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('McpTrigger Node', () => {
|
||||||
|
const sessionId = 'mock-session-id';
|
||||||
|
const mockContext = mock<IWebhookFunctions>();
|
||||||
|
const mockRequest = mock<Request>({ query: { sessionId }, path: '/custom-path/sse' });
|
||||||
|
const mockResponse = mock<Response>();
|
||||||
|
let mcpTrigger: McpTrigger;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mcpTrigger = new McpTrigger();
|
||||||
|
|
||||||
|
mockContext.getRequestObject.mockReturnValue(mockRequest);
|
||||||
|
mockContext.getResponseObject.mockReturnValue(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webhook method', () => {
|
||||||
|
it('should handle setup webhook', async () => {
|
||||||
|
// Configure the context for setup webhook
|
||||||
|
mockContext.getWebhookName.mockReturnValue('setup');
|
||||||
|
|
||||||
|
// Call the webhook method
|
||||||
|
const result = await mcpTrigger.webhook(mockContext);
|
||||||
|
|
||||||
|
// Verify that the connectTransport method was called with correct URL
|
||||||
|
expect(mockServer.connectTransport).toHaveBeenCalledWith(
|
||||||
|
'/custom-path/messages',
|
||||||
|
mockResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the returned result has noWebhookResponse: true
|
||||||
|
expect(result).toEqual({ noWebhookResponse: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle default webhook for tool execution', async () => {
|
||||||
|
// Configure the context for default webhook (tool execution)
|
||||||
|
mockContext.getWebhookName.mockReturnValue('default');
|
||||||
|
|
||||||
|
// Mock that the server executes a tool and returns true
|
||||||
|
mockServer.handlePostMessage.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
// Call the webhook method
|
||||||
|
const result = await mcpTrigger.webhook(mockContext);
|
||||||
|
|
||||||
|
// Verify that handlePostMessage was called with request, response and tools
|
||||||
|
expect(mockServer.handlePostMessage).toHaveBeenCalledWith(mockRequest, mockResponse, [
|
||||||
|
mockTool,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify the returned result when a tool was called
|
||||||
|
expect(result).toEqual({
|
||||||
|
noWebhookResponse: true,
|
||||||
|
workflowData: [[{ json: {} }]],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle default webhook when no tool was executed', async () => {
|
||||||
|
// Configure the context for default webhook
|
||||||
|
mockContext.getWebhookName.mockReturnValue('default');
|
||||||
|
|
||||||
|
// Mock that the server doesn't execute a tool and returns false
|
||||||
|
mockServer.handlePostMessage.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
// Call the webhook method
|
||||||
|
const result = await mcpTrigger.webhook(mockContext);
|
||||||
|
|
||||||
|
// Verify the returned result when no tool was called
|
||||||
|
expect(result).toEqual({ noWebhookResponse: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/@n8n/nodes-langchain/nodes/Mcp/mcp.dark.svg
Normal file
7
packages/@n8n/nodes-langchain/nodes/Mcp/mcp.dark.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="180" height="180" viewBox="0 0 195 195" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke="#fff" stroke-width="12" stroke-linecap="round">
|
||||||
|
<path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177"/>
|
||||||
|
<path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52"/>
|
||||||
|
<path d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 735 B |
7
packages/@n8n/nodes-langchain/nodes/Mcp/mcp.svg
Normal file
7
packages/@n8n/nodes-langchain/nodes/Mcp/mcp.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="180" height="180" viewBox="0 0 195 195" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke="#000" stroke-width="12" stroke-linecap="round">
|
||||||
|
<path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177"/>
|
||||||
|
<path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52"/>
|
||||||
|
<path d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 735 B |
@@ -1,19 +1,16 @@
|
|||||||
// ToolsAgent.test.ts
|
|
||||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import { HumanMessage } from '@langchain/core/messages';
|
import { HumanMessage } from '@langchain/core/messages';
|
||||||
import type { BaseMessagePromptTemplateLike } from '@langchain/core/prompts';
|
import type { BaseMessagePromptTemplateLike } from '@langchain/core/prompts';
|
||||||
import { FakeTool } from '@langchain/core/utils/testing';
|
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { ToolsAgentAction } from 'langchain/dist/agents/tool_calling/output_parser';
|
import type { ToolsAgentAction } from 'langchain/dist/agents/tool_calling/output_parser';
|
||||||
import type { Tool } from 'langchain/tools';
|
import type { Tool } from 'langchain/tools';
|
||||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||||
import { NodeOperationError, BINARY_ENCODING } from 'n8n-workflow';
|
import { NodeOperationError, BINARY_ENCODING, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
import type { ZodType } from 'zod';
|
import type { ZodType } from 'zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import * as helpersModule from '@utils/helpers';
|
|
||||||
import type { N8nOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
import type { N8nOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,31 +25,16 @@ import {
|
|||||||
getTools,
|
getTools,
|
||||||
} from '../agents/ToolsAgent/execute';
|
} from '../agents/ToolsAgent/execute';
|
||||||
|
|
||||||
// We need to override the imported getConnectedTools so that we control its output.
|
|
||||||
jest.spyOn(helpersModule, 'getConnectedTools').mockResolvedValue([FakeTool as unknown as Tool]);
|
|
||||||
|
|
||||||
function getFakeOutputParser(returnSchema?: ZodType): N8nOutputParser {
|
function getFakeOutputParser(returnSchema?: ZodType): N8nOutputParser {
|
||||||
const fakeOutputParser = mock<N8nOutputParser>();
|
const fakeOutputParser = mock<N8nOutputParser>();
|
||||||
(fakeOutputParser.getSchema as jest.Mock).mockReturnValue(returnSchema);
|
(fakeOutputParser.getSchema as jest.Mock).mockReturnValue(returnSchema);
|
||||||
return fakeOutputParser;
|
return fakeOutputParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFakeExecuteFunctions(overrides: Partial<IExecuteFunctions> = {}): IExecuteFunctions {
|
const mockHelpers = mock<IExecuteFunctions['helpers']>();
|
||||||
return {
|
const mockContext = mock<IExecuteFunctions>({ helpers: mockHelpers });
|
||||||
getNodeParameter: jest
|
|
||||||
.fn()
|
beforeEach(() => jest.resetAllMocks());
|
||||||
.mockImplementation((_arg1: string, _arg2: number, defaultValue?: unknown) => {
|
|
||||||
return defaultValue;
|
|
||||||
}),
|
|
||||||
getNode: jest.fn().mockReturnValue({}),
|
|
||||||
getInputConnectionData: jest.fn().mockResolvedValue({}),
|
|
||||||
getInputData: jest.fn().mockReturnValue([]),
|
|
||||||
continueOnFail: jest.fn().mockReturnValue(false),
|
|
||||||
logger: { debug: jest.fn() },
|
|
||||||
helpers: {},
|
|
||||||
...overrides,
|
|
||||||
} as unknown as IExecuteFunctions;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('getOutputParserSchema', () => {
|
describe('getOutputParserSchema', () => {
|
||||||
it('should return a default schema if getSchema returns undefined', () => {
|
it('should return a default schema if getSchema returns undefined', () => {
|
||||||
@@ -74,6 +56,7 @@ describe('getOutputParserSchema', () => {
|
|||||||
describe('extractBinaryMessages', () => {
|
describe('extractBinaryMessages', () => {
|
||||||
it('should extract a binary message from the input data when no id is provided', async () => {
|
it('should extract a binary message from the input data when no id is provided', async () => {
|
||||||
const fakeItem = {
|
const fakeItem = {
|
||||||
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
img1: {
|
img1: {
|
||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
@@ -82,11 +65,9 @@ describe('extractBinaryMessages', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const ctx = createFakeExecuteFunctions({
|
mockContext.getInputData.mockReturnValue([fakeItem]);
|
||||||
getInputData: jest.fn().mockReturnValue([fakeItem]),
|
|
||||||
});
|
|
||||||
|
|
||||||
const humanMsg: HumanMessage = await extractBinaryMessages(ctx, 0);
|
const humanMsg: HumanMessage = await extractBinaryMessages(mockContext, 0);
|
||||||
// Expect the HumanMessage's content to be an array containing one binary message.
|
// Expect the HumanMessage's content to be an array containing one binary message.
|
||||||
expect(Array.isArray(humanMsg.content)).toBe(true);
|
expect(Array.isArray(humanMsg.content)).toBe(true);
|
||||||
expect(humanMsg.content[0]).toEqual({
|
expect(humanMsg.content[0]).toEqual({
|
||||||
@@ -97,6 +78,7 @@ describe('extractBinaryMessages', () => {
|
|||||||
|
|
||||||
it('should extract a binary message using binary stream if id is provided', async () => {
|
it('should extract a binary message using binary stream if id is provided', async () => {
|
||||||
const fakeItem = {
|
const fakeItem = {
|
||||||
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
img2: {
|
img2: {
|
||||||
mimeType: 'image/jpeg',
|
mimeType: 'image/jpeg',
|
||||||
@@ -105,21 +87,16 @@ describe('extractBinaryMessages', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Cast fakeHelpers as any to satisfy type requirements.
|
|
||||||
const fakeHelpers = {
|
|
||||||
getBinaryStream: jest.fn().mockResolvedValue('stream'),
|
|
||||||
binaryToBuffer: jest.fn().mockResolvedValue(Buffer.from('fakebufferdata')),
|
|
||||||
} as unknown as IExecuteFunctions['helpers'];
|
|
||||||
const ctx = createFakeExecuteFunctions({
|
|
||||||
getInputData: jest.fn().mockReturnValue([fakeItem]),
|
|
||||||
helpers: fakeHelpers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const humanMsg: HumanMessage = await extractBinaryMessages(ctx, 0);
|
mockHelpers.getBinaryStream.mockResolvedValue(mock());
|
||||||
|
mockHelpers.binaryToBuffer.mockResolvedValue(Buffer.from('fakebufferdata'));
|
||||||
|
mockContext.getInputData.mockReturnValue([fakeItem]);
|
||||||
|
|
||||||
|
const humanMsg: HumanMessage = await extractBinaryMessages(mockContext, 0);
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
expect(fakeHelpers.getBinaryStream).toHaveBeenCalledWith('1234');
|
expect(mockHelpers.getBinaryStream).toHaveBeenCalledWith('1234');
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
expect(fakeHelpers.binaryToBuffer).toHaveBeenCalled();
|
expect(mockHelpers.binaryToBuffer).toHaveBeenCalled();
|
||||||
const expectedUrl = `data:image/jpeg;base64,${Buffer.from('fakebufferdata').toString(
|
const expectedUrl = `data:image/jpeg;base64,${Buffer.from('fakebufferdata').toString(
|
||||||
BINARY_ENCODING,
|
BINARY_ENCODING,
|
||||||
)}`;
|
)}`;
|
||||||
@@ -173,48 +150,48 @@ describe('getChatModel', () => {
|
|||||||
const fakeChatModel = mock<BaseChatModel>();
|
const fakeChatModel = mock<BaseChatModel>();
|
||||||
fakeChatModel.bindTools = jest.fn();
|
fakeChatModel.bindTools = jest.fn();
|
||||||
fakeChatModel.lc_namespace = ['chat_models'];
|
fakeChatModel.lc_namespace = ['chat_models'];
|
||||||
|
mockContext.getInputConnectionData.mockResolvedValue(fakeChatModel);
|
||||||
|
|
||||||
const ctx = createFakeExecuteFunctions({
|
const model = await getChatModel(mockContext);
|
||||||
getInputConnectionData: jest.fn().mockResolvedValue(fakeChatModel),
|
|
||||||
});
|
|
||||||
const model = await getChatModel(ctx);
|
|
||||||
expect(model).toEqual(fakeChatModel);
|
expect(model).toEqual(fakeChatModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if the model is not a valid chat model', async () => {
|
it('should throw if the model is not a valid chat model', async () => {
|
||||||
const fakeInvalidModel = mock<BaseChatModel>(); // missing bindTools & lc_namespace
|
const fakeInvalidModel = mock<BaseChatModel>(); // missing bindTools & lc_namespace
|
||||||
fakeInvalidModel.lc_namespace = [];
|
fakeInvalidModel.lc_namespace = [];
|
||||||
const ctx = createFakeExecuteFunctions({
|
mockContext.getInputConnectionData.mockResolvedValue(fakeInvalidModel);
|
||||||
getInputConnectionData: jest.fn().mockResolvedValue(fakeInvalidModel),
|
mockContext.getNode.mockReturnValue(mock());
|
||||||
getNode: jest.fn().mockReturnValue({}),
|
await expect(getChatModel(mockContext)).rejects.toThrow(NodeOperationError);
|
||||||
});
|
|
||||||
await expect(getChatModel(ctx)).rejects.toThrow(NodeOperationError);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getOptionalMemory', () => {
|
describe('getOptionalMemory', () => {
|
||||||
it('should return the memory if available', async () => {
|
it('should return the memory if available', async () => {
|
||||||
const fakeMemory = { some: 'memory' };
|
const fakeMemory = { some: 'memory' };
|
||||||
const ctx = createFakeExecuteFunctions({
|
mockContext.getInputConnectionData.mockResolvedValue(fakeMemory);
|
||||||
getInputConnectionData: jest.fn().mockResolvedValue(fakeMemory),
|
|
||||||
});
|
const memory = await getOptionalMemory(mockContext);
|
||||||
const memory = await getOptionalMemory(ctx);
|
|
||||||
expect(memory).toEqual(fakeMemory);
|
expect(memory).toEqual(fakeMemory);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getTools', () => {
|
describe('getTools', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const fakeTool = mock<Tool>();
|
||||||
|
mockContext.getInputConnectionData
|
||||||
|
.calledWith(NodeConnectionTypes.AiTool, 0)
|
||||||
|
.mockResolvedValue([fakeTool]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should retrieve tools without appending if outputParser is not provided', async () => {
|
it('should retrieve tools without appending if outputParser is not provided', async () => {
|
||||||
const ctx = createFakeExecuteFunctions();
|
const tools = await getTools(mockContext);
|
||||||
const tools = await getTools(ctx);
|
|
||||||
|
|
||||||
expect(tools.length).toEqual(1);
|
expect(tools.length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should retrieve tools and append the structured output parser tool if outputParser is provided', async () => {
|
it('should retrieve tools and append the structured output parser tool if outputParser is provided', async () => {
|
||||||
const fakeOutputParser = getFakeOutputParser(z.object({ text: z.string() }));
|
const fakeOutputParser = getFakeOutputParser(z.object({ text: z.string() }));
|
||||||
const ctx = createFakeExecuteFunctions();
|
const tools = await getTools(mockContext, fakeOutputParser);
|
||||||
const tools = await getTools(ctx, fakeOutputParser);
|
|
||||||
// Our fake getConnectedTools returns one tool; with outputParser, one extra is appended.
|
// Our fake getConnectedTools returns one tool; with outputParser, one extra is appended.
|
||||||
expect(tools.length).toEqual(2);
|
expect(tools.length).toEqual(2);
|
||||||
const dynamicTool = tools.find((t) => t.name === 'format_final_json_response');
|
const dynamicTool = tools.find((t) => t.name === 'format_final_json_response');
|
||||||
@@ -225,6 +202,7 @@ describe('getTools', () => {
|
|||||||
describe('prepareMessages', () => {
|
describe('prepareMessages', () => {
|
||||||
it('should include a binary message if binary data is present and passthroughBinaryImages is true', async () => {
|
it('should include a binary message if binary data is present and passthroughBinaryImages is true', async () => {
|
||||||
const fakeItem = {
|
const fakeItem = {
|
||||||
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
img1: {
|
img1: {
|
||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
@@ -232,10 +210,8 @@ describe('prepareMessages', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const ctx = createFakeExecuteFunctions({
|
mockContext.getInputData.mockReturnValue([fakeItem]);
|
||||||
getInputData: jest.fn().mockReturnValue([fakeItem]),
|
const messages = await prepareMessages(mockContext, 0, {
|
||||||
});
|
|
||||||
const messages = await prepareMessages(ctx, 0, {
|
|
||||||
systemMessage: 'Test system',
|
systemMessage: 'Test system',
|
||||||
passthroughBinaryImages: true,
|
passthroughBinaryImages: true,
|
||||||
});
|
});
|
||||||
@@ -248,10 +224,8 @@ describe('prepareMessages', () => {
|
|||||||
|
|
||||||
it('should not include a binary message if no binary data is present', async () => {
|
it('should not include a binary message if no binary data is present', async () => {
|
||||||
const fakeItem = { json: {} }; // no binary key
|
const fakeItem = { json: {} }; // no binary key
|
||||||
const ctx = createFakeExecuteFunctions({
|
mockContext.getInputData.mockReturnValue([fakeItem]);
|
||||||
getInputData: jest.fn().mockReturnValue([fakeItem]),
|
const messages = await prepareMessages(mockContext, 0, {
|
||||||
});
|
|
||||||
const messages = await prepareMessages(ctx, 0, {
|
|
||||||
systemMessage: 'Test system',
|
systemMessage: 'Test system',
|
||||||
passthroughBinaryImages: true,
|
passthroughBinaryImages: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"dist/nodes/llms/LMCohere/LmCohere.node.js",
|
"dist/nodes/llms/LMCohere/LmCohere.node.js",
|
||||||
"dist/nodes/llms/LMOllama/LmOllama.node.js",
|
"dist/nodes/llms/LMOllama/LmOllama.node.js",
|
||||||
"dist/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.js",
|
"dist/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.js",
|
||||||
|
"dist/nodes/Mcp/McpTrigger.node.js",
|
||||||
"dist/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.js",
|
"dist/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.js",
|
||||||
"dist/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.js",
|
"dist/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.js",
|
||||||
"dist/nodes/memory/MemoryPostgresChat/MemoryPostgresChat.node.js",
|
"dist/nodes/memory/MemoryPostgresChat/MemoryPostgresChat.node.js",
|
||||||
@@ -167,6 +168,7 @@
|
|||||||
"@langchain/qdrant": "0.1.1",
|
"@langchain/qdrant": "0.1.1",
|
||||||
"@langchain/redis": "0.1.0",
|
"@langchain/redis": "0.1.0",
|
||||||
"@langchain/textsplitters": "0.1.0",
|
"@langchain/textsplitters": "0.1.0",
|
||||||
|
"@modelcontextprotocol/sdk": "1.9.0",
|
||||||
"@mozilla/readability": "0.6.0",
|
"@mozilla/readability": "0.6.0",
|
||||||
"@n8n/json-schema-to-zod": "workspace:*",
|
"@n8n/json-schema-to-zod": "workspace:*",
|
||||||
"@n8n/typeorm": "0.3.20-12",
|
"@n8n/typeorm": "0.3.20-12",
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export function escapeSingleCurlyBrackets(text?: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getConnectedTools = async (
|
export const getConnectedTools = async (
|
||||||
ctx: IExecuteFunctions,
|
ctx: IExecuteFunctions | IWebhookFunctions,
|
||||||
enforceUniqueNames: boolean,
|
enforceUniqueNames: boolean,
|
||||||
convertStructuredTool: boolean = true,
|
convertStructuredTool: boolean = true,
|
||||||
escapeCurlyBrackets: boolean = false,
|
escapeCurlyBrackets: boolean = false,
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ export abstract class AbstractServer {
|
|||||||
|
|
||||||
protected endpointWebhookWaiting: string;
|
protected endpointWebhookWaiting: string;
|
||||||
|
|
||||||
|
protected endpointMcp: string;
|
||||||
|
|
||||||
|
protected endpointMcpTest: string;
|
||||||
|
|
||||||
protected webhooksEnabled = true;
|
protected webhooksEnabled = true;
|
||||||
|
|
||||||
protected testWebhooksEnabled = false;
|
protected testWebhooksEnabled = false;
|
||||||
@@ -73,15 +77,19 @@ export abstract class AbstractServer {
|
|||||||
this.sslKey = config.getEnv('ssl_key');
|
this.sslKey = config.getEnv('ssl_key');
|
||||||
this.sslCert = config.getEnv('ssl_cert');
|
this.sslCert = config.getEnv('ssl_cert');
|
||||||
|
|
||||||
this.restEndpoint = this.globalConfig.endpoints.rest;
|
const { endpoints } = this.globalConfig;
|
||||||
|
this.restEndpoint = endpoints.rest;
|
||||||
|
|
||||||
this.endpointForm = this.globalConfig.endpoints.form;
|
this.endpointForm = endpoints.form;
|
||||||
this.endpointFormTest = this.globalConfig.endpoints.formTest;
|
this.endpointFormTest = endpoints.formTest;
|
||||||
this.endpointFormWaiting = this.globalConfig.endpoints.formWaiting;
|
this.endpointFormWaiting = endpoints.formWaiting;
|
||||||
|
|
||||||
this.endpointWebhook = this.globalConfig.endpoints.webhook;
|
this.endpointWebhook = endpoints.webhook;
|
||||||
this.endpointWebhookTest = this.globalConfig.endpoints.webhookTest;
|
this.endpointWebhookTest = endpoints.webhookTest;
|
||||||
this.endpointWebhookWaiting = this.globalConfig.endpoints.webhookWaiting;
|
this.endpointWebhookWaiting = endpoints.webhookWaiting;
|
||||||
|
|
||||||
|
this.endpointMcp = endpoints.mcp;
|
||||||
|
this.endpointMcpTest = endpoints.mcpTest;
|
||||||
|
|
||||||
this.logger = Container.get(Logger);
|
this.logger = Container.get(Logger);
|
||||||
}
|
}
|
||||||
@@ -202,6 +210,9 @@ export abstract class AbstractServer {
|
|||||||
`/${this.endpointWebhookWaiting}/:path{/:suffix}`,
|
`/${this.endpointWebhookWaiting}/:path{/:suffix}`,
|
||||||
createWebhookHandlerFor(Container.get(WaitingWebhooks)),
|
createWebhookHandlerFor(Container.get(WaitingWebhooks)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register a handler for live MCP servers
|
||||||
|
this.app.all(`/${this.endpointMcp}/*path`, liveWebhooksRequestHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.testWebhooksEnabled) {
|
if (this.testWebhooksEnabled) {
|
||||||
@@ -210,6 +221,9 @@ export abstract class AbstractServer {
|
|||||||
// Register a handler
|
// Register a handler
|
||||||
this.app.all(`/${this.endpointFormTest}/*path`, testWebhooksRequestHandler);
|
this.app.all(`/${this.endpointFormTest}/*path`, testWebhooksRequestHandler);
|
||||||
this.app.all(`/${this.endpointWebhookTest}/*path`, testWebhooksRequestHandler);
|
this.app.all(`/${this.endpointWebhookTest}/*path`, testWebhooksRequestHandler);
|
||||||
|
|
||||||
|
// Register a handler for test MCP servers
|
||||||
|
this.app.all(`/${this.endpointMcpTest}/*path`, testWebhooksRequestHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block bots from scanning the application
|
// Block bots from scanning the application
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export const STARTING_NODES = [
|
|||||||
'n8n-nodes-base.manualTrigger',
|
'n8n-nodes-base.manualTrigger',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MCP_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.mcpTrigger';
|
||||||
|
|
||||||
export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';
|
export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';
|
||||||
|
|
||||||
export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export class FrontendService {
|
|||||||
endpointForm: this.globalConfig.endpoints.form,
|
endpointForm: this.globalConfig.endpoints.form,
|
||||||
endpointFormTest: this.globalConfig.endpoints.formTest,
|
endpointFormTest: this.globalConfig.endpoints.formTest,
|
||||||
endpointFormWaiting: this.globalConfig.endpoints.formWaiting,
|
endpointFormWaiting: this.globalConfig.endpoints.formWaiting,
|
||||||
|
endpointMcp: this.globalConfig.endpoints.mcp,
|
||||||
|
endpointMcpTest: this.globalConfig.endpoints.mcpTest,
|
||||||
endpointWebhook: this.globalConfig.endpoints.webhook,
|
endpointWebhook: this.globalConfig.endpoints.webhook,
|
||||||
endpointWebhookTest: this.globalConfig.endpoints.webhookTest,
|
endpointWebhookTest: this.globalConfig.endpoints.webhookTest,
|
||||||
endpointWebhookWaiting: this.globalConfig.endpoints.webhookWaiting,
|
endpointWebhookWaiting: this.globalConfig.endpoints.webhookWaiting,
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export class WaitingWebhooks implements IWebhookManager {
|
|||||||
webhook.httpMethod === req.method &&
|
webhook.httpMethod === req.method &&
|
||||||
webhook.path === (suffix ?? '') &&
|
webhook.path === (suffix ?? '') &&
|
||||||
webhook.webhookDescription.restartWebhook === true &&
|
webhook.webhookDescription.restartWebhook === true &&
|
||||||
(webhook.webhookDescription.isForm || false) === this.includeForms,
|
(webhook.webhookDescription.nodeType === 'form' || false) === this.includeForms,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (webhookData === undefined) {
|
if (webhookData === undefined) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { finished } from 'stream/promises';
|
|||||||
|
|
||||||
import { ActiveExecutions } from '@/active-executions';
|
import { ActiveExecutions } from '@/active-executions';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { MCP_TRIGGER_NODE_TYPE } from '@/constants';
|
||||||
import type { Project } from '@/databases/entities/project';
|
import type { Project } from '@/databases/entities/project';
|
||||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
@@ -446,6 +447,32 @@ export async function executeWebhook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove this hack, and make sure that execution data is properly created before the MCP trigger is executed
|
||||||
|
if (workflowStartNode.type === MCP_TRIGGER_NODE_TYPE) {
|
||||||
|
// Initialize the data of the webhook node
|
||||||
|
const nodeExecutionStack: IExecuteData[] = [];
|
||||||
|
nodeExecutionStack.push({
|
||||||
|
node: workflowStartNode,
|
||||||
|
data: {
|
||||||
|
main: [],
|
||||||
|
},
|
||||||
|
source: null,
|
||||||
|
});
|
||||||
|
runExecutionData =
|
||||||
|
runExecutionData ||
|
||||||
|
({
|
||||||
|
startData: {},
|
||||||
|
resultData: {
|
||||||
|
runData: {},
|
||||||
|
},
|
||||||
|
executionData: {
|
||||||
|
contextData: {},
|
||||||
|
nodeExecutionStack,
|
||||||
|
waitingExecution: {},
|
||||||
|
},
|
||||||
|
} as IRunExecutionData);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
webhookResultData = await Container.get(WebhookService).runWebhook(
|
webhookResultData = await Container.get(WebhookService).runWebhook(
|
||||||
workflow,
|
workflow,
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
{ json: this.additionalData.httpRequest?.body || {} },
|
{ json: this.additionalData.httpRequest?.body || {} },
|
||||||
];
|
];
|
||||||
const runExecutionData: IRunExecutionData = {
|
const runExecutionData: IRunExecutionData = this.runExecutionData ?? {
|
||||||
resultData: {
|
resultData: {
|
||||||
runData: {},
|
runData: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -954,6 +954,8 @@ export interface RootState {
|
|||||||
endpointForm: string;
|
endpointForm: string;
|
||||||
endpointFormTest: string;
|
endpointFormTest: string;
|
||||||
endpointFormWaiting: string;
|
endpointFormWaiting: string;
|
||||||
|
endpointMcp: string;
|
||||||
|
endpointMcpTest: string;
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
endpointWebhookWaiting: string;
|
endpointWebhookWaiting: string;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const defaultSettings: FrontendSettings = {
|
|||||||
endpointForm: '',
|
endpointForm: '',
|
||||||
endpointFormTest: '',
|
endpointFormTest: '',
|
||||||
endpointFormWaiting: '',
|
endpointFormWaiting: '',
|
||||||
|
endpointMcp: '',
|
||||||
|
endpointMcpTest: '',
|
||||||
endpointWebhook: '',
|
endpointWebhook: '',
|
||||||
endpointWebhookTest: '',
|
endpointWebhookTest: '',
|
||||||
endpointWebhookWaiting: '',
|
endpointWebhookWaiting: '',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useToast } from '@/composables/useToast';
|
|||||||
import {
|
import {
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
|
MCP_TRIGGER_NODE_TYPE,
|
||||||
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
|
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
|
||||||
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
|
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
@@ -31,7 +32,7 @@ const isMinimized = ref(
|
|||||||
props.nodeTypeDescription &&
|
props.nodeTypeDescription &&
|
||||||
!OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(props.nodeTypeDescription.name),
|
!OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(props.nodeTypeDescription.name),
|
||||||
);
|
);
|
||||||
const showUrlFor = ref('test');
|
const showUrlFor = ref<'test' | 'production'>('test');
|
||||||
|
|
||||||
const isProductionOnly = computed(() => {
|
const isProductionOnly = computed(() => {
|
||||||
return (
|
return (
|
||||||
@@ -95,6 +96,18 @@ const baseText = computed(() => {
|
|||||||
copyMessage: i18n.baseText('nodeWebhooks.showMessage.message.formTrigger'),
|
copyMessage: i18n.baseText('nodeWebhooks.showMessage.message.formTrigger'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case MCP_TRIGGER_NODE_TYPE:
|
||||||
|
return {
|
||||||
|
toggleTitle: i18n.baseText('nodeWebhooks.webhookUrls.mcpTrigger'),
|
||||||
|
clickToDisplay: i18n.baseText('nodeWebhooks.clickToDisplayWebhookUrls.mcpTrigger'),
|
||||||
|
clickToHide: i18n.baseText('nodeWebhooks.clickToHideWebhookUrls.mcpTrigger'),
|
||||||
|
clickToCopy: i18n.baseText('nodeWebhooks.clickToCopyWebhookUrls.mcpTrigger'),
|
||||||
|
testUrl: i18n.baseText('nodeWebhooks.testUrl'),
|
||||||
|
productionUrl: i18n.baseText('nodeWebhooks.productionUrl'),
|
||||||
|
copyTitle: i18n.baseText('nodeWebhooks.showMessage.title.mcpTrigger'),
|
||||||
|
copyMessage: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
toggleTitle: i18n.baseText('nodeWebhooks.webhookUrls'),
|
toggleTitle: i18n.baseText('nodeWebhooks.webhookUrls'),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|||||||
import {
|
import {
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
|
MCP_TRIGGER_NODE_TYPE,
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
||||||
WEBHOOK_NODE_TYPE,
|
WEBHOOK_NODE_TYPE,
|
||||||
@@ -1071,7 +1072,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
|||||||
|
|
||||||
// if it's a webhook and the path is empty set the UUID as the default path
|
// if it's a webhook and the path is empty set the UUID as the default path
|
||||||
if (
|
if (
|
||||||
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(node.type) &&
|
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, MCP_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||||
node.parameters.path === ''
|
node.parameters.path === ''
|
||||||
) {
|
) {
|
||||||
node.parameters.path = node.webhookId as string;
|
node.parameters.path = node.webhookId as string;
|
||||||
|
|||||||
@@ -679,20 +679,26 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||||||
function getWebhookUrl(
|
function getWebhookUrl(
|
||||||
webhookData: IWebhookDescription,
|
webhookData: IWebhookDescription,
|
||||||
node: INode,
|
node: INode,
|
||||||
showUrlFor?: string,
|
showUrlFor: 'test' | 'production',
|
||||||
): string {
|
): string {
|
||||||
const { isForm, restartWebhook } = webhookData;
|
const { nodeType, restartWebhook } = webhookData;
|
||||||
if (restartWebhook === true) {
|
if (restartWebhook === true) {
|
||||||
return isForm ? '$execution.resumeFormUrl' : '$execution.resumeUrl';
|
return nodeType === 'form' ? '$execution.resumeFormUrl' : '$execution.resumeUrl';
|
||||||
}
|
|
||||||
|
|
||||||
let baseUrl;
|
|
||||||
if (showUrlFor === 'test') {
|
|
||||||
baseUrl = isForm ? rootStore.formTestUrl : rootStore.webhookTestUrl;
|
|
||||||
} else {
|
|
||||||
baseUrl = isForm ? rootStore.formUrl : rootStore.webhookUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseUrls = {
|
||||||
|
test: {
|
||||||
|
form: rootStore.formTestUrl,
|
||||||
|
mcp: rootStore.mcpTestUrl,
|
||||||
|
webhook: rootStore.webhookTestUrl,
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
form: rootStore.formUrl,
|
||||||
|
mcp: rootStore.mcpUrl,
|
||||||
|
webhook: rootStore.webhookUrl,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
const baseUrl = baseUrls[showUrlFor][nodeType ?? 'webhook'];
|
||||||
const workflowId = workflowsStore.workflowId;
|
const workflowId = workflowsStore.workflowId;
|
||||||
const path = getWebhookExpressionValue(webhookData, 'path', true, node.name) ?? '';
|
const path = getWebhookExpressionValue(webhookData, 'path', true, node.name) ?? '';
|
||||||
const isFullPath =
|
const isFullPath =
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
|||||||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||||
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||||
export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger';
|
export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger';
|
||||||
|
export const MCP_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.mcpTrigger';
|
||||||
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
|
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
|
||||||
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
|
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
|
||||||
export const OPEN_AI_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAi';
|
export const OPEN_AI_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAi';
|
||||||
@@ -239,6 +240,7 @@ export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [
|
|||||||
WEBHOOK_NODE_TYPE,
|
WEBHOOK_NODE_TYPE,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
|
MCP_TRIGGER_NODE_TYPE,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SINGLE_WEBHOOK_TRIGGERS = [
|
export const SINGLE_WEBHOOK_TRIGGERS = [
|
||||||
|
|||||||
@@ -1495,23 +1495,28 @@
|
|||||||
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
|
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
|
||||||
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form URL",
|
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form URL",
|
||||||
"nodeWebhooks.clickToCopyWebhookUrls.chatTrigger": "Click to copy Chat URL",
|
"nodeWebhooks.clickToCopyWebhookUrls.chatTrigger": "Click to copy Chat URL",
|
||||||
|
"nodeWebhooks.clickToCopyWebhookUrls.mcpTrigger": "Click to copy MCP URL",
|
||||||
"nodeWebhooks.clickToDisplayWebhookUrls": "Click to display webhook URLs",
|
"nodeWebhooks.clickToDisplayWebhookUrls": "Click to display webhook URLs",
|
||||||
"nodeWebhooks.clickToDisplayWebhookUrls.formTrigger": "Click to display Form URL",
|
"nodeWebhooks.clickToDisplayWebhookUrls.formTrigger": "Click to display Form URL",
|
||||||
"nodeWebhooks.clickToDisplayWebhookUrls.chatTrigger": "Click to display Chat URL",
|
"nodeWebhooks.clickToDisplayWebhookUrls.chatTrigger": "Click to display Chat URL",
|
||||||
|
"nodeWebhooks.clickToDisplayWebhookUrls.mcpTrigger": "Click to display MCP URL",
|
||||||
"nodeWebhooks.clickToHideWebhookUrls": "Click to hide webhook URLs",
|
"nodeWebhooks.clickToHideWebhookUrls": "Click to hide webhook URLs",
|
||||||
"nodeWebhooks.clickToHideWebhookUrls.formTrigger": "Click to hide Form URL",
|
"nodeWebhooks.clickToHideWebhookUrls.formTrigger": "Click to hide Form URL",
|
||||||
"nodeWebhooks.clickToHideWebhookUrls.chatTrigger": "Click to hide Chat URL",
|
"nodeWebhooks.clickToHideWebhookUrls.chatTrigger": "Click to hide Chat URL",
|
||||||
|
"nodeWebhooks.clickToHideWebhookUrls.mcpTrigger": "Click to hide MCP URL",
|
||||||
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
|
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
|
||||||
"nodeWebhooks.productionUrl": "Production URL",
|
"nodeWebhooks.productionUrl": "Production URL",
|
||||||
"nodeWebhooks.showMessage.title": "URL copied",
|
"nodeWebhooks.showMessage.title": "URL copied",
|
||||||
"nodeWebhooks.showMessage.title.formTrigger": "Form URL copied",
|
"nodeWebhooks.showMessage.title.formTrigger": "Form URL copied",
|
||||||
"nodeWebhooks.showMessage.title.chatTrigger": "Chat URL copied",
|
"nodeWebhooks.showMessage.title.chatTrigger": "Chat URL copied",
|
||||||
|
"nodeWebhooks.showMessage.title.mcpTrigger": "MCP URL copied",
|
||||||
"nodeWebhooks.showMessage.message.formTrigger": "Form submissions made via this URL will trigger the workflow when it's activated",
|
"nodeWebhooks.showMessage.message.formTrigger": "Form submissions made via this URL will trigger the workflow when it's activated",
|
||||||
"nodeWebhooks.showMessage.message.chatTrigger": "Chat submissions made via this URL will trigger the workflow when it's activated",
|
"nodeWebhooks.showMessage.message.chatTrigger": "Chat submissions made via this URL will trigger the workflow when it's activated",
|
||||||
"nodeWebhooks.testUrl": "Test URL",
|
"nodeWebhooks.testUrl": "Test URL",
|
||||||
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
||||||
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
|
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
|
||||||
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
|
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
|
||||||
|
"nodeWebhooks.webhookUrls.mcpTrigger": "MCP URL",
|
||||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||||
"parameterInput.expressionResult": "e.g. {result}",
|
"parameterInput.expressionResult": "e.g. {result}",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
|
|||||||
const { VUE_APP_URL_BASE_API } = import.meta.env;
|
const { VUE_APP_URL_BASE_API } = import.meta.env;
|
||||||
|
|
||||||
export const useRootStore = defineStore(STORES.ROOT, () => {
|
export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||||
const state = ref({
|
const state = ref<RootState>({
|
||||||
baseUrl: VUE_APP_URL_BASE_API ?? window.BASE_PATH,
|
baseUrl: VUE_APP_URL_BASE_API ?? window.BASE_PATH,
|
||||||
restEndpoint:
|
restEndpoint:
|
||||||
!window.REST_ENDPOINT || window.REST_ENDPOINT === '{{REST_ENDPOINT}}'
|
!window.REST_ENDPOINT || window.REST_ENDPOINT === '{{REST_ENDPOINT}}'
|
||||||
@@ -17,6 +17,8 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
|||||||
endpointForm: 'form',
|
endpointForm: 'form',
|
||||||
endpointFormTest: 'form-test',
|
endpointFormTest: 'form-test',
|
||||||
endpointFormWaiting: 'form-waiting',
|
endpointFormWaiting: 'form-waiting',
|
||||||
|
endpointMcp: 'mcp',
|
||||||
|
endpointMcpTest: 'mcp-test',
|
||||||
endpointWebhook: 'webhook',
|
endpointWebhook: 'webhook',
|
||||||
endpointWebhookTest: 'webhook-test',
|
endpointWebhookTest: 'webhook-test',
|
||||||
endpointWebhookWaiting: 'webhook-waiting',
|
endpointWebhookWaiting: 'webhook-waiting',
|
||||||
@@ -49,10 +51,18 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
|||||||
|
|
||||||
const webhookUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointWebhook}`);
|
const webhookUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointWebhook}`);
|
||||||
|
|
||||||
|
const webhookTestUrl = computed(
|
||||||
|
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookTest}`,
|
||||||
|
);
|
||||||
|
|
||||||
const webhookWaitingUrl = computed(
|
const webhookWaitingUrl = computed(
|
||||||
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookWaiting}`,
|
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookWaiting}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mcpUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointMcp}`);
|
||||||
|
|
||||||
|
const mcpTestUrl = computed(() => `${state.value.urlBaseEditor}${state.value.endpointMcpTest}`);
|
||||||
|
|
||||||
const pushRef = computed(() => state.value.pushRef);
|
const pushRef = computed(() => state.value.pushRef);
|
||||||
|
|
||||||
const binaryDataMode = computed(() => state.value.binaryDataMode);
|
const binaryDataMode = computed(() => state.value.binaryDataMode);
|
||||||
@@ -67,10 +77,6 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
|||||||
|
|
||||||
const OAuthCallbackUrls = computed(() => state.value.oauthCallbackUrls);
|
const OAuthCallbackUrls = computed(() => state.value.oauthCallbackUrls);
|
||||||
|
|
||||||
const webhookTestUrl = computed(
|
|
||||||
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookTest}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const restUrl = computed(() => `${state.value.baseUrl}${state.value.restEndpoint}`);
|
const restUrl = computed(() => `${state.value.baseUrl}${state.value.restEndpoint}`);
|
||||||
|
|
||||||
const executionTimeout = computed(() => state.value.executionTimeout);
|
const executionTimeout = computed(() => state.value.executionTimeout);
|
||||||
@@ -164,7 +170,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
|||||||
state.value.defaultLocale = locale;
|
state.value.defaultLocale = locale;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBinaryDataMode = (binaryDataMode: string) => {
|
const setBinaryDataMode = (binaryDataMode: RootState['binaryDataMode']) => {
|
||||||
state.value.binaryDataMode = binaryDataMode;
|
state.value.binaryDataMode = binaryDataMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,6 +181,8 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
|||||||
formUrl,
|
formUrl,
|
||||||
formTestUrl,
|
formTestUrl,
|
||||||
formWaitingUrl,
|
formWaitingUrl,
|
||||||
|
mcpUrl,
|
||||||
|
mcpTestUrl,
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
webhookTestUrl,
|
webhookTestUrl,
|
||||||
webhookWaitingUrl,
|
webhookWaitingUrl,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { IAuthenticateGeneric, ICredentialType, INodeProperties, Icon } from 'n8n-workflow';
|
||||||
|
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/cred-class-name-unsuffixed
|
||||||
|
export class HttpBearerAuth implements ICredentialType {
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/cred-class-field-name-unsuffixed
|
||||||
|
name = 'httpBearerAuth';
|
||||||
|
|
||||||
|
displayName = 'Bearer Auth';
|
||||||
|
|
||||||
|
documentationUrl = 'httpRequest';
|
||||||
|
|
||||||
|
genericAuth = true;
|
||||||
|
|
||||||
|
icon: Icon = 'node:n8n-nodes-base.httpRequest';
|
||||||
|
|
||||||
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Bearer Token',
|
||||||
|
name: 'token',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'This credential uses the "Authorization" header. To use a custom header, use a "Custom Auth" credential instead',
|
||||||
|
name: 'useCustomAuth',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
authenticate: IAuthenticateGeneric = {
|
||||||
|
type: 'generic',
|
||||||
|
properties: {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ={{$credentials.token}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -283,7 +283,7 @@ export class Form extends Node {
|
|||||||
path: '',
|
path: '',
|
||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
isForm: true,
|
nodeType: 'form',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@@ -292,7 +292,7 @@ export class Form extends Node {
|
|||||||
path: '',
|
path: '',
|
||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
isForm: true,
|
nodeType: 'form',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
properties: [
|
properties: [
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const descriptionV2: INodeTypeDescription = {
|
|||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
path: '={{ $parameter["path"] || $parameter["options"]?.path || $webhookId }}',
|
path: '={{ $parameter["path"] || $parameter["options"]?.path || $webhookId }}',
|
||||||
ndvHideUrl: true,
|
ndvHideUrl: true,
|
||||||
isForm: true,
|
nodeType: 'form',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@@ -62,7 +62,7 @@ const descriptionV2: INodeTypeDescription = {
|
|||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
path: '={{ $parameter["path"] || $parameter["options"]?.path || $webhookId }}',
|
path: '={{ $parameter["path"] || $parameter["options"]?.path || $webhookId }}',
|
||||||
ndvHideMethod: true,
|
ndvHideMethod: true,
|
||||||
isForm: true,
|
nodeType: 'form',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
eventTriggerDescription: 'Waiting for you to submit the form',
|
eventTriggerDescription: 'Waiting for you to submit the form',
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export class Wait extends Webhook {
|
|||||||
path: webhookPath,
|
path: webhookPath,
|
||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
isForm: true,
|
nodeType: 'form',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@@ -266,7 +266,7 @@ export class Wait extends Webhook {
|
|||||||
path: webhookPath,
|
path: webhookPath,
|
||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
isForm: true,
|
nodeType: 'form',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
properties: [
|
properties: [
|
||||||
|
|||||||
@@ -208,6 +208,20 @@ export async function validateWebhookAuthentication(
|
|||||||
// Provided authentication data is wrong
|
// Provided authentication data is wrong
|
||||||
throw new WebhookAuthorizationError(403);
|
throw new WebhookAuthorizationError(403);
|
||||||
}
|
}
|
||||||
|
} else if (authentication === 'bearerAuth') {
|
||||||
|
let expectedAuth: ICredentialDataDecryptedObject | undefined;
|
||||||
|
try {
|
||||||
|
expectedAuth = await ctx.getCredentials<ICredentialDataDecryptedObject>('httpBearerAuth');
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const expectedToken = expectedAuth?.token as string;
|
||||||
|
if (!expectedToken) {
|
||||||
|
throw new WebhookAuthorizationError(500, 'No authentication data defined on node!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.authorization !== `Bearer ${expectedToken}`) {
|
||||||
|
throw new WebhookAuthorizationError(403);
|
||||||
|
}
|
||||||
} else if (authentication === 'headerAuth') {
|
} else if (authentication === 'headerAuth') {
|
||||||
// Special header with value is needed to call webhook
|
// Special header with value is needed to call webhook
|
||||||
let expectedAuth: ICredentialDataDecryptedObject | undefined;
|
let expectedAuth: ICredentialDataDecryptedObject | undefined;
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"dist/credentials/HighLevelOAuth2Api.credentials.js",
|
"dist/credentials/HighLevelOAuth2Api.credentials.js",
|
||||||
"dist/credentials/HomeAssistantApi.credentials.js",
|
"dist/credentials/HomeAssistantApi.credentials.js",
|
||||||
"dist/credentials/HttpBasicAuth.credentials.js",
|
"dist/credentials/HttpBasicAuth.credentials.js",
|
||||||
|
"dist/credentials/HttpBearerAuth.credentials.js",
|
||||||
"dist/credentials/HttpDigestAuth.credentials.js",
|
"dist/credentials/HttpDigestAuth.credentials.js",
|
||||||
"dist/credentials/HttpHeaderAuth.credentials.js",
|
"dist/credentials/HttpHeaderAuth.credentials.js",
|
||||||
"dist/credentials/HttpCustomAuth.credentials.js",
|
"dist/credentials/HttpCustomAuth.credentials.js",
|
||||||
|
|||||||
@@ -1988,7 +1988,7 @@ export interface IWebhookDescription {
|
|||||||
responseMode?: WebhookResponseMode | string;
|
responseMode?: WebhookResponseMode | string;
|
||||||
responseData?: WebhookResponseData | string;
|
responseData?: WebhookResponseData | string;
|
||||||
restartWebhook?: boolean;
|
restartWebhook?: boolean;
|
||||||
isForm?: boolean;
|
nodeType?: 'webhook' | 'form' | 'mcp';
|
||||||
ndvHideUrl?: string | boolean; // If true the webhook will not be displayed in the editor
|
ndvHideUrl?: string | boolean; // If true the webhook will not be displayed in the editor
|
||||||
ndvHideMethod?: string | boolean; // If true the method will not be displayed in the editor
|
ndvHideMethod?: string | boolean; // If true the method will not be displayed in the editor
|
||||||
}
|
}
|
||||||
|
|||||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -613,6 +613,9 @@ importers:
|
|||||||
'@langchain/textsplitters':
|
'@langchain/textsplitters':
|
||||||
specifier: 0.1.0
|
specifier: 0.1.0
|
||||||
version: 0.1.0(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))
|
version: 0.1.0(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: 1.9.0
|
||||||
|
version: 1.9.0
|
||||||
'@mozilla/readability':
|
'@mozilla/readability':
|
||||||
specifier: 0.6.0
|
specifier: 0.6.0
|
||||||
version: 0.6.0
|
version: 0.6.0
|
||||||
@@ -4574,6 +4577,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: '>= 3'
|
zod: '>= 3'
|
||||||
|
|
||||||
|
'@modelcontextprotocol/sdk@1.9.0':
|
||||||
|
resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@mongodb-js/saslprep@1.1.9':
|
'@mongodb-js/saslprep@1.1.9':
|
||||||
resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==}
|
resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==}
|
||||||
|
|
||||||
@@ -7643,6 +7650,10 @@ packages:
|
|||||||
core-util-is@1.0.3:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
|
cors@2.8.5:
|
||||||
|
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
cpu-features@0.0.10:
|
cpu-features@0.0.10:
|
||||||
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
|
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -8525,10 +8536,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
|
eventsource-parser@3.0.1:
|
||||||
|
resolution: {integrity: sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
eventsource@2.0.2:
|
eventsource@2.0.2:
|
||||||
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
|
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
eventsource@3.0.6:
|
||||||
|
resolution: {integrity: sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
execa@4.1.0:
|
execa@4.1.0:
|
||||||
resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
|
resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -16950,6 +16969,21 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
|
|
||||||
|
'@modelcontextprotocol/sdk@1.9.0':
|
||||||
|
dependencies:
|
||||||
|
content-type: 1.0.5
|
||||||
|
cors: 2.8.5
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
eventsource: 3.0.6
|
||||||
|
express: 5.1.0
|
||||||
|
express-rate-limit: 7.5.0(express@5.1.0)
|
||||||
|
pkce-challenge: 5.0.0(patch_hash=651e785d0b7bbf5be9210e1e895c39a16dc3ce8a5a3843b4819565fb6e175b90)
|
||||||
|
raw-body: 3.0.0
|
||||||
|
zod: 3.24.1
|
||||||
|
zod-to-json-schema: 3.24.1(zod@3.24.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@mongodb-js/saslprep@1.1.9':
|
'@mongodb-js/saslprep@1.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
@@ -20821,6 +20855,11 @@ snapshots:
|
|||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
|
cors@2.8.5:
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
|
||||||
cpu-features@0.0.10:
|
cpu-features@0.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
buildcheck: 0.0.6
|
buildcheck: 0.0.6
|
||||||
@@ -21697,7 +21736,7 @@ snapshots:
|
|||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
is-core-module: 2.13.1
|
is-core-module: 2.13.1
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -21722,7 +21761,7 @@ snapshots:
|
|||||||
|
|
||||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.8.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.8.2)
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
@@ -21742,7 +21781,7 @@ snapshots:
|
|||||||
array.prototype.findlastindex: 1.2.3
|
array.prototype.findlastindex: 1.2.3
|
||||||
array.prototype.flat: 1.3.2
|
array.prototype.flat: 1.3.2
|
||||||
array.prototype.flatmap: 1.3.2
|
array.prototype.flatmap: 1.3.2
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
@@ -21937,8 +21976,14 @@ snapshots:
|
|||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
|
eventsource-parser@3.0.1: {}
|
||||||
|
|
||||||
eventsource@2.0.2: {}
|
eventsource@2.0.2: {}
|
||||||
|
|
||||||
|
eventsource@3.0.6:
|
||||||
|
dependencies:
|
||||||
|
eventsource-parser: 3.0.1
|
||||||
|
|
||||||
execa@4.1.0:
|
execa@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -22555,7 +22600,7 @@ snapshots:
|
|||||||
array-parallel: 0.1.3
|
array-parallel: 0.1.3
|
||||||
array-series: 0.1.5
|
array-series: 0.1.5
|
||||||
cross-spawn: 4.0.2
|
cross-spawn: 4.0.2
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -25427,7 +25472,7 @@ snapshots:
|
|||||||
|
|
||||||
pdf-parse@1.1.1:
|
pdf-parse@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
node-ensure: 0.0.0
|
node-ensure: 0.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -26271,7 +26316,7 @@ snapshots:
|
|||||||
|
|
||||||
rhea@1.0.24:
|
rhea@1.0.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@8.1.1)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user