@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.
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/tools/get-plan-board.ts
4
+ import { z } from "zod";
5
+
6
+ // src/lib/config-links.ts
7
+ async function workspaceBase(apiClient) {
8
+ try {
9
+ const access = await apiClient.get(
10
+ "/v1/docs/portal-access"
11
+ );
12
+ const slug = (access.host ?? "").split(".")[0];
13
+ return slug ? `https://${slug}.systemdox.com` : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function configLink(path, base) {
19
+ return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
20
+ }
21
+
22
+ // src/tools/get-plan-board.ts
23
+ var API_ITEM_PAGE = 100;
24
+ var DEFAULT_LIMIT = 50;
25
+ var BUCKET_LABELS = {
26
+ inProgress: "In Progress",
27
+ todo: "Todo",
28
+ done: "Done"
29
+ };
30
+ var BUCKET_ORDER = ["inProgress", "todo", "done"];
31
+ var STATUS_ARG_TO_BUCKET = {
32
+ todo: "todo",
33
+ in_progress: "inProgress",
34
+ done: "done"
35
+ };
36
+ function bucketOf(item) {
37
+ const status = (item.fields?.Status ?? "").trim().toLowerCase();
38
+ if (status) {
39
+ if (status.includes("done") || status.includes("ship") || status.includes("complete")) {
40
+ return "done";
41
+ }
42
+ if (status.includes("progress") || status.includes("doing") || status.includes("review")) {
43
+ return "inProgress";
44
+ }
45
+ return "todo";
46
+ }
47
+ return item.state === "CLOSED" ? "done" : "todo";
48
+ }
49
+ function formatBoards(boards) {
50
+ const lines = boards.map((b) => {
51
+ const c = b.statusCounts ?? { todo: 0, inProgress: 0, done: 0 };
52
+ 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 ? `
53
+ ${b.description}` : "") + `
54
+ ${b.url}`;
55
+ });
56
+ return lines.join("\n");
57
+ }
58
+ function formatItem(item) {
59
+ const meta = [];
60
+ if (item.labels.length > 0) meta.push(`labels: ${item.labels.join(", ")}`);
61
+ if (item.assignees.length > 0)
62
+ meta.push(`assignees: ${item.assignees.join(", ")}`);
63
+ const epic = item.fields?.Epic;
64
+ if (epic) meta.push(`epic: ${epic}`);
65
+ return `- #${item.number} ${item.title}${item.repo ? ` \u2014 ${item.repo}` : ""}` + (meta.length > 0 ? `
66
+ ${meta.join(" \xB7 ")}` : "") + (item.url ? `
67
+ ${item.url}` : "");
68
+ }
69
+ function registerGetPlanBoard(server, apiClient) {
70
+ server.registerTool(
71
+ "get_plan_board",
72
+ {
73
+ title: "Get Plan Board",
74
+ 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.",
75
+ inputSchema: {
76
+ project_number: z.number().int().positive().optional().describe(
77
+ "Board number (the N in github.com/orgs/{org}/projects/N). Omit to list the open boards."
78
+ ),
79
+ status: z.enum(["todo", "in_progress", "done"]).optional().describe(
80
+ "With project_number, return only the items in this status group"
81
+ ),
82
+ limit: z.number().int().positive().max(200).optional().describe(
83
+ `Max items to list (default ${DEFAULT_LIMIT}). Group headings always show the true totals.`
84
+ )
85
+ }
86
+ },
87
+ async ({ project_number, status, limit }) => {
88
+ if (project_number === void 0) {
89
+ const boards = await apiClient.get(
90
+ "/v1/planning/projects"
91
+ );
92
+ if (boards.length === 0) {
93
+ return {
94
+ content: [
95
+ {
96
+ type: "text",
97
+ text: `No project boards found. Connect a GitHub organisation and create a board at ${configLink("/plan", await workspaceBase(apiClient))}`
98
+ }
99
+ ]
100
+ };
101
+ }
102
+ const open = boards.filter((b) => !b.closed);
103
+ if (open.length === 0) {
104
+ return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: `No open project boards (${boards.length} closed). Reopen one, or create a board at ${configLink("/plan", await workspaceBase(apiClient))}`
109
+ }
110
+ ]
111
+ };
112
+ }
113
+ const closedNote = boards.length > open.length ? `
114
+
115
+ _${boards.length - open.length} closed board(s) not listed._` : "";
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: `${open.length} open project board(s):
121
+
122
+ ${formatBoards(open)}` + closedNote + `
123
+
124
+ Call get_plan_board with project_number to list a board's issues.`
125
+ }
126
+ ]
127
+ };
128
+ }
129
+ const response = await apiClient.get(
130
+ `/v1/planning/projects/${project_number}/items`
131
+ );
132
+ const allItems = response.items ?? [];
133
+ const boardName = response.projectTitle ? `**${response.projectTitle}** \u2014 board #${project_number}` : `Board #${project_number}`;
134
+ if (allItems.length === 0) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `${boardName} has no issues, or it is not visible to this workspace's GitHub connection. Check the board at ${configLink("/plan", await workspaceBase(apiClient))}`
140
+ }
141
+ ]
142
+ };
143
+ }
144
+ const grouped = {
145
+ inProgress: [],
146
+ todo: [],
147
+ done: []
148
+ };
149
+ for (const item of allItems) grouped[bucketOf(item)].push(item);
150
+ const wanted = status ? [STATUS_ARG_TO_BUCKET[status]] : BUCKET_ORDER;
151
+ const shown = wanted.reduce((n, b) => n + grouped[b].length, 0);
152
+ if (shown === 0) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `${boardName}: no items with status "${status}" (of ${allItems.length} item(s) on the board).`
158
+ }
159
+ ]
160
+ };
161
+ }
162
+ const cap = limit ?? DEFAULT_LIMIT;
163
+ let budget = cap;
164
+ const sections = [];
165
+ let rendered = 0;
166
+ for (const b of wanted) {
167
+ const items = grouped[b];
168
+ if (items.length === 0) continue;
169
+ const slice = items.slice(0, Math.max(budget, 0));
170
+ budget -= slice.length;
171
+ rendered += slice.length;
172
+ const omitted = items.length - slice.length;
173
+ sections.push(
174
+ `### ${BUCKET_LABELS[b]} (${items.length})
175
+ ` + (slice.length > 0 ? slice.map(formatItem).join("\n") : "_(not listed \u2014 item limit reached)_") + (omitted > 0 && slice.length > 0 ? `
176
+ _\u2026${omitted} more not listed._` : "")
177
+ );
178
+ }
179
+ const notes = [];
180
+ if (rendered < shown) {
181
+ notes.push(
182
+ `_Listing ${rendered} of ${shown} matching item(s) \u2014 raise limit (max 200) or filter by status to see the rest._`
183
+ );
184
+ }
185
+ if (allItems.length >= API_ITEM_PAGE) {
186
+ notes.push(
187
+ `_The API reads the board's first ${API_ITEM_PAGE} items; a larger board may have more._`
188
+ );
189
+ }
190
+ return {
191
+ content: [
192
+ {
193
+ type: "text",
194
+ text: `${boardName} \u2014 ${allItems.length} item(s)
195
+
196
+ ` + sections.join("\n\n") + (notes.length > 0 ? `
197
+
198
+ ${notes.join("\n")}` : "")
199
+ }
200
+ ]
201
+ };
202
+ }
203
+ );
204
+ }
205
+ export {
206
+ bucketOf,
207
+ registerGetPlanBoard
208
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemdox/mcp-server",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "description": "MCP server that exposes SystemDox checks, specs, docs, and tech stack to AI coding tools",
6
6
  "main": "./dist/index.js",