@systemdox/mcp-server 0.1.14 → 0.1.16

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 CHANGED
@@ -216,6 +216,22 @@ function legacyToolDescription(canonical, description) {
216
216
  return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
217
217
  }
218
218
 
219
+ // src/lib/config-links.ts
220
+ async function workspaceBase(apiClient2) {
221
+ try {
222
+ const access = await apiClient2.get(
223
+ "/v1/docs/portal-access"
224
+ );
225
+ const slug = (access.host ?? "").split(".")[0];
226
+ return slug ? `https://${slug}.systemdox.com` : null;
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+ function configLink(path, base) {
232
+ return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
233
+ }
234
+
219
235
  // src/tools/get-guardrails.ts
220
236
  var DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
221
237
  var inputSchema = () => ({
@@ -249,13 +265,9 @@ function registerGetGuardrails(server2, apiClient2) {
249
265
  );
250
266
  }
251
267
  if (filtered.length === 0) {
268
+ const text2 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient2))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
252
269
  return {
253
- content: [
254
- {
255
- type: "text",
256
- text: `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`
257
- }
258
- ]
270
+ content: [{ type: "text", text: text2 }]
259
271
  };
260
272
  }
261
273
  const text = filtered.map(
@@ -510,7 +522,7 @@ function registerGetTechStack(server2, apiClient2) {
510
522
  content: [
511
523
  {
512
524
  type: "text",
513
- text: "No tech stack entries configured."
525
+ text: `No tech stack entries configured. Add your stack at ${configLink("/docs/tech-stack", await workspaceBase(apiClient2))}`
514
526
  }
515
527
  ]
516
528
  };
@@ -596,11 +608,21 @@ ${adrs}`);
596
608
 
597
609
  ${specs}`);
598
610
  }
611
+ if (sections.length === 0) {
612
+ return {
613
+ content: [
614
+ {
615
+ type: "text",
616
+ text: `No planning context available. Connect repositories at ${configLink("/settings/github", await workspaceBase(apiClient2))}`
617
+ }
618
+ ]
619
+ };
620
+ }
599
621
  return {
600
622
  content: [
601
623
  {
602
624
  type: "text",
603
- text: sections.join("\n\n") || "No planning context available."
625
+ text: sections.join("\n\n")
604
626
  }
605
627
  ]
606
628
  };
