Skip to content

Commit fe3ffb3

Browse files
committed
feat(scheduler): add manual trigger capability and fix docs parity
- Add 'trigger' tool to Scheduler MCP and 'ncp schedule trigger' CLI command - Implement 'runTaskNow' in Scheduler service for immediate execution - Fix documentation discrepancies in README and guides: - Correct schedule:add -> schedule:create in workflow guide - Add Analytics MCP to README - Correct MCP Management tool names in README - Replace deprecated 'ncp status' with 'ncp doctor' in advanced guide
1 parent 8a21701 commit fe3ffb3

7 files changed

Lines changed: 217 additions & 24 deletions

File tree

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ Install and configure MCPs dynamically through natural language.
440440
```bash
441441
# AI can discover and install MCPs for you
442442
ncp find "install mcp"
443-
# Shows: mcp:install, mcp:search, mcp:configure
443+
# Shows: mcp:add, mcp:remove, mcp:list
444444
```
445445

446446
**Features:**
@@ -480,6 +480,20 @@ const template = await skills.read_resource({
480480

481481
**[→ Full Skills Guide](./SKILLS.md)**
482482

483+
### **Analytics MCP** - Visualize Usage & Performance
484+
View usage statistics, token savings, and performance metrics directly in your chat.
485+
486+
```bash
487+
# View usage overview with ASCII charts
488+
ncp run analytics:overview --params '{"period": 7}'
489+
```
490+
491+
**Features:**
492+
- ✅ Usage trends and most used tools
493+
- ✅ Token savings analysis (Code-Mode efficiency)
494+
- ✅ Performance metrics (response times, error rates)
495+
- ✅ ASCII-formatted charts for AI consumption
496+
483497
**Configuration:**
484498
Internal MCPs are disabled by default. Enable in your profile settings:
485499

@@ -488,7 +502,8 @@ Internal MCPs are disabled by default. Enable in your profile settings:
488502
"settings": {
489503
"enable_schedule_mcp": true,
490504
"enable_mcp_management": true,
491-
"enable_skills": true
505+
"enable_skills": true,
506+
"enable_analytics_mcp": true
492507
}
493508
}
494509
```

docs/ADVANCED_USAGE_GUIDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ Debug logs include:
152152

153153
```bash
154154
# Check MCP health
155-
ncp status --detailed
155+
ncp doctor
156156

157157
# Verify configuration
158158
ncp config location # Check where configs are stored
@@ -171,7 +171,7 @@ ncp find "operation description"
171171
ncp list | grep "mcp-name"
172172

173173
# Verify MCP is loaded
174-
ncp status
174+
ncp doctor
175175
```
176176

177177
**Symptom: "Command execution timeout"**
@@ -485,6 +485,6 @@ Typical performance metrics on a modern system:
485485
## Getting Help
486486

487487
- Check debug logs: `ncp config debugLogging true`
488-
- Review MCP status: `ncp status --detailed`
488+
- Review MCP status: ncp doctor
489489
- Consult test-drive guide: `ncp resources read ncp:test-drive`
490490
- Review docs: `ncp resources read ncp:help/getting-started`

docs/workflow-mcp-guide.md

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -485,21 +485,24 @@ Returns:
485485

486486
```bash
487487
# Schedule daily standup for weekdays at 9am
488-
ncp run schedule:add \
488+
ncp run schedule:create \
489489
name="daily-standup" \
490-
command="ncp run workflow:execute workflowName=daily-standup context='{\"slackWebhook\":\"https://hooks.slack.com/...\",\"repo\":\"owner/repo\"}'" \
490+
tool="workflow:execute" \
491+
parameters='{"workflowName":"daily-standup", "context": {"slackWebhook":"https://hooks.slack.com/...", "repo":"owner/repo"}}' \
491492
schedule="0 9 * * 1-5"
492493

493494
# Schedule content pipeline daily at midnight
494-
ncp run schedule:add \
495+
ncp run schedule:create \
495496
name="content-pipeline" \
496-
command="ncp run workflow:execute workflowName=content-pipeline context='{\"sourceUrl\":\"https://...\",\"recipient\":\"team@company.com\"}'" \
497+
tool="workflow:execute" \
498+
parameters='{"workflowName":"content-pipeline", "context": {"sourceUrl":"https://...", "recipient":"user@example.com"}}' \
497499
schedule="0 0 * * *"
498500

499501
# Schedule data sync daily at 2am
500-
ncp run schedule:add \
502+
ncp run schedule:create \
501503
name="data-sync" \
502-
command="ncp run workflow:execute workflowName=data-sync context='{\"dbPath\":\"./data.db\",\"s3Bucket\":\"backups\",\"slackWebhook\":\"...\"}'" \
504+
tool="workflow:execute" \
505+
parameters='{"workflowName":"data-sync", "context": {"dbPath":"./data.db", "s3Bucket":"backups", "slackWebhook":"..."}}' \
503506
schedule="0 2 * * *"
504507
```
505508

@@ -535,44 +538,49 @@ ncp run workflow:execute \
535538
context='{"slackWebhook":"https://hooks.slack.com/services/T00/B00/XXX","repo":"anthropics/claude-code"}'
536539

537540
# 2. If successful, schedule it
538-
ncp run schedule:add \
541+
ncp run schedule:create \
539542
name="team-standup" \
540-
command="ncp run workflow:execute workflowName=daily-standup context='{\"slackWebhook\":\"https://hooks.slack.com/services/T00/B00/XXX\",\"repo\":\"anthropics/claude-code\"}'" \
543+
tool="workflow:execute" \
544+
parameters='{"workflowName":"daily-standup", "context": {"slackWebhook":"https://hooks.slack.com/services/T00/B00/XXX", "repo":"anthropics/claude-code"}}' \
541545
schedule="0 9 * * 1-5" \
542546
description="Daily standup summary posted to Slack"
543547

544548
# 3. Verify it's scheduled
545549
ncp run schedule:list
546550

547551
# 4. Remove if needed
548-
ncp run schedule:remove name="team-standup"
552+
ncp run schedule:delete job_id="team-standup"
549553
```
550554

551555
### Advanced: Multiple Workflows on Different Schedules
552556

553557
```bash
554558
# Morning standup
555-
ncp run schedule:add \
559+
ncp run schedule:create \
556560
name="morning-standup" \
557-
command="ncp run workflow:execute workflowName=daily-standup context='...'" \
561+
tool="workflow:execute" \
562+
parameters='{"workflowName":"daily-standup", "context": "..."}' \
558563
schedule="0 9 * * 1-5"
559564

560565
# Hourly content check
561-
ncp run schedule:add \
566+
ncp run schedule:create \
562567
name="hourly-content-monitor" \
563-
command="ncp run workflow:execute-task task='Check website for changes and alert if needed' context='...'" \
568+
tool="workflow:execute-task" \
569+
parameters='{"task": "Check website for changes and alert if needed", "context": "..."}' \
564570
schedule="0 * * * *"
565571

566572
# Nightly backup
567-
ncp run schedule:add \
573+
ncp run schedule:create \
568574
name="nightly-backup" \
569-
command="ncp run workflow:execute workflowName=data-sync context='...'" \
575+
tool="workflow:execute" \
576+
parameters='{"workflowName":"data-sync", "context": "..."}' \
570577
schedule="0 2 * * *"
571578

572579
# Weekly report
573-
ncp run schedule:add \
580+
ncp run schedule:create \
574581
name="weekly-report" \
575-
command="ncp run workflow:execute-custom workflow='...'" \
582+
tool="workflow:execute-custom" \
583+
parameters='{"workflow": "..."}' \
576584
schedule="0 9 * * 1"
577585
```
578586

src/cli/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2908,6 +2908,54 @@ scheduleCmd
29082908
}
29092909
});
29102910

