n8n
Version:
n8n Workflow Automation Tool
134 lines • 6.89 kB
JavaScript
;
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