Port the HAL confluence and jira integrations to the tools-only, outbound-only external-SaaS pattern proven by the merged gitlab module: runtime-only containers (container-runtime capability), own-secret token + broker, a settings-managed config.json for public config, and a per-module runtime image. Each module carries its own Atlassian API client and tools (ADR 0039), translated from HAL's @hal/sdk zod-schema/MCP-content shape into mesh-sdk's input/run-returns-data shape. Public config (ATLASSIAN_URL, ATLASSIAN_EMAIL) lives in config.json; the API token is the one own-secret. Clients are built lazily and never throw at registration, so each runtime serves its full tool surface with no credentials (the Servarr lesson) — confluence serves 3 tools, jira serves 8. jira's periodic ticket-poller (update-tickets.service/.timer) is NOT ported: the mesh has no scheduled-task primitive yet (a pending decision). Only jira's tools are ported; a top-of-file note records the deferral. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
// confluence's tools — confluence's own code (novox/hq ADR 0039), importing confluence's own
|
|
// Confluence API client. They return structured data; the mesh serves them through the sdk's tool
|
|
// harness. confluence is tools-only and outbound-only: no service, no events, no listener — it
|
|
// reaches out to an Atlassian Confluence instance and exposes its content search, pages and spaces.
|
|
//
|
|
// 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 { ConfluenceClient } 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 getConfluenceTools(confluence: ConfluenceClient): ToolDefinition[] {
|
|
return [
|
|
{
|
|
name: "confluence_search",
|
|
description: "Search Confluence content using CQL.",
|
|
input: {
|
|
cql: { type: "string", description: "CQL query string" },
|
|
limit: { type: "number", description: "max results (default 25)" },
|
|
},
|
|
run: async (args) => ({ results: await confluence.search(str(args.cql), num(args.limit, 25)) }),
|
|
},
|
|
{
|
|
name: "confluence_get_page",
|
|
description: "Get a Confluence page by ID.",
|
|
input: {
|
|
page_id: { type: "string", description: "Page ID" },
|
|
},
|
|
run: async (args) => ({ page: await confluence.getPage(str(args.page_id)) }),
|
|
},
|
|
{
|
|
name: "confluence_list_spaces",
|
|
description: "List Confluence spaces.",
|
|
input: {
|
|
page: { type: "number", description: "page number, 1-based (default 1)" },
|
|
limit: { type: "number", description: "results per page (default 25)" },
|
|
},
|
|
run: async (args) => {
|
|
const page = num(args.page, 1);
|
|
const limit = num(args.limit, 25);
|
|
const cursor = String((page - 1) * limit);
|
|
return { spaces: await confluence.listSpaces({ cursor, limit: String(limit) }) };
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
// confluence 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("confluence", (env) => {
|
|
try {
|
|
return getConfluenceTools(ConfluenceClient.fromEnv(env));
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|