// jira's tools — jira's own code (novox/hq ADR 0039), importing jira's own Jira API client. They // return structured data; the mesh serves them through the sdk's tool harness. jira is tools-only and // outbound-only: no service, no events, no listener — it reaches out to an Atlassian Jira instance // and exposes its issues, comments, transitions and projects. // // NOTE — the HAL jira module also shipped a periodic ticket-poller (update-tickets.service/.timer) // that pulled tickets on a schedule. It is deliberately NOT ported: the mesh has no scheduled-task // primitive yet (a pending decision). Only jira's TOOLS are ported here; the poller is deferred until // the scheduled-task capability lands. // // Every tool is registered unconditionally, even with no credentials 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/email/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 { JiraClient } from "../client.js"; function str(value: unknown): string { return String(value ?? ""); } function num(value: unknown, fallback: number): number { const n = Number(value); return Number.isFinite(n) ? n : fallback; } export function getJiraTools(jira: JiraClient): ToolDefinition[] { return [ // --- Issues --- { name: "jira_search_issues", description: "Search Jira issues using JQL.", input: { jql: { type: "string", description: "JQL query string" }, fields: { type: "string", description: "comma-separated field names to return (optional)" }, max_results: { type: "number", description: "max results (default 50)" }, }, run: async (args) => ({ results: await jira.searchIssues( str(args.jql), args.fields !== undefined ? str(args.fields) : undefined, num(args.max_results, 50), ), }), }, { name: "jira_get_issue", description: "Get a single Jira issue by key (e.g. PROJ-123).", input: { issue_key: { type: "string", description: "Issue key, e.g. PROJ-123" }, fields: { type: "string", description: "comma-separated field names to return (optional)" }, }, run: async (args) => ({ issue: await jira.getIssue(str(args.issue_key), args.fields !== undefined ? str(args.fields) : undefined), }), }, { name: "jira_create_issue", description: "Create a new Jira issue.", input: { project_key: { type: "string", description: "Project key, e.g. PROJ" }, issue_type: { type: "string", description: "Issue type name, e.g. Task, Bug, Story" }, summary: { type: "string", description: "Issue summary" }, description: { type: "string", description: "Plain text description, converted to ADF internally (optional)" }, }, run: async (args) => ({ issue: await jira.createIssue({ projectKey: str(args.project_key), issueType: str(args.issue_type), summary: str(args.summary), description: args.description !== undefined ? str(args.description) : undefined, }), }), }, { name: "jira_update_issue", description: "Update an existing Jira issue.", input: { issue_key: { type: "string", description: "Issue key, e.g. PROJ-123" }, summary: { type: "string", description: "new summary (optional)" }, description: { type: "string", description: "new plain text description, converted to ADF internally (optional)" }, }, run: async (args) => { await jira.updateIssue(str(args.issue_key), { summary: args.summary !== undefined ? str(args.summary) : undefined, description: args.description !== undefined ? str(args.description) : undefined, }); return { updated: true, issue_key: str(args.issue_key) }; }, }, { name: "jira_add_comment", description: "Add a comment to a Jira issue.", input: { issue_key: { type: "string", description: "Issue key, e.g. PROJ-123" }, body: { type: "string", description: "comment text" }, }, run: async (args) => ({ comment: await jira.addComment(str(args.issue_key), str(args.body)) }), }, { name: "jira_list_transitions", description: "List available transitions for a Jira issue.", input: { issue_key: { type: "string", description: "Issue key, e.g. PROJ-123" }, }, run: async (args) => ({ transitions: await jira.listTransitions(str(args.issue_key)) }), }, { name: "jira_transition_issue", description: "Transition a Jira issue to a new status.", input: { issue_key: { type: "string", description: "Issue key, e.g. PROJ-123" }, transition_id: { type: "string", description: "Transition ID (get from jira_list_transitions)" }, }, run: async (args) => { await jira.transitionIssue(str(args.issue_key), str(args.transition_id)); return { transitioned: true, issue_key: str(args.issue_key) }; }, }, // --- Projects --- { name: "jira_list_projects", description: "List Jira projects.", input: { query: { type: "string", description: "search query for project name (optional)" }, page: { type: "number", description: "page index, 0-based (default 0)" }, max_results: { type: "number", description: "results per page (default 50)" }, }, run: async (args) => { const page = num(args.page, 0); const maxResults = num(args.max_results, 50); const params: Record = { startAt: String(page * maxResults), maxResults: String(maxResults), }; if (args.query) params.query = str(args.query); return { projects: await jira.listProjects(params) }; }, }, ]; } // jira 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 the URL/email/token are configured (the lab has // no real Atlassian). A tool called before the module is configured fails with a clear error from // that call. registerModuleTools("jira", (env) => { try { return getJiraTools(JiraClient.fromEnv(env)); } catch { return []; } });