2911+
// schedule trigger
2912+
scheduleCmd
2913+
.command('trigger <job-id>')
2914+
.description('Trigger a scheduled job immediately (manual run)')
2915+
.action(async (jobId) => {
2916+
try {
2917+
const { Scheduler } = await import('../services/scheduler/scheduler.js');
2918+
const scheduler = new Scheduler();
2919+
2920+
let job = scheduler.getTask(jobId);
2921+
if (!job) {
2922+
job = scheduler.getTaskByName(jobId);
2923+
if (job) jobId = job.id;
2924+
}
2925+
2926+
if (!job) {
2927+
console.error(chalk.red(`❌ Job not found: ${jobId}`));
2928+
process.exit(1);
2929+
}
2930+
2931+
console.log(chalk.blue(`🚀 Triggering job: ${job.name}...`));
2932+
2933+
const result = await scheduler.runTaskNow(jobId);
2934+
2935+
const statusIcon = result.status === 'success' ? chalk.green('✅') : chalk.red('❌');
2936+
const duration = result.duration ? `${result.duration}ms` : 'N/A';
2937+
2938+
console.log(`${statusIcon} Manual execution completed`);
2939+
console.log(` ${chalk.dim('ID:')} ${result.executionId}`);
2940+
console.log(` ${chalk.dim('Status:')} ${result.status}`);
2941+
console.log(` ${chalk.dim('Duration:')} ${duration}`);
2942+
2943+
if (result.result) {
2944+
console.log(chalk.bold('\nResult:'));
2945+
console.log(result.result);
2946+
}
2947+
2948+
if (result.error) {
2949+
console.log(chalk.bold('\nError:'));
2950+
console.log(chalk.red(result.error));
2951+
}
2952+
2953+
} catch (error) {
2954+
console.error(chalk.red('❌ Trigger failed:'), error);
2955+
process.exit(1);
2956+
}
2957+
});
2958+
29112959
// schedule cleanup
29122960
scheduleCmd
29132961
.command('cleanup')

