@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/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
+ });