agent-orchestrator-mcp-server 0.8.5 → 0.8.7
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/README.md +11 -10
- package/package.json +2 -2
- package/shared/orchestrator-client/orchestrator-client.d.ts +13 -1
- package/shared/orchestrator-client/orchestrator-client.integration-mock.d.ts +2 -1
- package/shared/orchestrator-client/orchestrator-client.integration-mock.js +114 -0
- package/shared/orchestrator-client/orchestrator-client.js +21 -0
- package/shared/tools/get-session.js +2 -0
- package/shared/tools/manage-categories.d.ts +92 -0
- package/shared/tools/manage-categories.js +227 -0
- package/shared/tools/search-sessions.js +2 -0
- package/shared/tools/start-session.d.ts +2 -2
- package/shared/tools/start-session.js +11 -2
- package/shared/tools.js +9 -2
- package/shared/types.d.ts +37 -0
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ MCP server for PulseMCP's agent-orchestrator: a Claude Code + MCP-powered agent-
|
|
|
30
30
|
| `start_session` | sessions | write | Create and start a new agent session |
|
|
31
31
|
| `action_session` | sessions | write | Perform actions: follow_up, pause, restart, archive, unarchive, change_mcp_servers, change_model, fork, refresh, refresh_all, update_notes, update_title, toggle_favorite, bulk_archive |
|
|
32
32
|
| `manage_enqueued_messages` | sessions | write | Manage session message queue: list, get, create, update, delete, reorder, interrupt |
|
|
33
|
+
| `manage_categories` | sessions | write | Manage dashboard categories that organize sessions: list, create, update, delete, reorder, set_session_category |
|
|
33
34
|
| `get_notifications` | notifications | read | Get/list notifications and badge count |
|
|
34
35
|
| `send_push_notification` | notifications | write | Send a push notification about a session needing human attention |
|
|
35
36
|
| `action_notification` | notifications | write | Mark read, mark all read, dismiss, dismiss all read notifications |
|
|
@@ -56,16 +57,16 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
56
57
|
|
|
57
58
|
Control which tools are available via the `TOOL_GROUPS` environment variable:
|
|
58
59
|
|
|
59
|
-
| Group | Description
|
|
60
|
-
| ------------------------ |
|
|
61
|
-
| `sessions` | All session tools (read + write): search, get, configs, transcript_archive, start, action, enqueued msgs |
|
|
62
|
-
| `sessions_readonly` | Session tools (read only): quick_search_sessions, get_session, get_configs, get_transcript_archive
|
|
63
|
-
| `notifications` | All notification tools (read + write): get, send, mark read, dismiss
|
|
64
|
-
| `notifications_readonly` | Notification tools (read only): get_notifications
|
|
65
|
-
| `triggers` | All trigger tools (read + write): search, create, update, delete, toggle
|
|
66
|
-
| `triggers_readonly` | Trigger tools (read only): search_triggers
|
|
67
|
-
| `health` | All health tools (read + write): health report, CLI status, maintenance
|
|
68
|
-
| `health_readonly` | Health tools (read only): get_system_health
|
|
60
|
+
| Group | Description |
|
|
61
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `sessions` | All session tools (read + write): search, get, configs, transcript_archive, start, action, enqueued msgs, categories |
|
|
63
|
+
| `sessions_readonly` | Session tools (read only): quick_search_sessions, get_session, get_configs, get_transcript_archive |
|
|
64
|
+
| `notifications` | All notification tools (read + write): get, send, mark read, dismiss |
|
|
65
|
+
| `notifications_readonly` | Notification tools (read only): get_notifications |
|
|
66
|
+
| `triggers` | All trigger tools (read + write): search, create, update, delete, toggle |
|
|
67
|
+
| `triggers_readonly` | Trigger tools (read only): search_triggers |
|
|
68
|
+
| `health` | All health tools (read + write): health report, CLI status, maintenance |
|
|
69
|
+
| `health_readonly` | Health tools (read only): get_system_health |
|
|
69
70
|
|
|
70
71
|
**Examples:**
|
|
71
72
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-mcp-server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
4
4
|
"description": "Local implementation of agent-orchestrator MCP server",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.10.6",
|
|
37
|
-
"tsx": "^4.
|
|
37
|
+
"tsx": "^4.22.4",
|
|
38
38
|
"typescript": "^5.7.3"
|
|
39
39
|
},
|
|
40
40
|
"keywords": [
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A client for interacting with the Agent Orchestrator REST API.
|
|
5
5
|
*/
|
|
6
|
-
import type { Session, Log, SubagentTranscript, SessionsResponse, SearchSessionsResponse, SessionActionResponse, LogsResponse, SubagentTranscriptsResponse, CreateSessionRequest, UpdateSessionRequest, CreateLogRequest, UpdateLogRequest, CreateSubagentTranscriptRequest, UpdateSubagentTranscriptRequest, SessionStatus, LogLevel, SubagentStatus, MCPServerInfo, AgentRootInfo, ConfigsResponse, SendPushNotificationResponse, EnqueuedMessage, EnqueuedMessagesResponse, EnqueuedMessageInterruptResponse, EnqueuedMessageStatus, Trigger, TriggerType, TriggerStatus, TriggersResponse, TriggerResponse, TriggerChannelsResponse, CreateTriggerRequest, UpdateTriggerRequest, Notification, NotificationsResponse, NotificationBadgeResponse, NotificationMarkAllReadResponse, NotificationDismissAllReadResponse, HealthReport, HealthActionResponse, CliStatusResponse, CliActionResponse, ForkSessionResponse, RefreshSessionResponse, RefreshAllSessionsResponse, BulkArchiveResponse, TranscriptResponse, TranscriptArchiveStatusResponse } from '../types.js';
|
|
6
|
+
import type { Session, Log, SubagentTranscript, SessionsResponse, SearchSessionsResponse, SessionActionResponse, LogsResponse, SubagentTranscriptsResponse, CreateSessionRequest, UpdateSessionRequest, CreateLogRequest, UpdateLogRequest, CreateSubagentTranscriptRequest, UpdateSubagentTranscriptRequest, SessionStatus, LogLevel, SubagentStatus, MCPServerInfo, AgentRootInfo, ConfigsResponse, SendPushNotificationResponse, EnqueuedMessage, EnqueuedMessagesResponse, EnqueuedMessageInterruptResponse, EnqueuedMessageStatus, Category, CategoriesResponse, CreateCategoryRequest, UpdateCategoryRequest, SetSessionCategoryResponse, Trigger, TriggerType, TriggerStatus, TriggersResponse, TriggerResponse, TriggerChannelsResponse, CreateTriggerRequest, UpdateTriggerRequest, Notification, NotificationsResponse, NotificationBadgeResponse, NotificationMarkAllReadResponse, NotificationDismissAllReadResponse, HealthReport, HealthActionResponse, CliStatusResponse, CliActionResponse, ForkSessionResponse, RefreshSessionResponse, RefreshAllSessionsResponse, BulkArchiveResponse, TranscriptResponse, TranscriptArchiveStatusResponse } from '../types.js';
|
|
7
7
|
/** Raw agent root shape as returned by the Rails API */
|
|
8
8
|
export interface RawAgentRoot {
|
|
9
9
|
name: string;
|
|
@@ -107,6 +107,12 @@ export interface IAgentOrchestratorClient {
|
|
|
107
107
|
deleteEnqueuedMessage(sessionId: string | number, messageId: number): Promise<void>;
|
|
108
108
|
reorderEnqueuedMessage(sessionId: string | number, messageId: number, position: number): Promise<EnqueuedMessage>;
|
|
109
109
|
interruptEnqueuedMessage(sessionId: string | number, messageId: number): Promise<EnqueuedMessageInterruptResponse>;
|
|
110
|
+
listCategories(): Promise<CategoriesResponse>;
|
|
111
|
+
createCategory(data: CreateCategoryRequest): Promise<Category>;
|
|
112
|
+
updateCategory(id: number, data: UpdateCategoryRequest): Promise<Category>;
|
|
113
|
+
deleteCategory(id: number): Promise<void>;
|
|
114
|
+
reorderCategories(ids: Array<number | 'uncategorized'>): Promise<CategoriesResponse>;
|
|
115
|
+
setSessionCategory(sessionId: string | number, categoryId: number | null): Promise<SetSessionCategoryResponse>;
|
|
110
116
|
listTriggers(options?: {
|
|
111
117
|
trigger_type?: TriggerType;
|
|
112
118
|
status?: TriggerStatus;
|
|
@@ -234,6 +240,12 @@ export declare class AgentOrchestratorClient implements IAgentOrchestratorClient
|
|
|
234
240
|
deleteEnqueuedMessage(sessionId: string | number, messageId: number): Promise<void>;
|
|
235
241
|
reorderEnqueuedMessage(sessionId: string | number, messageId: number, position: number): Promise<EnqueuedMessage>;
|
|
236
242
|
interruptEnqueuedMessage(sessionId: string | number, messageId: number): Promise<EnqueuedMessageInterruptResponse>;
|
|
243
|
+
listCategories(): Promise<CategoriesResponse>;
|
|
244
|
+
createCategory(data: CreateCategoryRequest): Promise<Category>;
|
|
245
|
+
updateCategory(id: number, data: UpdateCategoryRequest): Promise<Category>;
|
|
246
|
+
deleteCategory(id: number): Promise<void>;
|
|
247
|
+
reorderCategories(ids: Array<number | 'uncategorized'>): Promise<CategoriesResponse>;
|
|
248
|
+
setSessionCategory(sessionId: string | number, categoryId: number | null): Promise<SetSessionCategoryResponse>;
|
|
237
249
|
listTriggers(options?: {
|
|
238
250
|
trigger_type?: TriggerType;
|
|
239
251
|
status?: TriggerStatus;
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Used in integration tests to simulate API responses without hitting real endpoints.
|
|
5
5
|
*/
|
|
6
6
|
import type { IAgentOrchestratorClient } from './orchestrator-client.js';
|
|
7
|
-
import type { Session, Log, SubagentTranscript } from '../types.js';
|
|
7
|
+
import type { Session, Log, SubagentTranscript, Category } from '../types.js';
|
|
8
8
|
interface MockData {
|
|
9
9
|
sessions?: Session[];
|
|
10
10
|
logs?: Log[];
|
|
11
11
|
subagentTranscripts?: SubagentTranscript[];
|
|
12
|
+
categories?: Category[];
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* Creates a mock Agent Orchestrator client for integration testing.
|
|
@@ -64,10 +64,23 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
|
|
|
64
64
|
updated_at: '2025-01-15T14:31:45Z',
|
|
65
65
|
},
|
|
66
66
|
],
|
|
67
|
+
categories: initialMockData?.categories || [
|
|
68
|
+
{
|
|
69
|
+
id: 1,
|
|
70
|
+
name: 'Active Work',
|
|
71
|
+
description: null,
|
|
72
|
+
position: 0,
|
|
73
|
+
is_frozen: false,
|
|
74
|
+
session_count: 1,
|
|
75
|
+
created_at: '2025-01-15T14:00:00Z',
|
|
76
|
+
updated_at: '2025-01-15T14:00:00Z',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
67
79
|
};
|
|
68
80
|
let sessionIdCounter = mockData.sessions?.length || 1;
|
|
69
81
|
let logIdCounter = mockData.logs?.length || 1;
|
|
70
82
|
let transcriptIdCounter = mockData.subagentTranscripts?.length || 1;
|
|
83
|
+
let categoryIdCounter = mockData.categories?.length || 1;
|
|
71
84
|
return {
|
|
72
85
|
mockData,
|
|
73
86
|
async listSessions(options) {
|
|
@@ -598,6 +611,107 @@ export function createIntegrationMockOrchestratorClient(initialMockData) {
|
|
|
598
611
|
throw new Error(`API Error (404): Session not found`);
|
|
599
612
|
return { session, message: 'Message sent as interrupt' };
|
|
600
613
|
},
|
|
614
|
+
// Categories
|
|
615
|
+
async listCategories() {
|
|
616
|
+
const categories = [...(mockData.categories || [])].sort((a, b) => a.position - b.position);
|
|
617
|
+
return { categories };
|
|
618
|
+
},
|
|
619
|
+
async createCategory(data) {
|
|
620
|
+
const exists = mockData.categories?.some((c) => c.name.toLowerCase() === data.name.toLowerCase());
|
|
621
|
+
if (exists) {
|
|
622
|
+
throw new Error(`API Error (422): Name has already been taken`);
|
|
623
|
+
}
|
|
624
|
+
categoryIdCounter++;
|
|
625
|
+
const maxPosition = (mockData.categories || []).reduce((max, c) => Math.max(max, c.position), -1);
|
|
626
|
+
const category = {
|
|
627
|
+
id: categoryIdCounter,
|
|
628
|
+
name: data.name,
|
|
629
|
+
description: data.description ? data.description : null,
|
|
630
|
+
position: maxPosition + 1,
|
|
631
|
+
is_frozen: false,
|
|
632
|
+
session_count: 0,
|
|
633
|
+
created_at: new Date().toISOString(),
|
|
634
|
+
updated_at: new Date().toISOString(),
|
|
635
|
+
};
|
|
636
|
+
mockData.categories?.push(category);
|
|
637
|
+
return category;
|
|
638
|
+
},
|
|
639
|
+
async updateCategory(id, data) {
|
|
640
|
+
const category = mockData.categories?.find((c) => c.id === id);
|
|
641
|
+
if (!category) {
|
|
642
|
+
throw new Error(`API Error (404): Category not found`);
|
|
643
|
+
}
|
|
644
|
+
if (data.name !== undefined)
|
|
645
|
+
category.name = data.name;
|
|
646
|
+
if (data.description !== undefined) {
|
|
647
|
+
category.description = data.description ? data.description : null;
|
|
648
|
+
}
|
|
649
|
+
if (data.is_frozen !== undefined)
|
|
650
|
+
category.is_frozen = data.is_frozen;
|
|
651
|
+
category.updated_at = new Date().toISOString();
|
|
652
|
+
return category;
|
|
653
|
+
},
|
|
654
|
+
async deleteCategory(id) {
|
|
655
|
+
const index = mockData.categories?.findIndex((c) => c.id === id);
|
|
656
|
+
if (index === undefined || index === -1) {
|
|
657
|
+
throw new Error(`API Error (404): Category not found`);
|
|
658
|
+
}
|
|
659
|
+
mockData.categories?.splice(index, 1);
|
|
660
|
+
// Member sessions fall back to Uncategorized
|
|
661
|
+
mockData.sessions?.forEach((s) => {
|
|
662
|
+
if (s.category_id === id) {
|
|
663
|
+
s.category_id = null;
|
|
664
|
+
s.category = null;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
async reorderCategories(ids) {
|
|
669
|
+
// Apply new positions for the numeric ids in the order provided.
|
|
670
|
+
// The "uncategorized" sentinel positions the Uncategorized section and is
|
|
671
|
+
// not represented in the returned category list. Categories omitted from
|
|
672
|
+
// `ids` keep their stored position. This is an approximation of the
|
|
673
|
+
// server's ordering; real position bookkeeping lives Rails-side.
|
|
674
|
+
let position = 0;
|
|
675
|
+
for (const id of ids) {
|
|
676
|
+
if (id === 'uncategorized') {
|
|
677
|
+
position++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const category = mockData.categories?.find((c) => c.id === id);
|
|
681
|
+
if (category) {
|
|
682
|
+
category.position = position;
|
|
683
|
+
category.updated_at = new Date().toISOString();
|
|
684
|
+
}
|
|
685
|
+
position++;
|
|
686
|
+
}
|
|
687
|
+
const categories = [...(mockData.categories || [])].sort((a, b) => a.position - b.position);
|
|
688
|
+
return { categories };
|
|
689
|
+
},
|
|
690
|
+
async setSessionCategory(sessionId, categoryId) {
|
|
691
|
+
const session = mockData.sessions?.find((s) => s.id === Number(sessionId) || s.slug === String(sessionId));
|
|
692
|
+
if (!session) {
|
|
693
|
+
throw new Error(`API Error (404): Session not found`);
|
|
694
|
+
}
|
|
695
|
+
if (categoryId == null) {
|
|
696
|
+
session.category_id = null;
|
|
697
|
+
session.category = null;
|
|
698
|
+
session.updated_at = new Date().toISOString();
|
|
699
|
+
return { session, message: 'Session moved to Uncategorized' };
|
|
700
|
+
}
|
|
701
|
+
const category = mockData.categories?.find((c) => c.id === categoryId);
|
|
702
|
+
if (!category) {
|
|
703
|
+
throw new Error(`API Error (404): Category #${categoryId} not found`);
|
|
704
|
+
}
|
|
705
|
+
session.category_id = category.id;
|
|
706
|
+
session.category = {
|
|
707
|
+
id: category.id,
|
|
708
|
+
name: category.name,
|
|
709
|
+
position: category.position,
|
|
710
|
+
is_frozen: category.is_frozen,
|
|
711
|
+
};
|
|
712
|
+
session.updated_at = new Date().toISOString();
|
|
713
|
+
return { session, message: 'Session assigned to category' };
|
|
714
|
+
},
|
|
601
715
|
// Triggers
|
|
602
716
|
async listTriggers(options) {
|
|
603
717
|
const page = options?.page || 1;
|
|
@@ -354,6 +354,27 @@ export class AgentOrchestratorClient {
|
|
|
354
354
|
async interruptEnqueuedMessage(sessionId, messageId) {
|
|
355
355
|
return this.request('POST', `/sessions/${sessionId}/enqueued_messages/${messageId}/interrupt`);
|
|
356
356
|
}
|
|
357
|
+
// Categories
|
|
358
|
+
async listCategories() {
|
|
359
|
+
return this.request('GET', '/categories');
|
|
360
|
+
}
|
|
361
|
+
async createCategory(data) {
|
|
362
|
+
const response = await this.request('POST', '/categories', data);
|
|
363
|
+
return response.category;
|
|
364
|
+
}
|
|
365
|
+
async updateCategory(id, data) {
|
|
366
|
+
const response = await this.request('PATCH', `/categories/${id}`, data);
|
|
367
|
+
return response.category;
|
|
368
|
+
}
|
|
369
|
+
async deleteCategory(id) {
|
|
370
|
+
await this.request('DELETE', `/categories/${id}`);
|
|
371
|
+
}
|
|
372
|
+
async reorderCategories(ids) {
|
|
373
|
+
return this.request('POST', '/categories/reorder', { ids });
|
|
374
|
+
}
|
|
375
|
+
async setSessionCategory(sessionId, categoryId) {
|
|
376
|
+
return this.request('PATCH', `/sessions/${sessionId}/set_category`, { category_id: categoryId });
|
|
377
|
+
}
|
|
357
378
|
// Triggers
|
|
358
379
|
async listTriggers(options) {
|
|
359
380
|
const { trigger_type, ...rest } = options ?? {};
|
|
@@ -58,6 +58,8 @@ function formatSessionDetails(session, includeTranscript) {
|
|
|
58
58
|
];
|
|
59
59
|
if (session.slug)
|
|
60
60
|
lines.push(`- **Slug:** ${session.slug}`);
|
|
61
|
+
if (session.category)
|
|
62
|
+
lines.push(`- **Category:** ${session.category.name}`);
|
|
61
63
|
lines.push('');
|
|
62
64
|
lines.push('### Git Configuration');
|
|
63
65
|
if (session.git_root)
|
|
@@ -0,0 +1,92 @@
|
|
|
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 ManageCategoriesSchema: z.ZodObject<{
|
|
5
|
+
action: z.ZodEnum<["list", "create", "update", "delete", "reorder", "set_session_category"]>;
|
|
6
|
+
category_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
7
|
+
name: z.ZodOptional<z.ZodString>;
|
|
8
|
+
description: z.ZodOptional<z.ZodString>;
|
|
9
|
+
is_frozen: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodNumber, z.ZodLiteral<"uncategorized">]>, "many">>;
|
|
11
|
+
session_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber]>>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
action: "list" | "create" | "update" | "delete" | "reorder" | "set_session_category";
|
|
14
|
+
description?: string | undefined;
|
|
15
|
+
name?: string | undefined;
|
|
16
|
+
session_id?: string | number | undefined;
|
|
17
|
+
category_id?: number | null | undefined;
|
|
18
|
+
is_frozen?: boolean | undefined;
|
|
19
|
+
ids?: (number | "uncategorized")[] | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
action: "list" | "create" | "update" | "delete" | "reorder" | "set_session_category";
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
name?: string | undefined;
|
|
24
|
+
session_id?: string | number | undefined;
|
|
25
|
+
category_id?: number | null | undefined;
|
|
26
|
+
is_frozen?: boolean | undefined;
|
|
27
|
+
ids?: (number | "uncategorized")[] | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function manageCategoriesTool(_server: Server, clientFactory: () => IAgentOrchestratorClient): {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object";
|
|
34
|
+
properties: {
|
|
35
|
+
action: {
|
|
36
|
+
type: string;
|
|
37
|
+
enum: readonly ["list", "create", "update", "delete", "reorder", "set_session_category"];
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
category_id: {
|
|
41
|
+
type: string[];
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
name: {
|
|
45
|
+
type: string;
|
|
46
|
+
description: string;
|
|
47
|
+
};
|
|
48
|
+
description: {
|
|
49
|
+
type: string;
|
|
50
|
+
description: string;
|
|
51
|
+
};
|
|
52
|
+
is_frozen: {
|
|
53
|
+
type: string;
|
|
54
|
+
description: string;
|
|
55
|
+
};
|
|
56
|
+
ids: {
|
|
57
|
+
type: string;
|
|
58
|
+
items: {
|
|
59
|
+
oneOf: ({
|
|
60
|
+
type: string;
|
|
61
|
+
enum?: undefined;
|
|
62
|
+
} | {
|
|
63
|
+
type: string;
|
|
64
|
+
enum: string[];
|
|
65
|
+
})[];
|
|
66
|
+
};
|
|
67
|
+
description: string;
|
|
68
|
+
};
|
|
69
|
+
session_id: {
|
|
70
|
+
oneOf: {
|
|
71
|
+
type: string;
|
|
72
|
+
}[];
|
|
73
|
+
description: string;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
required: string[];
|
|
77
|
+
};
|
|
78
|
+
handler: (args: unknown) => Promise<{
|
|
79
|
+
content: {
|
|
80
|
+
type: string;
|
|
81
|
+
text: string;
|
|
82
|
+
}[];
|
|
83
|
+
isError: boolean;
|
|
84
|
+
} | {
|
|
85
|
+
content: {
|
|
86
|
+
type: string;
|
|
87
|
+
text: string;
|
|
88
|
+
}[];
|
|
89
|
+
isError?: undefined;
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=manage-categories.d.ts.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const ACTION_ENUM = [
|
|
3
|
+
'list',
|
|
4
|
+
'create',
|
|
5
|
+
'update',
|
|
6
|
+
'delete',
|
|
7
|
+
'reorder',
|
|
8
|
+
'set_session_category',
|
|
9
|
+
];
|
|
10
|
+
export const ManageCategoriesSchema = z.object({
|
|
11
|
+
action: z.enum(ACTION_ENUM),
|
|
12
|
+
category_id: z.number().nullable().optional(),
|
|
13
|
+
name: z.string().optional(),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
is_frozen: z.boolean().optional(),
|
|
16
|
+
ids: z.array(z.union([z.number(), z.literal('uncategorized')])).optional(),
|
|
17
|
+
session_id: z.union([z.string(), z.number()]).optional(),
|
|
18
|
+
});
|
|
19
|
+
const TOOL_DESCRIPTION = `Manage categories used to organize sessions on the Agent Orchestrator dashboard.
|
|
20
|
+
|
|
21
|
+
Categories are the named sections sessions are grouped under. Sessions not assigned to a category fall under the built-in "Uncategorized" section.
|
|
22
|
+
|
|
23
|
+
**Actions:**
|
|
24
|
+
- **list**: List all categories ordered by position, with session counts.
|
|
25
|
+
- **create**: Create a new category (requires "name"; optional "description"). Names are unique case-insensitively (max 100 chars); description max 1000 chars.
|
|
26
|
+
- **update**: Rename, re-describe, or freeze/unfreeze a category (requires "category_id"; any subset of "name", "description", "is_frozen"). Omitted fields are left unchanged.
|
|
27
|
+
- **delete**: Delete a category (requires "category_id"). Sessions in it fall back to Uncategorized.
|
|
28
|
+
- **reorder**: Set the top-to-bottom order of categories (requires "ids" — an array of category IDs). Categories omitted keep their existing position. Include the string "uncategorized" to position the Uncategorized section.
|
|
29
|
+
- **set_session_category**: Assign a session to a category (requires "session_id"; "category_id" to assign, or omit/null to clear to Uncategorized).
|
|
30
|
+
|
|
31
|
+
**Note:** All freeze state uses "is_frozen".`;
|
|
32
|
+
export function manageCategoriesTool(_server, clientFactory) {
|
|
33
|
+
return {
|
|
34
|
+
name: 'manage_categories',
|
|
35
|
+
description: TOOL_DESCRIPTION,
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
action: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ACTION_ENUM,
|
|
42
|
+
description: 'The category management action to perform.',
|
|
43
|
+
},
|
|
44
|
+
category_id: {
|
|
45
|
+
type: ['number', 'null'],
|
|
46
|
+
description: 'Category ID. Required for "update" and "delete". For "set_session_category", the target category to assign (omit or null to clear to Uncategorized).',
|
|
47
|
+
},
|
|
48
|
+
name: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'Category name. Required for "create"; optional for "update". Unique case-insensitively, max 100 chars.',
|
|
51
|
+
},
|
|
52
|
+
description: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Category description. Optional for "create" and "update". Max 1000 chars; blank clears it.',
|
|
55
|
+
},
|
|
56
|
+
is_frozen: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
description: 'Freeze (true) or unfreeze (false) the category. Optional for "update".',
|
|
59
|
+
},
|
|
60
|
+
ids: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: { oneOf: [{ type: 'number' }, { type: 'string', enum: ['uncategorized'] }] },
|
|
63
|
+
description: 'Required for "reorder". New top-to-bottom order of category IDs. Categories omitted keep their position. Use the string "uncategorized" to position the Uncategorized section.',
|
|
64
|
+
},
|
|
65
|
+
session_id: {
|
|
66
|
+
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
67
|
+
description: 'Session ID (numeric) or slug (string). Required for "set_session_category".',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ['action'],
|
|
71
|
+
},
|
|
72
|
+
handler: async (args) => {
|
|
73
|
+
try {
|
|
74
|
+
const validated = ManageCategoriesSchema.parse(args);
|
|
75
|
+
const client = clientFactory();
|
|
76
|
+
const { action, category_id, name, description, is_frozen, ids, session_id } = validated;
|
|
77
|
+
const formatCategory = (cat) => [
|
|
78
|
+
`### ${cat.name} (ID: ${cat.id})`,
|
|
79
|
+
`- **Position:** ${cat.position}`,
|
|
80
|
+
`- **Frozen:** ${cat.is_frozen}`,
|
|
81
|
+
cat.description ? `- **Description:** ${cat.description}` : null,
|
|
82
|
+
`- **Sessions:** ${cat.session_count}`,
|
|
83
|
+
]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join('\n');
|
|
86
|
+
let result;
|
|
87
|
+
switch (action) {
|
|
88
|
+
case 'list': {
|
|
89
|
+
const response = await client.listCategories();
|
|
90
|
+
if (response.categories.length === 0) {
|
|
91
|
+
result = '## Categories\n\nNo categories found.';
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result = [
|
|
95
|
+
`## Categories (${response.categories.length})`,
|
|
96
|
+
'',
|
|
97
|
+
...response.categories.map(formatCategory),
|
|
98
|
+
].join('\n\n');
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case 'create': {
|
|
103
|
+
if (!name) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{ type: 'text', text: 'Error: "name" is required for the "create" action.' },
|
|
107
|
+
],
|
|
108
|
+
isError: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const cat = await client.createCategory({ name, description });
|
|
112
|
+
result = ['## Category Created', '', formatCategory(cat)].join('\n');
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'update': {
|
|
116
|
+
if (category_id == null) {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: 'Error: "category_id" is required for the "update" action.',
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
isError: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (name === undefined && description === undefined && is_frozen === undefined) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: 'Error: provide at least one of "name", "description", or "is_frozen" for the "update" action.',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const cat = await client.updateCategory(category_id, {
|
|
139
|
+
...(name !== undefined && { name }),
|
|
140
|
+
...(description !== undefined && { description }),
|
|
141
|
+
...(is_frozen !== undefined && { is_frozen }),
|
|
142
|
+
});
|
|
143
|
+
result = ['## Category Updated', '', formatCategory(cat)].join('\n');
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'delete': {
|
|
147
|
+
if (category_id == null) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: 'Error: "category_id" is required for the "delete" action.',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
await client.deleteCategory(category_id);
|
|
159
|
+
result = `## Category Deleted\n\nCategory ${category_id} has been deleted. Its sessions fall back to Uncategorized.`;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'reorder': {
|
|
163
|
+
if (!ids || ids.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: 'Error: "ids" (a non-empty array) is required for the "reorder" action.',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
isError: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const response = await client.reorderCategories(ids);
|
|
175
|
+
result = [
|
|
176
|
+
'## Categories Reordered',
|
|
177
|
+
'',
|
|
178
|
+
...response.categories.map(formatCategory),
|
|
179
|
+
].join('\n\n');
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'set_session_category': {
|
|
183
|
+
if (session_id === undefined) {
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: 'Error: "session_id" is required for the "set_session_category" action.',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
isError: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const response = await client.setSessionCategory(session_id, category_id ?? null);
|
|
195
|
+
result = [
|
|
196
|
+
'## Session Category Updated',
|
|
197
|
+
'',
|
|
198
|
+
`- **Session ID:** ${response.session.id}`,
|
|
199
|
+
`- **Category:** ${response.session.category ? response.session.category.name : 'Uncategorized'}`,
|
|
200
|
+
`- **Result:** ${response.message}`,
|
|
201
|
+
].join('\n');
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
default: {
|
|
205
|
+
const _exhaustiveCheck = action;
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: `Error: Unknown action "${_exhaustiveCheck}"` }],
|
|
208
|
+
isError: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { content: [{ type: 'text', text: result }] };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: `Error managing categories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -49,6 +49,8 @@ function formatSession(session) {
|
|
|
49
49
|
];
|
|
50
50
|
if (session.slug)
|
|
51
51
|
lines.push(`- **Slug:** ${session.slug}`);
|
|
52
|
+
if (session.category)
|
|
53
|
+
lines.push(`- **Category:** ${session.category.name}`);
|
|
52
54
|
if (session.git_root)
|
|
53
55
|
lines.push(`- **Repository:** ${session.git_root}`);
|
|
54
56
|
if (session.branch)
|
|
@@ -52,7 +52,7 @@ export declare function startSessionTool(_server: Server, clientFactory: () => I
|
|
|
52
52
|
properties: {
|
|
53
53
|
agent_runtime: {
|
|
54
54
|
type: string;
|
|
55
|
-
description:
|
|
55
|
+
description: string;
|
|
56
56
|
};
|
|
57
57
|
prompt: {
|
|
58
58
|
type: string;
|
|
@@ -102,7 +102,7 @@ export declare function startSessionTool(_server: Server, clientFactory: () => I
|
|
|
102
102
|
};
|
|
103
103
|
config: {
|
|
104
104
|
type: string;
|
|
105
|
-
description:
|
|
105
|
+
description: string;
|
|
106
106
|
};
|
|
107
107
|
custom_metadata: {
|
|
108
108
|
type: string;
|
|
@@ -2,7 +2,10 @@ import { z } from 'zod';
|
|
|
2
2
|
import { parseAllowedAgentRoots, validateAgentRootConstraints } from '../allowed-agent-roots.js';
|
|
3
3
|
import { getConfigsCache, setConfigsCache } from '../cache/configs-cache.js';
|
|
4
4
|
const PARAM_DESCRIPTIONS = {
|
|
5
|
-
agent_runtime: 'Per-spawn agent runtime override.
|
|
5
|
+
agent_runtime: 'Per-spawn agent runtime override. Valid values are "claude_code" (Claude Code) and "codex" (OpenAI Codex CLI). ' +
|
|
6
|
+
'When omitted, the session adopts the agent_root\'s default_runtime, falling back to "claude_code". ' +
|
|
7
|
+
"Call get_configs to see each agent root's default_runtime. Pair with `config.model` to pick a model valid for the chosen runtime " +
|
|
8
|
+
'(e.g. "opus"/"sonnet"/"haiku" for claude_code, "gpt-5.5"/"gpt-5.4" for codex).',
|
|
6
9
|
prompt: 'Initial prompt for the agent. If provided, the agent job is automatically queued. Omit for a clone-only session.',
|
|
7
10
|
agent_root: 'Agent root name from get_configs. The API resolves git_root, branch, subdirectory, default_model, and other defaults from the agent root configuration. Always pass this so the session inherits the correct repository, model, and settings.',
|
|
8
11
|
title: 'STRONGLY RECOMMENDED: Always set a title — treat it as effectively required. ' +
|
|
@@ -16,7 +19,11 @@ const PARAM_DESCRIPTIONS = {
|
|
|
16
19
|
mcp_servers: 'List of MCP server names to enable for this session. Example: ["github-development", "slack"]',
|
|
17
20
|
skills: 'List of skill names to enable for this session. Always include the agent root\'s default_skills from get_configs as the starting point — omitting skills means the session gets none. Add extras as needed; removing a default should be rare and intentional. Example: ["discovery-classify", "publish-and-pr"]',
|
|
18
21
|
plugins: 'List of plugin names to enable for this session. Plugins extend agent capabilities with additional integrations. Example: ["my-plugin"]',
|
|
19
|
-
config: 'Additional configuration as a JSON object.'
|
|
22
|
+
config: 'Additional configuration as a JSON object. Use `config.model` to choose the agent model for this session ' +
|
|
23
|
+
'(e.g. {"model": "gpt-5.4"} for a codex runtime, or {"model": "sonnet"} for claude_code). ' +
|
|
24
|
+
"The model must be valid for the resolved agent_runtime; call get_configs to see each agent root's default_model. " +
|
|
25
|
+
"When omitted, the session uses the agent root's default_model (or the runtime's default model). " +
|
|
26
|
+
"An explicit config.model always takes precedence over the agent root's default_model.",
|
|
20
27
|
custom_metadata: 'User-defined metadata as a JSON object. Useful for tracking tickets, projects, etc.',
|
|
21
28
|
auto_compact_window: 'Optional per-session auto-compact (context) window override, in tokens. ' +
|
|
22
29
|
'**You should generally rely on the default of 200,000** — omit this parameter and the API default applies. ' +
|
|
@@ -59,6 +66,8 @@ const TOOL_DESCRIPTION = `Start a new agent session in the Agent Orchestrator.
|
|
|
59
66
|
- **MCP servers:** Start with \`default_mcp_servers\`. Drop servers the task doesn't need (least-privilege). Add extras when the task requires tools beyond the defaults. When \`ALLOWED_AGENT_ROOTS\` is active, you cannot add servers beyond the defaults.
|
|
60
67
|
- **Skills:** Start with \`default_skills\`. You can freely add skills beyond the defaults. Removing a default skill should be rare and intentional — only when you have a specific reason, like replacing a skill with a more capable variant that covers the same ground. Skills are lightweight text files with no blast radius, so keeping all defaults costs nothing.
|
|
61
68
|
|
|
69
|
+
**Runtime and model selection:** Pass \`agent_runtime\` to override which agent runtime the session uses — \`claude_code\` (Claude Code) or \`codex\` (OpenAI Codex CLI). Pass \`config: { model: "..." }\` to choose the model (e.g. \`opus\`/\`sonnet\`/\`haiku\` for claude_code, \`gpt-5.5\`/\`gpt-5.4\` for codex). Both are optional: when omitted, the session inherits the agent root's \`default_runtime\` and \`default_model\`. Call get_configs to discover each root's defaults and pick a model that is valid for the chosen runtime.
|
|
70
|
+
|
|
62
71
|
**Use cases:**
|
|
63
72
|
- Start a new agent task on a repository
|
|
64
73
|
- Create a session to work on a specific branch
|
package/shared/tools.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
//
|
|
2
|
+
// 17 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
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
|
+
import { manageCategoriesTool } from './tools/manage-categories.js';
|
|
9
10
|
import { sendPushNotificationTool } from './tools/send-push-notification.js';
|
|
10
11
|
import { getNotificationsTool } from './tools/get-notifications.js';
|
|
11
12
|
import { actionNotificationTool } from './tools/action-notification.js';
|
|
@@ -61,7 +62,7 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
|
|
|
61
62
|
/**
|
|
62
63
|
* All available tools with their group assignments.
|
|
63
64
|
*
|
|
64
|
-
*
|
|
65
|
+
* 17 tools across 4 domains + 1 composite group:
|
|
65
66
|
* - quick_search_sessions: Quick title-based search/list/get sessions by ID (sessions, read)
|
|
66
67
|
* - get_session: Get detailed session info with optional logs/transcripts (sessions, read; self_session)
|
|
67
68
|
* - get_configs: Fetch all static configuration (sessions, read; self_session)
|
|
@@ -69,6 +70,7 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
|
|
|
69
70
|
* - start_session: Create a new session (sessions, write)
|
|
70
71
|
* - action_session: Perform session actions (sessions, write; self_session: filtered to update_notes, update_title, archive)
|
|
71
72
|
* - manage_enqueued_messages: Manage session message queue (sessions, write)
|
|
73
|
+
* - manage_categories: Manage dashboard categories and session assignment (sessions, write)
|
|
72
74
|
* - get_notifications: Get/list notifications and badge count (notifications, read)
|
|
73
75
|
* - send_push_notification: Send a push notification (notifications, write; self_session)
|
|
74
76
|
* - action_notification: Mark read, dismiss notifications (notifications, write)
|
|
@@ -119,6 +121,11 @@ const ALL_TOOLS = [
|
|
|
119
121
|
group: 'sessions',
|
|
120
122
|
isWriteOperation: true,
|
|
121
123
|
},
|
|
124
|
+
{
|
|
125
|
+
factory: manageCategoriesTool,
|
|
126
|
+
group: 'sessions',
|
|
127
|
+
isWriteOperation: true,
|
|
128
|
+
},
|
|
122
129
|
// Notification tools - read operations
|
|
123
130
|
{
|
|
124
131
|
factory: getNotificationsTool,
|
package/shared/types.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface Session {
|
|
|
32
32
|
job_id: string | null;
|
|
33
33
|
running_job_id: string | null;
|
|
34
34
|
archived_at: string | null;
|
|
35
|
+
category_id?: number | null;
|
|
36
|
+
category?: CategorySummary | null;
|
|
35
37
|
created_at: string;
|
|
36
38
|
updated_at: string;
|
|
37
39
|
transcript?: string;
|
|
@@ -193,6 +195,41 @@ export interface UpdateSubagentTranscriptRequest {
|
|
|
193
195
|
total_tokens?: number;
|
|
194
196
|
tool_use_count?: number;
|
|
195
197
|
}
|
|
198
|
+
export interface Category {
|
|
199
|
+
id: number;
|
|
200
|
+
name: string;
|
|
201
|
+
description: string | null;
|
|
202
|
+
position: number;
|
|
203
|
+
is_frozen: boolean;
|
|
204
|
+
session_count: number;
|
|
205
|
+
created_at: string;
|
|
206
|
+
updated_at: string;
|
|
207
|
+
}
|
|
208
|
+
export interface CategorySummary {
|
|
209
|
+
id: number;
|
|
210
|
+
name: string;
|
|
211
|
+
position: number;
|
|
212
|
+
is_frozen: boolean;
|
|
213
|
+
}
|
|
214
|
+
export interface CategoriesResponse {
|
|
215
|
+
categories: Category[];
|
|
216
|
+
}
|
|
217
|
+
export interface CategoryResponse {
|
|
218
|
+
category: Category;
|
|
219
|
+
}
|
|
220
|
+
export interface CreateCategoryRequest {
|
|
221
|
+
name: string;
|
|
222
|
+
description?: string;
|
|
223
|
+
}
|
|
224
|
+
export interface UpdateCategoryRequest {
|
|
225
|
+
name?: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
is_frozen?: boolean;
|
|
228
|
+
}
|
|
229
|
+
export interface SetSessionCategoryResponse {
|
|
230
|
+
session: Session;
|
|
231
|
+
message: string;
|
|
232
|
+
}
|
|
196
233
|
export type EnqueuedMessageStatus = 'pending' | 'processing' | 'sent';
|
|
197
234
|
export interface EnqueuedMessage {
|
|
198
235
|
id: number;
|