UNPKG

n8n

Version:

n8n Workflow Automation Tool

134 lines 6.89 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createSearchProjectsTool = void 0; const zod_1 = __importDefault(require("zod")); const mcp_constants_1 = require("../mcp.constants"); const schemas_1 = require("./schemas"); const MAX_RESULTS = 100; const inputSchema = { query: zod_1.default .string() .optional() .describe('Filter projects by name (case-insensitive partial match). Pass the exact project name the user mentioned — results are ranked with exact case-insensitive matches first, then partial matches.'), type: zod_1.default .enum(['personal', 'team']) .optional() .describe("Filter by project type. 'team' for shared team projects, 'personal' for personal projects."), limit: (0, schemas_1.createLimitSchema)(MAX_RESULTS), }; const outputSchema = { data: zod_1.default .array(zod_1.default.object({ id: zod_1.default.string().describe('The unique identifier of the project'), name: zod_1.default.string().describe('The name of the project'), type: zod_1.default.enum(['personal', 'team']).describe("The project type: 'personal' or 'team'"), matchType: zod_1.default .enum(['exact', 'partial']) .optional() .describe("Whether this project's name matches the query exactly (case-insensitive) or only partially. Only present when a query was provided."), })) .describe('List of projects matching the query, sorted with exact case-insensitive matches first.'), count: zod_1.default.number().int().min(0).describe('Total number of matching projects'), hint: zod_1.default .string() .optional() .describe('Guidance for picking a result. Present when the match is ambiguous — for example when no exact match was found but multiple partials were returned. When present, follow it before calling create_workflow_from_code.'), }; const createSearchProjectsTool = (user, projectRepository, telemetry) => ({ name: 'search_projects', config: { description: 'Search for projects accessible to the current user. Call this whenever the user names a project — pass the name as the query, then use the resolved ID with create_workflow_from_code or update_workflow. Results are ranked with exact case-insensitive name matches first. If no exact match is found but multiple partials are returned, the response includes a `hint` field telling you to clarify with the user before acting; follow it instead of guessing.', inputSchema, outputSchema, annotations: { title: 'Search Projects', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, handler: async ({ query, type, limit = MAX_RESULTS, }) => { const telemetryPayload = { user_id: user.id, tool_name: 'search_projects', parameters: { query, type, limit }, }; try { const effectiveLimit = Math.min(Math.max(1, limit), MAX_RESULTS); const trimmedQuery = query?.trim(); const [[partialProjects, count], exactProjects] = await Promise.all([ projectRepository.getAccessibleProjectsAndCount(user.id, { search: trimmedQuery, type, take: effectiveLimit, }), trimmedQuery ? projectRepository.getAccessibleProjectsByExactName(user.id, trimmedQuery, type) : Promise.resolve([]), ]); const partialIds = new Set(partialProjects.map((p) => p.id)); const novelExactProjects = exactProjects.filter((p) => !partialIds.has(p.id)); const mergedProjects = [...novelExactProjects, ...partialProjects]; const normalizedQuery = trimmedQuery?.toLowerCase(); const scoredProjects = mergedProjects.map((project) => ({ project, isExact: normalizedQuery !== undefined && project.name.trim().toLowerCase() === normalizedQuery, })); if (normalizedQuery) { scoredProjects.sort((a, b) => { if (a.isExact !== b.isExact) return a.isExact ? -1 : 1; return a.project.name.localeCompare(b.project.name); }); } const limitedScored = scoredProjects.slice(0, effectiveLimit); const data = limitedScored.map(({ project, isExact }) => { const base = { id: project.id, name: project.name, type: project.type }; if (!normalizedQuery) return base; const matchType = isExact ? 'exact' : 'partial'; return { ...base, matchType }; }); const exactMatchCount = scoredProjects.reduce((acc, p) => acc + (p.isExact ? 1 : 0), 0); let hint; if (normalizedQuery) { if (exactMatchCount === 0 && count > 1) { hint = `No exact match for "${query}". ${count} partial matches are available — ask the user to clarify which project they meant before creating or updating a workflow.`; } else if (exactMatchCount > 1) { hint = `Multiple projects are named "${query}". Ask the user to disambiguate (e.g. by team or owner) before creating or updating a workflow.`; } } telemetryPayload.results = { success: true, data: { count }, }; telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); const output = { data, count, ...(hint ? { hint } : {}) }; return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); telemetryPayload.results = { success: false, error: errorMessage, }; telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); const output = { data: [], count: 0, error: errorMessage }; return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output, isError: true, }; } }, }); exports.createSearchProjectsTool = createSearchProjectsTool; //# sourceMappingURL=search-projects.tool.js.map