agent-orchestrator-mcp-server 0.6.8 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -31,7 +31,7 @@ function validateEnvironment() {
31
31
  const optional = [
32
32
  {
33
33
  name: 'TOOL_GROUPS',
34
- description: 'Comma-separated list of tool groups to enable (sessions,sessions_readonly,notifications,notifications_readonly,triggers,triggers_readonly,health,health_readonly)',
34
+ description: 'Comma-separated list of tool groups to enable (sessions,sessions_readonly,notifications,notifications_readonly,triggers,triggers_readonly,health,health_readonly,self_session)',
35
35
  defaultValue: 'all groups enabled',
36
36
  },
37
37
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-orchestrator-mcp-server",
3
- "version": "0.6.8",
3
+ "version": "0.7.3",
4
4
  "description": "Local implementation of agent-orchestrator MCP server",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "stage-publish": "npm version"
30
30
  },
31
31
  "dependencies": {
32
- "@modelcontextprotocol/sdk": "^1.19.1",
32
+ "@modelcontextprotocol/sdk": "^1.26.0",
33
33
  "zod": "^3.24.1"
34
34
  },
35
35
  "devDependencies": {
@@ -50,8 +50,12 @@ export interface IAgentOrchestratorClient {
50
50
  deleteSession(id: string | number): Promise<void>;
51
51
  archiveSession(id: string | number): Promise<Session>;
52
52
  unarchiveSession(id: string | number): Promise<Session>;
53
- followUp(id: string | number, prompt: string): Promise<SessionActionResponse>;
53
+ followUp(id: string | number, prompt: string, options?: {
54
+ force_immediate?: boolean;
55
+ stop_condition?: string;
56
+ }): Promise<SessionActionResponse>;
54
57
  pauseSession(id: string | number): Promise<SessionActionResponse>;
58
+ sleepSession(id: string | number): Promise<Session>;
55
59
  restartSession(id: string | number): Promise<SessionActionResponse>;
56
60
  changeMcpServers(id: string | number, mcp_servers: string[]): Promise<Session>;
57
61
  changeModel(id: string | number, model: string): Promise<Session>;
@@ -173,8 +177,12 @@ export declare class AgentOrchestratorClient implements IAgentOrchestratorClient
173
177
  deleteSession(id: string | number): Promise<void>;
174
178
  archiveSession(id: string | number): Promise<Session>;
175
179
  unarchiveSession(id: string | number): Promise<Session>;
176
- followUp(id: string | number, prompt: string): Promise<SessionActionResponse>;
180
+ followUp(id: string | number, prompt: string, options?: {
181
+ force_immediate?: boolean;
182
+ stop_condition?: string;
183
+ }): Promise<SessionActionResponse>;
177
184
  pauseSession(id: string | number): Promise<SessionActionResponse>;
185
+ sleepSession(id: string | number): Promise<Session>;
178
186
  restartSession(id: string | number): Promise<SessionActionResponse>;
179
187
  changeMcpServers(id: string | number, mcp_servers: string[]): Promise<Session>;
180
188
  changeModel(id: string | number, model: string): Promise<Session>;
@@ -204,19 +204,27 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
204
204
  session.updated_at = new Date().toISOString();
205
205
  return session;
206
206
  },
207
- async followUp(id, prompt) {
207
+ async followUp(id, prompt, options) {
208
208
  const session = mockData.sessions?.find((s) => s.id === Number(id) || s.slug === String(id));
209
209
  if (!session) {
210
210
  throw new Error(`API Error (404): Session not found`);
211
211
  }
212
- if (session.status !== 'needs_input') {
212
+ if (options?.force_immediate) {
213
+ if (session.status === 'failed' || session.status === 'archived') {
214
+ throw new Error(`API Error (422): Session cannot receive follow-up in ${session.status} status`);
215
+ }
216
+ }
217
+ else if (session.status !== 'needs_input') {
213
218
  throw new Error(`API Error (422): Session is not in needs_input status`);
214
219
  }
215
220
  session.prompt = prompt;
216
221
  session.status = 'running';
217
222
  session.running_job_id = `job_${Date.now()}`;
218
223
  session.updated_at = new Date().toISOString();
219
- return { session, message: 'Follow-up prompt sent' };
224
+ const message = options?.force_immediate
225
+ ? 'Follow-up prompt sent immediately'
226
+ : 'Follow-up prompt sent';
227
+ return { session, message };
220
228
  },
221
229
  async pauseSession(id) {
222
230
  const session = mockData.sessions?.find((s) => s.id === Number(id) || s.slug === String(id));
@@ -228,6 +236,18 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
228
236
  session.updated_at = new Date().toISOString();
229
237
  return { session, message: 'Session paused' };
230
238
  },
239
+ async sleepSession(id) {
240
+ const session = mockData.sessions?.find((s) => s.id === Number(id) || s.slug === String(id));
241
+ if (!session) {
242
+ throw new Error(`API Error (404): Session not found`);
243
+ }
244
+ if (session.status !== 'needs_input') {
245
+ throw new Error(`API Error (422): Session must be in needs_input state to sleep`);
246
+ }
247
+ session.status = 'waiting';
248
+ session.updated_at = new Date().toISOString();
249
+ return session;
250
+ },
231
251
  async restartSession(id) {
232
252
  const session = mockData.sessions?.find((s) => s.id === Number(id) || s.slug === String(id));
233
253
  if (!session) {
@@ -204,12 +204,20 @@ export class AgentOrchestratorClient {
204
204
  const response = await this.request('POST', `/sessions/${id}/unarchive`);
205
205
  return response.session;
206
206
  }
207
- async followUp(id, prompt) {
208
- return this.request('POST', `/sessions/${id}/follow_up`, { prompt });
207
+ async followUp(id, prompt, options) {
208
+ return this.request('POST', `/sessions/${id}/follow_up`, {
209
+ prompt,
210
+ ...(options?.force_immediate && { force_immediate: true }),
211
+ ...(options?.stop_condition && { stop_condition: options.stop_condition }),
212
+ });
209
213
  }
210
214
  async pauseSession(id) {
211
215
  return this.request('POST', `/sessions/${id}/pause`);
212
216
  }
217
+ async sleepSession(id) {
218
+ const response = await this.request('POST', `/sessions/${id}/sleep`);
219
+ return response.session;
220
+ }
213
221
  async restartSession(id) {
214
222
  return this.request('POST', `/sessions/${id}/restart`);
215
223
  }
@@ -74,6 +74,7 @@ export function createRegisterResources(clientFactory) {
74
74
  triggers_readonly: 'Trigger tools (read only): search_triggers',
75
75
  health: 'All health tools (read + write): health report, CLI status, maintenance actions',
76
76
  health_readonly: 'Health tools (read only): get_system_health',
77
+ self_session: 'Self-management tools for auto-injected servers: get_session, get_configs (read), action_session (filtered: update_notes, update_title, archive), send_push_notification',
77
78
  },
78
79
  };
79
80
  return {
@@ -13,6 +13,9 @@ export declare function createMCPServer(options: CreateMCPServerOptions): {
13
13
  _meta?: {
14
14
  [x: string]: unknown;
15
15
  progressToken?: string | number | undefined;
16
+ "io.modelcontextprotocol/related-task"?: {
17
+ taskId: string;
18
+ } | undefined;
16
19
  } | undefined;
17
20
  } | undefined;
18
21
  }, {
@@ -21,12 +24,20 @@ export declare function createMCPServer(options: CreateMCPServerOptions): {
21
24
  [x: string]: unknown;
22
25
  _meta?: {
23
26
  [x: string]: unknown;
27
+ progressToken?: string | number | undefined;
28
+ "io.modelcontextprotocol/related-task"?: {
29
+ taskId: string;
30
+ } | undefined;
24
31
  } | undefined;
25
32
  } | undefined;
26
33
  }, {
27
34
  [x: string]: unknown;
28
35
  _meta?: {
29
36
  [x: string]: unknown;
37
+ progressToken?: string | number | undefined;
38
+ "io.modelcontextprotocol/related-task"?: {
39
+ taskId: string;
40
+ } | undefined;
30
41
  } | undefined;
31
42
  }>;
32
43
  registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
@@ -5,6 +5,7 @@ export declare const ActionSessionSchema: z.ZodObject<{
5
5
  session_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
6
6
  action: z.ZodEnum<["follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"]>;
7
7
  prompt: z.ZodOptional<z.ZodString>;
8
+ force_immediate: z.ZodOptional<z.ZodBoolean>;
8
9
  mcp_servers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
10
  model: z.ZodOptional<z.ZodString>;
10
11
  message_index: z.ZodOptional<z.ZodNumber>;
@@ -17,6 +18,7 @@ export declare const ActionSessionSchema: z.ZodObject<{
17
18
  title?: string | undefined;
18
19
  mcp_servers?: string[] | undefined;
19
20
  session_id?: string | number | undefined;
21
+ force_immediate?: boolean | undefined;
20
22
  model?: string | undefined;
21
23
  message_index?: number | undefined;
22
24
  session_notes?: string | undefined;
@@ -27,6 +29,7 @@ export declare const ActionSessionSchema: z.ZodObject<{
27
29
  title?: string | undefined;
28
30
  mcp_servers?: string[] | undefined;
29
31
  session_id?: string | number | undefined;
32
+ force_immediate?: boolean | undefined;
30
33
  model?: string | undefined;
31
34
  message_index?: number | undefined;
32
35
  session_notes?: string | undefined;
@@ -53,6 +56,10 @@ export declare function actionSessionTool(_server: Server, clientFactory: () =>
53
56
  type: string;
54
57
  description: "Required for \"follow_up\" action. The prompt to send to the agent. Not used for other actions.";
55
58
  };
59
+ force_immediate: {
60
+ type: string;
61
+ description: "Optional for \"follow_up\" action. When true, interrupts a running session to deliver the prompt immediately instead of queuing it. Not used for other actions.";
62
+ };
56
63
  mcp_servers: {
57
64
  type: string;
58
65
  items: {
@@ -100,4 +107,62 @@ export declare function actionSessionTool(_server: Server, clientFactory: () =>
100
107
  isError?: undefined;
101
108
  }>;
102
109
  };
110
+ export declare const SelfSessionActionSessionSchema: z.ZodObject<{
111
+ session_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
112
+ action: z.ZodEnum<["update_notes", "update_title", "archive"]>;
113
+ session_notes: z.ZodOptional<z.ZodString>;
114
+ title: z.ZodOptional<z.ZodString>;
115
+ }, "strip", z.ZodTypeAny, {
116
+ session_id: string | number;
117
+ action: "archive" | "update_notes" | "update_title";
118
+ title?: string | undefined;
119
+ session_notes?: string | undefined;
120
+ }, {
121
+ session_id: string | number;
122
+ action: "archive" | "update_notes" | "update_title";
123
+ title?: string | undefined;
124
+ session_notes?: string | undefined;
125
+ }>;
126
+ export declare function selfSessionActionSessionTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
127
+ name: string;
128
+ description: string;
129
+ inputSchema: {
130
+ type: "object";
131
+ properties: {
132
+ session_id: {
133
+ oneOf: {
134
+ type: string;
135
+ }[];
136
+ description: "Session ID (numeric) or slug (string). Required for most actions. Not required for \"refresh_all\" and \"bulk_archive\".";
137
+ };
138
+ action: {
139
+ type: string;
140
+ enum: readonly ["update_notes", "update_title", "archive"];
141
+ description: string;
142
+ };
143
+ session_notes: {
144
+ type: string;
145
+ description: "Required for \"update_notes\" action. The notes text to set on the session.";
146
+ };
147
+ title: {
148
+ type: string;
149
+ description: "Required for \"update_title\" action. The new title for the session.";
150
+ };
151
+ };
152
+ required: string[];
153
+ };
154
+ handler: (args: unknown) => Promise<{
155
+ content: {
156
+ type: string;
157
+ text: string;
158
+ }[];
159
+ isError: boolean;
160
+ } | {
161
+ content: {
162
+ type: string;
163
+ text: string;
164
+ }[];
165
+ isError?: undefined;
166
+ }>;
167
+ };
103
168
  //# sourceMappingURL=action-session.d.ts.map
@@ -4,6 +4,7 @@ const PARAM_DESCRIPTIONS = {
4
4
  session_id: 'Session ID (numeric) or slug (string). Required for most actions. Not required for "refresh_all" and "bulk_archive".',
5
5
  action: 'Action to perform: "follow_up", "pause", "restart", "archive", "unarchive", "change_mcp_servers", "change_model", "fork", "refresh", "refresh_all", "update_notes", "update_title", "toggle_favorite", "bulk_archive"',
6
6
  prompt: 'Required for "follow_up" action. The prompt to send to the agent. Not used for other actions.',
7
+ force_immediate: 'Optional for "follow_up" action. When true, interrupts a running session to deliver the prompt immediately instead of queuing it. Not used for other actions.',
7
8
  mcp_servers: 'Required for "change_mcp_servers" action. Array of MCP server names to set for the session.',
8
9
  model: 'Required for "change_model" action. The model identifier to use (e.g., "opus", "sonnet").',
9
10
  message_index: 'Required for "fork" action. The transcript message index to fork from.',
@@ -31,6 +32,7 @@ export const ActionSessionSchema = z.object({
31
32
  session_id: z.union([z.string(), z.number()]).optional().describe(PARAM_DESCRIPTIONS.session_id),
32
33
  action: z.enum(ACTION_ENUM).describe(PARAM_DESCRIPTIONS.action),
33
34
  prompt: z.string().optional().describe(PARAM_DESCRIPTIONS.prompt),
35
+ force_immediate: z.boolean().optional().describe(PARAM_DESCRIPTIONS.force_immediate),
34
36
  mcp_servers: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.mcp_servers),
35
37
  model: z.string().optional().describe(PARAM_DESCRIPTIONS.model),
36
38
  message_index: z.number().optional().describe(PARAM_DESCRIPTIONS.message_index),
@@ -41,7 +43,7 @@ export const ActionSessionSchema = z.object({
41
43
  const TOOL_DESCRIPTION = `Perform an action on an agent session.
42
44
 
43
45
  **Actions:**
44
- - **follow_up**: Send a follow-up prompt to an idle session (requires "prompt" parameter)
46
+ - **follow_up**: Send a follow-up prompt to a session (requires "prompt"; optional "force_immediate" to interrupt a running session). Without "force_immediate", uses smart routing: sends immediately if idle, auto-queues if running. Alternative: use manage_enqueued_messages "send_now" for one-step immediate delivery with stop_condition support.
45
47
  - **pause**: Pause a running session, transitioning it to idle "needs_input" status
46
48
  - **restart**: Restart an idle or failed session without providing new input
47
49
  - **archive**: Archive a session (marks as completed)
@@ -82,6 +84,10 @@ export function actionSessionTool(_server, clientFactory) {
82
84
  type: 'string',
83
85
  description: PARAM_DESCRIPTIONS.prompt,
84
86
  },
87
+ force_immediate: {
88
+ type: 'boolean',
89
+ description: PARAM_DESCRIPTIONS.force_immediate,
90
+ },
85
91
  mcp_servers: {
86
92
  type: 'array',
87
93
  items: { type: 'string' },
@@ -115,7 +121,7 @@ export function actionSessionTool(_server, clientFactory) {
115
121
  try {
116
122
  const validatedArgs = ActionSessionSchema.parse(args);
117
123
  const client = clientFactory();
118
- const { session_id, action, prompt, mcp_servers, model, message_index, session_notes, session_ids, title, } = validatedArgs;
124
+ const { session_id, action, prompt, force_immediate, mcp_servers, model, message_index, session_notes, session_ids, title, } = validatedArgs;
119
125
  // Actions that require session_id
120
126
  const requiresSessionId = [
121
127
  'follow_up',
@@ -241,9 +247,13 @@ export function actionSessionTool(_server, clientFactory) {
241
247
  let result;
242
248
  switch (action) {
243
249
  case 'follow_up': {
244
- const response = await client.followUp(session_id, prompt);
250
+ const response = await client.followUp(session_id, prompt, {
251
+ force_immediate,
252
+ });
253
+ const immediate = response.message?.toLowerCase().includes('immediately');
254
+ const heading = immediate ? 'Follow-up Sent Immediately' : 'Follow-up Sent';
245
255
  const lines = [
246
- `## Follow-up Sent`,
256
+ `## ${heading}`,
247
257
  '',
248
258
  `- **Session ID:** ${response.session.id}`,
249
259
  `- **Title:** ${response.session.title}`,
@@ -393,7 +403,9 @@ export function actionSessionTool(_server, clientFactory) {
393
403
  break;
394
404
  }
395
405
  case 'update_title': {
396
- const session = await client.updateSession(session_id, { title: title });
406
+ const session = await client.updateSession(session_id, {
407
+ title: title,
408
+ });
397
409
  const lines = [
398
410
  `## Session Title Updated`,
399
411
  '',
@@ -463,3 +475,155 @@ export function actionSessionTool(_server, clientFactory) {
463
475
  },
464
476
  };
465
477
  }
478
+ // =============================================================================
479
+ // SELF-SESSION VARIANT
480
+ // =============================================================================
481
+ // Restricted version of action_session for the self_session composite group.
482
+ // Only allows self-management actions: update_notes, update_title, archive.
483
+ // =============================================================================
484
+ const SELF_SESSION_ACTION_ENUM = ['update_notes', 'update_title', 'archive'];
485
+ export const SelfSessionActionSessionSchema = z.object({
486
+ session_id: z.union([z.string(), z.number()]).describe(PARAM_DESCRIPTIONS.session_id),
487
+ action: z
488
+ .enum(SELF_SESSION_ACTION_ENUM)
489
+ .describe('Action to perform: "update_notes", "update_title", "archive"'),
490
+ session_notes: z.string().optional().describe(PARAM_DESCRIPTIONS.session_notes),
491
+ title: z.string().optional().describe(PARAM_DESCRIPTIONS.title),
492
+ });
493
+ const SELF_SESSION_TOOL_DESCRIPTION = `Perform a self-management action on a session.
494
+
495
+ **Actions (limited to self-management):**
496
+ - **update_notes**: Update the notes on a session (requires "session_notes")
497
+ - **update_title**: Update the title of a session (requires "title")
498
+ - **archive**: Archive a session (marks as completed)
499
+
500
+ **Use cases:**
501
+ - Update session notes to record progress or context
502
+ - Set a meaningful session title
503
+ - Archive the session when work is complete`;
504
+ export function selfSessionActionSessionTool(_server, clientFactory) {
505
+ return {
506
+ name: 'action_session',
507
+ description: SELF_SESSION_TOOL_DESCRIPTION,
508
+ inputSchema: {
509
+ type: 'object',
510
+ properties: {
511
+ session_id: {
512
+ oneOf: [{ type: 'string' }, { type: 'number' }],
513
+ description: PARAM_DESCRIPTIONS.session_id,
514
+ },
515
+ action: {
516
+ type: 'string',
517
+ enum: SELF_SESSION_ACTION_ENUM,
518
+ description: 'Action to perform: "update_notes", "update_title", "archive"',
519
+ },
520
+ session_notes: {
521
+ type: 'string',
522
+ description: PARAM_DESCRIPTIONS.session_notes,
523
+ },
524
+ title: {
525
+ type: 'string',
526
+ description: PARAM_DESCRIPTIONS.title,
527
+ },
528
+ },
529
+ required: ['session_id', 'action'],
530
+ },
531
+ handler: async (args) => {
532
+ try {
533
+ const validatedArgs = SelfSessionActionSessionSchema.parse(args);
534
+ const client = clientFactory();
535
+ const { session_id, action, session_notes, title } = validatedArgs;
536
+ // Validate update_notes requires session_notes
537
+ if (action === 'update_notes' && session_notes === undefined) {
538
+ return {
539
+ content: [
540
+ {
541
+ type: 'text',
542
+ text: 'Error: The "session_notes" parameter is required for the "update_notes" action.',
543
+ },
544
+ ],
545
+ isError: true,
546
+ };
547
+ }
548
+ // Validate update_title requires title
549
+ if (action === 'update_title' && !title) {
550
+ return {
551
+ content: [
552
+ {
553
+ type: 'text',
554
+ text: 'Error: The "title" parameter is required for the "update_title" action.',
555
+ },
556
+ ],
557
+ isError: true,
558
+ };
559
+ }
560
+ let result;
561
+ switch (action) {
562
+ case 'archive': {
563
+ const session = await client.archiveSession(session_id);
564
+ const lines = [
565
+ `## Session Archived`,
566
+ '',
567
+ `- **Session ID:** ${session.id}`,
568
+ `- **Title:** ${session.title}`,
569
+ `- **New Status:** ${session.status}`,
570
+ `- **Archived At:** ${session.archived_at}`,
571
+ ];
572
+ result = lines.join('\n');
573
+ break;
574
+ }
575
+ case 'update_notes': {
576
+ const session = await client.updateSessionNotes(session_id, session_notes);
577
+ const lines = [
578
+ `## Session Notes Updated`,
579
+ '',
580
+ `- **Session ID:** ${session.id}`,
581
+ `- **Title:** ${session.title}`,
582
+ ];
583
+ result = lines.join('\n');
584
+ break;
585
+ }
586
+ case 'update_title': {
587
+ const session = await client.updateSession(session_id, {
588
+ title: title,
589
+ });
590
+ const lines = [
591
+ `## Session Title Updated`,
592
+ '',
593
+ `- **Session ID:** ${session.id}`,
594
+ `- **Title:** ${session.title}`,
595
+ ];
596
+ result = lines.join('\n');
597
+ break;
598
+ }
599
+ default: {
600
+ const _exhaustiveCheck = action;
601
+ return {
602
+ content: [
603
+ {
604
+ type: 'text',
605
+ text: `Error: Unknown action "${_exhaustiveCheck}"`,
606
+ },
607
+ ],
608
+ isError: true,
609
+ };
610
+ }
611
+ }
612
+ return {
613
+ content: [{ type: 'text', text: result }],
614
+ };
615
+ }
616
+ catch (error) {
617
+ return {
618
+ content: [
619
+ {
620
+ type: 'text',
621
+ text: `Error performing action: ${error instanceof Error ? error.message : 'Unknown error'}`,
622
+ },
623
+ ],
624
+ isError: true,
625
+ };
626
+ }
627
+ },
628
+ };
629
+ }
@@ -19,8 +19,8 @@ export declare const ActionTriggerSchema: z.ZodObject<{
19
19
  stop_condition?: string | undefined;
20
20
  mcp_servers?: string[] | undefined;
21
21
  trigger_type?: "slack" | "schedule" | undefined;
22
- name?: string | undefined;
23
22
  id?: number | undefined;
23
+ name?: string | undefined;
24
24
  agent_root_name?: string | undefined;
25
25
  prompt_template?: string | undefined;
26
26
  reuse_session?: boolean | undefined;
@@ -31,8 +31,8 @@ export declare const ActionTriggerSchema: z.ZodObject<{
31
31
  stop_condition?: string | undefined;
32
32
  mcp_servers?: string[] | undefined;
33
33
  trigger_type?: "slack" | "schedule" | undefined;
34
- name?: string | undefined;
35
34
  id?: number | undefined;
35
+ name?: string | undefined;
36
36
  agent_root_name?: string | undefined;
37
37
  prompt_template?: string | undefined;
38
38
  reuse_session?: boolean | undefined;
@@ -24,7 +24,11 @@ const TOOL_DESCRIPTION = `Create, update, delete, or toggle automation triggers.
24
24
 
25
25
  **Trigger types:**
26
26
  - **slack**: Triggered by Slack events (requires configuration with channel_id)
27
- - **schedule**: Triggered on a schedule (requires configuration with interval, unit, etc.)
27
+ - **schedule**: Triggered on a recurring or one-time schedule
28
+
29
+ **Schedule configuration:**
30
+ - **Recurring**: \`{"interval": 2, "unit": "hours", "timezone": "UTC"}\` — fires every N units
31
+ - **One-time**: \`{"scheduled_at": "2026-04-15T14:30:00", "timezone": "America/New_York"}\` — fires once at the specified datetime (ISO 8601), then auto-disables
28
32
 
29
33
  Use search_triggers first to see available triggers and Slack channels.`;
30
34
  export function actionTriggerTool(_server, clientFactory) {
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import type { IAgentOrchestratorClient } from '../orchestrator-client/orchestrator-client.js';
4
4
  export declare const ManageEnqueuedMessagesSchema: z.ZodObject<{
5
5
  session_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
6
- action: z.ZodEnum<["list", "get", "create", "update", "delete", "reorder", "interrupt"]>;
6
+ action: z.ZodEnum<["list", "get", "create", "update", "delete", "reorder", "interrupt", "send_now"]>;
7
7
  message_id: z.ZodOptional<z.ZodNumber>;
8
8
  content: z.ZodOptional<z.ZodString>;
9
9
  stop_condition: z.ZodOptional<z.ZodString>;
@@ -12,7 +12,7 @@ export declare const ManageEnqueuedMessagesSchema: z.ZodObject<{
12
12
  per_page: z.ZodOptional<z.ZodNumber>;
13
13
  }, "strip", z.ZodTypeAny, {
14
14
  session_id: string | number;
15
- action: "list" | "get" | "create" | "update" | "delete" | "reorder" | "interrupt";
15
+ action: "list" | "get" | "create" | "update" | "delete" | "reorder" | "interrupt" | "send_now";
16
16
  per_page?: number | undefined;
17
17
  page?: number | undefined;
18
18
  stop_condition?: string | undefined;
@@ -21,7 +21,7 @@ export declare const ManageEnqueuedMessagesSchema: z.ZodObject<{
21
21
  position?: number | undefined;
22
22
  }, {
23
23
  session_id: string | number;
24
- action: "list" | "get" | "create" | "update" | "delete" | "reorder" | "interrupt";
24
+ action: "list" | "get" | "create" | "update" | "delete" | "reorder" | "interrupt" | "send_now";
25
25
  per_page?: number | undefined;
26
26
  page?: number | undefined;
27
27
  stop_condition?: string | undefined;
@@ -43,7 +43,7 @@ export declare function manageEnqueuedMessagesTool(_server: Server, clientFactor
43
43
  };
44
44
  action: {
45
45
  type: string;
46
- enum: readonly ["list", "get", "create", "update", "delete", "reorder", "interrupt"];
46
+ enum: readonly ["list", "get", "create", "update", "delete", "reorder", "interrupt", "send_now"];
47
47
  description: string;
48
48
  };
49
49
  message_id: {
@@ -1,5 +1,14 @@
1
1
  import { z } from 'zod';
2
- const ACTION_ENUM = ['list', 'get', 'create', 'update', 'delete', 'reorder', 'interrupt'];
2
+ const ACTION_ENUM = [
3
+ 'list',
4
+ 'get',
5
+ 'create',
6
+ 'update',
7
+ 'delete',
8
+ 'reorder',
9
+ 'interrupt',
10
+ 'send_now',
11
+ ];
3
12
  export const ManageEnqueuedMessagesSchema = z.object({
4
13
  session_id: z.union([z.string(), z.number()]),
5
14
  action: z.enum(ACTION_ENUM),
@@ -10,18 +19,23 @@ export const ManageEnqueuedMessagesSchema = z.object({
10
19
  page: z.number().min(1).optional(),
11
20
  per_page: z.number().min(1).max(100).optional(),
12
21
  });
13
- const TOOL_DESCRIPTION = `Manage the enqueued message queue for an agent session.
22
+ const TOOL_DESCRIPTION = `Queue messages for later delivery or send messages immediately to an agent session.
14
23
 
15
- **Actions:**
16
- - **list**: List all enqueued messages for a session (supports pagination)
17
- - **get**: Get a specific enqueued message by ID
18
- - **create**: Add a new message to the queue (requires "content")
19
- - **update**: Update an existing message (requires "message_id")
24
+ **Quick guide — two ways to send a message:**
25
+ - **send_now**: Interrupt the session and deliver a message immediately, even if the session is running. One step — just provide "content".
26
+ - **create**: Add a message to the queue for delivery when the session finishes its current work. Does NOT send immediately.
27
+
28
+ Use "send_now" when you need the agent to act on something urgently. Use "create" when the message can wait until the session is idle.
29
+
30
+ **All actions:**
31
+ - **send_now**: Interrupt the session and deliver a message immediately (requires "content"). The session is paused, the message is sent, and the session resumes with this message. Works regardless of session state.
32
+ - **create**: Add a new message to the end of the queue for later delivery (requires "content"). The message waits until the session becomes idle.
33
+ - **list**: List all enqueued messages for a session (supports pagination with "page" and "per_page")
34
+ - **get**: Get a specific enqueued message by ID (requires "message_id")
35
+ - **update**: Update an existing queued message's content or stop condition (requires "message_id")
20
36
  - **delete**: Remove a message from the queue (requires "message_id")
21
37
  - **reorder**: Change a message's position in the queue (requires "message_id" and "position")
22
- - **interrupt**: Pause the session and send this message immediately (requires "message_id")
23
-
24
- Enqueued messages are follow-up prompts queued to be sent to the agent in order.`;
38
+ - **interrupt**: Pause the session and send an existing queued message immediately (requires "message_id"). Prefer "send_now" for new messages — "interrupt" is for promoting an already-queued message.`;
25
39
  export function manageEnqueuedMessagesTool(_server, clientFactory) {
26
40
  return {
27
41
  name: 'manage_enqueued_messages',
@@ -36,7 +50,7 @@ export function manageEnqueuedMessagesTool(_server, clientFactory) {
36
50
  action: {
37
51
  type: 'string',
38
52
  enum: ACTION_ENUM,
39
- description: 'Action to perform on enqueued messages.',
53
+ description: 'Action to perform. Use "send_now" to interrupt and deliver immediately. Use "create" to queue for later.',
40
54
  },
41
55
  message_id: {
42
56
  type: 'number',
@@ -44,11 +58,11 @@ export function manageEnqueuedMessagesTool(_server, clientFactory) {
44
58
  },
45
59
  content: {
46
60
  type: 'string',
47
- description: 'Message content. Required for create. Optional for update.',
61
+ description: 'Message content. Required for create and send_now. Optional for update.',
48
62
  },
49
63
  stop_condition: {
50
64
  type: 'string',
51
- description: 'Stop condition for this message. Optional for create and update.',
65
+ description: 'Stop condition for this message. Optional for create, send_now, and update.',
52
66
  },
53
67
  position: {
54
68
  type: 'number',
@@ -132,7 +146,9 @@ export function manageEnqueuedMessagesTool(_server, clientFactory) {
132
146
  stop_condition: stop_condition || undefined,
133
147
  });
134
148
  result = [
135
- '## Message Enqueued',
149
+ '## Message Queued',
150
+ '',
151
+ `Message added to queue — it will be delivered when the session becomes idle.`,
136
152
  '',
137
153
  `- **ID:** ${msg.id}`,
138
154
  `- **Position:** ${msg.position}`,
@@ -233,6 +249,33 @@ export function manageEnqueuedMessagesTool(_server, clientFactory) {
233
249
  ].join('\n');
234
250
  break;
235
251
  }
252
+ case 'send_now': {
253
+ if (!content) {
254
+ return {
255
+ content: [
256
+ {
257
+ type: 'text',
258
+ text: 'Error: "content" is required for the "send_now" action.',
259
+ },
260
+ ],
261
+ isError: true,
262
+ };
263
+ }
264
+ const response = await client.followUp(session_id, content, {
265
+ force_immediate: true,
266
+ stop_condition: stop_condition || undefined,
267
+ });
268
+ result = [
269
+ '## Message Sent Immediately',
270
+ '',
271
+ `The session was interrupted and the message was delivered.`,
272
+ '',
273
+ `- **Session ID:** ${response.session.id}`,
274
+ `- **Session Status:** ${response.session.status}`,
275
+ `- **Result:** ${response.message}`,
276
+ ].join('\n');
277
+ break;
278
+ }
236
279
  default: {
237
280
  const _exhaustiveCheck = action;
238
281
  return {
@@ -0,0 +1,61 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { z } from 'zod';
3
+ import type { IAgentOrchestratorClient } from '../orchestrator-client/orchestrator-client.js';
4
+ export declare const WakeMeUpLaterSchema: z.ZodObject<{
5
+ session_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
6
+ wake_at: z.ZodString;
7
+ timezone: z.ZodOptional<z.ZodString>;
8
+ prompt: z.ZodString;
9
+ }, "strip", z.ZodTypeAny, {
10
+ prompt: string;
11
+ session_id: string | number;
12
+ wake_at: string;
13
+ timezone?: string | undefined;
14
+ }, {
15
+ prompt: string;
16
+ session_id: string | number;
17
+ wake_at: string;
18
+ timezone?: string | undefined;
19
+ }>;
20
+ export declare function wakeMeUpLaterTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
21
+ name: string;
22
+ description: string;
23
+ inputSchema: {
24
+ type: "object";
25
+ properties: {
26
+ session_id: {
27
+ oneOf: {
28
+ type: string;
29
+ }[];
30
+ description: string;
31
+ };
32
+ wake_at: {
33
+ type: string;
34
+ description: string;
35
+ };
36
+ timezone: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ prompt: {
41
+ type: string;
42
+ description: string;
43
+ };
44
+ };
45
+ required: string[];
46
+ };
47
+ handler: (args: unknown) => Promise<{
48
+ content: {
49
+ type: string;
50
+ text: string;
51
+ }[];
52
+ isError: boolean;
53
+ } | {
54
+ content: {
55
+ type: string;
56
+ text: string;
57
+ }[];
58
+ isError?: undefined;
59
+ }>;
60
+ };
61
+ //# sourceMappingURL=wake-me-up-later.d.ts.map
@@ -0,0 +1,148 @@
1
+ import { z } from 'zod';
2
+ import { parseAllowedAgentRoots } from '../allowed-agent-roots.js';
3
+ export const WakeMeUpLaterSchema = z.object({
4
+ session_id: z.union([z.string(), z.number()]),
5
+ wake_at: z.string(),
6
+ timezone: z.string().optional(),
7
+ prompt: z.string(),
8
+ });
9
+ function buildToolDescription() {
10
+ const now = new Date();
11
+ const utcNow = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
12
+ return `Schedule this session to be woken up at a specific time. The session will be put to sleep (waiting status) and a one-time trigger will fire at the specified time to resume it with the given prompt. If the session is manually resumed before the scheduled time, the trigger will be silently dropped.
13
+
14
+ **Current server time:** ${utcNow} (UTC). Use this as your reference point when calculating wake-up times.
15
+
16
+ **Timezone handling:**
17
+ - The \`wake_at\` parameter is interpreted in the timezone specified by \`timezone\` (default: "UTC").
18
+ - To schedule "30 minutes from now": take the current UTC time above, add 30 minutes, and pass that as \`wake_at\` with timezone "UTC" (or omit timezone).
19
+ - To schedule at a wall-clock time in a specific timezone (e.g., "9am Eastern"): pass \`wake_at\` as "2026-04-15T09:00:00" with timezone "America/New_York". The server converts to UTC internally.
20
+ - Use IANA timezone names (e.g., "America/New_York", "Europe/London", "Asia/Tokyo"). Do NOT pass UTC offsets like "+05:00" in the timezone parameter.
21
+ - If you omit timezone, wake_at is treated as UTC.
22
+
23
+ **Parameters:**
24
+ - **session_id**: The session to wake up (must be in needs_input state)
25
+ - **wake_at**: ISO 8601 datetime without offset for when to wake up (e.g., "2026-04-15T14:30:00")
26
+ - **timezone**: IANA timezone for interpreting wake_at (default: "UTC", e.g., "America/New_York")
27
+ - **prompt**: The prompt to send when waking up the session
28
+
29
+ **What happens:**
30
+ 1. Validates the session is in needs_input state
31
+ 2. Puts the session to sleep (waiting status)
32
+ 3. Creates a one-time schedule trigger that fires at the specified time
33
+ 4. The trigger resumes the session with the provided prompt`;
34
+ }
35
+ export function wakeMeUpLaterTool(_server, clientFactory) {
36
+ return {
37
+ name: 'wake_me_up_later',
38
+ description: buildToolDescription(),
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ session_id: {
43
+ oneOf: [{ type: 'string' }, { type: 'number' }],
44
+ description: 'Session ID (numeric) or slug (string). Must be in needs_input state.',
45
+ },
46
+ wake_at: {
47
+ type: 'string',
48
+ description: 'ISO 8601 datetime for when to wake up (e.g., "2026-04-15T14:30:00").',
49
+ },
50
+ timezone: {
51
+ type: 'string',
52
+ description: 'Timezone for the wake_at datetime. Default: "UTC".',
53
+ },
54
+ prompt: {
55
+ type: 'string',
56
+ description: 'The prompt to send when waking up the session.',
57
+ },
58
+ },
59
+ required: ['session_id', 'wake_at', 'prompt'],
60
+ },
61
+ handler: async (args) => {
62
+ try {
63
+ const validated = WakeMeUpLaterSchema.parse(args);
64
+ const client = clientFactory();
65
+ const { session_id, wake_at, prompt } = validated;
66
+ const timezone = validated.timezone || 'UTC';
67
+ if (parseAllowedAgentRoots() !== null) {
68
+ return {
69
+ content: [
70
+ {
71
+ type: 'text',
72
+ text: 'Error: wake_me_up_later is not allowed when ALLOWED_AGENT_ROOTS is set. Triggers cannot be created because sessions are restricted to specific preconfigured agent roots.',
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ const session = await client.getSession(session_id);
79
+ if (session.status !== 'needs_input') {
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: `Error: Session must be in "needs_input" state to sleep (current: "${session.status}"). Only idle sessions can be scheduled for a delayed wake-up.`,
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ const sleepingSession = await client.sleepSession(session_id);
91
+ let trigger;
92
+ try {
93
+ trigger = await client.createTrigger({
94
+ name: `Wake session #${sleepingSession.id} at ${wake_at}`,
95
+ trigger_type: 'schedule',
96
+ agent_root_name: session.agent_type,
97
+ prompt_template: prompt,
98
+ reuse_session: true,
99
+ configuration: {
100
+ scheduled_at: wake_at,
101
+ timezone,
102
+ },
103
+ });
104
+ }
105
+ catch (triggerError) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: `Error: Session ${sleepingSession.id} was put to sleep (waiting status) but trigger creation failed: ${triggerError instanceof Error ? triggerError.message : 'Unknown error'}. The session is now in "waiting" state and needs manual intervention (use action_session with "restart" or "follow_up" to recover).`,
111
+ },
112
+ ],
113
+ isError: true,
114
+ };
115
+ }
116
+ await client.updateSession(session_id, {
117
+ custom_metadata: {
118
+ ...session.custom_metadata,
119
+ wake_trigger_id: trigger.id,
120
+ },
121
+ });
122
+ const lines = [
123
+ '## Session Scheduled for Wake-Up',
124
+ '',
125
+ `- **Session ID:** ${sleepingSession.id}`,
126
+ `- **Session Status:** ${sleepingSession.status}`,
127
+ `- **Wake At:** ${wake_at} (${timezone})`,
128
+ `- **Trigger ID:** ${trigger.id}`,
129
+ `- **Trigger Name:** ${trigger.name}`,
130
+ '',
131
+ 'The session is now sleeping. It will be automatically resumed at the scheduled time with the provided prompt.',
132
+ ];
133
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
134
+ }
135
+ catch (error) {
136
+ return {
137
+ content: [
138
+ {
139
+ type: 'text',
140
+ text: `Error scheduling wake-up: ${error instanceof Error ? error.message : 'Unknown error'}`,
141
+ },
142
+ ],
143
+ isError: true,
144
+ };
145
+ }
146
+ },
147
+ };
148
+ }
package/shared/tools.d.ts CHANGED
@@ -2,9 +2,10 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { ClientFactory } from './server.js';
3
3
  /**
4
4
  * Available tool groups for agent-orchestrator.
5
- * Each domain has a base group (full access) and a _readonly variant (read-only).
5
+ * - Domain groups: each domain has a base group (full access) and a _readonly variant (read-only)
6
+ * - Composite groups: curated cross-domain tool sets (e.g., self_session)
6
7
  */
7
- export type ToolGroup = 'sessions' | 'sessions_readonly' | 'notifications' | 'notifications_readonly' | 'triggers' | 'triggers_readonly' | 'health' | 'health_readonly';
8
+ export type ToolGroup = 'sessions' | 'sessions_readonly' | 'notifications' | 'notifications_readonly' | 'triggers' | 'triggers_readonly' | 'health' | 'health_readonly' | 'self_session';
8
9
  /**
9
10
  * Parse enabled tool groups from environment variable or parameter.
10
11
  * @param enabledGroupsParam - Comma-separated list of groups (e.g., "sessions,notifications")
package/shared/tools.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
- // 13 tools across 4 domains
2
+ // 15 tools across 4 domains + 1 composite group
3
3
  import { quickSearchSessionsTool } from './tools/search-sessions.js';
4
4
  import { startSessionTool } from './tools/start-session.js';
5
5
  import { getSessionTool } from './tools/get-session.js';
6
- import { actionSessionTool } from './tools/action-session.js';
6
+ import { actionSessionTool, selfSessionActionSessionTool } from './tools/action-session.js';
7
7
  import { getConfigsTool } from './tools/get-configs.js';
8
8
  import { manageEnqueuedMessagesTool } from './tools/manage-enqueued-messages.js';
9
9
  import { sendPushNotificationTool } from './tools/send-push-notification.js';
@@ -11,11 +11,12 @@ import { getNotificationsTool } from './tools/get-notifications.js';
11
11
  import { actionNotificationTool } from './tools/action-notification.js';
12
12
  import { searchTriggersTool } from './tools/search-triggers.js';
13
13
  import { actionTriggerTool } from './tools/action-trigger.js';
14
+ import { wakeMeUpLaterTool } from './tools/wake-me-up-later.js';
14
15
  import { getSystemHealthTool } from './tools/get-system-health.js';
15
16
  import { actionHealthTool } from './tools/action-health.js';
16
17
  import { getTranscriptArchiveTool } from './tools/get-transcript-archive.js';
17
18
  /**
18
- * All valid tool groups (base groups and their _readonly variants)
19
+ * All valid tool groups (domain groups, their _readonly variants, and composite groups)
19
20
  */
20
21
  const VALID_TOOL_GROUPS = [
21
22
  'sessions',
@@ -26,6 +27,7 @@ const VALID_TOOL_GROUPS = [
26
27
  'triggers_readonly',
27
28
  'health',
28
29
  'health_readonly',
30
+ 'self_session',
29
31
  ];
30
32
  /**
31
33
  * Base groups (without _readonly suffix) - used for default "all groups" behavior
@@ -58,41 +60,91 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
58
60
  /**
59
61
  * All available tools with their group assignments.
60
62
  *
61
- * 14 tools across 4 domains:
63
+ * 15 tools across 4 domains + 1 composite group:
62
64
  * - quick_search_sessions: Quick title-based search/list/get sessions by ID (sessions, read)
63
- * - get_session: Get detailed session info with optional logs/transcripts (sessions, read)
64
- * - get_configs: Fetch all static configuration (sessions, read)
65
+ * - get_session: Get detailed session info with optional logs/transcripts (sessions, read; self_session)
66
+ * - get_configs: Fetch all static configuration (sessions, read; self_session)
65
67
  * - get_transcript_archive: Get transcript archive download URL and metadata (sessions, read)
66
68
  * - start_session: Create a new session (sessions, write)
67
- * - action_session: Perform session actions (sessions, write)
69
+ * - action_session: Perform session actions (sessions, write; self_session: filtered to update_notes, update_title, archive)
68
70
  * - manage_enqueued_messages: Manage session message queue (sessions, write)
69
71
  * - get_notifications: Get/list notifications and badge count (notifications, read)
70
- * - send_push_notification: Send a push notification (notifications, write)
72
+ * - send_push_notification: Send a push notification (notifications, write; self_session)
71
73
  * - action_notification: Mark read, dismiss notifications (notifications, write)
72
74
  * - search_triggers: Search/list automation triggers (triggers, read)
73
75
  * - action_trigger: Create, update, delete, toggle triggers (triggers, write)
76
+ * - wake_me_up_later: Schedule a session to be woken up at a specific time (triggers, write; self_session)
74
77
  * - get_system_health: Get system health report and CLI status (health, read)
75
78
  * - action_health: System maintenance actions (health, write)
76
79
  */
77
80
  const ALL_TOOLS = [
78
81
  // Session tools - read operations
79
- { factory: quickSearchSessionsTool, group: 'sessions', isWriteOperation: false },
80
- { factory: getSessionTool, group: 'sessions', isWriteOperation: false },
81
- { factory: getConfigsTool, group: 'sessions', isWriteOperation: false },
82
- { factory: getTranscriptArchiveTool, group: 'sessions', isWriteOperation: false },
82
+ {
83
+ factory: quickSearchSessionsTool,
84
+ group: 'sessions',
85
+ isWriteOperation: false,
86
+ },
87
+ {
88
+ factory: getSessionTool,
89
+ group: 'sessions',
90
+ isWriteOperation: false,
91
+ compositeGroups: ['self_session'],
92
+ },
93
+ {
94
+ factory: getConfigsTool,
95
+ group: 'sessions',
96
+ isWriteOperation: false,
97
+ compositeGroups: ['self_session'],
98
+ },
99
+ {
100
+ factory: getTranscriptArchiveTool,
101
+ group: 'sessions',
102
+ isWriteOperation: false,
103
+ },
83
104
  // Session tools - write operations
84
105
  { factory: startSessionTool, group: 'sessions', isWriteOperation: true },
85
- { factory: actionSessionTool, group: 'sessions', isWriteOperation: true },
86
- { factory: manageEnqueuedMessagesTool, group: 'sessions', isWriteOperation: true },
106
+ {
107
+ factory: actionSessionTool,
108
+ group: 'sessions',
109
+ isWriteOperation: true,
110
+ compositeGroups: ['self_session'],
111
+ compositeGroupFactoryOverrides: {
112
+ self_session: selfSessionActionSessionTool,
113
+ },
114
+ },
115
+ {
116
+ factory: manageEnqueuedMessagesTool,
117
+ group: 'sessions',
118
+ isWriteOperation: true,
119
+ },
87
120
  // Notification tools - read operations
88
- { factory: getNotificationsTool, group: 'notifications', isWriteOperation: false },
121
+ {
122
+ factory: getNotificationsTool,
123
+ group: 'notifications',
124
+ isWriteOperation: false,
125
+ },
89
126
  // Notification tools - write operations
90
- { factory: sendPushNotificationTool, group: 'notifications', isWriteOperation: true },
91
- { factory: actionNotificationTool, group: 'notifications', isWriteOperation: true },
127
+ {
128
+ factory: sendPushNotificationTool,
129
+ group: 'notifications',
130
+ isWriteOperation: true,
131
+ compositeGroups: ['self_session'],
132
+ },
133
+ {
134
+ factory: actionNotificationTool,
135
+ group: 'notifications',
136
+ isWriteOperation: true,
137
+ },
92
138
  // Trigger tools - read operations
93
139
  { factory: searchTriggersTool, group: 'triggers', isWriteOperation: false },
94
140
  // Trigger tools - write operations
95
141
  { factory: actionTriggerTool, group: 'triggers', isWriteOperation: true },
142
+ {
143
+ factory: wakeMeUpLaterTool,
144
+ group: 'triggers',
145
+ isWriteOperation: true,
146
+ compositeGroups: ['self_session'],
147
+ },
96
148
  // Health tools - read operations
97
149
  { factory: getSystemHealthTool, group: 'health', isWriteOperation: false },
98
150
  // Health tools - write operations
@@ -115,8 +167,48 @@ function shouldIncludeTool(toolDef, enabledGroups) {
115
167
  if (enabledGroups.includes(readonlyGroup) && !toolDef.isWriteOperation) {
116
168
  return true;
117
169
  }
170
+ // Check if any composite group that includes this tool is enabled
171
+ if (toolDef.compositeGroups) {
172
+ for (const compositeGroup of toolDef.compositeGroups) {
173
+ if (enabledGroups.includes(compositeGroup)) {
174
+ return true;
175
+ }
176
+ }
177
+ }
118
178
  return false;
119
179
  }
180
+ /**
181
+ * Determine which factory to use for a tool based on enabled groups.
182
+ * Domain groups (base/readonly) use the default factory. Composite groups
183
+ * may have factory overrides (e.g., restricted action_session for self_session).
184
+ * Domain base group takes precedence over composite group overrides.
185
+ *
186
+ * Priority: base group > readonly group > composite group override > default factory.
187
+ * A write operation with only the readonly group enabled is NOT covered by the
188
+ * domain group — it falls through to composite group override logic.
189
+ */
190
+ function getToolFactory(toolDef, enabledGroups) {
191
+ const baseGroup = toolDef.group;
192
+ const readonlyGroup = `${baseGroup}_readonly`;
193
+ // If included via base group (full access), always use the default factory
194
+ if (enabledGroups.includes(baseGroup)) {
195
+ return toolDef.factory;
196
+ }
197
+ // If included via readonly group AND this is a read operation, use the default factory
198
+ if (enabledGroups.includes(readonlyGroup) && !toolDef.isWriteOperation) {
199
+ return toolDef.factory;
200
+ }
201
+ // Otherwise, the tool is included via a composite group — check for factory overrides
202
+ if (toolDef.compositeGroupFactoryOverrides && toolDef.compositeGroups) {
203
+ for (const compositeGroup of toolDef.compositeGroups) {
204
+ if (enabledGroups.includes(compositeGroup) &&
205
+ toolDef.compositeGroupFactoryOverrides[compositeGroup]) {
206
+ return toolDef.compositeGroupFactoryOverrides[compositeGroup];
207
+ }
208
+ }
209
+ }
210
+ return toolDef.factory;
211
+ }
120
212
  /**
121
213
  * Creates a function to register all tools with the server.
122
214
  * This pattern uses individual tool files for better modularity and testability.
@@ -133,8 +225,11 @@ export function createRegisterTools(clientFactory, enabledGroups) {
133
225
  const enabledToolGroups = parseEnabledToolGroups(enabledGroups);
134
226
  // Filter tools based on enabled groups
135
227
  const enabledTools = ALL_TOOLS.filter((toolDef) => shouldIncludeTool(toolDef, enabledToolGroups));
136
- // Create tool instances
137
- const tools = enabledTools.map((toolDef) => toolDef.factory(server, clientFactory));
228
+ // Create tool instances (using factory overrides for composite groups when applicable)
229
+ const tools = enabledTools.map((toolDef) => {
230
+ const factory = getToolFactory(toolDef, enabledToolGroups);
231
+ return factory(server, clientFactory);
232
+ });
138
233
  // List available tools
139
234
  server.setRequestHandler(ListToolsRequestSchema, async () => {
140
235
  return {