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:
jeanpaul
2025-04-09 14:45:24 +02:00
committed by GitHub
parent a495d81c13
commit 8360283c6f
36 changed files with 942 additions and 105 deletions

View File

@@ -954,6 +954,8 @@ export interface RootState {
endpointForm: string;
endpointFormTest: string;
endpointFormWaiting: string;
endpointMcp: string;
endpointMcpTest: string;
endpointWebhook: string;
endpointWebhookTest: string;
endpointWebhookWaiting: string;

View File

@@ -15,6 +15,8 @@ export const defaultSettings: FrontendSettings = {
endpointForm: '',
endpointFormTest: '',
endpointFormWaiting: '',
endpointMcp: '',
endpointMcpTest: '',
endpointWebhook: '',
endpointWebhookTest: '',
endpointWebhookWaiting: '',

View File

@@ -4,6 +4,7 @@ import { useToast } from '@/composables/useToast';
import {
CHAT_TRIGGER_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
MCP_TRIGGER_NODE_TYPE,
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
} from '@/constants';
@@ -31,7 +32,7 @@ const isMinimized = ref(
props.nodeTypeDescription &&
!OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(props.nodeTypeDescription.name),
);
const showUrlFor = ref('test');
const showUrlFor = ref<'test' | 'production'>('test');
const isProductionOnly = computed(() => {
return (
@@ -95,6 +96,18 @@ const baseText = computed(() => {
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:
return {
toggleTitle: i18n.baseText('nodeWebhooks.webhookUrls'),

View File

@@ -27,6 +27,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import {
EnterpriseEditionFeature,
FORM_TRIGGER_NODE_TYPE,
MCP_TRIGGER_NODE_TYPE,
STICKY_NODE_TYPE,
UPDATE_WEBHOOK_ID_NODE_TYPES,
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 (
[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.webhookId as string;

View File

@@ -679,20 +679,26 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
function getWebhookUrl(
webhookData: IWebhookDescription,
node: INode,
showUrlFor?: string,
showUrlFor: 'test' | 'production',
): string {
const { isForm, restartWebhook } = webhookData;
const { nodeType, restartWebhook } = webhookData;
if (restartWebhook === true) {
return isForm ? '$execution.resumeFormUrl' : '$execution.resumeUrl';
}
let baseUrl;
if (showUrlFor === 'test') {
baseUrl = isForm ? rootStore.formTestUrl : rootStore.webhookTestUrl;
} else {
baseUrl = isForm ? rootStore.formUrl : rootStore.webhookUrl;
return nodeType === 'form' ? '$execution.resumeFormUrl' : '$execution.resumeUrl';
}
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 path = getWebhookExpressionValue(webhookData, 'path', true, node.name) ?? '';
const isFullPath =

View File

@@ -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 MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
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 AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
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,
FORM_TRIGGER_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
MCP_TRIGGER_NODE_TYPE,
];
export const SINGLE_WEBHOOK_TRIGGERS = [

View File

@@ -1495,23 +1495,28 @@
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form 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.formTrigger": "Click to display Form 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.formTrigger": "Click to hide Form URL",
"nodeWebhooks.clickToHideWebhookUrls.chatTrigger": "Click to hide Chat URL",
"nodeWebhooks.clickToHideWebhookUrls.mcpTrigger": "Click to hide MCP URL",
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
"nodeWebhooks.productionUrl": "Production URL",
"nodeWebhooks.showMessage.title": "URL copied",
"nodeWebhooks.showMessage.title.formTrigger": "Form 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.chatTrigger": "Chat submissions made via this URL will trigger the workflow when it's activated",
"nodeWebhooks.testUrl": "Test URL",
"nodeWebhooks.webhookUrls": "Webhook URLs",
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
"nodeWebhooks.webhookUrls.mcpTrigger": "MCP URL",
"openWorkflow.workflowImportError": "Could not import workflow",
"openWorkflow.workflowNotFoundError": "Could not find workflow",
"parameterInput.expressionResult": "e.g. {result}",

View File

@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
const { VUE_APP_URL_BASE_API } = import.meta.env;
export const useRootStore = defineStore(STORES.ROOT, () => {
const state = ref({
const state = ref<RootState>({
baseUrl: VUE_APP_URL_BASE_API ?? window.BASE_PATH,
restEndpoint:
!window.REST_ENDPOINT || window.REST_ENDPOINT === '{{REST_ENDPOINT}}'
@@ -17,6 +17,8 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
endpointForm: 'form',
endpointFormTest: 'form-test',
endpointFormWaiting: 'form-waiting',
endpointMcp: 'mcp',
endpointMcpTest: 'mcp-test',
endpointWebhook: 'webhook',
endpointWebhookTest: 'webhook-test',
endpointWebhookWaiting: 'webhook-waiting',
@@ -49,10 +51,18 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
const webhookUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointWebhook}`);
const webhookTestUrl = computed(
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookTest}`,
);
const webhookWaitingUrl = computed(
() => `${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 binaryDataMode = computed(() => state.value.binaryDataMode);
@@ -67,10 +77,6 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
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 executionTimeout = computed(() => state.value.executionTimeout);
@@ -164,7 +170,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
state.value.defaultLocale = locale;
};
const setBinaryDataMode = (binaryDataMode: string) => {
const setBinaryDataMode = (binaryDataMode: RootState['binaryDataMode']) => {
state.value.binaryDataMode = binaryDataMode;
};
@@ -175,6 +181,8 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
formUrl,
formTestUrl,
formWaitingUrl,
mcpUrl,
mcpTestUrl,
webhookUrl,
webhookTestUrl,
webhookWaitingUrl,