src/internal-mcps/scheduler.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,20 @@ export class SchedulerMCP implements InternalMCP {
320320
}
321321
}
322322
}
323+
},
324+
{
325+
name: 'trigger',
326+
description: 'Trigger an immediate execution of a scheduled job (manual run).',
327+
inputSchema: {
328+
type: 'object',
329+
properties: {
330+
job_id: {
331+
type: 'string',
332+
description: 'Job ID or name'
333+
}
334+
},
335+
required: ['job_id']
336+
}
323337
}
324338
];
325339

@@ -360,14 +374,16 @@ export class SchedulerMCP implements InternalMCP {
360374
return await this.handleResume(args);
361375
case 'executions':
362376
return await this.handleExecutions(args);
377+
case 'trigger':
378+
return await this.handleTrigger(args);
363379

364380
default:
365381
return {
366382
success: false,
367-
error: `Unknown schedule tool: ${toolName}. Available: validate, create, list, get, retrieve, pause, resume, cancel, executions`,
383+
error: `Unknown schedule tool: ${toolName}. Available: validate, create, list, get, retrieve, pause, resume, cancel, executions, trigger`,
368384
content: [{
369385
type: 'text',
370-
text: `❌ Unknown schedule tool: ${toolName}\n\n📋 Available tools: validate, create, list, get, retrieve, pause, resume, cancel, executions`
386+
text: `❌ Unknown schedule tool: ${toolName}\n\n📋 Available tools: validate, create, list, get, retrieve, pause, resume, cancel, executions, trigger`
371387
}]
372388
};
373389
}
@@ -990,4 +1006,76 @@ export class SchedulerMCP implements InternalMCP {
9901006
limit: args?.limit || 50
9911007
});
9921008
}
1009+
1010+
/**
1011+
* Trigger immediate execution of a job
1012+
*/
1013+
private async handleTrigger(args: any): Promise<InternalToolResult> {
1014+
if (!args?.job_id) {
1015+
return {
1016+
success: false,
1017+
error: 'job_id parameter is required',
1018+
content: [{
1019+
type: 'text',
1020+
text: '❌ job_id parameter is required'
1021+
}]
1022+
};
1023+
}
1024+
1025+
let jobId = args.job_id;
1026+
let job = this.scheduler.getJob(jobId);
1027+
if (!job) {
1028+
job = this.scheduler.getJobByName(jobId);
1029+
if (job) jobId = job.id;
1030+
}
1031+
1032+
if (!job) {
1033+
return {
1034+
success: false,
1035+
error: `Job not found: ${args.job_id}`,
1036+
content: [{
1037+
type: 'text',
1038+
text: `❌ Job not found: ${args.job_id}`
1039+
}]
1040+
};
1041+
}
1042+
1043+
try {
1044+
const result = await this.scheduler.runTaskNow(jobId);
1045+
1046+
const statusIcon = result.status === 'success' ? '✅' : '❌';
1047+
const duration = result.duration ? `${result.duration}ms` : 'N/A';
1048+
1049+
let message = `${statusIcon} Manual execution completed: ${job.name}\n\n` +
1050+
` ID: ${result.executionId}\n` +
1051+
` Status: ${result.status}\n` +
1052+
` Duration: ${duration}\n`;
1053+
1054+
if (result.result) {
1055+
message += `\n📤 Result:\n${result.result}`;
1056+
}
1057+
1058+
if (result.error) {
1059+
message += `\n❌ Error:\n${result.error}`;
1060+
}
1061+
1062+
return {
1063+
success: result.status === 'success',
1064+
content: [{
1065+
type: 'text',
1066+
text: message
1067+
}]
1068+
};
1069+
} catch (error) {
1070+
const errorMessage = error instanceof Error ? error.message : String(error);
1071+
return {
1072+
success: false,
1073+
error: errorMessage,
1074+
content: [{
1075+
type: 'text',
1076+
text: `❌ Trigger failed: ${errorMessage}`
1077+
}]
1078+
};
1079+
}
1080+
}
9931081
}

