/**
* Developed by eBrook Group.
* Copyright © 2026 eBrook Group (https://www.ebrook.com.tw)
*/
/**
* MCP Server setup and tool registration
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { getConfig } from "./config/env.js";
import { ClickUpService } from "./services/clickup.js";
import { handleGetTaskDetails } from "./tools/get-task-details.js";
import { handleAnalyzeWorkflow } from "./tools/analyze-workflow.js";
import { handleUpdateStatus } from "./tools/update-status.js";
import { handleUpdateGithubBranch } from "./tools/update-github-branch.js";
import { handleAddComment } from "./tools/add-comment.js";
import { debug, info } from "./utils/logger.js";
/**
* Start the MCP server
* @returns Promise that resolves when server is started
*/
export async function startServer(): Promise<void> {
// Load configuration
const config = getConfig();
// Initialize services
const clickUpService = new ClickUpService(config);
// Create MCP server instance
const server = new McpServer({
name: "clickup-mcp",
version: "3.1.0",
});
// Register the get_task_details tool
server.tool(
"get_task_details",
"Get detailed information about a ClickUp task, including status, assignees, tags, custom fields, and more.",
{
task_id: z
.string()
.min(1, "Task ID cannot be empty")
.max(100, "Task ID exceeds maximum length of 100 characters")
.describe("ClickUp task ID (can be custom ID like ST2IN1-123 or numeric ID)"),
},
async (args) => {
debug(`Tool 'get_task_details' called with args: ${JSON.stringify(args)}`);
const taskId = args.task_id;
if (!taskId || typeof taskId !== "string" || taskId.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: task_id is required and cannot be empty.",
},
],
isError: true,
};
}
return await handleGetTaskDetails(taskId.trim(), config, clickUpService);
}
);
// Register the analyze_workflow tool
server.tool(
"analyze_workflow",
"Analyze a ClickUp task's workflow, including time tracking, dependencies, risk factors, and provide recommendations for improvement.",
{
task_id: z
.string()
.min(1, "Task ID cannot be empty")
.max(100, "Task ID exceeds maximum length of 100 characters")
.describe("ClickUp task ID (can be custom ID like ST2IN1-123 or numeric ID)"),
},
async (args) => {
debug(`Tool 'analyze_workflow' called with args: ${JSON.stringify(args)}`);
const taskId = args.task_id;
if (!taskId || typeof taskId !== "string" || taskId.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: task_id is required and cannot be empty.",
},
],
isError: true,
};
}
return await handleAnalyzeWorkflow(taskId.trim(), config, clickUpService);
}
);
// Register the update_status tool
server.tool(
"update_status",
"Update the status of a ClickUp task.",
{
task_id: z
.string()
.min(1, "Task ID cannot be empty")
.max(100, "Task ID exceeds maximum length of 100 characters")
.describe("ClickUp task ID (can be custom ID like ST2IN1-123 or numeric ID)"),
status: z
.string()
.min(1, "Status cannot be empty")
.max(100, "Status exceeds maximum length of 100 characters")
.regex(/^[a-z0-9_\s]+$/i, "Status contains invalid characters")
.describe("The new status value to set for the task"),
},
async (args) => {
debug(`Tool 'update_status' called with args: ${JSON.stringify(args)}`);
const taskId = args.task_id;
const status = args.status;
if (!taskId || typeof taskId !== "string" || taskId.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: task_id is required and cannot be empty.",
},
],
isError: true,
};
}
if (!status || typeof status !== "string" || status.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: status is required and cannot be empty.",
},
],
isError: true,
};
}
return await handleUpdateStatus(taskId.trim(), status.trim(), config, clickUpService);
}
);
// Register the update_github_branch tool
server.tool(
"update_github_branch",
"Update the GitHub Branch custom field of a ClickUp task.",
{
task_id: z
.string()
.min(1, "Task ID cannot be empty")
.max(100, "Task ID exceeds maximum length of 100 characters")
.describe("ClickUp task ID (can be custom ID like ST2IN1-123 or numeric ID)"),
branch_name: z
.string()
.min(1, "Branch name cannot be empty")
.max(255, "Branch name exceeds maximum length of 255 characters")
.regex(/^[a-zA-Z0-9/_-]+$/, "Branch name contains invalid characters")
.describe("The GitHub branch name to set"),
},
async (args) => {
debug(`Tool 'update_github_branch' called with args: ${JSON.stringify(args)}`);
const taskId = args.task_id;
const branchName = args.branch_name;
if (!taskId || typeof taskId !== "string" || taskId.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: task_id is required and cannot be empty.",
},
],
isError: true,
};
}
if (!branchName || typeof branchName !== "string" || branchName.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: branch_name is required and cannot be empty.",
},
],
isError: true,
};
}
return await handleUpdateGithubBranch(taskId.trim(), branchName.trim(), config, clickUpService);
}
);
// Register the add_comment tool
server.tool(
"add_comment",
"Add a comment to a ClickUp task.",
{
task_id: z
.string()
.min(1, "Task ID cannot be empty")
.max(100, "Task ID exceeds maximum length of 100 characters")
.describe("ClickUp task ID (can be custom ID like ST2IN1-123 or numeric ID)"),
comment_text: z
.string()
.min(1, "Comment cannot be empty")
.max(10000, "Comment exceeds maximum length of 10000 characters")
.describe("The comment text to add to the task"),
},
async (args) => {
debug(`Tool 'add_comment' called with args: ${JSON.stringify(args)}`);
const taskId = args.task_id;
const commentText = args.comment_text;
if (!taskId || typeof taskId !== "string" || taskId.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: task_id is required and cannot be empty.",
},
],
isError: true,
};
}
if (!commentText || typeof commentText !== "string" || commentText.trim().length === 0) {
return {
content: [
{
type: "text",
text: "Error: comment_text is required and cannot be empty.",
},
],
isError: true,
};
}
return await handleAddComment(taskId.trim(), commentText.trim(), config, clickUpService);
}
);
// Connect server to transport
const transport = new StdioServerTransport();
await server.connect(transport);
info("MCP ClickUp server started with 5 tools:");
info(" - get_task_details: View detailed task information");
info(" - analyze_workflow: Analyze task workflow and get insights");
info(" - update_status: Update task status");
info(" - update_github_branch: Update GitHub Branch field");
info(" - add_comment: Add a comment to a task");
}