@@ -608,8 +630,193 @@ ${specs}`);
608
630
  );
609
631
  }
610
632
 
611
- // src/tools/get-context.ts
633
+ // src/tools/get-plan-board.ts
612
634
  import { z as z5 } from "zod";
635
+ var API_ITEM_PAGE = 100;
636
+ var DEFAULT_LIMIT = 50;
637
+ var BUCKET_LABELS = {
638
+ inProgress: "In Progress",
639
+ todo: "Todo",
640
+ done: "Done"
641
+ };
642
+ var BUCKET_ORDER = ["inProgress", "todo", "done"];
643
+ var STATUS_ARG_TO_BUCKET = {
644
+ todo: "todo",
645
+ in_progress: "inProgress",
646
+ done: "done"
647
+ };
648
+ function bucketOf(item) {
649
+ const status = (item.fields?.Status ?? "").trim().toLowerCase();
650
+ if (status) {
651
+ if (status.includes("done") || status.includes("ship") || status.includes("complete")) {
652
+ return "done";
653
+ }
654
+ if (status.includes("progress") || status.includes("doing") || status.includes("review")) {
655
+ return "inProgress";
656
+ }
657
+ return "todo";
658
+ }
659
+ return item.state === "CLOSED" ? "done" : "todo";
660
+ }
661
+ function formatBoards(boards) {
662
+ const lines = boards.map((b) => {
663
+ const c = b.statusCounts ?? { todo: 0, inProgress: 0, done: 0 };
664
+ return `- **${b.title}** \u2014 board #${b.number} \xB7 ${b.itemCount} item(s) (todo ${c.todo} \xB7 in progress ${c.inProgress} \xB7 done ${c.done})` + (b.description ? `
665
+ ${b.description}` : "") + `
666
+ ${b.url}`;
667
+ });
668
+ return lines.join("\n");
669
+ }
670
+ function formatItem(item) {
671
+ const meta = [];
672
+ if (item.labels.length > 0) meta.push(`labels: ${item.labels.join(", ")}`);
673
+ if (item.assignees.length > 0)
674
+ meta.push(`assignees: ${item.assignees.join(", ")}`);
675
+ const epic = item.fields?.Epic;
676
+ if (epic) meta.push(`epic: ${epic}`);
677
+ return `- #${item.number} ${item.title}${item.repo ? ` \u2014 ${item.repo}` : ""}` + (meta.length > 0 ? `
678
+ ${meta.join(" \xB7 ")}` : "") + (item.url ? `
679
+ ${item.url}` : "");
680
+ }
681
+ function registerGetPlanBoard(server2, apiClient2) {
682
+ server2.registerTool(
683
+ "get_plan_board",
684
+ {
685
+ title: "Get Plan Board",
686
+ description: "Read the GitHub Projects boards connected to this workspace. With no arguments, lists the open boards with their item counts by status. With project_number, lists that board's issues grouped by status (In Progress / Todo / Done), each with issue number, title, repo, labels, assignees and URL \u2014 so you can see what is planned or in flight and pick up the right issue. Read-only: this tool cannot create, move, or close board items.",
687
+ inputSchema: {
688
+ project_number: z5.number().int().positive().optional().describe(
689
+ "Board number (the N in github.com/orgs/{org}/projects/N). Omit to list the open boards."
690
+ ),
691
+ status: z5.enum(["todo", "in_progress", "done"]).optional().describe(
692
+ "With project_number, return only the items in this status group"
693
+ ),
694
+ limit: z5.number().int().positive().max(200).optional().describe(
695
+ `Max items to list (default ${DEFAULT_LIMIT}). Group headings always show the true totals.`
696
+ )
697
+ }
698
+ },
699
+ async ({ project_number, status, limit }) => {
700
+ if (project_number === void 0) {
701
+ const boards = await apiClient2.get(
702
+ "/v1/planning/projects"
703
+ );
704
+ if (boards.length === 0) {
705
+ return {
706
+ content: [
707
+ {
708
+ type: "text",
709
+ text: `No project boards found. Connect a GitHub organisation and create a board at ${configLink("/plan", await workspaceBase(apiClient2))}`
710
+ }
711
+ ]
712
+ };
713
+ }
714
+ const open = boards.filter((b) => !b.closed);
715
+ if (open.length === 0) {
716
+ return {
717
+ content: [
718
+ {
719
+ type: "text",
720
+ text: `No open project boards (${boards.length} closed). Reopen one, or create a board at ${configLink("/plan", await workspaceBase(apiClient2))}`
721
+ }
722
+ ]
723
+ };
724
+ }
725
+ const closedNote = boards.length > open.length ? `
726
+
727
+ _${boards.length - open.length} closed board(s) not listed._` : "";
728
+ return {
729
+ content: [
730
+ {
731
+ type: "text",
732
+ text: `${open.length} open project board(s):
733
+
734
+ ${formatBoards(open)}` + closedNote + `
735
+
736
+ Call get_plan_board with project_number to list a board's issues.`
737
+ }
738
+ ]
739
+ };
740
+ }
741
+ const response = await apiClient2.get(
742
+ `/v1/planning/projects/${project_number}/items`
743
+ );
744
+ const allItems = response.items ?? [];
745
+ const boardName = response.projectTitle ? `**${response.projectTitle}** \u2014 board #${project_number}` : `Board #${project_number}`;
746
+ if (allItems.length === 0) {
747
+ return {
748
+ content: [
749
+ {
750
+ type: "text",
751
+ text: `${boardName} has no issues, or it is not visible to this workspace's GitHub connection. Check the board at ${configLink("/plan", await workspaceBase(apiClient2))}`
752
+ }
753
+ ]
754
+ };
755
+ }
756
+ const grouped = {
757
+ inProgress: [],
758
+ todo: [],
759
+ done: []
760
+ };
761
+ for (const item of allItems) grouped[bucketOf(item)].push(item);
762
+ const wanted = status ? [STATUS_ARG_TO_BUCKET[status]] : BUCKET_ORDER;
763
+ const shown = wanted.reduce((n, b) => n + grouped[b].length, 0);
764
+ if (shown === 0) {
765
+ return {
766
+ content: [
767
+ {
768
+ type: "text",
769
+ text: `${boardName}: no items with status "${status}" (of ${allItems.length} item(s) on the board).`
770
+ }
771
+ ]
772
+ };
773
+ }
774
+ const cap = limit ?? DEFAULT_LIMIT;
775
+ let budget = cap;
776
+ const sections = [];
777
+ let rendered = 0;
778
+ for (const b of wanted) {
779
+ const items = grouped[b];
780
+ if (items.length === 0) continue;
781
+ const slice = items.slice(0, Math.max(budget, 0));
782
+ budget -= slice.length;
783
+ rendered += slice.length;
784
+ const omitted = items.length - slice.length;
785
+ sections.push(
786
+ `### ${BUCKET_LABELS[b]} (${items.length})
787
+ ` + (slice.length > 0 ? slice.map(formatItem).join("\n") : "_(not listed \u2014 item limit reached)_") + (omitted > 0 && slice.length > 0 ? `
788
+ _\u2026${omitted} more not listed._` : "")
789
+ );
790
+ }
791
+ const notes = [];
792
+ if (rendered < shown) {
793
+ notes.push(
794
+ `_Listing ${rendered} of ${shown} matching item(s) \u2014 raise limit (max 200) or filter by status to see the rest._`
795
+ );
796
+ }
797
+ if (allItems.length >= API_ITEM_PAGE) {
798
+ notes.push(
799
+ `_The API reads the board's first ${API_ITEM_PAGE} items; a larger board may have more._`
800
+ );
801
+ }
802
+ return {
803
+ content: [
804
+ {
805
+ type: "text",
806
+ text: `${boardName} \u2014 ${allItems.length} item(s)
807
+
808
+ ` + sections.join("\n\n") + (notes.length > 0 ? `
809
+
810
+ ${notes.join("\n")}` : "")
811
+ }
812
+ ]
813
+ };
814
+ }
815
+ );
816
+ }
817
+
818
+ // src/tools/get-context.ts
819
+ import { z as z6 } from "zod";
613
820
 