src/services/scheduler/scheduler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,24 @@ export class Scheduler {
505505
logger.info(`[Scheduler] Task deleted: ${task.name} (${taskId})`);
506506
}
507507

508+
/**
509+
* Run a task immediately (manual trigger)
510+
*/
511+
async runTaskNow(taskId: string): Promise<any> {
512+
if (!this.scheduleManager) {
513+
throw new Error('Scheduler not available on this platform');
514+
}
515+
516+
const task = this.taskManager.getTask(taskId);
517+
if (!task) {
518+
throw new Error(`Task ${taskId} not found`);
519+
}
520+
521+
logger.info(`[Scheduler] Manually triggering task: ${task.name} (${taskId})`);
522+
523+
return this.timingExecutor.executeSingleTask(taskId);
524+
}
525+
508526
/**
509527
* Get executions for a task
510528
*/

src/services/scheduler/timing-executor.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ export class TimingExecutor {
4343
this.executionRecorder = new ExecutionRecorder();
4444
}
4545

46+
/**
47+
* Execute a single task immediately
48+
*/
49+
async executeSingleTask(taskId: string, timeout?: number): Promise<TaskExecutionResult> {
50+
const task = this.taskManager.getTask(taskId);
51+
52+
if (!task) {
53+
throw new Error(`Task ${taskId} not found`);
54+
}
55+
56+
logger.info(`[TimingExecutor] Executing single task: ${task.name} (${task.id})`);
57+
58+
// Execute in child process for isolation
59+
return this.executeTaskInChildProcess(task, timeout);
60+
}
61+
4662
/**
4763
* Execute all active tasks for a timing group in parallel
4864
*/

0 commit comments

Comments
 (0)