@xferops/forge-mcp 2.0.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/.github/workflows/release.yml +50 -0
- package/README.md +225 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +739 -0
- package/package.json +40 -0
- package/skill/SKILL.md +13 -0
- package/skill/forge-board-admin/SKILL.md +99 -0
- package/skill/forge-dev-workflow/SKILL.md +120 -0
- package/skill/forge-setup/SKILL.md +55 -0
- package/src/index.ts +1006 -0
- package/tsconfig.json +14 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Forge MCP Server
|
|
5
|
+
*
|
|
6
|
+
* MCP (Model Context Protocol) server for interacting with the Forge API.
|
|
7
|
+
* Forge is XferOps' project management system (formerly Flower/Kanban).
|
|
8
|
+
*
|
|
9
|
+
* Works with: Claude Code, OpenAI Codex, OpenClaw/mcporter, and any MCP consumer.
|
|
10
|
+
*
|
|
11
|
+
* @see https://forge.xferops.dev/docs for API documentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import {
|
|
17
|
+
CallToolRequestSchema,
|
|
18
|
+
ListToolsRequestSchema,
|
|
19
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Configuration
|
|
23
|
+
// Accepts FORGE_* (preferred) or FLOWER_* (legacy, backward compat)
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const FORGE_URL =
|
|
27
|
+
process.env.FORGE_URL ||
|
|
28
|
+
process.env.FLOWER_URL ||
|
|
29
|
+
process.env.KANBAN_URL ||
|
|
30
|
+
"https://forge.xferops.dev";
|
|
31
|
+
|
|
32
|
+
const FORGE_TOKEN =
|
|
33
|
+
process.env.FORGE_TOKEN ||
|
|
34
|
+
process.env.FLOWER_TOKEN ||
|
|
35
|
+
process.env.KANBAN_TOKEN ||
|
|
36
|
+
"";
|
|
37
|
+
|
|
38
|
+
if (!FORGE_TOKEN) {
|
|
39
|
+
console.error("⚠️ Warning: FORGE_TOKEN environment variable not set");
|
|
40
|
+
console.error(" Get your token at: https://forge.xferops.dev/settings");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// API Client
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
interface ApiError {
|
|
48
|
+
error: string;
|
|
49
|
+
details?: Array<{ field: string; message: string }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function apiCall<T = unknown>(
|
|
53
|
+
path: string,
|
|
54
|
+
options: RequestInit = {}
|
|
55
|
+
): Promise<T> {
|
|
56
|
+
const url = `${FORGE_URL}${path}`;
|
|
57
|
+
|
|
58
|
+
const res = await fetch(url, {
|
|
59
|
+
...options,
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
Authorization: `Bearer ${FORGE_TOKEN}`,
|
|
63
|
+
...options.headers,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const apiError = data as ApiError;
|
|
71
|
+
const message = apiError.details
|
|
72
|
+
? apiError.details.map((d) => `${d.field}: ${d.message}`).join(", ")
|
|
73
|
+
: apiError.error || `HTTP ${res.status}`;
|
|
74
|
+
throw new Error(message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return data as T;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// Type Definitions
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
interface User {
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
email: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface TeamMember {
|
|
91
|
+
userId: string;
|
|
92
|
+
role: "OWNER" | "ADMIN" | "MEMBER";
|
|
93
|
+
user: User;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface Team {
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
slug: string;
|
|
100
|
+
members?: TeamMember[];
|
|
101
|
+
projects?: ProjectSummary[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface ProjectSummary {
|
|
105
|
+
id: string;
|
|
106
|
+
name: string;
|
|
107
|
+
slug: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface Column {
|
|
111
|
+
id: string;
|
|
112
|
+
name: string;
|
|
113
|
+
position: number;
|
|
114
|
+
tasks?: Task[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface Task {
|
|
118
|
+
id: string;
|
|
119
|
+
ticketNumber: number;
|
|
120
|
+
ticketId: string;
|
|
121
|
+
title: string;
|
|
122
|
+
description: string | null;
|
|
123
|
+
position: number;
|
|
124
|
+
priority: "LOW" | "MEDIUM" | "HIGH" | "URGENT";
|
|
125
|
+
type: "TASK" | "BUG" | "STORY";
|
|
126
|
+
columnId: string;
|
|
127
|
+
projectId: string;
|
|
128
|
+
assignees?: Array<{ user: User }>;
|
|
129
|
+
creator?: User;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
updatedAt: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface Project {
|
|
135
|
+
id: string;
|
|
136
|
+
name: string;
|
|
137
|
+
slug: string;
|
|
138
|
+
teamId: string;
|
|
139
|
+
prefix: string | null;
|
|
140
|
+
nextTicketNumber: number;
|
|
141
|
+
columns: Column[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface Comment {
|
|
145
|
+
id: string;
|
|
146
|
+
content: string;
|
|
147
|
+
taskId: string;
|
|
148
|
+
userId: string;
|
|
149
|
+
user: User;
|
|
150
|
+
createdAt: string;
|
|
151
|
+
updatedAt: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface SearchResult {
|
|
155
|
+
tasks: Task[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Tool Implementations - Health
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async function healthCheck(): Promise<{ status: string; url: string }> {
|
|
163
|
+
const data = await apiCall<{ status: string }>("/api/health");
|
|
164
|
+
return { ...data, url: FORGE_URL };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// Tool Implementations - Teams
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
async function listTeams(): Promise<Team[]> {
|
|
172
|
+
return apiCall<Team[]>("/api/teams");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function createTeam(name: string): Promise<Team> {
|
|
176
|
+
return apiCall<Team>("/api/teams", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({ name }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function listTeamMembers(teamId: string): Promise<TeamMember[]> {
|
|
183
|
+
return apiCall<TeamMember[]>(`/api/teams/${teamId}/members`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function addTeamMember(
|
|
187
|
+
teamId: string,
|
|
188
|
+
email: string,
|
|
189
|
+
role?: string
|
|
190
|
+
): Promise<TeamMember> {
|
|
191
|
+
return apiCall<TeamMember>(`/api/teams/${teamId}/members`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
body: JSON.stringify({ email, role: role || "MEMBER" }),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
198
|
+
// Tool Implementations - Projects
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
async function listProjects(teamId: string): Promise<ProjectSummary[]> {
|
|
202
|
+
return apiCall<ProjectSummary[]>(`/api/teams/${teamId}/projects`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function getProject(projectId: string): Promise<Project> {
|
|
206
|
+
return apiCall<Project>(`/api/projects/${projectId}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function createProject(
|
|
210
|
+
teamId: string,
|
|
211
|
+
name: string,
|
|
212
|
+
prefix?: string
|
|
213
|
+
): Promise<Project> {
|
|
214
|
+
return apiCall<Project>(`/api/teams/${teamId}/projects`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
body: JSON.stringify({ name, prefix }),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function updateProject(
|
|
221
|
+
projectId: string,
|
|
222
|
+
updates: { name?: string; prefix?: string }
|
|
223
|
+
): Promise<Project> {
|
|
224
|
+
return apiCall<Project>(`/api/projects/${projectId}`, {
|
|
225
|
+
method: "PATCH",
|
|
226
|
+
body: JSON.stringify(updates),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function deleteProject(projectId: string): Promise<{ success: boolean }> {
|
|
231
|
+
await apiCall(`/api/projects/${projectId}`, { method: "DELETE" });
|
|
232
|
+
return { success: true };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
236
|
+
// Tool Implementations - Columns
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
async function createColumn(projectId: string, name: string): Promise<Column> {
|
|
240
|
+
return apiCall<Column>("/api/columns", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
body: JSON.stringify({ projectId, name }),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function updateColumn(
|
|
247
|
+
columnId: string,
|
|
248
|
+
updates: { name?: string }
|
|
249
|
+
): Promise<Column> {
|
|
250
|
+
return apiCall<Column>(`/api/columns/${columnId}`, {
|
|
251
|
+
method: "PATCH",
|
|
252
|
+
body: JSON.stringify(updates),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function deleteColumn(
|
|
257
|
+
columnId: string,
|
|
258
|
+
moveTasksTo?: string
|
|
259
|
+
): Promise<{ success: boolean }> {
|
|
260
|
+
const url = moveTasksTo
|
|
261
|
+
? `/api/columns/${columnId}?moveTasksTo=${moveTasksTo}`
|
|
262
|
+
: `/api/columns/${columnId}`;
|
|
263
|
+
await apiCall(url, { method: "DELETE" });
|
|
264
|
+
return { success: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function reorderColumns(
|
|
268
|
+
projectId: string,
|
|
269
|
+
columnIds: string[]
|
|
270
|
+
): Promise<{ success: boolean }> {
|
|
271
|
+
await apiCall("/api/columns/reorder", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: JSON.stringify({ projectId, columnIds }),
|
|
274
|
+
});
|
|
275
|
+
return { success: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
279
|
+
// Tool Implementations - Tasks
|
|
280
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
async function getTask(taskId: string): Promise<Task> {
|
|
283
|
+
return apiCall<Task>(`/api/tasks/${taskId}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function listTasks(projectId: string): Promise<Task[]> {
|
|
287
|
+
const project = await apiCall<Project>(`/api/projects/${projectId}`);
|
|
288
|
+
const tasks: Task[] = [];
|
|
289
|
+
for (const column of project.columns || []) {
|
|
290
|
+
if (column.tasks) {
|
|
291
|
+
tasks.push(...column.tasks);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return tasks;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function searchTasks(query: string, projectId?: string): Promise<Task[]> {
|
|
298
|
+
const params = new URLSearchParams({ q: query });
|
|
299
|
+
if (projectId) params.append("projectId", projectId);
|
|
300
|
+
const result = await apiCall<SearchResult>(`/api/tasks/search?${params}`);
|
|
301
|
+
return result.tasks;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function createTask(params: {
|
|
305
|
+
projectId: string;
|
|
306
|
+
columnId: string;
|
|
307
|
+
title: string;
|
|
308
|
+
description?: string;
|
|
309
|
+
type?: "TASK" | "BUG" | "STORY";
|
|
310
|
+
priority?: "LOW" | "MEDIUM" | "HIGH" | "URGENT";
|
|
311
|
+
assigneeId?: string;
|
|
312
|
+
}): Promise<Task> {
|
|
313
|
+
return apiCall<Task>("/api/tasks", {
|
|
314
|
+
method: "POST",
|
|
315
|
+
body: JSON.stringify(params),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function updateTask(
|
|
320
|
+
taskId: string,
|
|
321
|
+
updates: {
|
|
322
|
+
title?: string;
|
|
323
|
+
description?: string;
|
|
324
|
+
columnId?: string;
|
|
325
|
+
assigneeId?: string | null;
|
|
326
|
+
priority?: "LOW" | "MEDIUM" | "HIGH" | "URGENT";
|
|
327
|
+
type?: "TASK" | "BUG" | "STORY";
|
|
328
|
+
prUrl?: string | null;
|
|
329
|
+
prNumber?: number | null;
|
|
330
|
+
prRepo?: string | null;
|
|
331
|
+
}
|
|
332
|
+
): Promise<Task> {
|
|
333
|
+
return apiCall<Task>(`/api/tasks/${taskId}`, {
|
|
334
|
+
method: "PATCH",
|
|
335
|
+
body: JSON.stringify(updates),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function moveTask(taskId: string, columnId: string): Promise<Task> {
|
|
340
|
+
return apiCall<Task>(`/api/tasks/${taskId}`, {
|
|
341
|
+
method: "PATCH",
|
|
342
|
+
body: JSON.stringify({ columnId }),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function deleteTask(taskId: string): Promise<{ success: boolean }> {
|
|
347
|
+
await apiCall(`/api/tasks/${taskId}`, { method: "DELETE" });
|
|
348
|
+
return { success: true };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function reorderTask(
|
|
352
|
+
taskId: string,
|
|
353
|
+
columnId: string,
|
|
354
|
+
position: number
|
|
355
|
+
): Promise<{ success: boolean }> {
|
|
356
|
+
await apiCall("/api/tasks/reorder", {
|
|
357
|
+
method: "POST",
|
|
358
|
+
body: JSON.stringify({ taskId, columnId, position }),
|
|
359
|
+
});
|
|
360
|
+
return { success: true };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
364
|
+
// Tool Implementations - Comments
|
|
365
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
async function listComments(taskId: string): Promise<Comment[]> {
|
|
368
|
+
return apiCall<Comment[]>(`/api/comments?taskId=${taskId}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function createComment(taskId: string, content: string): Promise<Comment> {
|
|
372
|
+
return apiCall<Comment>("/api/comments", {
|
|
373
|
+
method: "POST",
|
|
374
|
+
body: JSON.stringify({ taskId, content }),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function updateComment(commentId: string, content: string): Promise<Comment> {
|
|
379
|
+
return apiCall<Comment>(`/api/comments/${commentId}`, {
|
|
380
|
+
method: "PATCH",
|
|
381
|
+
body: JSON.stringify({ content }),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function deleteComment(commentId: string): Promise<{ success: boolean }> {
|
|
386
|
+
await apiCall(`/api/comments/${commentId}`, { method: "DELETE" });
|
|
387
|
+
return { success: true };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
391
|
+
// MCP Server Setup
|
|
392
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
const server = new Server(
|
|
395
|
+
{
|
|
396
|
+
name: "forge-mcp",
|
|
397
|
+
version: "2.0.0",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
capabilities: {
|
|
401
|
+
tools: {},
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
407
|
+
// Tool Definitions
|
|
408
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
const tools = [
|
|
411
|
+
// Health
|
|
412
|
+
{
|
|
413
|
+
name: "forge_health_check",
|
|
414
|
+
description:
|
|
415
|
+
"Check if the Forge API is healthy and reachable. Returns the API status and configured URL.",
|
|
416
|
+
inputSchema: { type: "object", properties: {} },
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
// Teams
|
|
420
|
+
{
|
|
421
|
+
name: "forge_list_teams",
|
|
422
|
+
description:
|
|
423
|
+
"List all teams you belong to. Returns team IDs needed for other operations.",
|
|
424
|
+
inputSchema: { type: "object", properties: {} },
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "forge_create_team",
|
|
428
|
+
description: "Create a new team. You become the owner.",
|
|
429
|
+
inputSchema: {
|
|
430
|
+
type: "object",
|
|
431
|
+
properties: {
|
|
432
|
+
name: {
|
|
433
|
+
type: "string",
|
|
434
|
+
description: "Team name (e.g., 'Engineering', 'Product')",
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
required: ["name"],
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: "forge_list_team_members",
|
|
442
|
+
description:
|
|
443
|
+
"List all members of a team with their roles. Useful for finding user IDs for task assignment.",
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties: {
|
|
447
|
+
teamId: { type: "string", description: "Team ID" },
|
|
448
|
+
},
|
|
449
|
+
required: ["teamId"],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: "forge_add_team_member",
|
|
454
|
+
description: "Add a user to a team by their email address.",
|
|
455
|
+
inputSchema: {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
teamId: { type: "string", description: "Team ID" },
|
|
459
|
+
email: {
|
|
460
|
+
type: "string",
|
|
461
|
+
description: "Email address of the user to add",
|
|
462
|
+
},
|
|
463
|
+
role: {
|
|
464
|
+
type: "string",
|
|
465
|
+
enum: ["ADMIN", "MEMBER"],
|
|
466
|
+
description: "Role to assign (default: MEMBER)",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
required: ["teamId", "email"],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
// Projects
|
|
474
|
+
{
|
|
475
|
+
name: "forge_list_projects",
|
|
476
|
+
description: "List all projects in a team.",
|
|
477
|
+
inputSchema: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
teamId: { type: "string", description: "Team ID" },
|
|
481
|
+
},
|
|
482
|
+
required: ["teamId"],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "forge_get_project",
|
|
487
|
+
description:
|
|
488
|
+
"Get full project details including all columns and tasks. This is the main way to see the board state.",
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {
|
|
492
|
+
projectId: { type: "string", description: "Project ID" },
|
|
493
|
+
},
|
|
494
|
+
required: ["projectId"],
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "forge_create_project",
|
|
499
|
+
description: "Create a new project (board) in a team.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
teamId: { type: "string", description: "Team ID" },
|
|
504
|
+
name: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description:
|
|
507
|
+
"Project name (e.g., 'Q1 Sprint', 'Product Roadmap')",
|
|
508
|
+
},
|
|
509
|
+
prefix: {
|
|
510
|
+
type: "string",
|
|
511
|
+
description:
|
|
512
|
+
"Optional ticket prefix (e.g., 'PROD' for PROD-123 style tickets)",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
required: ["teamId", "name"],
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: "forge_update_project",
|
|
520
|
+
description: "Update project name or ticket prefix.",
|
|
521
|
+
inputSchema: {
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
projectId: { type: "string", description: "Project ID" },
|
|
525
|
+
name: { type: "string", description: "New project name" },
|
|
526
|
+
prefix: { type: "string", description: "New ticket prefix" },
|
|
527
|
+
},
|
|
528
|
+
required: ["projectId"],
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: "forge_delete_project",
|
|
533
|
+
description:
|
|
534
|
+
"Delete a project and all its columns/tasks. This cannot be undone!",
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: "object",
|
|
537
|
+
properties: {
|
|
538
|
+
projectId: { type: "string", description: "Project ID to delete" },
|
|
539
|
+
},
|
|
540
|
+
required: ["projectId"],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
// Columns
|
|
545
|
+
{
|
|
546
|
+
name: "forge_create_column",
|
|
547
|
+
description:
|
|
548
|
+
"Create a new column in a project board (e.g., 'Backlog', 'In Progress', 'Done').",
|
|
549
|
+
inputSchema: {
|
|
550
|
+
type: "object",
|
|
551
|
+
properties: {
|
|
552
|
+
projectId: { type: "string", description: "Project ID" },
|
|
553
|
+
name: { type: "string", description: "Column name" },
|
|
554
|
+
},
|
|
555
|
+
required: ["projectId", "name"],
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: "forge_update_column",
|
|
560
|
+
description: "Rename a column.",
|
|
561
|
+
inputSchema: {
|
|
562
|
+
type: "object",
|
|
563
|
+
properties: {
|
|
564
|
+
columnId: { type: "string", description: "Column ID" },
|
|
565
|
+
name: { type: "string", description: "New column name" },
|
|
566
|
+
},
|
|
567
|
+
required: ["columnId", "name"],
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "forge_delete_column",
|
|
572
|
+
description:
|
|
573
|
+
"Delete a column. If the column has tasks, you must specify where to move them.",
|
|
574
|
+
inputSchema: {
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: {
|
|
577
|
+
columnId: { type: "string", description: "Column ID to delete" },
|
|
578
|
+
moveTasksTo: {
|
|
579
|
+
type: "string",
|
|
580
|
+
description:
|
|
581
|
+
"Column ID to move existing tasks to (required if column has tasks)",
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
required: ["columnId"],
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "forge_reorder_columns",
|
|
589
|
+
description:
|
|
590
|
+
"Reorder columns in a project by providing the column IDs in the desired order.",
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: {
|
|
594
|
+
projectId: { type: "string", description: "Project ID" },
|
|
595
|
+
columnIds: {
|
|
596
|
+
type: "array",
|
|
597
|
+
items: { type: "string" },
|
|
598
|
+
description: "Array of column IDs in the desired order",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
required: ["projectId", "columnIds"],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
// Tasks
|
|
606
|
+
{
|
|
607
|
+
name: "forge_get_task",
|
|
608
|
+
description: "Get a single task by ID with all its details.",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
properties: {
|
|
612
|
+
taskId: { type: "string", description: "Task ID" },
|
|
613
|
+
},
|
|
614
|
+
required: ["taskId"],
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: "forge_list_tasks",
|
|
619
|
+
description: "List all tasks in a project (from all columns).",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: "object",
|
|
622
|
+
properties: {
|
|
623
|
+
projectId: { type: "string", description: "Project ID" },
|
|
624
|
+
},
|
|
625
|
+
required: ["projectId"],
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "forge_search_tasks",
|
|
630
|
+
description:
|
|
631
|
+
"Search tasks by title, description, or ticket ID (e.g., '#123' or 'PROJ-123').",
|
|
632
|
+
inputSchema: {
|
|
633
|
+
type: "object",
|
|
634
|
+
properties: {
|
|
635
|
+
query: { type: "string", description: "Search query" },
|
|
636
|
+
projectId: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Optional: limit search to a specific project",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
required: ["query"],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "forge_create_task",
|
|
646
|
+
description: "Create a new task in a project column.",
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: "object",
|
|
649
|
+
properties: {
|
|
650
|
+
projectId: { type: "string", description: "Project ID" },
|
|
651
|
+
columnId: {
|
|
652
|
+
type: "string",
|
|
653
|
+
description: "Column ID to place the task in",
|
|
654
|
+
},
|
|
655
|
+
title: { type: "string", description: "Task title" },
|
|
656
|
+
description: {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "Task description (supports markdown)",
|
|
659
|
+
},
|
|
660
|
+
type: {
|
|
661
|
+
type: "string",
|
|
662
|
+
enum: ["TASK", "BUG", "STORY"],
|
|
663
|
+
description: "Task type (default: TASK)",
|
|
664
|
+
},
|
|
665
|
+
priority: {
|
|
666
|
+
type: "string",
|
|
667
|
+
enum: ["LOW", "MEDIUM", "HIGH", "URGENT"],
|
|
668
|
+
description: "Priority level (default: MEDIUM)",
|
|
669
|
+
},
|
|
670
|
+
assigneeId: {
|
|
671
|
+
type: "string",
|
|
672
|
+
description: "User ID to assign the task to",
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
required: ["projectId", "columnId", "title"],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
name: "forge_update_task",
|
|
680
|
+
description:
|
|
681
|
+
"Update a task. Use columnId to move it to a different column.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
type: "object",
|
|
684
|
+
properties: {
|
|
685
|
+
taskId: { type: "string", description: "Task ID" },
|
|
686
|
+
title: { type: "string", description: "New title" },
|
|
687
|
+
description: { type: "string", description: "New description" },
|
|
688
|
+
columnId: { type: "string", description: "Move to this column ID" },
|
|
689
|
+
assigneeId: {
|
|
690
|
+
type: "string",
|
|
691
|
+
description: "Assign to this user ID (use null to unassign)",
|
|
692
|
+
},
|
|
693
|
+
priority: {
|
|
694
|
+
type: "string",
|
|
695
|
+
enum: ["LOW", "MEDIUM", "HIGH", "URGENT"],
|
|
696
|
+
description: "New priority",
|
|
697
|
+
},
|
|
698
|
+
type: {
|
|
699
|
+
type: "string",
|
|
700
|
+
enum: ["TASK", "BUG", "STORY"],
|
|
701
|
+
description: "New type",
|
|
702
|
+
},
|
|
703
|
+
prUrl: { type: "string", description: "Link to a Pull Request" },
|
|
704
|
+
prNumber: { type: "number", description: "PR number" },
|
|
705
|
+
prRepo: {
|
|
706
|
+
type: "string",
|
|
707
|
+
description: "PR repository (e.g., 'XferOps/nexus')",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
required: ["taskId"],
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: "forge_move_task",
|
|
715
|
+
description:
|
|
716
|
+
"Move a task to a different column. Convenience wrapper around forge_update_task.",
|
|
717
|
+
inputSchema: {
|
|
718
|
+
type: "object",
|
|
719
|
+
properties: {
|
|
720
|
+
taskId: { type: "string", description: "Task ID to move" },
|
|
721
|
+
columnId: { type: "string", description: "Target column ID" },
|
|
722
|
+
},
|
|
723
|
+
required: ["taskId", "columnId"],
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: "forge_delete_task",
|
|
728
|
+
description:
|
|
729
|
+
"Delete a task and all its comments. This cannot be undone!",
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: "object",
|
|
732
|
+
properties: {
|
|
733
|
+
taskId: { type: "string", description: "Task ID to delete" },
|
|
734
|
+
},
|
|
735
|
+
required: ["taskId"],
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
name: "forge_reorder_task",
|
|
740
|
+
description:
|
|
741
|
+
"Move a task to a specific position within a column (for drag-and-drop reordering).",
|
|
742
|
+
inputSchema: {
|
|
743
|
+
type: "object",
|
|
744
|
+
properties: {
|
|
745
|
+
taskId: { type: "string", description: "Task ID to move" },
|
|
746
|
+
columnId: { type: "string", description: "Target column ID" },
|
|
747
|
+
position: {
|
|
748
|
+
type: "number",
|
|
749
|
+
description: "Target position (0-indexed)",
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
required: ["taskId", "columnId", "position"],
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
// Comments
|
|
757
|
+
{
|
|
758
|
+
name: "forge_list_comments",
|
|
759
|
+
description: "List all comments on a task.",
|
|
760
|
+
inputSchema: {
|
|
761
|
+
type: "object",
|
|
762
|
+
properties: {
|
|
763
|
+
taskId: { type: "string", description: "Task ID" },
|
|
764
|
+
},
|
|
765
|
+
required: ["taskId"],
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "forge_create_comment",
|
|
770
|
+
description: "Add a comment to a task.",
|
|
771
|
+
inputSchema: {
|
|
772
|
+
type: "object",
|
|
773
|
+
properties: {
|
|
774
|
+
taskId: { type: "string", description: "Task ID" },
|
|
775
|
+
content: {
|
|
776
|
+
type: "string",
|
|
777
|
+
description: "Comment content (supports markdown)",
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
required: ["taskId", "content"],
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "forge_update_comment",
|
|
785
|
+
description: "Update a comment you authored.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {
|
|
789
|
+
commentId: { type: "string", description: "Comment ID" },
|
|
790
|
+
content: { type: "string", description: "New comment content" },
|
|
791
|
+
},
|
|
792
|
+
required: ["commentId", "content"],
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "forge_delete_comment",
|
|
797
|
+
description: "Delete a comment you authored.",
|
|
798
|
+
inputSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
commentId: { type: "string", description: "Comment ID" },
|
|
802
|
+
},
|
|
803
|
+
required: ["commentId"],
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
];
|
|
807
|
+
|
|
808
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
809
|
+
|
|
810
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
811
|
+
// Tool Call Handler
|
|
812
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
813
|
+
|
|
814
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
815
|
+
const { name, arguments: args } = request.params;
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
let result: unknown;
|
|
819
|
+
|
|
820
|
+
switch (name) {
|
|
821
|
+
// Health
|
|
822
|
+
case "forge_health_check":
|
|
823
|
+
result = await healthCheck();
|
|
824
|
+
break;
|
|
825
|
+
|
|
826
|
+
// Teams
|
|
827
|
+
case "forge_list_teams":
|
|
828
|
+
result = await listTeams();
|
|
829
|
+
break;
|
|
830
|
+
case "forge_create_team":
|
|
831
|
+
result = await createTeam(args?.name as string);
|
|
832
|
+
break;
|
|
833
|
+
case "forge_list_team_members":
|
|
834
|
+
result = await listTeamMembers(args?.teamId as string);
|
|
835
|
+
break;
|
|
836
|
+
case "forge_add_team_member":
|
|
837
|
+
result = await addTeamMember(
|
|
838
|
+
args?.teamId as string,
|
|
839
|
+
args?.email as string,
|
|
840
|
+
args?.role as string | undefined
|
|
841
|
+
);
|
|
842
|
+
break;
|
|
843
|
+
|
|
844
|
+
// Projects
|
|
845
|
+
case "forge_list_projects":
|
|
846
|
+
result = await listProjects(args?.teamId as string);
|
|
847
|
+
break;
|
|
848
|
+
case "forge_get_project":
|
|
849
|
+
result = await getProject(args?.projectId as string);
|
|
850
|
+
break;
|
|
851
|
+
case "forge_create_project":
|
|
852
|
+
result = await createProject(
|
|
853
|
+
args?.teamId as string,
|
|
854
|
+
args?.name as string,
|
|
855
|
+
args?.prefix as string | undefined
|
|
856
|
+
);
|
|
857
|
+
break;
|
|
858
|
+
case "forge_update_project":
|
|
859
|
+
result = await updateProject(args?.projectId as string, {
|
|
860
|
+
name: args?.name as string | undefined,
|
|
861
|
+
prefix: args?.prefix as string | undefined,
|
|
862
|
+
});
|
|
863
|
+
break;
|
|
864
|
+
case "forge_delete_project":
|
|
865
|
+
result = await deleteProject(args?.projectId as string);
|
|
866
|
+
break;
|
|
867
|
+
|
|
868
|
+
// Columns
|
|
869
|
+
case "forge_create_column":
|
|
870
|
+
result = await createColumn(
|
|
871
|
+
args?.projectId as string,
|
|
872
|
+
args?.name as string
|
|
873
|
+
);
|
|
874
|
+
break;
|
|
875
|
+
case "forge_update_column":
|
|
876
|
+
result = await updateColumn(args?.columnId as string, {
|
|
877
|
+
name: args?.name as string | undefined,
|
|
878
|
+
});
|
|
879
|
+
break;
|
|
880
|
+
case "forge_delete_column":
|
|
881
|
+
result = await deleteColumn(
|
|
882
|
+
args?.columnId as string,
|
|
883
|
+
args?.moveTasksTo as string | undefined
|
|
884
|
+
);
|
|
885
|
+
break;
|
|
886
|
+
case "forge_reorder_columns":
|
|
887
|
+
result = await reorderColumns(
|
|
888
|
+
args?.projectId as string,
|
|
889
|
+
args?.columnIds as string[]
|
|
890
|
+
);
|
|
891
|
+
break;
|
|
892
|
+
|
|
893
|
+
// Tasks
|
|
894
|
+
case "forge_get_task":
|
|
895
|
+
result = await getTask(args?.taskId as string);
|
|
896
|
+
break;
|
|
897
|
+
case "forge_list_tasks":
|
|
898
|
+
result = await listTasks(args?.projectId as string);
|
|
899
|
+
break;
|
|
900
|
+
case "forge_search_tasks":
|
|
901
|
+
result = await searchTasks(
|
|
902
|
+
args?.query as string,
|
|
903
|
+
args?.projectId as string | undefined
|
|
904
|
+
);
|
|
905
|
+
break;
|
|
906
|
+
case "forge_create_task":
|
|
907
|
+
result = await createTask({
|
|
908
|
+
projectId: args?.projectId as string,
|
|
909
|
+
columnId: args?.columnId as string,
|
|
910
|
+
title: args?.title as string,
|
|
911
|
+
description: args?.description as string | undefined,
|
|
912
|
+
type: args?.type as "TASK" | "BUG" | "STORY" | undefined,
|
|
913
|
+
priority: args?.priority as
|
|
914
|
+
| "LOW"
|
|
915
|
+
| "MEDIUM"
|
|
916
|
+
| "HIGH"
|
|
917
|
+
| "URGENT"
|
|
918
|
+
| undefined,
|
|
919
|
+
assigneeId: args?.assigneeId as string | undefined,
|
|
920
|
+
});
|
|
921
|
+
break;
|
|
922
|
+
case "forge_update_task":
|
|
923
|
+
result = await updateTask(args?.taskId as string, {
|
|
924
|
+
title: args?.title as string | undefined,
|
|
925
|
+
description: args?.description as string | undefined,
|
|
926
|
+
columnId: args?.columnId as string | undefined,
|
|
927
|
+
assigneeId: args?.assigneeId as string | null | undefined,
|
|
928
|
+
priority: args?.priority as
|
|
929
|
+
| "LOW"
|
|
930
|
+
| "MEDIUM"
|
|
931
|
+
| "HIGH"
|
|
932
|
+
| "URGENT"
|
|
933
|
+
| undefined,
|
|
934
|
+
type: args?.type as "TASK" | "BUG" | "STORY" | undefined,
|
|
935
|
+
prUrl: args?.prUrl as string | null | undefined,
|
|
936
|
+
prNumber: args?.prNumber as number | null | undefined,
|
|
937
|
+
prRepo: args?.prRepo as string | null | undefined,
|
|
938
|
+
});
|
|
939
|
+
break;
|
|
940
|
+
case "forge_move_task":
|
|
941
|
+
result = await moveTask(
|
|
942
|
+
args?.taskId as string,
|
|
943
|
+
args?.columnId as string
|
|
944
|
+
);
|
|
945
|
+
break;
|
|
946
|
+
case "forge_delete_task":
|
|
947
|
+
result = await deleteTask(args?.taskId as string);
|
|
948
|
+
break;
|
|
949
|
+
case "forge_reorder_task":
|
|
950
|
+
result = await reorderTask(
|
|
951
|
+
args?.taskId as string,
|
|
952
|
+
args?.columnId as string,
|
|
953
|
+
args?.position as number
|
|
954
|
+
);
|
|
955
|
+
break;
|
|
956
|
+
|
|
957
|
+
// Comments
|
|
958
|
+
case "forge_list_comments":
|
|
959
|
+
result = await listComments(args?.taskId as string);
|
|
960
|
+
break;
|
|
961
|
+
case "forge_create_comment":
|
|
962
|
+
result = await createComment(
|
|
963
|
+
args?.taskId as string,
|
|
964
|
+
args?.content as string
|
|
965
|
+
);
|
|
966
|
+
break;
|
|
967
|
+
case "forge_update_comment":
|
|
968
|
+
result = await updateComment(
|
|
969
|
+
args?.commentId as string,
|
|
970
|
+
args?.content as string
|
|
971
|
+
);
|
|
972
|
+
break;
|
|
973
|
+
case "forge_delete_comment":
|
|
974
|
+
result = await deleteComment(args?.commentId as string);
|
|
975
|
+
break;
|
|
976
|
+
|
|
977
|
+
default:
|
|
978
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return {
|
|
982
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
983
|
+
};
|
|
984
|
+
} catch (error) {
|
|
985
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
986
|
+
return {
|
|
987
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
988
|
+
isError: true,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
994
|
+
// Start Server
|
|
995
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
996
|
+
|
|
997
|
+
async function main() {
|
|
998
|
+
const transport = new StdioServerTransport();
|
|
999
|
+
await server.connect(transport);
|
|
1000
|
+
console.error("⚒️ Forge MCP server running on stdio");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
main().catch((error) => {
|
|
1004
|
+
console.error("Failed to start Forge MCP server:", error);
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
});
|