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
text/typescript
/**
* 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);
});