@atollhq/mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1216 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/http.ts
7
+ import { createServer } from "http";
8
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
+
10
+ // src/tools.ts
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { z } from "zod";
13
+
14
+ // src/api.ts
15
+ var DEFAULT_BASE_URL = "https://atollhq.com";
16
+ var DEFAULT_TIMEOUT_MS = 3e4;
17
+ var AtollApiClient = class {
18
+ baseUrl;
19
+ apiKey;
20
+ timeoutMs;
21
+ fetchImpl;
22
+ constructor(options) {
23
+ this.apiKey = options.apiKey;
24
+ this.baseUrl = options.baseUrl ?? process.env.ATOLL_BASE_URL ?? DEFAULT_BASE_URL;
25
+ this.timeoutMs = options.timeoutMs ?? resolveTimeoutMs();
26
+ this.fetchImpl = options.fetch ?? fetch;
27
+ }
28
+ async request(method, path, body) {
29
+ if (!path.startsWith("/")) {
30
+ throw new Error('Atoll API paths must start with "/". Example: /api/orgs');
31
+ }
32
+ if (!path.startsWith("/api/")) {
33
+ throw new Error("Atoll API paths must stay under /api/. Example: /api/orgs");
34
+ }
35
+ const headers = {
36
+ Authorization: `Bearer ${this.apiKey}`,
37
+ "Content-Type": "application/json",
38
+ "User-Agent": "atoll-mcp-server",
39
+ "X-Atoll-Client": "mcp-server"
40
+ };
41
+ let response;
42
+ try {
43
+ response = await this.fetchImpl(`${this.baseUrl}${path}`, {
44
+ method,
45
+ headers,
46
+ body: body === void 0 ? void 0 : JSON.stringify(body),
47
+ signal: AbortSignal.timeout(this.timeoutMs)
48
+ });
49
+ } catch (error) {
50
+ if (error.name === "TimeoutError" || error.name === "AbortError") {
51
+ throw new Error(`Atoll API request timed out after ${this.timeoutMs}ms: ${method} ${path}`);
52
+ }
53
+ throw error;
54
+ }
55
+ const text = await response.text();
56
+ const parsed = parseResponseBody(text);
57
+ if (!response.ok) {
58
+ const details = typeof parsed === "string" ? parsed : JSON.stringify(parsed);
59
+ throw new Error(`Atoll API ${response.status} for ${method} ${path}: ${details}`);
60
+ }
61
+ return parsed;
62
+ }
63
+ get(path) {
64
+ return this.request("GET", path);
65
+ }
66
+ post(path, body) {
67
+ return this.request("POST", path, body);
68
+ }
69
+ put(path, body) {
70
+ return this.request("PUT", path, body);
71
+ }
72
+ patch(path, body) {
73
+ return this.request("PATCH", path, body);
74
+ }
75
+ delete(path) {
76
+ return this.request("DELETE", path);
77
+ }
78
+ };
79
+ function resolveAuthToken(authorizationHeader) {
80
+ const requestToken = parseBearerToken(authorizationHeader);
81
+ const token = requestToken ?? process.env.ATOLL_API_KEY;
82
+ if (!token) {
83
+ throw new Error(
84
+ "No Atoll API key available. Pass Authorization: Bearer <ATOLL_API_KEY> to the remote MCP server or set ATOLL_API_KEY for single-tenant deployments."
85
+ );
86
+ }
87
+ return token;
88
+ }
89
+ async function resolveOrgId(client, explicitOrgId) {
90
+ if (explicitOrgId) return explicitOrgId;
91
+ if (process.env.ATOLL_ORG_ID) return process.env.ATOLL_ORG_ID;
92
+ const data = await client.get("/api/orgs");
93
+ const orgs = data.orgs ?? [];
94
+ if (orgs.length === 1 && orgs[0]?.id) return orgs[0].id;
95
+ if (orgs.length === 0) {
96
+ throw new Error("No Atoll orgs are accessible for this API key. Create or join an org, then retry.");
97
+ }
98
+ const choices = orgs.map((org) => `${org.name ?? org.id} (${org.id})`).join(", ");
99
+ throw new Error(`Multiple Atoll orgs are accessible. Pass org_id to choose one. Available orgs: ${choices}`);
100
+ }
101
+ function parseBearerToken(header) {
102
+ if (!header) return void 0;
103
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
104
+ if (!match) return void 0;
105
+ return match[1];
106
+ }
107
+ function resolveTimeoutMs() {
108
+ const raw = process.env.ATOLL_TIMEOUT_MS;
109
+ if (!raw) return DEFAULT_TIMEOUT_MS;
110
+ const parsed = Number.parseInt(raw, 10);
111
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
112
+ }
113
+ function parseResponseBody(text) {
114
+ if (!text) return void 0;
115
+ try {
116
+ return JSON.parse(text);
117
+ } catch {
118
+ return text;
119
+ }
120
+ }
121
+
122
+ // src/tools.ts
123
+ var VERSION = "0.1.0";
124
+ var SUPPORTED_API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
125
+ var ATOLL_MCP_INSTRUCTIONS = `Atoll connects strategy to execution: goals -> KPIs -> initiatives -> milestones and issues.
126
+ KPIs measure business outcomes; initiative targets measure initiative commitments. Use progress targets for output tracking and gate targets for launch prerequisites where KPI pacing would be misleading.
127
+
128
+ Use atoll_get_heartbeat first to orient an agent, then inspect and update the highest-leverage work.
129
+ Remote clients should pass Authorization: Bearer <ATOLL_API_KEY>. If org_id is omitted, the server uses ATOLL_ORG_ID or the only accessible org.`;
130
+ var ATOLL_TOOL_NAMES = [
131
+ "atoll_get_auth_context",
132
+ "atoll_list_orgs",
133
+ "atoll_get_heartbeat",
134
+ "atoll_list_issues",
135
+ "atoll_get_issue",
136
+ "atoll_create_issue",
137
+ "atoll_update_issue",
138
+ "atoll_archive_issue",
139
+ "atoll_unarchive_issue",
140
+ "atoll_list_comments",
141
+ "atoll_add_comment",
142
+ "atoll_list_projects",
143
+ "atoll_get_project",
144
+ "atoll_create_project",
145
+ "atoll_list_goals",
146
+ "atoll_get_goal",
147
+ "atoll_create_goal",
148
+ "atoll_update_goal",
149
+ "atoll_list_kpis",
150
+ "atoll_get_kpi",
151
+ "atoll_create_kpi",
152
+ "atoll_update_kpi",
153
+ "atoll_list_kpi_snapshots",
154
+ "atoll_record_kpi_snapshot",
155
+ "atoll_create_kpi_http_sync_draft",
156
+ "atoll_validate_kpi_http_sync_config",
157
+ "atoll_list_initiatives",
158
+ "atoll_get_initiative",
159
+ "atoll_create_initiative",
160
+ "atoll_update_initiative",
161
+ "atoll_link_initiative_issue",
162
+ "atoll_link_initiative_milestone",
163
+ "atoll_link_initiative_kpi",
164
+ "atoll_list_initiative_targets",
165
+ "atoll_get_initiative_target",
166
+ "atoll_create_initiative_target",
167
+ "atoll_update_initiative_target",
168
+ "atoll_delete_initiative_target",
169
+ "atoll_link_initiative_target_issue",
170
+ "atoll_unlink_initiative_target_issue",
171
+ "atoll_link_initiative_target_milestone",
172
+ "atoll_unlink_initiative_target_milestone",
173
+ "atoll_list_milestones",
174
+ "atoll_create_milestone",
175
+ "atoll_upsert_milestone",
176
+ "atoll_list_dependencies",
177
+ "atoll_add_dependency",
178
+ "atoll_remove_dependency",
179
+ "atoll_list_webhooks",
180
+ "atoll_create_webhook",
181
+ "atoll_delete_webhook",
182
+ "atoll_send_feedback",
183
+ "atoll_api_request"
184
+ ];
185
+ var orgId = z.string().min(1).optional().describe("Atoll org UUID. Optional when ATOLL_ORG_ID is configured or the API key has one accessible org.");
186
+ var issueId = z.string().min(1).describe("Issue UUID or API-accepted issue identifier.");
187
+ var projectId = z.string().min(1).describe("Project UUID.");
188
+ var goalId = z.string().min(1).describe("Goal UUID.");
189
+ var kpiId = z.string().min(1).describe("KPI UUID.");
190
+ var initiativeId = z.string().min(1).describe("Initiative UUID.");
191
+ var initiativeTargetId = z.string().min(1).describe("Initiative target UUID.");
192
+ var milestoneId = z.string().min(1).describe("Milestone UUID.");
193
+ var limit = z.number().int().min(1).max(100).default(25).describe("Maximum records to return, 1-100.");
194
+ var offset = z.number().int().min(0).default(0).describe("Pagination offset.");
195
+ var priority = z.number().int().min(0).max(3).optional().describe("Priority: 0 urgent, 1 high, 2 medium, 3 low.");
196
+ var kpiHttpSyncName = z.string().min(1).describe("Human-readable draft name.");
197
+ var kpiHttpSyncSchedule = z.enum(["hourly", "daily"]).describe("Polling schedule.");
198
+ var kpiHttpSyncRequestConfig = z.record(z.string(), z.unknown()).describe("Safe request_config: GET, HTTPS JSON URL, no request body, no inline query strings, and only secretRef placeholders for Authorization Bearer or X-API-Key.");
199
+ var kpiHttpSyncExtractionConfig = z.record(z.string(), z.unknown()).describe('JSON Pointer extraction config, e.g. { contentType: "json", pointer: "/results/0/value", numeric: { mode: "number" } }.');
200
+ var kpiHttpSyncFreshnessConfig = z.record(z.string(), z.unknown()).optional().describe("Optional freshness_config. Use {} for v1 unless a documented field is needed.");
201
+ function listAtollToolNames() {
202
+ return [...ATOLL_TOOL_NAMES];
203
+ }
204
+ function createAtollMcpServer() {
205
+ const server = new McpServer(
206
+ {
207
+ name: "atoll-mcp-server",
208
+ version: VERSION,
209
+ websiteUrl: "https://atollhq.com"
210
+ },
211
+ {
212
+ instructions: ATOLL_MCP_INSTRUCTIONS,
213
+ capabilities: {
214
+ resources: {},
215
+ tools: {}
216
+ }
217
+ }
218
+ );
219
+ registerAtollTools(server);
220
+ registerAtollResources(server);
221
+ return { server, instructions: ATOLL_MCP_INSTRUCTIONS, toolNames: listAtollToolNames() };
222
+ }
223
+ function registerAtollResources(server) {
224
+ server.registerResource(
225
+ "atoll_skill_packaging_guidance",
226
+ "atoll://skills/packaging",
227
+ {
228
+ title: "Atoll Skill Packaging Guidance",
229
+ description: "Explains why Atoll skills remain separate packages from the MCP server.",
230
+ mimeType: "text/markdown"
231
+ },
232
+ async (uri) => ({
233
+ contents: [
234
+ {
235
+ uri: uri.href,
236
+ mimeType: "text/markdown",
237
+ text: `# Atoll skills and MCP packaging
238
+
239
+ Keep Atoll agent skills as separate installable packages. The MCP server is a remote tool surface and should stay runtime-focused: auth, transport, validation, API calls, and structured responses.
240
+
241
+ Skills remain better as client-side guidance because different agent environments install and invoke skills differently. The MCP server can expose this resource and Atoll docs links so clients know which skill package to install, without coupling server deployment to local skill files.`
242
+ }
243
+ ]
244
+ })
245
+ );
246
+ }
247
+ function registerAtollTools(server) {
248
+ registerTool(server, "atoll_get_auth_context", {
249
+ title: "Get Atoll Auth Context",
250
+ description: "Validate the active Atoll API key and return its auth context. Mirrors `atoll auth status --json` for remote clients.",
251
+ inputSchema: {},
252
+ readOnly: true,
253
+ handler: async (_args, extra) => clientFromExtra(extra).get("/api/auth/me")
254
+ });
255
+ registerTool(server, "atoll_list_orgs", {
256
+ title: "List Atoll Orgs",
257
+ description: "List organizations accessible to the Atoll API key. Use this when org_id is unknown.",
258
+ inputSchema: {},
259
+ readOnly: true,
260
+ handler: async (_args, extra) => clientFromExtra(extra).get("/api/orgs")
261
+ });
262
+ registerTool(server, "atoll_get_heartbeat", {
263
+ title: "Get Atoll Heartbeat",
264
+ description: "Get the agent heartbeat briefing: goal/KPI pace, assigned work, stalled initiatives, and prioritized signals. Mirrors `atoll heartbeat --json`.",
265
+ inputSchema: {
266
+ org_id: orgId,
267
+ severity: z.enum(["critical", "warning", "info"]).optional().describe("Optional signal severity filter."),
268
+ signals_only: z.boolean().default(false).describe("Return only filtered heartbeat signals.")
269
+ },
270
+ readOnly: true,
271
+ handler: async (args, extra) => {
272
+ const client = clientFromExtra(extra);
273
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
274
+ const data = await client.get(`/api/orgs/${encodeURIComponent(resolvedOrgId)}/heartbeat`);
275
+ const severity = stringArg(args.severity);
276
+ const signals = Array.isArray(data.signals) ? data.signals.filter((signal) => !severity || signal.severity === severity) : [];
277
+ return buildHeartbeatToolPayload(data, signals, Boolean(args.signals_only));
278
+ }
279
+ });
280
+ registerIssueTools(server);
281
+ registerProjectTools(server);
282
+ registerGoalTools(server);
283
+ registerKpiTools(server);
284
+ registerInitiativeTools(server);
285
+ registerMilestoneTools(server);
286
+ registerDependencyTools(server);
287
+ registerWebhookTools(server);
288
+ registerFeedbackTools(server);
289
+ registerGenericApiTool(server);
290
+ }
291
+ function registerIssueTools(server) {
292
+ registerTool(server, "atoll_list_issues", {
293
+ title: "List Atoll Issues",
294
+ description: "List Atoll tasks/issues with CLI-compatible pagination. Mirrors `atoll issue list --json`.",
295
+ inputSchema: {
296
+ org_id: orgId,
297
+ status: z.string().optional(),
298
+ priority,
299
+ project_id: z.string().optional(),
300
+ assignee_id: z.string().optional(),
301
+ team_id: z.string().optional(),
302
+ milestone_id: z.string().optional(),
303
+ q: z.string().optional().describe("Search query for title/description."),
304
+ include_archived: z.boolean().optional(),
305
+ order_by: z.enum(["created_at", "updated_at", "priority", "due_date", "title", "status"]).optional(),
306
+ order_dir: z.enum(["asc", "desc"]).optional(),
307
+ limit,
308
+ offset
309
+ },
310
+ readOnly: true,
311
+ handler: async (args, extra) => orgGet(extra, args, "/issues", issueListParams(args))
312
+ });
313
+ registerTool(server, "atoll_get_issue", {
314
+ title: "Get Atoll Issue",
315
+ description: "Get one Atoll task/issue with enriched detail. Mirrors `atoll issue get --json`.",
316
+ inputSchema: { org_id: orgId, issue_id: issueId },
317
+ readOnly: true,
318
+ handler: async (args, extra) => orgGet(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}`)
319
+ });
320
+ registerTool(server, "atoll_create_issue", {
321
+ title: "Create Atoll Issue",
322
+ description: "Create an Atoll task/issue. Mirrors `atoll issue create --json`.",
323
+ inputSchema: {
324
+ org_id: orgId,
325
+ title: z.string().min(1),
326
+ description: z.string().optional(),
327
+ status: z.string().optional(),
328
+ priority,
329
+ project_id: z.string().optional(),
330
+ milestone_id: z.string().optional(),
331
+ team_id: z.string().optional(),
332
+ assignee_id: z.string().optional(),
333
+ assignee_ids: z.array(z.string()).optional(),
334
+ start_date: z.string().optional(),
335
+ due_date: z.string().optional(),
336
+ label_ids: z.array(z.string()).optional()
337
+ },
338
+ readOnly: false,
339
+ handler: async (args, extra) => orgPost(extra, args, "/issues", buildIssueBody(args))
340
+ });
341
+ registerTool(server, "atoll_update_issue", {
342
+ title: "Update Atoll Issue",
343
+ description: "Update title, description, status, priority, project, milestone, team, assignees, dates, labels, or comment_body on an Atoll task.",
344
+ inputSchema: {
345
+ org_id: orgId,
346
+ issue_id: issueId,
347
+ title: z.string().optional(),
348
+ description: z.string().nullable().optional(),
349
+ comment_body: z.string().optional().describe("Optional Markdown comment body to create with this update. Use this to preserve strategy evidence when changing status."),
350
+ commentBody: z.string().optional().describe("CamelCase alias for comment_body."),
351
+ status: z.string().optional(),
352
+ priority,
353
+ project_id: z.string().nullable().optional(),
354
+ milestone_id: z.string().nullable().optional(),
355
+ team_id: z.string().nullable().optional(),
356
+ assignee_id: z.string().nullable().optional(),
357
+ assignee_ids: z.array(z.string()).optional(),
358
+ start_date: z.string().nullable().optional(),
359
+ due_date: z.string().nullable().optional(),
360
+ label_ids: z.array(z.string()).optional()
361
+ },
362
+ readOnly: false,
363
+ handler: async (args, extra) => orgPatch(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}`, nonEmptyBody(buildIssueBody(args)))
364
+ });
365
+ registerTool(server, "atoll_archive_issue", {
366
+ title: "Archive Atoll Issue",
367
+ description: "Soft-delete/archive an issue without requiring owner/admin permanent delete permissions. Mirrors `atoll issue archive`.",
368
+ inputSchema: { org_id: orgId, issue_id: issueId },
369
+ readOnly: false,
370
+ handler: async (args, extra) => orgPost(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/archive`, void 0)
371
+ });
372
+ registerTool(server, "atoll_unarchive_issue", {
373
+ title: "Unarchive Atoll Issue",
374
+ description: "Restore an archived issue. Mirrors `atoll issue unarchive`.",
375
+ inputSchema: { org_id: orgId, issue_id: issueId },
376
+ readOnly: false,
377
+ handler: async (args, extra) => orgDelete(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/archive`)
378
+ });
379
+ registerTool(server, "atoll_list_comments", {
380
+ title: "List Atoll Issue Comments",
381
+ description: "List comments for an Atoll issue.",
382
+ inputSchema: { org_id: orgId, issue_id: issueId },
383
+ readOnly: true,
384
+ handler: async (args, extra) => orgGet(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/comments`)
385
+ });
386
+ registerTool(server, "atoll_add_comment", {
387
+ title: "Add Atoll Issue Comment",
388
+ description: "Add a Markdown comment to an Atoll issue. Mirrors `atoll comment add`.",
389
+ inputSchema: {
390
+ org_id: orgId,
391
+ issue_id: issueId,
392
+ body: z.string().min(1).describe("Markdown comment body.")
393
+ },
394
+ readOnly: false,
395
+ handler: async (args, extra) => orgPost(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/comments`, {
396
+ body: requiredString(args.body, "body")
397
+ })
398
+ });
399
+ }
400
+ function registerProjectTools(server) {
401
+ registerTool(server, "atoll_list_projects", {
402
+ title: "List Atoll Projects",
403
+ description: "List projects visible to the API key. Mirrors `atoll project list --json`.",
404
+ inputSchema: { org_id: orgId, limit },
405
+ readOnly: true,
406
+ handler: async (args, extra) => buildProjectListPayload(await orgGet(extra, args, "/projects"), numberArg(args.limit, 25))
407
+ });
408
+ registerTool(server, "atoll_get_project", {
409
+ title: "Get Atoll Project",
410
+ description: "Get project detail and issues. Mirrors `atoll project get --json`.",
411
+ inputSchema: { org_id: orgId, project_id: projectId },
412
+ readOnly: true,
413
+ handler: async (args, extra) => orgGet(extra, args, `/projects/${encodeURIComponent(requiredString(args.project_id, "project_id"))}`)
414
+ });
415
+ registerTool(server, "atoll_create_project", {
416
+ title: "Create Atoll Project",
417
+ description: "Create a project. Mirrors `atoll project create --json`.",
418
+ inputSchema: {
419
+ org_id: orgId,
420
+ name: z.string().min(1),
421
+ description: z.string().optional(),
422
+ visibility: z.string().optional(),
423
+ color: z.string().optional(),
424
+ icon: z.string().optional(),
425
+ github_repo: z.string().optional()
426
+ },
427
+ readOnly: false,
428
+ handler: async (args, extra) => orgPost(extra, args, "/projects", pickDefined(args, ["name", "description", "visibility", "color", "icon", "github_repo"]))
429
+ });
430
+ }
431
+ function registerGoalTools(server) {
432
+ registerCrudTools(server, "goal", "/goals", goalId, ["title", "description", "owner_id", "status", "target_date"], {
433
+ listFilters: { status: z.string().optional(), limit },
434
+ createRequired: { title: z.string().min(1) }
435
+ });
436
+ }
437
+ function registerKpiTools(server) {
438
+ registerCrudTools(server, "kpi", "/kpis", kpiId, [
439
+ "name",
440
+ "goal_id",
441
+ "unit",
442
+ "unit_label",
443
+ "target_value",
444
+ "target_direction",
445
+ "current_value",
446
+ "stale_after_hours"
447
+ ], {
448
+ listFilters: { goal_id: z.string().optional(), limit },
449
+ createRequired: { name: z.string().min(1) }
450
+ });
451
+ registerTool(server, "atoll_list_kpi_snapshots", {
452
+ title: "List Atoll KPI Snapshots",
453
+ description: "List snapshots for a KPI. Mirrors `atoll kpi snapshot list --json`.",
454
+ inputSchema: { org_id: orgId, kpi_id: kpiId, limit },
455
+ readOnly: true,
456
+ handler: async (args, extra) => orgGet(extra, args, `/kpis/${encodeURIComponent(requiredString(args.kpi_id, "kpi_id"))}/snapshots`, {
457
+ limit: args.limit
458
+ })
459
+ });
460
+ registerTool(server, "atoll_record_kpi_snapshot", {
461
+ title: "Record Atoll KPI Snapshot",
462
+ description: "Record a KPI measurement. Mirrors `atoll kpi snapshot add --json`.",
463
+ inputSchema: {
464
+ org_id: orgId,
465
+ kpi_id: kpiId,
466
+ value: z.number(),
467
+ recorded_at: z.string().optional(),
468
+ attribution_note: z.string().optional(),
469
+ attributed_to_initiative_id: z.string().optional(),
470
+ attributed_to_issue_id: z.string().optional()
471
+ },
472
+ readOnly: false,
473
+ handler: async (args, extra) => orgPost(extra, args, `/kpis/${encodeURIComponent(requiredString(args.kpi_id, "kpi_id"))}/snapshots`, {
474
+ source: "manual",
475
+ ...pickDefined(args, ["value", "recorded_at", "attribution_note", "attributed_to_initiative_id", "attributed_to_issue_id"])
476
+ })
477
+ });
478
+ registerTool(server, "atoll_create_kpi_http_sync_draft", {
479
+ title: "Create Atoll KPI HTTP Sync Draft",
480
+ description: "Create a draft-only generic HTTPS JSON KPI sync for human admin review. Do not include secret values. Human admins must add secrets, dry-run, and publish in Atoll.",
481
+ inputSchema: {
482
+ org_id: orgId,
483
+ kpi_id: kpiId,
484
+ name: kpiHttpSyncName,
485
+ schedule: kpiHttpSyncSchedule,
486
+ request_config: kpiHttpSyncRequestConfig,
487
+ extraction_config: kpiHttpSyncExtractionConfig,
488
+ freshness_config: kpiHttpSyncFreshnessConfig
489
+ },
490
+ readOnly: false,
491
+ handler: async (args, extra) => orgPost(extra, args, `/kpis/${encodeURIComponent(requiredString(args.kpi_id, "kpi_id"))}/http-syncs`, safeKpiHttpSyncAgentBody({
492
+ name: args.name,
493
+ schedule: args.schedule,
494
+ request_config: args.request_config,
495
+ extraction_config: args.extraction_config,
496
+ freshness_config: args.freshness_config ?? {}
497
+ }))
498
+ });
499
+ registerTool(server, "atoll_validate_kpi_http_sync_config", {
500
+ title: "Validate Atoll KPI HTTP Sync Config",
501
+ description: "Validate a proposed KPI HTTP sync config without storing it, running network requests, adding secrets, publishing, or writing snapshots.",
502
+ inputSchema: {
503
+ org_id: orgId,
504
+ kpi_id: kpiId,
505
+ name: kpiHttpSyncName,
506
+ schedule: kpiHttpSyncSchedule,
507
+ request_config: kpiHttpSyncRequestConfig,
508
+ extraction_config: kpiHttpSyncExtractionConfig,
509
+ freshness_config: kpiHttpSyncFreshnessConfig
510
+ },
511
+ readOnly: true,
512
+ handler: async (args, extra) => orgPut(extra, args, `/kpis/${encodeURIComponent(requiredString(args.kpi_id, "kpi_id"))}/http-syncs`, safeKpiHttpSyncAgentBody({
513
+ name: args.name,
514
+ schedule: args.schedule,
515
+ request_config: args.request_config,
516
+ extraction_config: args.extraction_config,
517
+ freshness_config: args.freshness_config ?? {}
518
+ }))
519
+ });
520
+ }
521
+ function registerInitiativeTools(server) {
522
+ registerCrudTools(server, "initiative", "/initiatives", initiativeId, ["title", "name", "description", "goal_id", "owner_id", "status", "target_date"], {
523
+ listFilters: { goal_id: z.string().optional(), status: z.string().optional(), owner_id: z.string().optional(), limit },
524
+ createRequired: { title: z.string().min(1) }
525
+ });
526
+ registerLinkTool(server, "atoll_link_initiative_issue", "issue", "issues", "issue_id", issueId);
527
+ registerLinkTool(server, "atoll_link_initiative_milestone", "milestone", "milestones", "milestone_id", milestoneId);
528
+ registerLinkTool(server, "atoll_link_initiative_kpi", "KPI impact", "kpi-impacts", "kpi_id", kpiId, {
529
+ expected_impact: z.string().optional()
530
+ });
531
+ registerInitiativeTargetTools(server);
532
+ }
533
+ function registerInitiativeTargetTools(server) {
534
+ const targetBodySchema = {
535
+ title: z.string().min(1).optional(),
536
+ description: z.string().nullable().optional(),
537
+ mode: z.enum(["progress", "gate"]).optional(),
538
+ unit: z.enum(["count", "percentage", "currency", "duration", "ratio", "custom"]).optional(),
539
+ unit_label: z.string().nullable().optional(),
540
+ current_value: z.number().nullable().optional(),
541
+ target_value: z.number().nullable().optional(),
542
+ target_direction: z.enum(["increase", "decrease", "maintain"]).optional(),
543
+ target_date: z.string().nullable().optional(),
544
+ due_soon_days: z.number().int().min(0).nullable().optional()
545
+ };
546
+ registerTool(server, "atoll_list_initiative_targets", {
547
+ title: "List Atoll Initiative Targets",
548
+ description: "List progress and gate targets under an initiative. Gate targets represent launch prerequisites and should not be interpreted as KPI pace.",
549
+ inputSchema: { org_id: orgId, initiative_id: initiativeId },
550
+ readOnly: true,
551
+ handler: async (args, extra) => orgGet(extra, args, `/initiatives/${encodeURIComponent(requiredString(args.initiative_id, "initiative_id"))}/targets`)
552
+ });
553
+ registerTool(server, "atoll_get_initiative_target", {
554
+ title: "Get Atoll Initiative Target",
555
+ description: "Get one progress or gate target under an initiative.",
556
+ inputSchema: { org_id: orgId, initiative_id: initiativeId, target_id: initiativeTargetId },
557
+ readOnly: true,
558
+ handler: async (args, extra) => orgGet(extra, args, `/initiatives/${encodeURIComponent(requiredString(args.initiative_id, "initiative_id"))}/targets/${encodeURIComponent(requiredString(args.target_id, "target_id"))}`)
559
+ });
560
+ registerTool(server, "atoll_create_initiative_target", {
561
+ title: "Create Atoll Initiative Target",
562
+ description: "Create an initiative target. Use mode=progress for output tracking and mode=gate for hard launch prerequisites.",
563
+ inputSchema: { org_id: orgId, initiative_id: initiativeId, ...targetBodySchema, title: z.string().min(1) },
564
+ readOnly: false,
565
+ handler: async (args, extra) => orgPost(extra, args, `/initiatives/${encodeURIComponent(requiredString(args.initiative_id, "initiative_id"))}/targets`, nonEmptyBody(pickDefined(args, Object.keys(targetBodySchema))))
566
+ });
567
+ registerTool(server, "atoll_update_initiative_target", {
568
+ title: "Update Atoll Initiative Target",
569
+ description: "Update an initiative target current value, target value, mode, date, or descriptive fields.",
570
+ inputSchema: { org_id: orgId, initiative_id: initiativeId, target_id: initiativeTargetId, ...targetBodySchema },
571
+ readOnly: false,
572
+ handler: async (args, extra) => orgPatch(
573
+ extra,
574
+ args,
575
+ `/initiatives/${encodeURIComponent(requiredString(args.initiative_id, "initiative_id"))}/targets/${encodeURIComponent(requiredString(args.target_id, "target_id"))}`,
576
+ nonEmptyBody(pickDefined(args, Object.keys(targetBodySchema)))
577
+ )
578
+ });
579
+ registerTool(server, "atoll_delete_initiative_target", {
580
+ title: "Delete Atoll Initiative Target",
581
+ description: "Delete an initiative target and its target-specific work links.",
582
+ inputSchema: { org_id: orgId, initiative_id: initiativeId, target_id: initiativeTargetId },
583
+ readOnly: false,
584
+ destructive: true,
585
+ handler: async (args, extra) => orgDelete(extra, args, `/initiatives/${encodeURIComponent(requiredString(args.initiative_id, "initiative_id"))}/targets/${encodeURIComponent(requiredString(args.target_id, "target_id"))}`)
586
+ });
587
+ registerInitiativeTargetLinkTool(server, "atoll_link_initiative_target_issue", "issue", "issues", "issue_id", issueId, false);
588
+ registerInitiativeTargetLinkTool(server, "atoll_unlink_initiative_target_issue", "issue", "issues", "issue_id", issueId, true);
589
+ registerInitiativeTargetLinkTool(server, "atoll_link_initiative_target_milestone", "milestone", "milestones", "milestone_id", milestoneId, false);
590
+ registerInitiativeTargetLinkTool(server, "atoll_unlink_initiative_target_milestone", "milestone", "milestones", "milestone_id", milestoneId, true);
591
+ }
592
+ function registerMilestoneTools(server) {
593
+ registerTool(server, "atoll_list_milestones", {
594
+ title: "List Atoll Milestones",
595
+ description: "List milestones for a project. Mirrors `atoll milestone list --json`.",
596
+ inputSchema: { org_id: orgId, project_id: projectId },
597
+ readOnly: true,
598
+ handler: async (args, extra) => orgGet(extra, args, `/projects/${encodeURIComponent(requiredString(args.project_id, "project_id"))}/milestones`)
599
+ });
600
+ registerTool(server, "atoll_create_milestone", {
601
+ title: "Create Atoll Milestone",
602
+ description: "Create a milestone in a project. Mirrors `atoll milestone create --json`.",
603
+ inputSchema: {
604
+ org_id: orgId,
605
+ project_id: projectId,
606
+ name: z.string().min(1),
607
+ description: z.string().optional(),
608
+ status: z.string().optional(),
609
+ target_date: z.string().optional(),
610
+ due_date: z.string().optional()
611
+ },
612
+ readOnly: false,
613
+ handler: async (args, extra) => orgPost(extra, args, `/projects/${encodeURIComponent(requiredString(args.project_id, "project_id"))}/milestones`, pickDefined(args, ["name", "description", "status", "target_date", "due_date"]))
614
+ });
615
+ registerTool(server, "atoll_upsert_milestone", {
616
+ title: "Upsert Atoll Milestone",
617
+ description: "Retry-safe milestone sync by name within a project. If the API lacks direct upsert support, this tool performs list-then-create/update.",
618
+ inputSchema: {
619
+ org_id: orgId,
620
+ project_id: projectId,
621
+ name: z.string().min(1),
622
+ description: z.string().optional(),
623
+ status: z.string().optional(),
624
+ target_date: z.string().optional(),
625
+ due_date: z.string().optional()
626
+ },
627
+ readOnly: false,
628
+ handler: async (args, extra) => {
629
+ const client = clientFromExtra(extra);
630
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
631
+ const path = `/api/orgs/${encodeURIComponent(resolvedOrgId)}/projects/${encodeURIComponent(requiredString(args.project_id, "project_id"))}/milestones`;
632
+ const data = await client.get(path);
633
+ const existing = (data.milestones ?? []).find((milestone) => milestone.name === args.name);
634
+ const body = pickDefined(args, ["name", "description", "status", "target_date", "due_date"]);
635
+ if (!existing) {
636
+ return { action: "created", ...await client.post(path, body) };
637
+ }
638
+ return {
639
+ action: "updated",
640
+ ...await client.patch(`/api/orgs/${encodeURIComponent(resolvedOrgId)}/milestones/${encodeURIComponent(existing.id)}`, body)
641
+ };
642
+ }
643
+ });
644
+ }
645
+ function registerDependencyTools(server) {
646
+ registerTool(server, "atoll_list_dependencies", {
647
+ title: "List Atoll Issue Dependencies",
648
+ description: "List tasks this issue blocks and tasks blocking it. Mirrors `atoll dependency list --json`.",
649
+ inputSchema: { org_id: orgId, issue_id: issueId },
650
+ readOnly: true,
651
+ handler: async (args, extra) => orgGet(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/dependencies`)
652
+ });
653
+ registerTool(server, "atoll_add_dependency", {
654
+ title: "Add Atoll Issue Dependency",
655
+ description: "Add a blocking relationship. Provide exactly one of blocked_by_issue_id or blocking_issue_id.",
656
+ inputSchema: {
657
+ org_id: orgId,
658
+ issue_id: issueId,
659
+ blocked_by_issue_id: z.string().optional(),
660
+ blocking_issue_id: z.string().optional()
661
+ },
662
+ readOnly: false,
663
+ handler: async (args, extra) => {
664
+ const hasBlockedBy = typeof args.blocked_by_issue_id === "string";
665
+ const hasBlocking = typeof args.blocking_issue_id === "string";
666
+ if (hasBlockedBy === hasBlocking) throw new Error("Provide exactly one of blocked_by_issue_id or blocking_issue_id.");
667
+ return orgPost(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/dependencies`, {
668
+ ...hasBlockedBy ? { blockedByIssueId: args.blocked_by_issue_id } : {},
669
+ ...hasBlocking ? { blockingIssueId: args.blocking_issue_id } : {}
670
+ });
671
+ }
672
+ });
673
+ registerTool(server, "atoll_remove_dependency", {
674
+ title: "Remove Atoll Issue Dependency",
675
+ description: "Remove a dependency edge from an issue.",
676
+ inputSchema: { org_id: orgId, issue_id: issueId, dependency_id: z.string().min(1) },
677
+ readOnly: false,
678
+ handler: async (args, extra) => orgDelete(extra, args, `/issues/${encodeURIComponent(requiredString(args.issue_id, "issue_id"))}/dependencies/${encodeURIComponent(requiredString(args.dependency_id, "dependency_id"))}`)
679
+ });
680
+ }
681
+ function registerWebhookTools(server) {
682
+ registerTool(server, "atoll_list_webhooks", {
683
+ title: "List Atoll Webhooks",
684
+ description: "List org webhooks. Mirrors `atoll webhook list --json`.",
685
+ inputSchema: { org_id: orgId },
686
+ readOnly: true,
687
+ handler: async (args, extra) => {
688
+ const client = clientFromExtra(extra);
689
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
690
+ return client.get(`/api/webhooks?${new URLSearchParams({ orgId: resolvedOrgId }).toString()}`);
691
+ }
692
+ });
693
+ registerTool(server, "atoll_create_webhook", {
694
+ title: "Create Atoll Webhook",
695
+ description: "Create an outbound webhook. The returned secret is shown once by the API.",
696
+ inputSchema: {
697
+ org_id: orgId,
698
+ url: z.string().url(),
699
+ events: z.array(z.string()).min(1),
700
+ enabled: z.boolean().optional()
701
+ },
702
+ readOnly: false,
703
+ handler: async (args, extra) => {
704
+ const client = clientFromExtra(extra);
705
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
706
+ return client.post(`/api/webhooks?${new URLSearchParams({ orgId: resolvedOrgId }).toString()}`, pickDefined(args, ["url", "events", "enabled"]));
707
+ }
708
+ });
709
+ registerTool(server, "atoll_delete_webhook", {
710
+ title: "Delete Atoll Webhook",
711
+ description: "Delete an outbound webhook.",
712
+ inputSchema: { webhook_id: z.string().min(1) },
713
+ readOnly: false,
714
+ destructive: true,
715
+ handler: async (args, extra) => clientFromExtra(extra).delete(`/api/webhooks/${encodeURIComponent(requiredString(args.webhook_id, "webhook_id"))}`)
716
+ });
717
+ }
718
+ function registerFeedbackTools(server) {
719
+ registerTool(server, "atoll_send_feedback", {
720
+ title: "Send Atoll Platform Feedback",
721
+ description: "Send bug reports or feature requests to the Atoll platform team, not to the current org.",
722
+ inputSchema: {
723
+ type: z.enum(["bug", "feature"]).default("bug"),
724
+ description: z.string().min(1),
725
+ userEmail: z.string().email().optional(),
726
+ userName: z.string().optional(),
727
+ url: z.string().url().optional()
728
+ },
729
+ readOnly: false,
730
+ handler: async (args) => {
731
+ const response = await fetch(`${process.env.ATOLL_BASE_URL ?? "https://atollhq.com"}/api/feedback`, {
732
+ method: "POST",
733
+ headers: { "Content-Type": "application/json", "User-Agent": "atoll-mcp-server" },
734
+ body: JSON.stringify(pickDefined(args, ["type", "description", "userEmail", "userName", "url"]))
735
+ });
736
+ const text = await response.text();
737
+ const data = text ? JSON.parse(text) : void 0;
738
+ if (!response.ok) throw new Error(`Atoll feedback API ${response.status}: ${text}`);
739
+ return data;
740
+ }
741
+ });
742
+ }
743
+ function registerGenericApiTool(server) {
744
+ registerTool(server, "atoll_api_request", {
745
+ title: "Call Atoll API",
746
+ description: "Advanced fallback for API operations not yet represented as first-class MCP tools. Path must be relative and under /api/.",
747
+ inputSchema: {
748
+ method: z.enum(SUPPORTED_API_METHODS),
749
+ path: z.string().min(1).describe("Relative Atoll API path, e.g. /api/orgs/{orgId}/issues?limit=10. Full URLs are rejected."),
750
+ body: z.unknown().optional()
751
+ },
752
+ readOnly: false,
753
+ handler: async (args, extra) => {
754
+ const path = requiredString(args.path, "path");
755
+ if (isKpiHttpSyncApiPath(path)) {
756
+ throw new Error("KPI HTTP sync routes are not available through atoll_api_request. Use atoll_create_kpi_http_sync_draft or atoll_validate_kpi_http_sync_config; human admins must use Atoll UI for secrets, dry-run, publish, and execution.");
757
+ }
758
+ return clientFromExtra(extra).request(requiredString(args.method, "method"), path, args.body);
759
+ }
760
+ });
761
+ }
762
+ function isKpiHttpSyncApiPath(path) {
763
+ if (!path.startsWith("/")) return false;
764
+ let pathname;
765
+ try {
766
+ pathname = new URL(path, "https://atoll.local").pathname;
767
+ } catch {
768
+ return false;
769
+ }
770
+ return /^\/api\/orgs\/[^/]+\/kpis\/[^/]+\/http-syncs(?:\/|$)/.test(pathname);
771
+ }
772
+ function safeKpiHttpSyncAgentBody(body) {
773
+ const request = body.request_config;
774
+ if (isObjectRecord(request) && typeof request.url === "string" && new URLSearchParams(request.url.split("?", 2)[1] ?? "").toString()) {
775
+ throw new Error("KPI HTTP sync URLs must not include inline query strings. Query params are disabled in v1.");
776
+ }
777
+ if (isObjectRecord(request) && isObjectRecord(request.headers)) {
778
+ for (const [name, value] of Object.entries(request.headers)) {
779
+ const normalizedName = name.trim().toLowerCase();
780
+ if (normalizedName === "authorization" || normalizedName === "x-api-key") {
781
+ if (!isObjectRecord(value) || typeof value.secretRef !== "string") {
782
+ throw new Error(`${name} must use a non-secret secretRef placeholder, not a header value.`);
783
+ }
784
+ assertSafeKpiHttpSyncSecretRef(value.secretRef, `${name}.secretRef`);
785
+ } else if (isObjectRecord(value) && "secretRef" in value) {
786
+ throw new Error("Secret refs are only supported for Authorization and X-API-Key headers.");
787
+ }
788
+ }
789
+ }
790
+ const unsafe = findUnsafeKpiHttpSyncString(body, []);
791
+ if (unsafe) {
792
+ throw new Error(`Refusing possible secret value at ${unsafe.path}. Use a short placeholder secretRef and enter the secret in Atoll UI.`);
793
+ }
794
+ return body;
795
+ }
796
+ function buildProjectListPayload(data, requestedLimit) {
797
+ const projects = Array.isArray(data.projects) ? data.projects : [];
798
+ const limitValue = Math.max(1, Math.min(100, requestedLimit));
799
+ const items = projects.slice(0, limitValue);
800
+ const truncated = projects.length > items.length;
801
+ return {
802
+ resource: "projects",
803
+ items,
804
+ total: projects.length,
805
+ limit: limitValue,
806
+ offset: 0,
807
+ nextOffset: truncated ? items.length : null,
808
+ truncated,
809
+ hint: truncated ? "Increase limit up to 100 to return more projects." : null
810
+ };
811
+ }
812
+ function buildHeartbeatToolPayload(data, signals, signalsOnly) {
813
+ if (signalsOnly) {
814
+ return {
815
+ signals,
816
+ recommended_action: data.recommended_action ?? null
817
+ };
818
+ }
819
+ return { ...data, signals };
820
+ }
821
+ function registerCrudTools(server, resource, basePath, idSchema, bodyFields, options) {
822
+ const idField = `${resource}_id`;
823
+ const resourcePlural = resource === "kpi" ? "kpis" : `${resource}s`;
824
+ const bodySchema = Object.fromEntries(bodyFields.map((field) => [field, z.unknown().optional()]));
825
+ registerTool(server, `atoll_list_${resourcePlural}`, {
826
+ title: `List Atoll ${resourcePlural}`,
827
+ description: `List Atoll ${resourcePlural}. Mirrors \`atoll ${resource} list --json\`.`,
828
+ inputSchema: { org_id: orgId, ...options.listFilters },
829
+ readOnly: true,
830
+ handler: async (args, extra) => orgGet(extra, args, basePath, pickDefined(args, Object.keys(options.listFilters)))
831
+ });
832
+ registerTool(server, `atoll_get_${resource}`, {
833
+ title: `Get Atoll ${resource}`,
834
+ description: `Get one Atoll ${resource}. Mirrors \`atoll ${resource} get --json\`.`,
835
+ inputSchema: { org_id: orgId, [idField]: idSchema },
836
+ readOnly: true,
837
+ handler: async (args, extra) => orgGet(extra, args, `${basePath}/${encodeURIComponent(requiredString(args[idField], idField))}`)
838
+ });
839
+ registerTool(server, `atoll_create_${resource}`, {
840
+ title: `Create Atoll ${resource}`,
841
+ description: `Create an Atoll ${resource}. Mirrors \`atoll ${resource} create --json\`.`,
842
+ inputSchema: { org_id: orgId, ...bodySchema, ...options.createRequired },
843
+ readOnly: false,
844
+ handler: async (args, extra) => orgPost(extra, args, basePath, nonEmptyBody(pickDefined(args, bodyFields)))
845
+ });
846
+ registerTool(server, `atoll_update_${resource}`, {
847
+ title: `Update Atoll ${resource}`,
848
+ description: `Update an Atoll ${resource}. Mirrors \`atoll ${resource} update --json\`.`,
849
+ inputSchema: { org_id: orgId, [idField]: idSchema, ...bodySchema },
850
+ readOnly: false,
851
+ handler: async (args, extra) => orgPatch(extra, args, `${basePath}/${encodeURIComponent(requiredString(args[idField], idField))}`, nonEmptyBody(pickDefined(args, bodyFields)))
852
+ });
853
+ }
854
+ function registerLinkTool(server, name, label, linkPath, idField, idFieldSchema, extraSchema = {}) {
855
+ registerTool(server, name, {
856
+ title: `Link Atoll Initiative ${label}`,
857
+ description: `Link an initiative to a ${label}.`,
858
+ inputSchema: { org_id: orgId, initiative_id: initiativeId, [idField]: idFieldSchema, ...extraSchema },
859
+ readOnly: false,
860
+ handler: async (args, extra) => orgPost(extra, args, `/initiatives/${encodeURIComponent(requiredString(args.initiative_id, "initiative_id"))}/${linkPath}`, {
861
+ [idField]: args[idField],
862
+ ...pickDefined(args, Object.keys(extraSchema))
863
+ })
864
+ });
865
+ }
866
+ function registerInitiativeTargetLinkTool(server, name, label, linkPath, idField, idFieldSchema, unlink) {
867
+ registerTool(server, name, {
868
+ title: `${unlink ? "Unlink" : "Link"} Atoll Initiative Target ${label}`,
869
+ description: `${unlink ? "Unlink" : "Link"} an initiative target to a ${label}.`,
870
+ inputSchema: {
871
+ org_id: orgId,
872
+ initiative_id: initiativeId,
873
+ target_id: initiativeTargetId,
874
+ [idField]: idFieldSchema
875
+ },
876
+ readOnly: false,
877
+ handler: async (args, extra) => {
878
+ const initiative = encodeURIComponent(requiredString(args.initiative_id, "initiative_id"));
879
+ const target = encodeURIComponent(requiredString(args.target_id, "target_id"));
880
+ const linkedId = encodeURIComponent(requiredString(args[idField], idField));
881
+ const path = `/initiatives/${initiative}/targets/${target}/${linkPath}`;
882
+ if (unlink) return orgDelete(extra, args, `${path}/${linkedId}`);
883
+ return orgPost(extra, args, path, { [idField]: args[idField] });
884
+ }
885
+ });
886
+ }
887
+ function registerTool(server, name, config) {
888
+ server.registerTool(
889
+ name,
890
+ {
891
+ title: config.title,
892
+ description: config.description,
893
+ inputSchema: config.inputSchema,
894
+ annotations: {
895
+ readOnlyHint: config.readOnly,
896
+ destructiveHint: config.destructive ?? false,
897
+ idempotentHint: config.readOnly,
898
+ openWorldHint: true
899
+ }
900
+ },
901
+ async (args, extra) => {
902
+ try {
903
+ const data = await config.handler(args, extra);
904
+ return toolResult(data);
905
+ } catch (error) {
906
+ return toolError(error);
907
+ }
908
+ }
909
+ );
910
+ }
911
+ function clientFromExtra(extra) {
912
+ const token = extra.authInfo?.token;
913
+ return new AtollApiClient({ apiKey: resolveAuthToken(token ? `Bearer ${token}` : void 0) });
914
+ }
915
+ async function orgGet(extra, args, path, params) {
916
+ const client = clientFromExtra(extra);
917
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
918
+ return client.get(`/api/orgs/${encodeURIComponent(resolvedOrgId)}${path}${queryString(params)}`);
919
+ }
920
+ async function orgPost(extra, args, path, body) {
921
+ const client = clientFromExtra(extra);
922
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
923
+ return client.post(`/api/orgs/${encodeURIComponent(resolvedOrgId)}${path}`, body);
924
+ }
925
+ async function orgPut(extra, args, path, body) {
926
+ const client = clientFromExtra(extra);
927
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
928
+ return client.put(`/api/orgs/${encodeURIComponent(resolvedOrgId)}${path}`, body);
929
+ }
930
+ async function orgPatch(extra, args, path, body) {
931
+ const client = clientFromExtra(extra);
932
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
933
+ return client.patch(`/api/orgs/${encodeURIComponent(resolvedOrgId)}${path}`, body);
934
+ }
935
+ async function orgDelete(extra, args, path) {
936
+ const client = clientFromExtra(extra);
937
+ const resolvedOrgId = await resolveOrgId(client, stringArg(args.org_id));
938
+ return client.delete(`/api/orgs/${encodeURIComponent(resolvedOrgId)}${path}`);
939
+ }
940
+ function issueListParams(args) {
941
+ return {
942
+ status: args.status,
943
+ priority: args.priority,
944
+ projectId: args.project_id,
945
+ assigneeId: args.assignee_id,
946
+ teamId: args.team_id,
947
+ milestoneId: args.milestone_id,
948
+ q: args.q,
949
+ includeArchived: args.include_archived,
950
+ orderBy: args.order_by,
951
+ orderDir: args.order_dir,
952
+ limit: args.limit,
953
+ offset: args.offset,
954
+ shape: "envelope"
955
+ };
956
+ }
957
+ function buildIssueBody(args) {
958
+ return pickDefined(args, [
959
+ "title",
960
+ "description",
961
+ "commentBody",
962
+ "comment_body",
963
+ "status",
964
+ "priority",
965
+ "project_id",
966
+ "milestone_id",
967
+ "team_id",
968
+ "assignee_id",
969
+ "assignee_ids",
970
+ "start_date",
971
+ "due_date",
972
+ "label_ids"
973
+ ], {
974
+ project_id: "projectId",
975
+ milestone_id: "milestoneId",
976
+ team_id: "teamId",
977
+ assignee_id: "assigneeId",
978
+ assignee_ids: "assigneeIds",
979
+ start_date: "startDate",
980
+ due_date: "dueDate",
981
+ label_ids: "labelIds",
982
+ commentBody: "comment_body"
983
+ });
984
+ }
985
+ function pickDefined(args, fields, aliases = {}) {
986
+ const body = {};
987
+ for (const field of fields) {
988
+ if (args[field] !== void 0) {
989
+ body[aliases[field] ?? field] = args[field];
990
+ }
991
+ }
992
+ return body;
993
+ }
994
+ function nonEmptyBody(body) {
995
+ if (Object.keys(body).length === 0) {
996
+ throw new Error("No fields to send. Provide at least one update/create field.");
997
+ }
998
+ return body;
999
+ }
1000
+ function queryString(params) {
1001
+ if (!params) return "";
1002
+ const search = new URLSearchParams();
1003
+ for (const [key, value] of Object.entries(params)) {
1004
+ if (value !== void 0 && value !== null && value !== "") search.set(key, String(value));
1005
+ }
1006
+ const qs = search.toString();
1007
+ return qs ? `?${qs}` : "";
1008
+ }
1009
+ function assertSafeKpiHttpSyncSecretRef(value, label) {
1010
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,39}$/.test(value)) {
1011
+ throw new Error(`${label} must be a short placeholder name, such as posthog_api_key, not a secret value.`);
1012
+ }
1013
+ if (looksLikeKpiHttpSyncSecretValue(value)) {
1014
+ throw new Error(`${label} looks like a secret value. Use a placeholder name and enter the secret in Atoll UI.`);
1015
+ }
1016
+ }
1017
+ function findUnsafeKpiHttpSyncString(value, path) {
1018
+ if (typeof value === "string") {
1019
+ if (path[path.length - 1] === "format" && (value === "Bearer {value}" || value === "{value}")) return null;
1020
+ if (looksLikeKpiHttpSyncSecretValue(value)) return { path: path.join(".") || "<root>" };
1021
+ return null;
1022
+ }
1023
+ if (Array.isArray(value)) {
1024
+ for (let index = 0; index < value.length; index += 1) {
1025
+ const found = findUnsafeKpiHttpSyncString(value[index], [...path, String(index)]);
1026
+ if (found) return found;
1027
+ }
1028
+ return null;
1029
+ }
1030
+ if (isObjectRecord(value)) {
1031
+ for (const [key, nested] of Object.entries(value)) {
1032
+ if (key === "secretRef" && typeof nested === "string") {
1033
+ assertSafeKpiHttpSyncSecretRef(nested, [...path, key].join("."));
1034
+ continue;
1035
+ }
1036
+ const found = findUnsafeKpiHttpSyncString(nested, [...path, key]);
1037
+ if (found) return found;
1038
+ }
1039
+ }
1040
+ return null;
1041
+ }
1042
+ function looksLikeKpiHttpSyncSecretValue(value) {
1043
+ const trimmed = value.trim();
1044
+ if (!trimmed) return false;
1045
+ if (/^Bearer\s+\S+/i.test(trimmed)) return true;
1046
+ if (/^sk_(?:atoll|live|test|proj)_[A-Za-z0-9_-]{8,}/.test(trimmed)) return true;
1047
+ if (/^(?:ghp|gho|github_pat|xox[baprs]|glpat)-?[A-Za-z0-9_-]{16,}/.test(trimmed)) return true;
1048
+ if (/^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/.test(trimmed)) return true;
1049
+ if (/^(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,}$/i.test(trimmed)) return true;
1050
+ if (/^[A-Za-z0-9+/=_-]{48,}$/.test(trimmed) && /[A-Z]/.test(trimmed) && /[a-z]/.test(trimmed) && /\d/.test(trimmed)) {
1051
+ return true;
1052
+ }
1053
+ return false;
1054
+ }
1055
+ function isObjectRecord(value) {
1056
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1057
+ }
1058
+ function requiredString(value, name) {
1059
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${name} is required.`);
1060
+ return value;
1061
+ }
1062
+ function stringArg(value) {
1063
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1064
+ }
1065
+ function numberArg(value, fallback) {
1066
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1067
+ }
1068
+ function toolResult(data) {
1069
+ return {
1070
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
1071
+ structuredContent: data
1072
+ };
1073
+ }
1074
+ function toolError(error) {
1075
+ return {
1076
+ isError: true,
1077
+ content: [
1078
+ {
1079
+ type: "text",
1080
+ text: `Error: ${error.message ?? String(error)}`
1081
+ }
1082
+ ]
1083
+ };
1084
+ }
1085
+
1086
+ // src/http.ts
1087
+ function corsHeaders() {
1088
+ return {
1089
+ "access-control-allow-origin": "*",
1090
+ "access-control-allow-methods": "GET,POST,OPTIONS",
1091
+ "access-control-allow-headers": "authorization,content-type,mcp-session-id,mcp-protocol-version",
1092
+ "access-control-expose-headers": "mcp-session-id"
1093
+ };
1094
+ }
1095
+ function extractBearerToken(header) {
1096
+ if (!header) return void 0;
1097
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
1098
+ if (!match) {
1099
+ throw new Error("Authorization must use Bearer authentication. Example: Authorization: Bearer sk_atoll_...");
1100
+ }
1101
+ return match[1];
1102
+ }
1103
+ function healthResponse() {
1104
+ return {
1105
+ status: 200,
1106
+ headers: { "content-type": "application/json", ...corsHeaders() },
1107
+ body: JSON.stringify({ ok: true, service: "atoll-mcp-server" })
1108
+ };
1109
+ }
1110
+ function preflightResponse() {
1111
+ return {
1112
+ status: 204,
1113
+ headers: corsHeaders(),
1114
+ body: ""
1115
+ };
1116
+ }
1117
+ function writeJson(res, status, payload) {
1118
+ res.writeHead(status, { "content-type": "application/json", ...corsHeaders() });
1119
+ res.end(JSON.stringify(payload));
1120
+ }
1121
+ async function handleMcpHttpRequest(req, res) {
1122
+ if (req.method === "OPTIONS") {
1123
+ const preflight = preflightResponse();
1124
+ res.writeHead(preflight.status, preflight.headers);
1125
+ res.end(preflight.body);
1126
+ return;
1127
+ }
1128
+ if (req.url === "/health" && req.method === "GET") {
1129
+ const health = healthResponse();
1130
+ res.writeHead(health.status, health.headers);
1131
+ res.end(health.body);
1132
+ return;
1133
+ }
1134
+ if (req.url !== "/mcp") {
1135
+ writeJson(res, 404, { error: "Not found. Use POST /mcp for MCP requests or GET /health." });
1136
+ return;
1137
+ }
1138
+ if (req.method !== "POST") {
1139
+ writeJson(res, 405, {
1140
+ jsonrpc: "2.0",
1141
+ error: { code: -32e3, message: "Method not allowed. Use POST /mcp." },
1142
+ id: null
1143
+ });
1144
+ return;
1145
+ }
1146
+ try {
1147
+ const token = extractBearerToken(req.headers.authorization);
1148
+ if (token) {
1149
+ req.auth = {
1150
+ token,
1151
+ clientId: "atoll-mcp-client",
1152
+ scopes: []
1153
+ };
1154
+ }
1155
+ const { server } = createAtollMcpServer();
1156
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
1157
+ await server.connect(transport);
1158
+ for (const [key, value] of Object.entries(corsHeaders())) {
1159
+ res.setHeader(key, value);
1160
+ }
1161
+ res.on("close", () => {
1162
+ void transport.close();
1163
+ void server.close();
1164
+ });
1165
+ await transport.handleRequest(req, res);
1166
+ } catch (error) {
1167
+ if (!res.headersSent) {
1168
+ writeJson(res, 500, {
1169
+ jsonrpc: "2.0",
1170
+ error: {
1171
+ code: -32603,
1172
+ message: error.message
1173
+ },
1174
+ id: null
1175
+ });
1176
+ }
1177
+ }
1178
+ }
1179
+ function createAtollHttpServer() {
1180
+ return createServer((req, res) => {
1181
+ void handleMcpHttpRequest(req, res);
1182
+ });
1183
+ }
1184
+
1185
+ // src/index.ts
1186
+ async function main() {
1187
+ const args = new Set(process.argv.slice(2));
1188
+ if (args.has("--stdio")) {
1189
+ const { server: server2 } = createAtollMcpServer();
1190
+ await server2.connect(new StdioServerTransport());
1191
+ return;
1192
+ }
1193
+ const port = Number.parseInt(process.env.PORT ?? "8787", 10);
1194
+ const server = createAtollHttpServer();
1195
+ server.listen(port, () => {
1196
+ console.error(`atoll-mcp-server listening on http://127.0.0.1:${port}/mcp`);
1197
+ });
1198
+ }
1199
+ if (import.meta.url === `file://${process.argv[1]}`) {
1200
+ main().catch((error) => {
1201
+ console.error(error);
1202
+ process.exit(1);
1203
+ });
1204
+ }
1205
+ export {
1206
+ AtollApiClient,
1207
+ createAtollHttpServer,
1208
+ createAtollMcpServer,
1209
+ extractBearerToken,
1210
+ handleMcpHttpRequest,
1211
+ healthResponse,
1212
+ listAtollToolNames,
1213
+ resolveAuthToken,
1214
+ resolveOrgId
1215
+ };
1216
+ //# sourceMappingURL=index.js.map