UNPKG

schedule-task-mcp

Version:

MCP server for scheduled task management and execution with support for interval, cron, and date-based triggers

1,094 lines (983 loc) 34.8 kB
#!/usr/bin/env node /** * Schedule Task MCP Server * A universal scheduled task management MCP server */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'; import * as path from 'path'; import * as os from 'os'; import { TaskScheduler } from './scheduler.js'; import * as cron from 'node-cron'; import { formatInTimezone, getSystemTimeZone } from './format.js'; // Default database path and timezone const DEFAULT_DB_PATH = path.join(os.homedir(), '.schedule-task-mcp', 'tasks.db'); const DB_PATH = process.env.SCHEDULE_TASK_DB_PATH || DEFAULT_DB_PATH; const TIMEZONE = process.env.SCHEDULE_TASK_TIMEZONE || getSystemTimeZone() || 'Asia/Shanghai'; type DescribedTask = ReturnType<TaskScheduler['describeTask']>; function resolveTaskTitle(task: DescribedTask): string { const prompt = typeof task.agent_prompt === 'string' ? task.agent_prompt.trim() : ''; if (prompt.length > 0) { return truncateText(prompt, 80); } if (task.mcp_server && task.mcp_tool) { return `${task.mcp_server}.${task.mcp_tool}`; } return task.id; } function truncateText(value: string, limit = 160): string { if (value.length <= limit) { return value; } return `${value.slice(0, limit - 3)}...`; } function formatTimestamp(localValue?: string, rawValue?: string, fallback = '暂无记录'): string { return localValue ?? rawValue ?? fallback; } function humanReadableStatus(status?: string): string { const mapping: Record<string, string> = { scheduled: '已计划', running: '执行中', paused: '已暂停', completed: '已完成', error: '执行失败' }; return status ? `${mapping[status] ?? status} (${status})` : '未知'; } function humanReadableLastStatus(status?: string | null): string { if (!status) return '暂无'; const mapping: Record<string, string> = { success: '成功', error: '失败', running: '执行中' }; return `${mapping[status] ?? status} (${status})`; } function describeTrigger(task: DescribedTask): string { const summary = task.trigger_summary ? `(${task.trigger_summary})` : ''; switch (task.trigger_type) { case 'interval': return `间隔任务${summary || '(未配置详细间隔)'}`; case 'cron': return `Cron 表达式${summary || ''}`; case 'date': return `一次性任务${summary || ''}`; default: return task.trigger_type; } } function buildTaskSummary(task: DescribedTask, actionLabel: string): string { const nextRunLabel = formatTimestamp(task.next_run_local, task.next_run, '尚未安排(可能已执行完毕或已停用)'); const lastRunLabel = formatTimestamp(task.last_run_local, task.last_run); const createdLabel = formatTimestamp(task.created_at_local, task.created_at, '未知'); const updatedLabel = formatTimestamp(task.updated_at_local, task.updated_at, '未知'); const taskTitle = resolveTaskTitle(task); const historyCount = Array.isArray(task.history) ? task.history.length : 0; const latestHistory = historyCount > 0 ? task.history![0] : undefined; const latestHistoryTime = latestHistory ? formatTimestamp(latestHistory.run_at_local, latestHistory.run_at) : undefined; const latestHistoryMessage = latestHistory?.message ? truncateText(latestHistory.message) : undefined; const triggerConfig = task.trigger_config ?? {}; const triggerConfigLocal = (task as any).trigger_config_local ?? {}; const runDateLocal = triggerConfigLocal.run_date_local ?? (triggerConfig.run_date ? formatTimestamp(undefined, triggerConfig.run_date) : undefined); const detailLines = [ `任务「${taskTitle}」已${actionLabel}:`, `- **任务ID**:${task.id}`, `- **触发类型**:${describeTrigger(task)}`, task.trigger_type === 'cron' && triggerConfig.expression ? `- **Cron 表达式**:${triggerConfig.expression}` : null, task.trigger_type === 'interval' ? `- **间隔配置**:${JSON.stringify(triggerConfig)}` : null, task.trigger_type === 'date' ? `- **执行时间**:${runDateLocal ?? '未指定'}` : null, task.agent_prompt ? `- **任务指令**:${task.agent_prompt}` : null, task.mcp_server && task.mcp_tool ? `- **Legacy MCP 调用**:${task.mcp_server}.${task.mcp_tool}` : null, `- **任务状态**:${humanReadableStatus(task.status)}`, `- **是否启用**:${task.enabled ? '是 (enabled)' : '否 (disabled)'}`, `- **创建时间**:${createdLabel}`, `- **最后更新时间**:${updatedLabel}`, `- **上次执行时间**:${lastRunLabel}`, `- **上次执行状态**:${humanReadableLastStatus(task.last_status)}`, latestHistoryMessage ? `- **上次执行消息**:${latestHistoryMessage}` : null, `- **下次执行时间**:${nextRunLabel}`, `- **历史记录条数**:${historyCount}`, latestHistoryTime ? `- **最近历史时间**:${latestHistoryTime}` : null ].filter(Boolean) as string[]; return detailLines.join('\n'); } function formatTaskResponse(task: DescribedTask, actionLabel: string, extra: Record<string, any> = {}) { return { success: true, action: actionLabel, summary: buildTaskSummary(task, actionLabel), detail: task, ...extra, }; } function formatNotFoundResponse(taskId: string) { return { success: false, error: `Task not found: ${taskId}`, }; } // Initialize scheduler (will receive MCP client after server starts) let scheduler: TaskScheduler; type IntervalTriggerConfig = { seconds?: number; minutes?: number; hours?: number; days?: number; }; type CronTriggerConfig = { expression: string; }; type DateTriggerConfig = { run_date: string; }; type SupportedTriggerConfig = IntervalTriggerConfig | CronTriggerConfig | DateTriggerConfig; type SupportedTriggerType = 'interval' | 'cron' | 'date'; function ensureNumber(value: unknown, field: string): number { if (typeof value === 'number') { return value; } if (typeof value === 'string' && value.trim().length > 0) { const num = Number(value); if (!Number.isNaN(num)) { return num; } } throw new Error(`Invalid numeric value for ${field}`); } function validateIntervalConfig(rawConfig: Record<string, any>): IntervalTriggerConfig { const allowedKeys = ['seconds', 'minutes', 'hours', 'days']; const config: IntervalTriggerConfig = {}; let totalMs = 0; for (const key of Object.keys(rawConfig)) { if (!allowedKeys.includes(key)) { throw new Error(`Unknown interval option: ${key}`); } } if ('seconds' in rawConfig) { const seconds = ensureNumber(rawConfig.seconds, 'trigger_config.seconds'); if (seconds < 0.001) { throw new Error('Seconds must be greater than 0'); } config.seconds = seconds; totalMs += seconds * 1000; } if ('minutes' in rawConfig) { const minutes = ensureNumber(rawConfig.minutes, 'trigger_config.minutes'); if (minutes < 0.001) { throw new Error('Minutes must be greater than 0'); } config.minutes = minutes; totalMs += minutes * 60 * 1000; } if ('hours' in rawConfig) { const hours = ensureNumber(rawConfig.hours, 'trigger_config.hours'); if (hours < 0.001) { throw new Error('Hours must be greater than 0'); } config.hours = hours; totalMs += hours * 60 * 60 * 1000; } if ('days' in rawConfig) { const days = ensureNumber(rawConfig.days, 'trigger_config.days'); if (days < 0.001) { throw new Error('Days must be greater than 0'); } config.days = days; totalMs += days * 24 * 60 * 60 * 1000; } if (totalMs <= 0) { throw new Error('Interval trigger requires at least one positive duration field'); } return config; } function validateCronConfig(rawConfig: Record<string, any>): CronTriggerConfig { if (typeof rawConfig.expression !== 'string' || rawConfig.expression.trim().length === 0) { throw new Error('Cron trigger requires a non-empty expression'); } if (!cron.validate(rawConfig.expression)) { throw new Error(`Invalid cron expression: ${rawConfig.expression}`); } return { expression: rawConfig.expression }; } function validateDateConfig(rawConfig: Record<string, any>): DateTriggerConfig { const config = { ...rawConfig }; const now = new Date(); const delayFields: Array<[keyof typeof config, number]> = [ ['delay_seconds', 1000], ['delay_minutes', 60 * 1000], ['delay_hours', 60 * 60 * 1000], ['delay_days', 24 * 60 * 60 * 1000], ]; let delayMs = 0; for (const [field, multiplier] of delayFields) { if (config[field] !== undefined) { const value = ensureNumber(config[field], `trigger_config.${String(field)}`); if (value < 0) { throw new Error(`${String(field)} must be >= 0`); } delayMs += value * multiplier; delete config[field]; } } let runDate: Date | undefined; if (typeof config.run_date === 'string' && config.run_date.trim().length > 0) { runDate = new Date(config.run_date); if (Number.isNaN(runDate.getTime())) { throw new Error(`Invalid ISO date string for run_date: ${config.run_date}`); } } if (!runDate && delayMs > 0) { runDate = new Date(now.getTime() + delayMs); } if (!runDate) { throw new Error('Date trigger requires either run_date or delay fields (delay_seconds/minutes/hours/days)'); } if (runDate.getTime() <= now.getTime()) { if (delayMs > 0) { runDate = new Date(now.getTime() + delayMs); } else { console.warn('[schedule-task-mcp] run_date was in the past, auto-adjusting to now + 1s'); runDate = new Date(now.getTime() + 1000); } } return { run_date: runDate.toISOString() }; } function buildTriggerConfigFromFlatArgs(triggerType: SupportedTriggerType, args: Record<string, any>): Record<string, any> { if (triggerType === 'interval') { const config: Record<string, any> = {}; if (typeof args.interval_seconds === 'number') config.seconds = args.interval_seconds; if (typeof args.interval_minutes === 'number') config.minutes = args.interval_minutes; if (typeof args.interval_hours === 'number') config.hours = args.interval_hours; if (typeof args.interval_days === 'number') config.days = args.interval_days; return config; } if (triggerType === 'cron') { if (typeof args.cron_expression === 'string') { return { expression: args.cron_expression }; } return {}; } if (triggerType === 'date') { const config: Record<string, any> = {}; if (typeof args.run_date === 'string') config.run_date = args.run_date; if (typeof args.delay_seconds === 'number') config.delay_seconds = args.delay_seconds; if (typeof args.delay_minutes === 'number') config.delay_minutes = args.delay_minutes; if (typeof args.delay_hours === 'number') config.delay_hours = args.delay_hours; if (typeof args.delay_days === 'number') config.delay_days = args.delay_days; return config; } return {}; } function validateTriggerConfig(triggerType: SupportedTriggerType, rawConfig: any): SupportedTriggerConfig { if (!rawConfig || typeof rawConfig !== 'object') { throw new Error('trigger_config must be an object'); } if (triggerType === 'interval') { return validateIntervalConfig(rawConfig); } if (triggerType === 'cron') { return validateCronConfig(rawConfig); } if (triggerType === 'date') { return validateDateConfig(rawConfig); } throw new Error(`Unsupported trigger_type: ${triggerType}`); } function sanitizeAgentPrompt(value: any): string | undefined { if (value === undefined || value === null) { return undefined; } if (typeof value !== 'string' || value.trim().length === 0) { throw new Error('agent_prompt must be a non-empty string when provided'); } return value.trim(); } function extractAgentPrompt(args: Record<string, any> | undefined): string | undefined { // Try new field name first const instruction = sanitizeAgentPrompt(args?.instruction); if (instruction) { return instruction; } // Fall back to legacy field names const prompt = sanitizeAgentPrompt(args?.agent_prompt); if (prompt) { return prompt; } const legacyPrompt = sanitizeAgentPrompt(args?.mcp_arguments?.agent_prompt); return legacyPrompt; } // Create MCP server const server = new Server( { name: 'schedule-task-mcp', version: '0.2.0' // 🎯 Version bump for sampling support }, { capabilities: { tools: {}, sampling: {} // 🎯 Enable sampling capability } } ); // Define tools const tools: Tool[] = [ { name: 'create_task', description: 'Create a scheduled task with interval, cron, or one-time date trigger', inputSchema: { type: 'object', properties: { instruction: { type: 'string', description: 'Task to execute when triggered. Extract only the action, removing time expressions. Example: "每天9点检查新视频" → "检查新视频"' }, trigger_type: { type: 'string', enum: ['interval', 'cron', 'date'], description: 'Schedule type: interval (recurring delay), cron (specific times), or date (one-time)' }, cron_expression: { type: 'string', description: 'Cron expression (required for trigger_type=cron). Example: "0 9 * * *" for daily at 9am' }, interval_seconds: { type: 'number', minimum: 0.001, description: 'Interval in seconds (for trigger_type=interval)' }, interval_minutes: { type: 'number', minimum: 0.001, description: 'Interval in minutes (for trigger_type=interval)' }, interval_hours: { type: 'number', minimum: 0.001, description: 'Interval in hours (for trigger_type=interval)' }, interval_days: { type: 'number', minimum: 0.001, description: 'Interval in days (for trigger_type=interval)' }, run_date: { type: 'string', format: 'date-time', description: 'ISO 8601 timestamp for one-time execution (for trigger_type=date)' }, delay_seconds: { type: 'number', minimum: 0, description: 'Delay in seconds (for trigger_type=date)' }, delay_minutes: { type: 'number', minimum: 0, description: 'Delay in minutes (for trigger_type=date)' }, delay_hours: { type: 'number', minimum: 0, description: 'Delay in hours (for trigger_type=date)' }, delay_days: { type: 'number', minimum: 0, description: 'Delay in days (for trigger_type=date)' }, mcp_server: { type: 'string', description: 'Legacy: MCP server name to call' }, mcp_tool: { type: 'string', description: 'Legacy: MCP tool name to call' }, mcp_arguments: { type: 'object', description: 'Legacy: Arguments to pass to MCP tool' } }, required: ['instruction', 'trigger_type'] } }, { name: 'list_tasks', description: 'Return every stored task with its Markdown summary and raw detail payload. Use this right after creating/updating schedules or whenever the user wants a dashboard view. If the user says “show my paused jobs”, call this with status="paused".', inputSchema: { type: 'object', properties: { status: { type: 'string', description: 'Optional filter: scheduled | running | paused | completed | error. Leave empty to return everything.' } }, examples: [ {}, {"status": "paused"}, {"status": "error"} ] } }, { name: 'get_task', description: 'Fetch a single task by ID and return the same Markdown summary + raw detail structure used elsewhere. Invoke when the user wants to inspect status, next run, history, or agent instructions for one task.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task identifier reported in previous responses (e.g. "task-1760236021086-j6krg8f").' } }, required: ['task_id'], examples: [ {"task_id": "task-1760236021086-j6krg8f"} ] } }, { name: 'update_task', description: 'Modify an existing task (schedule, instructions). Flow: confirm the user really means to edit the schedule, call get_current_time when the timing changes, adjust trigger_type/trigger_config accordingly, and return the updated summary.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task identifier that was returned when the job was created or listed.' }, trigger_type: { type: 'string', enum: ['interval', 'cron', 'date'], description: 'Optional new trigger type. If you change this, you must also provide trigger_config in the matching shape.' }, trigger_config: { type: 'object', description: 'Optional new configuration. Must align with trigger_type (see create_task for the same schema).' }, mcp_server: { type: 'string', description: 'New MCP server name (optional)' }, mcp_tool: { type: 'string', description: 'New MCP tool name (optional)' }, mcp_arguments: { type: 'object', description: 'New MCP arguments (optional)' }, agent_prompt: { type: 'string', description: 'Updated natural-language instruction executed when the task fires. Keep it friendly prose without scheduling keywords or tool syntax.', examples: [ '重新检查最新视频并只整理前3条,再发送邮件给运营', '生成AI早报后发到liaofanyishi1@163.com' ] } }, required: ['task_id'], examples: [ { "task_id": "task-1760236021086-j6krg8f", "trigger_type": "interval", "trigger_config": {"minutes": 60}, "agent_prompt": "检查有没有新视频并发送摘要" }, { "task_id": "task-abc", "trigger_type": "date", "trigger_config": {"delay_minutes": 15} } ] } }, { name: 'delete_task', description: 'Permanently remove a task. Confirm the user really wants it gone. The server returns the last-known snapshot (summary + detail) for audit purposes.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to delete' } }, required: ['task_id'], examples: [ {"task_id": "task-1760111762490-vfbh6tf"} ] } }, { name: 'clear_task_history', description: 'Clear stored run history for a task but keep the schedule active. Use when the user wants to reset success/error logs for a new reporting window.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to clear history for' } }, required: ['task_id'], examples: [ {"task_id": "task-123"} ] } }, { name: 'pause_task', description: 'Disable future runs without deleting the task. Ideal for vacations, maintenance windows, or temporary shutdowns.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to pause' } }, required: ['task_id'], examples: [ {"task_id": "task-abc"} ] } }, { name: 'resume_task', description: 'Re-enable a previously paused task and recompute the next run time.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to resume' } }, required: ['task_id'], examples: [ {"task_id": "task-abc"} ] } }, { name: 'execute_task', description: 'Manually run a task immediately (useful for testing or catching up). The response shows the live tool output and the refreshed schedule summary.', inputSchema: { type: 'object', properties: { task_id: { type: 'string', description: 'Task ID to execute' } }, required: ['task_id'], examples: [ {"task_id": "task-123"} ] } }, { name: 'get_current_time', description: 'Return the scheduler\'s current time in the configured timezone (default Asia/Shanghai unless overridden). Always call this before creating or updating schedules so delay calculations stay accurate.', inputSchema: { type: 'object', properties: { format: { type: 'string', enum: ['iso', 'readable'], description: 'Optional. "iso" returns an ISO 8601 string in the configured timezone (default Asia/Shanghai); "readable" returns a localized human-readable string.' } }, examples: [ {}, {"format": "readable"} ] } } ]; // Tool list handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Tool call handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name } = request.params; let args = (request.params.arguments ?? {}) as Record<string, any>; // Handle nested arguments (OpenAI Agents SDK wraps params in 'arguments' field) if (args.arguments && typeof args.arguments === 'object') { args = args.arguments as Record<string, any>; } try { switch (name) { case 'create_task': { const triggerType = args.trigger_type as SupportedTriggerType; if (!['interval', 'cron', 'date'].includes(triggerType)) { throw new Error('trigger_type must be one of interval, cron, date'); } // Build trigger_config from flat args (new format) or use provided trigger_config (legacy format) const rawConfig = args.trigger_config || buildTriggerConfigFromFlatArgs(triggerType, args); const triggerConfig = validateTriggerConfig(triggerType, rawConfig); const agentPrompt = extractAgentPrompt(args); const created = await scheduler.createTask({ trigger_type: triggerType, trigger_config: triggerConfig as Record<string, any>, mcp_server: args.mcp_server as string | undefined, mcp_tool: args.mcp_tool as string | undefined, mcp_arguments: args.mcp_arguments as Record<string, any> | undefined, agent_prompt: agentPrompt, }); const task = scheduler.describeTask(created); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '创建成功', { message: 'Task created' }), null, 2 ) } ] }; } case 'list_tasks': { const statusFilter = args.status as string | undefined; const describedTasks = scheduler .listTasks() .filter((task) => (statusFilter ? task.status === statusFilter : true)) .map((task) => scheduler.describeTask(task)); const taskEntries = describedTasks.map((task) => ({ summary: buildTaskSummary(task, '当前状态'), detail: task })); return { content: [ { type: 'text', text: JSON.stringify({ success: true, count: taskEntries.length, tasks: taskEntries }, null, 2) } ] }; } case 'get_task': { const taskId = args.task_id; if (typeof taskId !== 'string' || taskId.trim().length === 0) { throw new Error('task_id is required'); } const taskRecord = scheduler.getTask(taskId); if (!taskRecord) { throw new Error(`Task not found: ${taskId}`); } const task = scheduler.describeTask(taskRecord); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '任务详情', { message: 'Task fetched' }), null, 2 ) } ] }; } case 'update_task': { const taskIdRaw = args.task_id; if (typeof taskIdRaw !== 'string' || taskIdRaw.trim().length === 0) { throw new Error('task_id is required'); } const taskId = taskIdRaw.trim(); const existingTask = scheduler.getTask(taskId); if (!existingTask) { throw new Error(`Task not found: ${taskId}`); } const updates: any = {}; const hasTriggerType = typeof args.trigger_type === 'string'; const nextTriggerType = (hasTriggerType ? args.trigger_type : existingTask.trigger_type) as SupportedTriggerType; if (!['interval', 'cron', 'date'].includes(nextTriggerType)) { throw new Error('trigger_type must be one of interval, cron, date'); } if (hasTriggerType) { updates.trigger_type = nextTriggerType; } const hasTriggerConfig = Object.prototype.hasOwnProperty.call(args, 'trigger_config'); const hasFlatConfig = args.cron_expression || args.interval_seconds || args.interval_minutes || args.interval_hours || args.interval_days || args.run_date || args.delay_seconds || args.delay_minutes || args.delay_hours || args.delay_days; if (hasTriggerConfig) { updates.trigger_config = validateTriggerConfig(nextTriggerType, args.trigger_config) as Record<string, any>; } else if (hasFlatConfig) { const rawConfig = buildTriggerConfigFromFlatArgs(nextTriggerType, args); updates.trigger_config = validateTriggerConfig(nextTriggerType, rawConfig) as Record<string, any>; } else if (hasTriggerType) { throw new Error('Updating trigger_type requires providing trigger configuration'); } if (typeof args.mcp_server === 'string') updates.mcp_server = args.mcp_server; if (typeof args.mcp_tool === 'string') updates.mcp_tool = args.mcp_tool; if (Object.prototype.hasOwnProperty.call(args, 'mcp_arguments')) updates.mcp_arguments = args.mcp_arguments; // Support both new 'instruction' and legacy 'agent_prompt' if (Object.prototype.hasOwnProperty.call(args, 'instruction')) { updates.agent_prompt = sanitizeAgentPrompt(args.instruction); } else if (Object.prototype.hasOwnProperty.call(args, 'agent_prompt')) { updates.agent_prompt = sanitizeAgentPrompt(args.agent_prompt); } const updated = await scheduler.updateTask(taskId, updates); const task = scheduler.describeTask(updated); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '更新完成', { message: 'Task updated' }), null, 2 ) } ] }; } case 'delete_task': { const deleteId = args.task_id; if (typeof deleteId !== 'string' || deleteId.trim().length === 0) { throw new Error('task_id is required'); } const preparedId = deleteId.trim(); const snapshot = scheduler.getTask(preparedId); if (!snapshot) { const notFound = formatNotFoundResponse(preparedId); return { content: [ { type: 'text', text: JSON.stringify(notFound, null, 2) } ], isError: true }; } const describedSnapshot = scheduler.describeTask(snapshot); const deleted = await scheduler.deleteTask(preparedId); return { content: [ { type: 'text', text: JSON.stringify( deleted ? formatTaskResponse(describedSnapshot, '已删除', { message: 'Task deleted', detail_note: 'detail 字段为删除前的任务快照' }) : formatNotFoundResponse(preparedId), null, 2 ) } ] }; } case 'clear_task_history': { const clearId = args.task_id; if (typeof clearId !== 'string' || clearId.trim().length === 0) { throw new Error('task_id is required'); } const cleared = await scheduler.clearTaskHistory(clearId.trim()); const task = scheduler.describeTask(cleared); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '历史已清空', { message: 'Task history cleared' }), null, 2 ) } ] }; } case 'pause_task': { const pauseId = args.task_id; if (typeof pauseId !== 'string' || pauseId.trim().length === 0) { throw new Error('task_id is required'); } const paused = await scheduler.pauseTask(pauseId.trim()); const task = scheduler.describeTask(paused); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '已暂停', { message: 'Task paused' }), null, 2 ) } ] }; } case 'resume_task': { const resumeId = args.task_id; if (typeof resumeId !== 'string' || resumeId.trim().length === 0) { throw new Error('task_id is required'); } const resumed = await scheduler.resumeTask(resumeId.trim()); const task = scheduler.describeTask(resumed); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '已恢复', { message: 'Task resumed' }), null, 2 ) } ] }; } case 'execute_task': { const executeId = args.task_id; if (typeof executeId !== 'string' || executeId.trim().length === 0) { throw new Error('task_id is required'); } const trimmedId = executeId.trim(); const result = await scheduler.executeTask(trimmedId); if (!result.success) { return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ], isError: true }; } const updatedTaskRecord = scheduler.getTask(trimmedId); if (!updatedTaskRecord) { return { content: [ { type: 'text', text: JSON.stringify(formatNotFoundResponse(trimmedId), null, 2) } ], isError: true }; } const task = scheduler.describeTask(updatedTaskRecord); return { content: [ { type: 'text', text: JSON.stringify( formatTaskResponse(task, '手动执行完成', { message: result.message }), null, 2 ) } ] }; } case 'get_current_time': { const format = typeof args.format === 'string' ? args.format : 'iso'; const now = new Date(); // Convert to configured timezone const timeInTimezone = new Date(now.toLocaleString('en-US', { timeZone: TIMEZONE })); let result; if (format === 'readable') { result = { success: true, timezone: TIMEZONE, current_time: timeInTimezone.toLocaleString('zh-CN', { timeZone: TIMEZONE }), iso_time: timeInTimezone.toISOString(), timestamp: timeInTimezone.getTime() }; } else { result = { success: true, timezone: TIMEZONE, current_time: timeInTimezone.toISOString(), timestamp: timeInTimezone.getTime() }; } return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error: any) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: error.message, stack: error.stack }, null, 2) } ], isError: true }; } }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); // 🎯 Initialize scheduler with access to this MCP server for sampling scheduler = new TaskScheduler({ dbPath: DB_PATH, mcpServer: server }); await scheduler.initialize(); console.error('Schedule Task MCP Server running on stdio'); console.error(`Database path: ${DB_PATH}`); console.error('✅ MCP Sampling enabled - tasks with agent_prompt will execute via sampling'); // Graceful shutdown process.on('SIGINT', async () => { await scheduler.shutdown(); process.exit(0); }); process.on('SIGTERM', async () => { await scheduler.shutdown(); process.exit(0); }); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });