import "dotenv/config";
import { loadPromptFromTemplate } from "./prompts/loader.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import {
CallToolRequest,
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import express, { Request, Response, NextFunction } from "express";
import getPort from "get-port";
import path from "path";
import fs from "fs";
import fsPromises from "fs/promises";
import { fileURLToPath } from "url";
// 導入所有工具函數和 schema
import {
planTask,
planTaskSchema,
analyzeTask,
analyzeTaskSchema,
reflectTask,
reflectTaskSchema,
splitTasks,
splitTasksSchema,
splitTasksRaw,
splitTasksRawSchema,
listTasksSchema,
listTasks,
executeTask,
executeTaskSchema,
verifyTask,
verifyTaskSchema,
deleteTask,
deleteTaskSchema,
clearAllTasks,
clearAllTasksSchema,
updateTaskContent,
updateTaskContentSchema,
queryTask,
queryTaskSchema,
getTaskDetail,
getTaskDetailSchema,
processThought,
processThoughtSchema,
initProjectRules,
initProjectRulesSchema,
researchMode,
researchModeSchema,
} from "./tools/index.js";
// Pre-defined tool definitions for fast Smithery scanning
const TOOLS_DEFINITIONS = [
{
name: "plan_task",
description: "Plan tasks with step-by-step guidance and complexity analysis",
inputSchema: zodToJsonSchema(planTaskSchema),
},
{
name: "analyze_task",
description: "Analyze task requirements, constraints and complexity",
inputSchema: zodToJsonSchema(analyzeTaskSchema),
},
{
name: "reflect_task",
description: "Reflect on task solutions and optimize approaches",
inputSchema: zodToJsonSchema(reflectTaskSchema),
},
{
name: "split_tasks",
description: "Split complex tasks into manageable subtasks",
inputSchema: zodToJsonSchema(splitTasksSchema),
},
{
name: "split_tasks_raw",
description: "Raw task splitting without analysis",
inputSchema: zodToJsonSchema(splitTasksRawSchema),
},
{
name: "list_tasks",
description: "List all current tasks with status and details",
inputSchema: zodToJsonSchema(listTasksSchema),
},
{
name: "execute_task",
description: "Execute a specific task with AI guidance",
inputSchema: zodToJsonSchema(executeTaskSchema),
},
{
name: "verify_task",
description: "Verify task completion and quality",
inputSchema: zodToJsonSchema(verifyTaskSchema),
},
{
name: "delete_task",
description: "Delete a task from the system",
inputSchema: zodToJsonSchema(deleteTaskSchema),
},
{
name: "clear_all_tasks",
description: "Clear all tasks with backup to memory",
inputSchema: zodToJsonSchema(clearAllTasksSchema),
},
{
name: "update_task",
description: "Update task content and related files",
inputSchema: zodToJsonSchema(updateTaskContentSchema),
},
{
name: "query_task",
description: "Search and query tasks by keywords",
inputSchema: zodToJsonSchema(queryTaskSchema),
},
{
name: "get_task_detail",
description: "Get detailed information about a specific task",
inputSchema: zodToJsonSchema(getTaskDetailSchema),
},
{
name: "process_thought",
description: "Process and organize thought patterns",
inputSchema: zodToJsonSchema(processThoughtSchema),
},
{
name: "init_project_rules",
description: "Initialize project-specific rules and guidelines",
inputSchema: zodToJsonSchema(initProjectRulesSchema),
},
{
name: "research_mode",
description: "Enter research mode for information gathering",
inputSchema: zodToJsonSchema(researchModeSchema),
},
];
// Check and initialize environment
async function initializeEnvironment() {
// Check for required environment variables
if (!process.env.GITHUB_TOKEN) {
console.error('❌ Missing required environment variable: GITHUB_TOKEN');
console.log('Please set your GitHub Personal Access Token with gist permissions');
console.log('Get one at: https://github.com/settings/tokens');
console.log('Note: This will be provided by Smithery during deployment');
throw new Error('Missing GITHUB_TOKEN environment variable');
}
if (!process.env.GIST_ID) {
console.error('❌ Missing required environment variable: GIST_ID');
console.log('Please create a gist and set the GIST_ID from the URL');
console.log('Example: https://gist.github.com/username/GIST_ID_HERE');
console.log('Note: This will be provided by Smithery during deployment');
throw new Error('Missing GIST_ID environment variable');
}
// Initialize gist if needed
try {
const response = await fetch(`https://api.github.com/gists/${process.env.GIST_ID}`, {
headers: {
'Authorization': `token ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
},
});
if (!response.ok) {
throw new Error(`Cannot access gist: ${response.status}`);
}
const gist = await response.json();
const filename = process.env.GIST_FILENAME || 'tasks.json';
// Create initial file if it doesn't exist
if (!gist.files[filename]) {
console.log('🔧 Creating initial task file in gist...');
await fetch(`https://api.github.com/gists/${process.env.GIST_ID}`, {
method: 'PATCH',
headers: {
'Authorization': `token ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
files: {
[filename]: {
content: JSON.stringify({ tasks: [] }, null, 2)
}
}
})
});
console.log('✅ Initial task file created');
}
console.log('✅ Gist connection successful');
console.log(`📄 Using file: ${filename}`);
console.log(`🔗 Gist URL: ${gist.html_url}`);
} catch (error) {
console.error('❌ Failed to initialize gist:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
async function main() {
try {
// Handle Smithery configuration through environment variables
const config = {
githubToken: process.env.SMITHERY_CONFIG_GITHUB_TOKEN || process.env.GITHUB_TOKEN,
gistId: process.env.SMITHERY_CONFIG_GIST_ID || process.env.GIST_ID,
gistFilename: process.env.SMITHERY_CONFIG_GIST_FILENAME || process.env.GIST_FILENAME || 'tasks.json',
templatesUse: process.env.SMITHERY_CONFIG_TEMPLATES_USE || process.env.TEMPLATES_USE || 'en',
enableGui: (process.env.SMITHERY_CONFIG_ENABLE_GUI || process.env.ENABLE_GUI) === 'true'
};
// Set environment variables from config
if (config.githubToken) process.env.GITHUB_TOKEN = config.githubToken;
if (config.gistId) process.env.GIST_ID = config.gistId;
if (config.gistFilename) process.env.GIST_FILENAME = config.gistFilename;
if (config.templatesUse) process.env.TEMPLATES_USE = config.templatesUse;
if (config.enableGui) process.env.ENABLE_GUI = config.enableGui.toString();
// Initialize environment first (skip if no env vars for build)
if (process.env.GITHUB_TOKEN && process.env.GIST_ID) {
await initializeEnvironment();
} else {
console.log('⚠️ Environment variables not set - running in configuration mode');
console.log('This is normal during Docker build or initial setup');
}
const ENABLE_GUI = process.env.ENABLE_GUI === "true";
if (ENABLE_GUI) {
// 創建 Express 應用
const app = express();
// 儲存 SSE 客戶端的列表
let sseClients: Response[] = [];
// 發送 SSE 事件的輔助函數
function sendSseUpdate() {
sseClients.forEach((client) => {
// 檢查客戶端是否仍然連接
if (!client.writableEnded) {
client.write(
`event: update\ndata: ${JSON.stringify({
timestamp: Date.now(),
})}\n\n`
);
}
});
// 清理已斷開的客戶端 (可選,但建議)
sseClients = sseClients.filter((client) => !client.writableEnded);
}
// 設置靜態文件目錄
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const publicPath = path.join(__dirname, "public");
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "data");
const TASKS_FILE_PATH = path.join(DATA_DIR, "tasks.json"); // 提取檔案路徑
app.use(express.static(publicPath));
// 設置 API 路由
app.get("/api/tasks", async (req: Request, res: Response) => {
try {
// 使用 fsPromises 保持異步讀取
const tasksData = await fsPromises.readFile(TASKS_FILE_PATH, "utf-8");
res.json(JSON.parse(tasksData));
} catch (error) {
// 確保檔案不存在時返回空任務列表
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
res.json({ tasks: [] });
} else {
res.status(500).json({ error: "Failed to read tasks data" });
}
}
});
// 新增:SSE 端點
app.get("/api/tasks/stream", (req: Request, res: Response) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
// 可選: CORS 頭,如果前端和後端不在同一個 origin
// "Access-Control-Allow-Origin": "*",
});
// 發送一個初始事件或保持連接
res.write("data: connected\n\n");
// 將客戶端添加到列表
sseClients.push(res);
// 當客戶端斷開連接時,將其從列表中移除
req.on("close", () => {
sseClients = sseClients.filter((client) => client !== res);
});
});
// 獲取可用埠
const port = await getPort();
// 啟動 HTTP 伺服器
const httpServer = app.listen(port, () => {
// 在伺服器啟動後開始監聽檔案變化
try {
// 檢查檔案是否存在,如果不存在則不監聽 (避免 watch 報錯)
if (fs.existsSync(TASKS_FILE_PATH)) {
fs.watch(TASKS_FILE_PATH, (eventType, filename) => {
if (
filename &&
(eventType === "change" || eventType === "rename")
) {
// 稍微延遲發送,以防短時間內多次觸發 (例如編輯器保存)
// debounce sendSseUpdate if needed
sendSseUpdate();
}
});
}
} catch (watchError) {}
});
// 將 URL 寫入 WebGUI.md
try {
// 讀取 TEMPLATES_USE 環境變數並轉換為語言代碼
const templatesUse = process.env.TEMPLATES_USE || "en";
const getLanguageFromTemplate = (template: string): string => {
if (template === "zh") return "zh-TW";
if (template === "en") return "en";
// 自訂範本預設使用英文
return "en";
};
const language = getLanguageFromTemplate(templatesUse);
const websiteUrl = `[Task Manager UI](http://localhost:${port}?lang=${language})`;
const websiteFilePath = path.join(DATA_DIR, "WebGUI.md");
await fsPromises.writeFile(websiteFilePath, websiteUrl, "utf-8");
} catch (error) {}
// 設置進程終止事件處理 (確保移除 watcher)
const shutdownHandler = async () => {
// 關閉所有 SSE 連接
sseClients.forEach((client) => client.end());
sseClients = [];
// 關閉 HTTP 伺服器
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
process.exit(0);
};
process.on("SIGINT", shutdownHandler);
process.on("SIGTERM", shutdownHandler);
}
// 創建MCP服務器
const server = new Server(
{
name: "Gist Task Manager MCP",
version: "1.0.21",
},
{
capabilities: {
tools: {},
},
}
);
// Fast tool listing for Smithery - no file I/O!
server.setRequestHandler(ListToolsRequestSchema, async () => {
try {
return {
tools: TOOLS_DEFINITIONS,
};
} catch (error) {
console.error('Tool listing failed:', error);
return { tools: [] };
}
});
server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest) => {
try {
if (!request.params.arguments) {
throw new Error("No arguments provided");
}
let parsedArgs;
switch (request.params.name) {
case "plan_task":
parsedArgs = await planTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await planTask(parsedArgs.data);
case "analyze_task":
parsedArgs = await analyzeTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await analyzeTask(parsedArgs.data);
case "reflect_task":
parsedArgs = await reflectTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await reflectTask(parsedArgs.data);
case "split_tasks":
parsedArgs = await splitTasksRawSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await splitTasksRaw(parsedArgs.data);
case "list_tasks":
parsedArgs = await listTasksSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await listTasks(parsedArgs.data);
case "execute_task":
parsedArgs = await executeTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await executeTask(parsedArgs.data);
case "verify_task":
parsedArgs = await verifyTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await verifyTask(parsedArgs.data);
case "delete_task":
parsedArgs = await deleteTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await deleteTask(parsedArgs.data);
case "clear_all_tasks":
parsedArgs = await clearAllTasksSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await clearAllTasks(parsedArgs.data);
case "update_task":
parsedArgs = await updateTaskContentSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await updateTaskContent(parsedArgs.data);
case "query_task":
parsedArgs = await queryTaskSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await queryTask(parsedArgs.data);
case "get_task_detail":
parsedArgs = await getTaskDetailSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await getTaskDetail(parsedArgs.data);
case "process_thought":
parsedArgs = await processThoughtSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await processThought(parsedArgs.data);
case "init_project_rules":
return await initProjectRules();
case "research_mode":
parsedArgs = await researchModeSchema.safeParseAsync(
request.params.arguments
);
if (!parsedArgs.success) {
throw new Error(
`Invalid arguments for tool ${request.params.name}: ${parsedArgs.error.message}`
);
}
return await researchMode(parsedArgs.data);
default:
throw new Error(`Tool ${request.params.name} does not exist`);
}
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error occurred: ${errorMsg} \n Please try correcting the error and calling the tool again`,
},
],
};
}
}
);
// 建立連接
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (error) {
process.exit(1);
}
}
main().catch(console.error);