// gitlab's tools — gitlab's own code (novox/hq ADR 0039), importing gitlab's own GitLab API client. // They return structured data; the mesh serves them through the sdk's tool harness. gitlab is // tools-only and outbound-only: no service, no events, no listener — it reaches out to a GitLab // instance and exposes its projects, merge requests, pipelines, jobs and CI/CD variables. // // Every tool is registered unconditionally, even with no token configured (the Servarr lesson): the // client is built lazily and never throws, so the runtime always comes up and serves the full tool // surface — a call made before the URL/token are set fails with a clear error, but the runtime does // not refuse to serve. The install proof is the runtime logging `[mesh-tools] serving N tool(s)`. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { GitLabClient } from "../client.js"; /** Coerce a project/group identifier: a numeric id stays a number, a path stays a string. */ function id(value: unknown): number | string { const s = String(value ?? ""); return /^\d+$/.test(s) ? Number(s) : s; } function num(value: unknown): number { return Number(value); } function str(value: unknown): string { return String(value ?? ""); } /** Optional CI/CD variable fields, forwarded only when the caller supplied them. */ function variableOptions(args: Readonly>): { protected?: boolean; masked?: boolean; environment_scope?: string; } { const out: { protected?: boolean; masked?: boolean; environment_scope?: string } = {}; if (args.protected !== undefined) out.protected = Boolean(args.protected); if (args.masked !== undefined) out.masked = Boolean(args.masked); if (args.environment_scope !== undefined) out.environment_scope = str(args.environment_scope); return out; } const project = { type: "string", description: "Project ID or URL-encoded path (e.g. 'group/project')" }; const group = { type: "string", description: "Group ID or URL-encoded path" }; export function getGitLabTools(gitlab: GitLabClient): ToolDefinition[] { return [ // --- Projects --- { name: "gitlab_list_projects", description: "List GitLab projects, with optional search.", input: { search: { type: "string", description: "search query for project name" }, page: { type: "number", description: "page number (default 1)" }, per_page: { type: "number", description: "results per page (default 20)" }, }, run: async (args) => { const params: Record = { page: String(args.page ?? 1), per_page: String(args.per_page ?? 20), }; if (args.search) params.search = str(args.search); return { projects: await gitlab.listProjects(params) }; }, }, { name: "gitlab_get_project", description: "Get a single GitLab project by ID or path (e.g. 'group/project').", input: { project_id: project }, run: async (args) => ({ project: await gitlab.getProject(id(args.project_id)) }), }, // --- Merge Requests --- { name: "gitlab_list_merge_requests", description: "List merge requests for a GitLab project.", input: { project_id: project, state: { type: "string", description: "opened, closed, merged or all (default opened)" }, author_username: { type: "string", description: "filter by author username" }, page: { type: "number", description: "page number (default 1)" }, per_page: { type: "number", description: "results per page (default 20)" }, }, run: async (args) => { const params: Record = { state: str(args.state || "opened"), page: String(args.page ?? 1), per_page: String(args.per_page ?? 20), }; if (args.author_username) params.author_username = str(args.author_username); return { merge_requests: await gitlab.listMergeRequests(id(args.project_id), params) }; }, }, { name: "gitlab_get_merge_request", description: "Get a single merge request by IID.", input: { project_id: project, mr_iid: { type: "number", description: "merge request IID" } }, run: async (args) => ({ merge_request: await gitlab.getMergeRequest(id(args.project_id), num(args.mr_iid)) }), }, { name: "gitlab_create_merge_request", description: "Create a new merge request.", input: { project_id: project, source_branch: { type: "string", description: "source branch" }, target_branch: { type: "string", description: "target branch" }, title: { type: "string", description: "MR title" }, description: { type: "string", description: "MR description (optional)" }, }, run: async (args) => ({ merge_request: await gitlab.createMergeRequest(id(args.project_id), { source_branch: str(args.source_branch), target_branch: str(args.target_branch), title: str(args.title), description: args.description !== undefined ? str(args.description) : undefined, }), }), }, { name: "gitlab_approve_merge_request", description: "Approve a merge request.", input: { project_id: project, mr_iid: { type: "number", description: "merge request IID" } }, run: async (args) => ({ approved: await gitlab.approveMergeRequest(id(args.project_id), num(args.mr_iid)) }), }, { name: "gitlab_add_mr_note", description: "Add a comment/note to a merge request.", input: { project_id: project, mr_iid: { type: "number", description: "merge request IID" }, body: { type: "string", description: "note content" }, }, run: async (args) => ({ note: await gitlab.addMergeRequestNote(id(args.project_id), num(args.mr_iid), str(args.body)) }), }, // --- Pipelines --- { name: "gitlab_list_pipelines", description: "List pipelines for a GitLab project.", input: { project_id: project, ref: { type: "string", description: "filter by branch/tag name" }, status: { type: "string", description: "filter by status (running, pending, success, failed, ...)" }, page: { type: "number", description: "page number (default 1)" }, per_page: { type: "number", description: "results per page (default 20)" }, }, run: async (args) => { const params: Record = { page: String(args.page ?? 1), per_page: String(args.per_page ?? 20), }; if (args.ref) params.ref = str(args.ref); if (args.status) params.status = str(args.status); return { pipelines: await gitlab.listPipelines(id(args.project_id), params) }; }, }, { name: "gitlab_get_pipeline", description: "Get details of a specific pipeline.", input: { project_id: project, pipeline_id: { type: "number", description: "pipeline ID" } }, run: async (args) => ({ pipeline: await gitlab.getPipeline(id(args.project_id), num(args.pipeline_id)) }), }, { name: "gitlab_retry_pipeline", description: "Retry a failed pipeline.", input: { project_id: project, pipeline_id: { type: "number", description: "pipeline ID" } }, run: async (args) => ({ pipeline: await gitlab.retryPipeline(id(args.project_id), num(args.pipeline_id)) }), }, { name: "gitlab_cancel_pipeline", description: "Cancel a running pipeline.", input: { project_id: project, pipeline_id: { type: "number", description: "pipeline ID" } }, run: async (args) => ({ pipeline: await gitlab.cancelPipeline(id(args.project_id), num(args.pipeline_id)) }), }, { name: "gitlab_list_pipeline_jobs", description: "List jobs for a specific pipeline.", input: { project_id: project, pipeline_id: { type: "number", description: "pipeline ID" } }, run: async (args) => ({ jobs: await gitlab.listPipelineJobs(id(args.project_id), num(args.pipeline_id)) }), }, { name: "gitlab_get_job_log", description: "Get the log/trace output of a specific job (truncated to the last 2000 lines).", input: { project_id: project, job_id: { type: "number", description: "job ID" } }, run: async (args) => { const log = await gitlab.getJobLog(id(args.project_id), num(args.job_id)); const lines = log.split("\n"); const truncated = lines.length > 2000 ? lines.slice(-2000).join("\n") : log; return { log: truncated, truncated: lines.length > 2000 }; }, }, // --- Project variables --- { name: "gitlab_list_project_variables", description: "List CI/CD variables for a project.", input: { project_id: project }, run: async (args) => ({ variables: await gitlab.listProjectVariables(id(args.project_id)) }), }, { name: "gitlab_get_project_variable", description: "Get a single project CI/CD variable.", input: { project_id: project, key: { type: "string", description: "variable key" } }, run: async (args) => ({ variable: await gitlab.getProjectVariable(id(args.project_id), str(args.key)) }), }, { name: "gitlab_create_project_variable", description: "Create a new project CI/CD variable.", input: { project_id: project, key: { type: "string", description: "variable key" }, value: { type: "string", description: "variable value" }, protected: { type: "boolean", description: "protected (default false)" }, masked: { type: "boolean", description: "masked (default false)" }, environment_scope: { type: "string", description: "environment scope (default *)" }, }, run: async (args) => ({ variable: await gitlab.createProjectVariable(id(args.project_id), { key: str(args.key), value: str(args.value), ...variableOptions(args), }), }), }, { name: "gitlab_update_project_variable", description: "Update an existing project CI/CD variable.", input: { project_id: project, key: { type: "string", description: "variable key" }, value: { type: "string", description: "new value" }, protected: { type: "boolean", description: "protected (optional)" }, masked: { type: "boolean", description: "masked (optional)" }, environment_scope: { type: "string", description: "environment scope (optional)" }, }, run: async (args) => ({ variable: await gitlab.updateProjectVariable(id(args.project_id), str(args.key), { value: str(args.value), ...variableOptions(args), }), }), }, { name: "gitlab_delete_project_variable", description: "Delete a project CI/CD variable.", input: { project_id: project, key: { type: "string", description: "variable key" } }, run: async (args) => { await gitlab.deleteProjectVariable(id(args.project_id), str(args.key)); return { deleted: true, key: str(args.key) }; }, }, // --- Group variables --- { name: "gitlab_list_group_variables", description: "List CI/CD variables for a group.", input: { group_id: group }, run: async (args) => ({ variables: await gitlab.listGroupVariables(id(args.group_id)) }), }, { name: "gitlab_get_group_variable", description: "Get a single group CI/CD variable.", input: { group_id: group, key: { type: "string", description: "variable key" } }, run: async (args) => ({ variable: await gitlab.getGroupVariable(id(args.group_id), str(args.key)) }), }, { name: "gitlab_create_group_variable", description: "Create a new group CI/CD variable.", input: { group_id: group, key: { type: "string", description: "variable key" }, value: { type: "string", description: "variable value" }, protected: { type: "boolean", description: "protected (default false)" }, masked: { type: "boolean", description: "masked (default false)" }, environment_scope: { type: "string", description: "environment scope (default *)" }, }, run: async (args) => ({ variable: await gitlab.createGroupVariable(id(args.group_id), { key: str(args.key), value: str(args.value), ...variableOptions(args), }), }), }, { name: "gitlab_update_group_variable", description: "Update an existing group CI/CD variable.", input: { group_id: group, key: { type: "string", description: "variable key" }, value: { type: "string", description: "new value" }, protected: { type: "boolean", description: "protected (optional)" }, masked: { type: "boolean", description: "masked (optional)" }, environment_scope: { type: "string", description: "environment scope (optional)" }, }, run: async (args) => ({ variable: await gitlab.updateGroupVariable(id(args.group_id), str(args.key), { value: str(args.value), ...variableOptions(args), }), }), }, { name: "gitlab_delete_group_variable", description: "Delete a group CI/CD variable.", input: { group_id: group, key: { type: "string", description: "variable key" } }, run: async (args) => { await gitlab.deleteGroupVariable(id(args.group_id), str(args.key)); return { deleted: true, key: str(args.key) }; }, }, ]; } // gitlab always registers its full tool surface: the client is built lazily and never throws, so the // runtime comes up and serves every tool even before a URL/token is configured (the lab has no real // GitLab). A tool called before the module is configured fails with a clear error from that call. registerModuleTools("gitlab", (env) => { try { return getGitLabTools(GitLabClient.fromEnv(env)); } catch { return []; } });