614
821
  // src/lib/scoring.ts
615
822
  var WEIGHT_PATH = 0.4;
@@ -1176,13 +1383,13 @@ function registerGetContext(server2, apiClient2) {
1176
1383
  title: "Get Context",
1177
1384
  description: "Intelligent context aggregator: fetches checks, specs, docs, and tech stack, ranks them by relevance to your current task, and returns the full text of the highest-ranked items within a token budget \u2014 each with the ID needed to fetch the rest. Use this instead of calling individual tools when you want curated, relevant context.",
1178
1385
  inputSchema: {
1179
- task_description: z5.string().describe(
1386
+ task_description: z6.string().describe(
1180
1387
  "What you're working on (e.g., 'add a new REST endpoint for user preferences')"
1181
1388
  ),
1182
- file_paths: z5.array(z5.string()).optional().describe(
1389
+ file_paths: z6.array(z6.string()).optional().describe(
1183
1390
  "File paths being edited \u2014 used for language/category detection"
1184
1391
  ),
1185
- max_tokens: z5.number().optional().describe("Maximum token budget for the response (default: 4000)")
1392
+ max_tokens: z6.number().optional().describe("Maximum token budget for the response (default: 4000)")
1186
1393
  }
1187
1394
  },
1188
1395
  async ({ task_description, file_paths, max_tokens }) => {
@@ -1199,7 +1406,7 @@ function registerGetContext(server2, apiClient2) {
1199
1406
  }
1200
1407
 
1201
1408
  // src/tools/create-guardrail.ts
1202
- import { z as z6 } from "zod";
1409
+ import { z as z7 } from "zod";
1203
1410
  var DESCRIPTION2 = "Create a new check in SystemDox. Use this when you discover a pattern that should be enforced (banned pattern, required pattern, coding rule).";
1204
1411
  var CATEGORIES = [
1205
1412
  "imports",
@@ -1212,13 +1419,13 @@ var CATEGORIES = [
1212
1419
  "data-access"
1213
1420
  ];
1214
1421
  var inputSchema2 = () => ({
1215
- title: z6.string().describe("Short title for the check"),
1216
- description: z6.string().describe("Detailed description of what the check enforces and why"),
1217
- severity: z6.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
1218
- category: z6.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
1219
- pattern: z6.string().optional().describe("Bad code pattern that should be avoided"),
1220
- fix: z6.string().optional().describe("Correct code pattern to use instead"),
1221
- fitness_test: z6.string().optional().describe("Shell command to detect violations in CI")
1422
+ title: z7.string().describe("Short title for the check"),
1423
+ description: z7.string().describe("Detailed description of what the check enforces and why"),
1424
+ severity: z7.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
1425
+ category: z7.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
1426
+ pattern: z7.string().optional().describe("Bad code pattern that should be avoided"),
1427
+ fix: z7.string().optional().describe("Correct code pattern to use instead"),
1428
+ fitness_test: z7.string().optional().describe("Shell command to detect violations in CI")
1222
1429
  });
1223
1430
  function registerCreateGuardrail(server2, apiClient2) {
1224
1431
  const handler = async ({
@@ -1284,7 +1491,7 @@ function registerCreateGuardrail(server2, apiClient2) {
1284
1491
  }
1285
1492
 
1286
1493
  // src/tools/update-spec.ts
1287
- import { z as z7 } from "zod";
1494
+ import { z as z8 } from "zod";
1288
1495
  function registerUpdateSpec(server2, apiClient2) {
1289
1496
  server2.registerTool(
1290
1497
  "update_spec",
@@ -1292,12 +1499,12 @@ function registerUpdateSpec(server2, apiClient2) {
1292
1499
  title: "Update Spec",
1293
1500
  description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
1294
1501
  inputSchema: {
1295
- spec_id: z7.string().describe("ID of the spec to update"),
1296
- section: z7.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
1297
- content: z7.string().describe(
1502
+ spec_id: z8.string().describe("ID of the spec to update"),
1503
+ section: z8.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
1504
+ content: z8.string().describe(
1298
1505
  "New markdown content for the section. For requirements, provide the full requirement content."
1299
1506
  ),
1300
- requirement_name: z7.string().optional().describe(
1507
+ requirement_name: z8.string().optional().describe(
1301
1508
  "Name of the requirement (required when section is 'requirements')"
1302
1509
  )
1303
1510
  },
@@ -1347,7 +1554,7 @@ function registerUpdateSpec(server2, apiClient2) {
1347
1554
  }
1348
1555
 
1349
1556
  // src/tools/report-decision.ts
1350
- import { z as z8 } from "zod";
1557
+ import { z as z9 } from "zod";
1351
1558
  function registerReportDecision(server2, apiClient2) {
1352
1559
  server2.registerTool(
1353
1560
  "report_decision",
@@ -1355,11 +1562,11 @@ function registerReportDecision(server2, apiClient2) {
1355
1562
  title: "Report Decision",
1356
1563
  description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
1357
1564
  inputSchema: {
1358
- title: z8.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
1359
- context: z8.string().describe("What prompted this decision \u2014 the problem or constraint"),
1360
- decision: z8.string().describe("What was decided and why"),
1361
- consequences: z8.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
1362
- related_files: z8.array(z8.string()).optional().describe("File paths related to this decision")
1565
+ title: z9.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
1566
+ context: z9.string().describe("What prompted this decision \u2014 the problem or constraint"),
1567
+ decision: z9.string().describe("What was decided and why"),
1568
+ consequences: z9.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
1569
+ related_files: z9.array(z9.string()).optional().describe("File paths related to this decision")
1363
1570
  },
1364
1571
  annotations: {
1365
1572
  readOnlyHint: false
@@ -1410,7 +1617,7 @@ Status: draft \u2014 review in SystemDox before publishing.`
1410
1617
  }
1411
1618
 
1412
1619
  // src/tools/get-project-rules.ts
1413
- import { z as z9 } from "zod";
1620
+ import { z as z10 } from "zod";
1414
1621
  var CATEGORY_LABELS = {
1415
1622
  deployment: "Deployment",
1416
1623
  architecture: "Architecture",
@@ -1426,7 +1633,7 @@ function registerGetProjectRules(server2, apiClient2) {
1426
1633
  title: "Get Project Rules",
1427
1634
  description: "Retrieve project rules defined in SystemDox Standards. These are team-defined principles covering deployment, architecture, security, testing, and process. Filter by category to get only relevant rules.",
1428
1635
  inputSchema: {
1429
- category: z9.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
1636
+ category: z10.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
1430
1637
  }
1431
1638
  },
1432
1639
  async ({ category }) => {
@@ -1440,13 +1647,9 @@ function registerGetProjectRules(server2, apiClient2) {
1440
1647
  );
1441
1648
  }
1442
1649
  if (filtered.length === 0) {
1650
+ const text2 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1443
1651
  return {
1444
- content: [
1445
- {
1446
- type: "text",
1447
- text: `No project rules found${category ? ` in category "${category}"` : ""}.`
1448
- }
1449
- ]
1652
+ content: [{ type: "text", text: text2 }]
1450
1653
  };
1451
1654
  }
1452
1655
  const grouped = /* @__PURE__ */ new Map();
@@ -1482,7 +1685,7 @@ function registerGetProjectRules(server2, apiClient2) {
1482
1685
  }
1483
1686
 
1484
1687
  // src/tools/create-project-rule.ts
1485
- import { z as z10 } from "zod";
1688
+ import { z as z11 } from "zod";
1486
1689
  function registerCreateProjectRule(server2, apiClient2) {
1487
1690
  server2.registerTool(
1488
1691
  "create_project_rule",
@@ -1490,10 +1693,10 @@ function registerCreateProjectRule(server2, apiClient2) {
1490
1693
  title: "Create Project Rule",
1491
1694
  description: "Create a new project rule in SystemDox Standards. Use this when you discover a team principle or convention that should be enforced across the project.",
1492
1695
  inputSchema: {
1493
- text: z10.string().describe(
1696
+ text: z11.string().describe(
1494
1697
  "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
1495
1698
  ),
1496
- category: z10.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
1699
+ category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
1497
1700
  },
1498
1701
  annotations: {
1499
1702
  readOnlyHint: false
@@ -1535,7 +1738,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
1535
1738
  content: [
1536
1739
  {
1537
1740
  type: "text",
1538
- text: "No prompt templates found."
1741
+ text: `No prompt templates found. Add templates at ${configLink("/docs/prompts", await workspaceBase(apiClient2))}`
1539
1742
  }
1540
1743
  ]
1541
1744
  };
@@ -1564,7 +1767,7 @@ ${text}`
1564
1767
  }
1565
1768
 
1566
1769
  // src/tools/create-prompt-template.ts
1567
- import { z as z11 } from "zod";
1770
+ import { z as z12 } from "zod";
1568
1771
  function registerCreatePromptTemplate(server2, apiClient2) {
1569
1772
  server2.registerTool(
1570
1773
  "create_prompt_template",
@@ -1572,9 +1775,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
1572
1775
  title: "Create Prompt Template",
1573
1776
  description: "Create a new reusable prompt template in SystemDox Standards. Use this to store preconfigured prompts for common AI tasks like code reviews, documentation generation, or analysis frameworks.",
1574
1777
  inputSchema: {
1575
- name: z11.string().describe("Short name for the template"),
1576
- description: z11.string().optional().describe("Brief description of when to use this template"),
1577
- prompt: z11.string().describe("The full prompt text that Gen AI will use when generating content")
1778
+ name: z12.string().describe("Short name for the template"),
1779
+ description: z12.string().optional().describe("Brief description of when to use this template"),
1780
+ prompt: z12.string().describe("The full prompt text that Gen AI will use when generating content")
1578
1781
  },
1579
1782
  annotations: {
1580
1783
  readOnlyHint: false
@@ -1613,6 +1816,7 @@ function createServer(apiClient2) {
1613
1816
  registerGetDocs(server2, apiClient2);
1614
1817
  registerGetTechStack(server2, apiClient2);
1615
1818
  registerGetPlanningContext(server2, apiClient2);
1819
+ registerGetPlanBoard(server2, apiClient2);
1616
1820
  registerGetContext(server2, apiClient2);
1617
1821
  registerGetProjectRules(server2, apiClient2);
1618
1822
  registerGetPromptTemplates(server2, apiClient2);
@@ -0,0 +1,18 @@
1
+ import type { SystemDoxApiClient } from "./api-client.js";
2
+ /**
3
+ * The tenant's workspace base URL (https://{slug}.systemdox.com), or null
4
+ * when it can't be resolved. The slug is the first label of the portal host
5
+ * that GET /v1/docs/portal-access reports — the API builds that host as
6
+ * `{slug}.{DOCS_DOMAIN}`, so the first label is the slug whichever docs
7
+ * domain is live. The GET goes through the api client's response cache, so
8
+ * repeated empty states don't re-fetch.
9
+ */
10
+ export declare function workspaceBase(apiClient: SystemDoxApiClient): Promise<string | null>;
11
+ /**
12
+ * Where to add missing configuration: a full URL when the workspace base is
13
+ * known, the in-app path otherwise.
14
+ *
15
+ * Mirrored by apps/app/src/pages/ai/mcpPreview.ts — keep the output
16
+ * byte-identical when either side changes.
17
+ */
18
+ export declare function configLink(path: string, base: string | null): string;
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/config-links.ts
4
+ async function workspaceBase(apiClient) {
5
+ try {
6
+ const access = await apiClient.get(
7
+ "/v1/docs/portal-access"
8
+ );
9
+ const slug = (access.host ?? "").split(".")[0];
10
+ return slug ? `https://${slug}.systemdox.com` : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function configLink(path, base) {
16
+ return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
17
+ }
18
+ export {
19
+ configLink,
20
+ workspaceBase
21
+ };
@@ -0,0 +1,27 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SystemDoxApiClient } from "../lib/api-client.js";
3
+ interface BoardItem {
4
+ id: string;
5
+ number: number;
6
+ title: string;
7
+ state: string;
8
+ url: string;
9
+ repo: string;
10
+ labels: string[];
11
+ assignees: string[];
12
+ fields: Record<string, string>;
13
+ }
14
+ type Bucket = "todo" | "inProgress" | "done";
15
+ /**
16
+ * Bucket one board item by its Status column.
17
+ *
18
+ * Boards name their columns differently, so match loosely — the same
19
+ * done/ship/complete and progress/doing/review matching the backend uses for
20
+ * the board-level counts (planning_api `_status_counts`). Where that helper
21
+ * treats a *missing* Status as todo, this falls back to the issue's own
22
+ * open/closed state, so a closed issue with no Status is never presented to an
23
+ * agent as available work.
24
+ */
25
+ export declare function bucketOf(item: Pick<BoardItem, "state" | "fields">): Bucket;
26
+ export declare function registerGetPlanBoard(server: McpServer, apiClient: SystemDoxApiClient): void;
27
+ export {};