n8n-nodes-feishu-lark
Version:
n8n custom nodes for n8n to interact with Feishu/Lark, including Lark Bot, Lark MCP, and Lark Trigger.
357 lines • 17.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LarkMcp = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const tools_1 = require("@langchain/core/tools");
const zod_1 = require("zod");
const zod_to_json_schema_1 = require("zod-to-json-schema");
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
class LarkMcp {
constructor() {
this.description = {
displayName: 'Lark MCP',
name: 'larkMcp',
icon: 'file:lark_icon.svg',
group: ['transform'],
version: [1],
defaultVersion: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Use Lark MCP client',
defaults: {
name: 'Lark MCP',
},
inputs: [{ type: "main" }],
outputs: [{ type: "main" }],
usableAsTool: true,
credentials: [
{
name: "larkApi",
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Execute Tool | 执行工具',
value: 'executeTool',
description: 'Execute a specific tool',
action: 'Execute a tool',
},
{
name: 'List Tools | 获取工具列表',
value: 'listTools',
description: 'Get available tools',
action: 'List available tools',
},
],
default: 'listTools',
required: true,
},
{
displayName: 'Tool Name',
name: 'toolName',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['executeTool'],
},
},
default: '',
description: 'Name of the tool to execute',
},
{
displayName: 'Tool Parameters',
name: 'toolParameters',
type: 'json',
required: true,
displayOptions: {
show: {
operation: ['executeTool'],
},
},
default: '{}',
description: 'Parameters to pass to the tool in JSON format',
},
],
};
}
async execute() {
const credentials = (await this.getCredentials("larkApi"));
if (!(credentials.appid && credentials.appsecret)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Missing required Lark credentials');
}
const returnData = [];
const operation = this.getNodeParameter('operation', 0);
let transport;
let timeout = 600000;
try {
const env = {
PATH: process.env.PATH || '',
};
this.logger.debug(`Original PATH: ${process.env.PATH}`);
for (const key in process.env) {
if (key.startsWith('MCP_') && process.env[key]) {
const envName = key.substring(4);
env[envName] = process.env[key];
}
}
const { appid: appId, appsecret: appSecret, baseUrl } = credentials;
const args = `-y @larksuiteoapi/lark-mcp mcp -a ${appId} -s ${appSecret} -d https://${baseUrl} --oauth`;
transport = new stdio_js_1.StdioClientTransport({
command: 'npx',
args: args.split(' ') || [],
env: env,
});
this.logger.debug(`Transport created for MCP client PATH: ${env.PATH}`);
if (transport) {
transport.onerror = (error) => {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transport error: ${error.message}`);
};
}
const client = new index_js_1.Client({
name: `LarkMcp-client`,
version: '1.0.0',
}, {
capabilities: {
prompts: {},
resources: {},
tools: {},
},
});
try {
if (!transport) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No transport available');
}
await client.connect(transport);
this.logger.debug('Client connected to MCP server');
}
catch (connectionError) {
this.logger.error(`MCP client connection error: ${connectionError.message}`);
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to connect to MCP server: ${connectionError.message}`);
}
const requestOptions = {};
requestOptions.timeout = timeout;
switch (operation) {
case 'listResources': {
const resources = await client.listResources();
returnData.push({
json: { resources },
});
break;
}
case 'listResourceTemplates': {
const resourceTemplates = await client.listResourceTemplates();
returnData.push({
json: { resourceTemplates },
});
break;
}
case 'readResource': {
const uri = this.getNodeParameter('resourceUri', 0);
const resource = await client.readResource({
uri,
});
returnData.push({
json: { resource },
});
break;
}
case 'listTools': {
const rawTools = await client.listTools();
const tools = Array.isArray(rawTools)
? rawTools
: Array.isArray(rawTools === null || rawTools === void 0 ? void 0 : rawTools.tools)
? rawTools.tools
: typeof (rawTools === null || rawTools === void 0 ? void 0 : rawTools.tools) === 'object' && rawTools.tools !== null
? Object.values(rawTools.tools)
: [];
if (!tools.length) {
this.logger.warn('No tools found from MCP client response.');
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No tools found from MCP client');
}
const aiTools = tools.map((tool) => {
var _a;
const paramSchema = ((_a = tool.inputSchema) === null || _a === void 0 ? void 0 : _a.properties)
? zod_1.z.object(Object.entries(tool.inputSchema.properties).reduce((acc, [key, prop]) => {
var _a, _b, _c, _d, _e;
let zodType;
switch (prop.type) {
case 'string':
zodType = zod_1.z.string();
break;
case 'number':
zodType = zod_1.z.number();
break;
case 'integer':
zodType = zod_1.z.number().int();
break;
case 'boolean':
zodType = zod_1.z.boolean();
break;
case 'array':
if (((_a = prop.items) === null || _a === void 0 ? void 0 : _a.type) === 'string') {
zodType = zod_1.z.array(zod_1.z.string());
}
else if (((_b = prop.items) === null || _b === void 0 ? void 0 : _b.type) === 'number') {
zodType = zod_1.z.array(zod_1.z.number());
}
else if (((_c = prop.items) === null || _c === void 0 ? void 0 : _c.type) === 'boolean') {
zodType = zod_1.z.array(zod_1.z.boolean());
}
else {
zodType = zod_1.z.array(zod_1.z.any());
}
break;
case 'object':
zodType = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
break;
default:
zodType = zod_1.z.any();
}
if (prop.description) {
zodType = zodType.describe(prop.description);
}
if (!((_e = (_d = tool.inputSchema) === null || _d === void 0 ? void 0 : _d.required) === null || _e === void 0 ? void 0 : _e.includes(key))) {
zodType = zodType.optional();
}
return {
...acc,
[key]: zodType,
};
}, {}))
: zod_1.z.object({});
return new tools_1.DynamicStructuredTool({
name: tool.name,
description: tool.description || `Execute the ${tool.name} tool`,
schema: paramSchema,
func: async (params) => {
try {
const result = await client.callTool({
name: tool.name,
arguments: params,
}, types_js_1.CallToolResultSchema, requestOptions);
return typeof result === 'object' ? JSON.stringify(result) : String(result);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to execute ${tool.name}: ${error.message}`);
}
},
});
});
returnData.push({
json: {
tools: aiTools.map((t) => ({
name: t.name,
description: t.description,
schema: (0, zod_to_json_schema_1.zodToJsonSchema)(t.schema || zod_1.z.object({})),
})),
},
});
break;
}
case 'executeTool': {
const toolName = this.getNodeParameter('toolName', 0);
let toolParams;
try {
const rawParams = this.getNodeParameter('toolParameters', 0);
this.logger.debug(`Raw tool parameters: ${JSON.stringify(rawParams)}`);
if (rawParams === undefined || rawParams === null) {
toolParams = {};
}
else if (typeof rawParams === 'string') {
if (!rawParams || rawParams.trim() === '') {
toolParams = {};
}
else {
toolParams = JSON.parse(rawParams);
}
}
else if (typeof rawParams === 'object') {
toolParams = rawParams;
}
else {
try {
toolParams = JSON.parse(JSON.stringify(rawParams));
}
catch (parseError) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid parameter type: ${typeof rawParams}`);
}
}
if (typeof toolParams !== 'object' ||
toolParams === null ||
Array.isArray(toolParams)) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Tool parameters must be a JSON object');
}
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to parse tool parameters: ${error.message}. Make sure the parameters are valid JSON.`);
}
try {
const availableTools = await client.listTools();
const toolsList = Array.isArray(availableTools)
? availableTools
: Array.isArray(availableTools === null || availableTools === void 0 ? void 0 : availableTools.tools)
? availableTools.tools
: Object.values((availableTools === null || availableTools === void 0 ? void 0 : availableTools.tools) || {});
const toolExists = toolsList.some((tool) => tool.name === toolName);
if (!toolExists) {
const availableToolNames = toolsList.map((t) => t.name).join(', ');
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Tool '${toolName}' does not exist. Available tools: ${availableToolNames}`);
}
this.logger.debug(`Executing tool: ${toolName} with params: ${JSON.stringify(toolParams)}`);
const result = await client.callTool({
name: toolName,
arguments: toolParams,
}, types_js_1.CallToolResultSchema, requestOptions);
this.logger.debug(`Tool executed successfully: ${JSON.stringify(result)}`);
returnData.push({
json: { result },
});
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to execute tool '${toolName}': ${error.message}`);
}
break;
}
case 'listPrompts': {
const prompts = await client.listPrompts();
returnData.push({
json: { prompts },
});
break;
}
case 'getPrompt': {
const promptName = this.getNodeParameter('promptName', 0);
const prompt = await client.getPrompt({
name: promptName,
});
returnData.push({
json: { prompt },
});
break;
}
default:
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation ${operation} not supported`);
}
return [returnData];
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to execute operation: ${error.message}`);
}
finally {
if (transport) {
await transport.close();
}
}
}
}
exports.LarkMcp = LarkMcp;
//# sourceMappingURL=LarkMcp.node.js.map