tascan-mcp 2.3.0 → 3.0.0
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 +3 -2
- package/index.js +32 -825
- package/package.json +3 -2
- package/tools.cjs +895 -0
package/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ============================================================
|
|
4
|
-
// TaScan MCP Server
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// TaScan MCP Server — stdio transport
|
|
5
|
+
// Thin transport over the canonical tool registry (tools.cjs),
|
|
6
|
+
// which is shared with the remote HTTP endpoint at
|
|
7
|
+
// https://app.tascan.io/mcp. Add or change tools in tools.cjs —
|
|
8
|
+
// never here.
|
|
7
9
|
// ============================================================
|
|
8
10
|
|
|
9
|
-
import {
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
12
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
-
import {
|
|
13
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { TOOLS } = require('./tools.cjs');
|
|
12
18
|
|
|
13
19
|
const API_BASE = process.env.TASCAN_API_URL || 'https://app.tascan.io/api/v1';
|
|
14
20
|
const API_KEY = process.env.TASCAN_API_KEY || '';
|
|
@@ -18,9 +24,8 @@ if (!API_KEY) {
|
|
|
18
24
|
process.exit(1);
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
//
|
|
22
|
-
async function
|
|
23
|
-
const url = API_BASE + path;
|
|
27
|
+
// Transport-bound REST call handed to every tool handler
|
|
28
|
+
async function api(method, path, body) {
|
|
24
29
|
const opts = {
|
|
25
30
|
method,
|
|
26
31
|
headers: {
|
|
@@ -28,839 +33,41 @@ async function apiCall(method, path, body) {
|
|
|
28
33
|
'Content-Type': 'application/json'
|
|
29
34
|
}
|
|
30
35
|
};
|
|
31
|
-
|
|
32
36
|
if (body && (method === 'POST' || method === 'PUT')) {
|
|
33
37
|
opts.body = JSON.stringify(body);
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
const resp = await fetch(url, opts);
|
|
39
|
+
const resp = await fetch(API_BASE + path, opts);
|
|
37
40
|
const data = await resp.json();
|
|
38
|
-
|
|
39
41
|
if (!resp.ok) {
|
|
40
42
|
throw new Error(data.error || `API error ${resp.status}`);
|
|
41
43
|
}
|
|
42
|
-
|
|
43
44
|
return data;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
version: '2.0.0'
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// ─── Tool: List Projects ───────────────────────────────────
|
|
54
|
-
server.registerTool(
|
|
55
|
-
'tascan_list_projects',
|
|
56
|
-
{
|
|
57
|
-
title: 'List Projects',
|
|
58
|
-
description: 'List all TaScan projects in the organization',
|
|
59
|
-
inputSchema: {},
|
|
60
|
-
annotations: {
|
|
61
|
-
readOnlyHint: true,
|
|
62
|
-
destructiveHint: false
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
async () => {
|
|
66
|
-
const result = await apiCall('GET', '/projects');
|
|
67
|
-
return {
|
|
68
|
-
content: [{
|
|
69
|
-
type: 'text',
|
|
70
|
-
text: JSON.stringify(result.data, null, 2)
|
|
71
|
-
}]
|
|
72
|
-
};
|
|
73
|
-
}
|
|
47
|
+
const server = new Server(
|
|
48
|
+
{ name: 'tascan', version: '3.0.0' },
|
|
49
|
+
{ capabilities: { tools: {} } }
|
|
74
50
|
);
|
|
75
51
|
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
53
|
+
tools: TOOLS.map(({ name, description, inputSchema, annotations }) => ({
|
|
54
|
+
name, description, inputSchema, annotations
|
|
55
|
+
}))
|
|
56
|
+
}));
|
|
76
57
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
title: 'Create Project',
|
|
82
|
-
description: 'Create a new TaScan project (top-level container for events)',
|
|
83
|
-
inputSchema: {
|
|
84
|
-
name: z.string().describe('Project name'),
|
|
85
|
-
location: z.string().optional().describe('Project location / venue')
|
|
86
|
-
},
|
|
87
|
-
annotations: {
|
|
88
|
-
readOnlyHint: false,
|
|
89
|
-
destructiveHint: false
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
async ({ name, location }) => {
|
|
93
|
-
const result = await apiCall('POST', '/projects', { name, location });
|
|
94
|
-
return {
|
|
95
|
-
content: [{
|
|
96
|
-
type: 'text',
|
|
97
|
-
text: `Project created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`
|
|
98
|
-
}]
|
|
99
|
-
};
|
|
58
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
59
|
+
const tool = TOOLS.find(t => t.name === req.params.name);
|
|
60
|
+
if (!tool) {
|
|
61
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }], isError: true };
|
|
100
62
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
'tascan_create_event',
|
|
107
|
-
{
|
|
108
|
-
title: 'Create Event',
|
|
109
|
-
description: 'Create a new event (task list) within a project',
|
|
110
|
-
inputSchema: {
|
|
111
|
-
project_id: z.string().describe('Project ID to add the event to'),
|
|
112
|
-
name: z.string().describe('Event name'),
|
|
113
|
-
description: z.string().optional().describe('Event description')
|
|
114
|
-
},
|
|
115
|
-
annotations: {
|
|
116
|
-
readOnlyHint: false,
|
|
117
|
-
destructiveHint: false
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
async ({ project_id, name, description }) => {
|
|
121
|
-
const result = await apiCall('POST', `/projects/${project_id}/lists`, { name, description });
|
|
122
|
-
return {
|
|
123
|
-
content: [{
|
|
124
|
-
type: 'text',
|
|
125
|
-
text: `Event created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`
|
|
126
|
-
}]
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// ─── Tool: Add Tasks (Bulk) ────────────────────────────────
|
|
133
|
-
server.registerTool(
|
|
134
|
-
'tascan_add_tasks',
|
|
135
|
-
{
|
|
136
|
-
title: 'Add Tasks',
|
|
137
|
-
description: 'Add one or more tasks to an event (task list). Supports bulk creation. IMPORTANT: Set response_type correctly — use "text" for info collection (names, phones, emails, notes), "photo" for visual verification (inspections, serial numbers, damage checks), "checkbox" only for simple confirmations.',
|
|
138
|
-
inputSchema: {
|
|
139
|
-
list_id: z.string().describe('Task list (event) ID'),
|
|
140
|
-
tasks: z.array(z.object({
|
|
141
|
-
title: z.string().describe('Task title'),
|
|
142
|
-
description: z.string().optional().describe('Task description'),
|
|
143
|
-
response_type: z.enum(['checkbox', 'photo', 'text', 'number', 'date', 'choice']).optional().describe('CRITICAL: "text" for names, phones, emails, notes, any free-form input. "photo" for visual proof (inspections, serial numbers, packed cases). "checkbox" ONLY for yes/no confirmations. "number" for numeric values. "date" for dates. "choice" for multiple-choice (needs response_config.options). Default is checkbox but most info-collection tasks should be "text".'),
|
|
144
|
-
is_safety_checkpoint: z.boolean().optional().describe('Safety-critical task flag'),
|
|
145
|
-
requires_photo: z.boolean().optional().describe('Require photo on completion')
|
|
146
|
-
})).describe('Array of tasks to create')
|
|
147
|
-
},
|
|
148
|
-
annotations: {
|
|
149
|
-
readOnlyHint: false,
|
|
150
|
-
destructiveHint: false
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
async ({ list_id, tasks }) => {
|
|
154
|
-
const result = await apiCall('POST', `/lists/${list_id}/tasks`, tasks);
|
|
155
|
-
return {
|
|
156
|
-
content: [{
|
|
157
|
-
type: 'text',
|
|
158
|
-
text: `${result.count} task(s) created in list ${list_id}\n\n${JSON.stringify(result.data, null, 2)}`
|
|
159
|
-
}]
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// ─── Tool: List Workers ────────────────────────────────────
|
|
166
|
-
server.registerTool(
|
|
167
|
-
'tascan_list_workers',
|
|
168
|
-
{
|
|
169
|
-
title: 'List Workers',
|
|
170
|
-
description: 'List all workers (taskees) in the organization',
|
|
171
|
-
inputSchema: {},
|
|
172
|
-
annotations: {
|
|
173
|
-
readOnlyHint: true,
|
|
174
|
-
destructiveHint: false
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
async () => {
|
|
178
|
-
const result = await apiCall('GET', '/workers');
|
|
179
|
-
return {
|
|
180
|
-
content: [{
|
|
181
|
-
type: 'text',
|
|
182
|
-
text: JSON.stringify(result.data, null, 2)
|
|
183
|
-
}]
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// ─── Tool: Create Worker ───────────────────────────────────
|
|
190
|
-
server.registerTool(
|
|
191
|
-
'tascan_create_worker',
|
|
192
|
-
{
|
|
193
|
-
title: 'Create Worker',
|
|
194
|
-
description: 'Create a new worker (taskee) in the organization',
|
|
195
|
-
inputSchema: {
|
|
196
|
-
name: z.string().describe('Worker name'),
|
|
197
|
-
phone: z.string().optional().describe('Worker phone number'),
|
|
198
|
-
email: z.string().optional().describe('Worker email')
|
|
199
|
-
},
|
|
200
|
-
annotations: {
|
|
201
|
-
readOnlyHint: false,
|
|
202
|
-
destructiveHint: false
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
async ({ name, phone, email }) => {
|
|
206
|
-
const result = await apiCall('POST', '/workers', { name, phone, email });
|
|
207
|
-
return {
|
|
208
|
-
content: [{
|
|
209
|
-
type: 'text',
|
|
210
|
-
text: `Worker created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`
|
|
211
|
-
}]
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// ─── Tool: Generate QR Code ────────────────────────────────
|
|
218
|
-
server.registerTool(
|
|
219
|
-
'tascan_generate_qr',
|
|
220
|
-
{
|
|
221
|
-
title: 'Generate QR Code',
|
|
222
|
-
description: 'Generate a QR code for a task list (event) that workers can scan to access tasks',
|
|
223
|
-
inputSchema: {
|
|
224
|
-
list_id: z.string().describe('Task list (event) ID')
|
|
225
|
-
},
|
|
226
|
-
annotations: {
|
|
227
|
-
readOnlyHint: false,
|
|
228
|
-
destructiveHint: false
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
async ({ list_id }) => {
|
|
232
|
-
const result = await apiCall('POST', `/lists/${list_id}/qr`);
|
|
233
|
-
return {
|
|
234
|
-
content: [{
|
|
235
|
-
type: 'text',
|
|
236
|
-
text: `QR Code generated for "${result.data.list_name}":\n- Scan URL: ${result.data.scan_url}\n- QR Image: ${result.data.qr_image_url}`
|
|
237
|
-
}]
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// ─── Tool: Apply Template ──────────────────────────────────
|
|
244
|
-
server.registerTool(
|
|
245
|
-
'tascan_apply_template',
|
|
246
|
-
{
|
|
247
|
-
title: 'Apply Template',
|
|
248
|
-
description: 'Apply a pre-built template to a task list, adding all template tasks',
|
|
249
|
-
inputSchema: {
|
|
250
|
-
list_id: z.string().describe('Task list (event) ID to apply template to'),
|
|
251
|
-
template_slug: z.string().describe('Template slug (e.g. "conference-load-in", "warehouse-receiving", "restaurant-opening", "hotel-room-turnover")')
|
|
252
|
-
},
|
|
253
|
-
annotations: {
|
|
254
|
-
readOnlyHint: false,
|
|
255
|
-
destructiveHint: false
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
async ({ list_id, template_slug }) => {
|
|
259
|
-
const result = await apiCall('POST', `/lists/${list_id}/apply-template`, { template_slug });
|
|
260
|
-
return {
|
|
261
|
-
content: [{
|
|
262
|
-
type: 'text',
|
|
263
|
-
text: `Template applied! ${result.count} tasks added to list ${list_id}\n\n${JSON.stringify(result.data, null, 2)}`
|
|
264
|
-
}]
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// ─── Tool: Get Report ──────────────────────────────────────
|
|
271
|
-
server.registerTool(
|
|
272
|
-
'tascan_get_report',
|
|
273
|
-
{
|
|
274
|
-
title: 'Get Completion Report',
|
|
275
|
-
description: 'Get completion report for a task list (event) including task status, completions, workers, and photos',
|
|
276
|
-
inputSchema: {
|
|
277
|
-
list_id: z.string().describe('Task list (event) ID')
|
|
278
|
-
},
|
|
279
|
-
annotations: {
|
|
280
|
-
readOnlyHint: true,
|
|
281
|
-
destructiveHint: false
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
async ({ list_id }) => {
|
|
285
|
-
const result = await apiCall('GET', `/lists/${list_id}/report`);
|
|
286
|
-
const r = result.data;
|
|
287
|
-
let text = `Report for "${r.list_name}":\n`;
|
|
288
|
-
text += `Completion: ${r.completed_tasks}/${r.total_tasks} (${r.completion_rate}%)\n\n`;
|
|
289
|
-
|
|
290
|
-
for (const task of r.tasks) {
|
|
291
|
-
const status = task.completed ? '[x]' : '[ ]';
|
|
292
|
-
text += `${status} ${task.title}`;
|
|
293
|
-
if (task.completions && task.completions.length > 0) {
|
|
294
|
-
text += ` (by ${task.completions.map(c => c.worker_name).join(', ')})`;
|
|
295
|
-
}
|
|
296
|
-
text += '\n';
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return { content: [{ type: 'text', text }] };
|
|
300
|
-
}
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// ─── Tool: List Templates ──────────────────────────────────
|
|
305
|
-
server.registerTool(
|
|
306
|
-
'tascan_list_templates',
|
|
307
|
-
{
|
|
308
|
-
title: 'List Templates',
|
|
309
|
-
description: 'List available task templates (built-in and saved)',
|
|
310
|
-
inputSchema: {
|
|
311
|
-
category: z.string().optional().describe('Filter by category (e.g. "live-events", "hospitality", "logistics", "construction", "facilities", "general", "field-service", "education")')
|
|
312
|
-
},
|
|
313
|
-
annotations: {
|
|
314
|
-
readOnlyHint: true,
|
|
315
|
-
destructiveHint: false
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
async ({ category }) => {
|
|
319
|
-
const path = category ? `/templates?category=${encodeURIComponent(category)}` : '/templates';
|
|
320
|
-
const result = await apiCall('GET', path);
|
|
321
|
-
return {
|
|
322
|
-
content: [{
|
|
323
|
-
type: 'text',
|
|
324
|
-
text: JSON.stringify(result.data, null, 2)
|
|
325
|
-
}]
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// ─── Tool: List Events (Task Lists) ──────────────────────
|
|
332
|
-
server.registerTool(
|
|
333
|
-
'tascan_list_events',
|
|
334
|
-
{
|
|
335
|
-
title: 'List Events',
|
|
336
|
-
description: 'List all events (task lists) within a project',
|
|
337
|
-
inputSchema: {
|
|
338
|
-
project_id: z.string().describe('Project ID')
|
|
339
|
-
},
|
|
340
|
-
annotations: {
|
|
341
|
-
readOnlyHint: true,
|
|
342
|
-
destructiveHint: false
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
async ({ project_id }) => {
|
|
346
|
-
const result = await apiCall('GET', `/projects/${project_id}/lists`);
|
|
347
|
-
return {
|
|
348
|
-
content: [{
|
|
349
|
-
type: 'text',
|
|
350
|
-
text: JSON.stringify(result.data, null, 2)
|
|
351
|
-
}]
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// ─── Tool: Get Event ─────────────────────────────────────
|
|
358
|
-
server.registerTool(
|
|
359
|
-
'tascan_get_event',
|
|
360
|
-
{
|
|
361
|
-
title: 'Get Event',
|
|
362
|
-
description: 'Get details of a specific event (task list) including its tasks',
|
|
363
|
-
inputSchema: {
|
|
364
|
-
list_id: z.string().describe('Task list (event) ID')
|
|
365
|
-
},
|
|
366
|
-
annotations: {
|
|
367
|
-
readOnlyHint: true,
|
|
368
|
-
destructiveHint: false
|
|
369
|
-
}
|
|
370
|
-
},
|
|
371
|
-
async ({ list_id }) => {
|
|
372
|
-
const result = await apiCall('GET', `/lists/${list_id}`);
|
|
373
|
-
return {
|
|
374
|
-
content: [{
|
|
375
|
-
type: 'text',
|
|
376
|
-
text: JSON.stringify(result.data, null, 2)
|
|
377
|
-
}]
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// ─── Tool: List Tasks ────────────────────────────────────
|
|
384
|
-
server.registerTool(
|
|
385
|
-
'tascan_list_tasks',
|
|
386
|
-
{
|
|
387
|
-
title: 'List Tasks',
|
|
388
|
-
description: 'List all tasks in an event (task list)',
|
|
389
|
-
inputSchema: {
|
|
390
|
-
list_id: z.string().describe('Task list (event) ID')
|
|
391
|
-
},
|
|
392
|
-
annotations: {
|
|
393
|
-
readOnlyHint: true,
|
|
394
|
-
destructiveHint: false
|
|
395
|
-
}
|
|
396
|
-
},
|
|
397
|
-
async ({ list_id }) => {
|
|
398
|
-
const result = await apiCall('GET', `/lists/${list_id}/tasks`);
|
|
399
|
-
return {
|
|
400
|
-
content: [{
|
|
401
|
-
type: 'text',
|
|
402
|
-
text: JSON.stringify(result.data, null, 2)
|
|
403
|
-
}]
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// ─── Tool: Get Project ───────────────────────────────────
|
|
410
|
-
server.registerTool(
|
|
411
|
-
'tascan_get_project',
|
|
412
|
-
{
|
|
413
|
-
title: 'Get Project',
|
|
414
|
-
description: 'Get details of a specific project',
|
|
415
|
-
inputSchema: {
|
|
416
|
-
project_id: z.string().describe('Project ID')
|
|
417
|
-
},
|
|
418
|
-
annotations: {
|
|
419
|
-
readOnlyHint: true,
|
|
420
|
-
destructiveHint: false
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
async ({ project_id }) => {
|
|
424
|
-
const result = await apiCall('GET', `/projects/${project_id}`);
|
|
425
|
-
return {
|
|
426
|
-
content: [{
|
|
427
|
-
type: 'text',
|
|
428
|
-
text: JSON.stringify(result.data, null, 2)
|
|
429
|
-
}]
|
|
430
|
-
};
|
|
63
|
+
try {
|
|
64
|
+
const text = await tool.handler(req.params.arguments || {}, api);
|
|
65
|
+
return { content: [{ type: 'text', text: String(text) }] };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
431
68
|
}
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
// ─── Tool: Get Task ──────────────────────────────────────
|
|
436
|
-
server.registerTool(
|
|
437
|
-
'tascan_get_task',
|
|
438
|
-
{
|
|
439
|
-
title: 'Get Task',
|
|
440
|
-
description: 'Get details of a specific task including completions',
|
|
441
|
-
inputSchema: {
|
|
442
|
-
task_id: z.string().describe('Task ID')
|
|
443
|
-
},
|
|
444
|
-
annotations: {
|
|
445
|
-
readOnlyHint: true,
|
|
446
|
-
destructiveHint: false
|
|
447
|
-
}
|
|
448
|
-
},
|
|
449
|
-
async ({ task_id }) => {
|
|
450
|
-
const result = await apiCall('GET', `/tasks/${task_id}`);
|
|
451
|
-
return {
|
|
452
|
-
content: [{
|
|
453
|
-
type: 'text',
|
|
454
|
-
text: JSON.stringify(result.data, null, 2)
|
|
455
|
-
}]
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// ─── Tool: Update Project ────────────────────────────────
|
|
462
|
-
server.registerTool(
|
|
463
|
-
'tascan_update_project',
|
|
464
|
-
{
|
|
465
|
-
title: 'Update Project',
|
|
466
|
-
description: 'Update a project (name, location, status, dates)',
|
|
467
|
-
inputSchema: {
|
|
468
|
-
project_id: z.string().describe('Project ID'),
|
|
469
|
-
name: z.string().optional().describe('New project name'),
|
|
470
|
-
location: z.string().optional().describe('New location'),
|
|
471
|
-
status: z.enum(['active', 'archived']).optional().describe('Project status'),
|
|
472
|
-
start_date: z.string().optional().describe('Start date (ISO format)'),
|
|
473
|
-
end_date: z.string().optional().describe('End date (ISO format)')
|
|
474
|
-
},
|
|
475
|
-
annotations: {
|
|
476
|
-
readOnlyHint: false,
|
|
477
|
-
destructiveHint: false
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
async ({ project_id, ...updates }) => {
|
|
481
|
-
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
482
|
-
const result = await apiCall('PUT', `/projects/${project_id}`, body);
|
|
483
|
-
return {
|
|
484
|
-
content: [{
|
|
485
|
-
type: 'text',
|
|
486
|
-
text: `Project updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`
|
|
487
|
-
}]
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// ─── Tool: Update Event ──────────────────────────────────
|
|
494
|
-
server.registerTool(
|
|
495
|
-
'tascan_update_event',
|
|
496
|
-
{
|
|
497
|
-
title: 'Update Event',
|
|
498
|
-
description: 'Update an event / task list (name, description, team_mode, timer_mode)',
|
|
499
|
-
inputSchema: {
|
|
500
|
-
list_id: z.string().describe('Task list (event) ID'),
|
|
501
|
-
name: z.string().optional().describe('New event name'),
|
|
502
|
-
description: z.string().optional().describe('New description'),
|
|
503
|
-
team_mode: z.boolean().optional().describe('Team mode on/off'),
|
|
504
|
-
timer_mode: z.boolean().optional().describe('Timer mode on/off')
|
|
505
|
-
},
|
|
506
|
-
annotations: {
|
|
507
|
-
readOnlyHint: false,
|
|
508
|
-
destructiveHint: false
|
|
509
|
-
}
|
|
510
|
-
},
|
|
511
|
-
async ({ list_id, ...updates }) => {
|
|
512
|
-
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
513
|
-
const result = await apiCall('PUT', `/lists/${list_id}`, body);
|
|
514
|
-
return {
|
|
515
|
-
content: [{
|
|
516
|
-
type: 'text',
|
|
517
|
-
text: `Event updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`
|
|
518
|
-
}]
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
// ─── Tool: Update Task ───────────────────────────────────
|
|
525
|
-
server.registerTool(
|
|
526
|
-
'tascan_update_task',
|
|
527
|
-
{
|
|
528
|
-
title: 'Update Task',
|
|
529
|
-
description: 'Update a task (title, description, response_type, flags, sort_order)',
|
|
530
|
-
inputSchema: {
|
|
531
|
-
task_id: z.string().describe('Task ID'),
|
|
532
|
-
title: z.string().optional().describe('New title'),
|
|
533
|
-
description: z.string().optional().describe('New description'),
|
|
534
|
-
response_type: z.enum(['checkbox', 'photo', 'text', 'number', 'date', 'choice']).optional().describe('See tascan_add_tasks for guidance. "text" for info collection, "photo" for visual proof, "checkbox" for yes/no only.'),
|
|
535
|
-
is_safety_checkpoint: z.boolean().optional().describe('Safety-critical flag'),
|
|
536
|
-
requires_photo: z.boolean().optional().describe('Require photo'),
|
|
537
|
-
sort_order: z.number().optional().describe('Sort order position')
|
|
538
|
-
},
|
|
539
|
-
annotations: {
|
|
540
|
-
readOnlyHint: false,
|
|
541
|
-
destructiveHint: false
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
async ({ task_id, ...updates }) => {
|
|
545
|
-
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
546
|
-
const result = await apiCall('PUT', `/tasks/${task_id}`, body);
|
|
547
|
-
return {
|
|
548
|
-
content: [{
|
|
549
|
-
type: 'text',
|
|
550
|
-
text: `Task updated: ${result.data.title}\n\n${JSON.stringify(result.data, null, 2)}`
|
|
551
|
-
}]
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// ─── Tool: Delete Project ────────────────────────────────
|
|
558
|
-
server.registerTool(
|
|
559
|
-
'tascan_delete_project',
|
|
560
|
-
{
|
|
561
|
-
title: 'Delete Project',
|
|
562
|
-
description: 'Delete a project and all its events, tasks, and completions',
|
|
563
|
-
inputSchema: {
|
|
564
|
-
project_id: z.string().describe('Project ID to delete')
|
|
565
|
-
},
|
|
566
|
-
annotations: {
|
|
567
|
-
readOnlyHint: false,
|
|
568
|
-
destructiveHint: true
|
|
569
|
-
}
|
|
570
|
-
},
|
|
571
|
-
async ({ project_id }) => {
|
|
572
|
-
await apiCall('DELETE', `/projects/${project_id}`);
|
|
573
|
-
return {
|
|
574
|
-
content: [{
|
|
575
|
-
type: 'text',
|
|
576
|
-
text: `Project ${project_id} deleted successfully.`
|
|
577
|
-
}]
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// ─── Tool: Delete Event ──────────────────────────────────
|
|
584
|
-
server.registerTool(
|
|
585
|
-
'tascan_delete_event',
|
|
586
|
-
{
|
|
587
|
-
title: 'Delete Event',
|
|
588
|
-
description: 'Delete an event (task list) and all its tasks and completions',
|
|
589
|
-
inputSchema: {
|
|
590
|
-
list_id: z.string().describe('Task list (event) ID to delete')
|
|
591
|
-
},
|
|
592
|
-
annotations: {
|
|
593
|
-
readOnlyHint: false,
|
|
594
|
-
destructiveHint: true
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
async ({ list_id }) => {
|
|
598
|
-
await apiCall('DELETE', `/lists/${list_id}`);
|
|
599
|
-
return {
|
|
600
|
-
content: [{
|
|
601
|
-
type: 'text',
|
|
602
|
-
text: `Event ${list_id} deleted successfully.`
|
|
603
|
-
}]
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
// ─── Tool: Delete Task ───────────────────────────────────
|
|
610
|
-
server.registerTool(
|
|
611
|
-
'tascan_delete_task',
|
|
612
|
-
{
|
|
613
|
-
title: 'Delete Task',
|
|
614
|
-
description: 'Delete a specific task and its completions',
|
|
615
|
-
inputSchema: {
|
|
616
|
-
task_id: z.string().describe('Task ID to delete')
|
|
617
|
-
},
|
|
618
|
-
annotations: {
|
|
619
|
-
readOnlyHint: false,
|
|
620
|
-
destructiveHint: true
|
|
621
|
-
}
|
|
622
|
-
},
|
|
623
|
-
async ({ task_id }) => {
|
|
624
|
-
await apiCall('DELETE', `/tasks/${task_id}`);
|
|
625
|
-
return {
|
|
626
|
-
content: [{
|
|
627
|
-
type: 'text',
|
|
628
|
-
text: `Task ${task_id} deleted successfully.`
|
|
629
|
-
}]
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// ─── Tool: Update Worker ─────────────────────────────────
|
|
636
|
-
server.registerTool(
|
|
637
|
-
'tascan_update_worker',
|
|
638
|
-
{
|
|
639
|
-
title: 'Update Worker',
|
|
640
|
-
description: 'Update a worker profile (name, phone, email)',
|
|
641
|
-
inputSchema: {
|
|
642
|
-
worker_id: z.string().describe('Worker ID'),
|
|
643
|
-
name: z.string().optional().describe('New name'),
|
|
644
|
-
phone: z.string().optional().describe('New phone number'),
|
|
645
|
-
email: z.string().optional().describe('New email')
|
|
646
|
-
},
|
|
647
|
-
annotations: {
|
|
648
|
-
readOnlyHint: false,
|
|
649
|
-
destructiveHint: false
|
|
650
|
-
}
|
|
651
|
-
},
|
|
652
|
-
async ({ worker_id, ...updates }) => {
|
|
653
|
-
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
654
|
-
const result = await apiCall('PUT', `/workers/${worker_id}`, body);
|
|
655
|
-
return {
|
|
656
|
-
content: [{
|
|
657
|
-
type: 'text',
|
|
658
|
-
text: `Worker updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`
|
|
659
|
-
}]
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// ─── Closed-Loop Autonomous Operations Protocol ───────────
|
|
666
|
-
|
|
667
|
-
// Tool 23: List Issues (discover issues for analysis)
|
|
668
|
-
server.registerTool(
|
|
669
|
-
'tascan_list_issues',
|
|
670
|
-
{
|
|
671
|
-
title: 'List Issues',
|
|
672
|
-
description: 'List all issues for a task list (event). Returns open, acknowledged, and resolved issues. Use to discover issues for AI analysis via tascan_analyze_issue.',
|
|
673
|
-
inputSchema: {
|
|
674
|
-
list_id: z.string().describe('Task list (event) ID')
|
|
675
|
-
},
|
|
676
|
-
annotations: {
|
|
677
|
-
readOnlyHint: true,
|
|
678
|
-
destructiveHint: false
|
|
679
|
-
}
|
|
680
|
-
},
|
|
681
|
-
async ({ list_id }) => {
|
|
682
|
-
const result = await apiCall('GET', `/lists/${list_id}/issues`);
|
|
683
|
-
const issues = result.data;
|
|
684
|
-
if (!issues.length) return { content: [{ type: 'text', text: 'No issues reported for this task list.' }] };
|
|
685
|
-
let text = `${issues.length} issue(s) found:\n\n`;
|
|
686
|
-
issues.forEach((iss, i) => {
|
|
687
|
-
text += `${i+1}. [${iss.status.toUpperCase()}] ${iss.title}\n`;
|
|
688
|
-
text += ` ID: ${iss.id} | Type: ${iss.type} | Category: ${iss.category} | Severity: ${iss.severity || 'unset'}\n`;
|
|
689
|
-
if (iss.tasks) text += ` Task: ${iss.tasks.title}\n`;
|
|
690
|
-
text += ` Reported: ${iss.created_at}\n\n`;
|
|
691
|
-
});
|
|
692
|
-
return { content: [{ type: 'text', text }] };
|
|
693
|
-
}
|
|
694
|
-
);
|
|
695
|
-
|
|
696
|
-
// Tool 24: Analyze Issue (Step 1 of Closed-Loop — read issue with full context + optional server AI)
|
|
697
|
-
server.registerTool(
|
|
698
|
-
'tascan_analyze_issue',
|
|
699
|
-
{
|
|
700
|
-
title: 'Analyze Issue',
|
|
701
|
-
description: 'Step 1 of the Closed-Loop Autonomous Operations Protocol. Returns full issue context including worker info, message thread, project history, and recent similar issues for AI reasoning. Supports optional server-side AI analysis via Anthropic API.',
|
|
702
|
-
inputSchema: {
|
|
703
|
-
issue_id: z.string().describe('Issue ID to analyze'),
|
|
704
|
-
server_side_ai: z.boolean().optional().describe('If true, server calls Anthropic API for analysis (default: false)')
|
|
705
|
-
},
|
|
706
|
-
annotations: {
|
|
707
|
-
readOnlyHint: false,
|
|
708
|
-
destructiveHint: false
|
|
709
|
-
}
|
|
710
|
-
},
|
|
711
|
-
async ({ issue_id, server_side_ai }) => {
|
|
712
|
-
if (server_side_ai) {
|
|
713
|
-
const result = await apiCall('POST', `/issues/${issue_id}/analyze`);
|
|
714
|
-
const d = result.data;
|
|
715
|
-
let text = `=== AI ANALYSIS (Server-Side) ===\n`;
|
|
716
|
-
text += `Issue: ${d.issue_title} (${d.issue_id})\nAnalyzed: ${d.analyzed_at}\n\n`;
|
|
717
|
-
text += JSON.stringify(d.analysis, null, 2);
|
|
718
|
-
return { content: [{ type: 'text', text }] };
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const result = await apiCall('GET', `/issues/${issue_id}`);
|
|
722
|
-
const d = result.data;
|
|
723
|
-
let text = `=== ISSUE CONTEXT FOR ANALYSIS ===\n`;
|
|
724
|
-
text += `Issue: ${d.title}\n`;
|
|
725
|
-
text += `Type: ${d.type} | Category: ${d.category} | Severity: ${d.severity || 'unset'} | Status: ${d.status}\n`;
|
|
726
|
-
text += `Description: ${d.description || 'none'}\n`;
|
|
727
|
-
if (d.tasks) text += `Task: ${d.tasks.title} (${d.tasks.response_type || 'checkbox'})\nTask Description: ${d.tasks.description || 'none'}\n`;
|
|
728
|
-
if (d.worker) text += `Worker: ${d.worker.name} (ID: ${d.worker.id})\n`;
|
|
729
|
-
if (d.project) text += `Project: ${d.project.name} | Location: ${d.project.location || 'unknown'}\n`;
|
|
730
|
-
if (d.injury_data) text += `Injury Data: ${JSON.stringify(d.injury_data)}\n`;
|
|
731
|
-
text += `Reported: ${d.created_at}\n`;
|
|
732
|
-
if (d.ai_analysis) text += `\n--- PREVIOUS AI ANALYSIS ---\n${JSON.stringify(d.ai_analysis, null, 2)}\n`;
|
|
733
|
-
if (d.messages && d.messages.length) {
|
|
734
|
-
text += `\n--- Message Thread (${d.messages.length}) ---\n`;
|
|
735
|
-
d.messages.forEach(m => { text += `[${m.sender_type}/${m.sender_name}] ${m.message}\n`; });
|
|
736
|
-
}
|
|
737
|
-
if (d.recent_issues && d.recent_issues.length) {
|
|
738
|
-
text += `\n--- Recent Issues on Same Project (${d.recent_issues.length}) ---\n`;
|
|
739
|
-
d.recent_issues.forEach(ri => { text += `- [${ri.severity}/${ri.category}] ${(ri.description || '').substring(0, 80)} (${ri.status})\n`; });
|
|
740
|
-
}
|
|
741
|
-
return { content: [{ type: 'text', text }] };
|
|
742
|
-
}
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
// Tool 25: Recommend Fix (Step 2 — post recommendation + optional structured tasks)
|
|
746
|
-
server.registerTool(
|
|
747
|
-
'tascan_recommend_fix',
|
|
748
|
-
{
|
|
749
|
-
title: 'Recommend Fix',
|
|
750
|
-
description: 'Step 2 of the Closed-Loop. Post AI recommendation to issue thread. Accepts optional structured_recommendation with task definitions for auto-dispatch. Persisted in AI audit trail.',
|
|
751
|
-
inputSchema: {
|
|
752
|
-
issue_id: z.string().describe('Issue ID to recommend a fix for'),
|
|
753
|
-
recommendation: z.string().describe('AI recommendation text (clear, actionable instructions)'),
|
|
754
|
-
structured_recommendation: z.any().optional().describe('Structured recommendation object with tasks for dispatch'),
|
|
755
|
-
ai_agent: z.string().optional().describe('AI agent name (default: TaScan AI)')
|
|
756
|
-
},
|
|
757
|
-
annotations: {
|
|
758
|
-
readOnlyHint: false,
|
|
759
|
-
destructiveHint: false
|
|
760
|
-
}
|
|
761
|
-
},
|
|
762
|
-
async ({ issue_id, recommendation, structured_recommendation, ai_agent }) => {
|
|
763
|
-
const body = { recommendation };
|
|
764
|
-
if (structured_recommendation) body.structured_recommendation = structured_recommendation;
|
|
765
|
-
if (ai_agent) body.ai_agent = ai_agent;
|
|
766
|
-
const result = await apiCall('POST', `/issues/${issue_id}/recommend`, body);
|
|
767
|
-
const d = result.data;
|
|
768
|
-
let text = `Recommendation posted to issue "${d.issue_title}":\n`;
|
|
769
|
-
text += `Project: ${d.project_name} | List: ${d.list_name}\n`;
|
|
770
|
-
text += `Message ID: ${d.message_id}\n\nRecommendation:\n${d.recommendation}`;
|
|
771
|
-
if (d.structured_recommendation?.tasks) {
|
|
772
|
-
text += `\n\nStructured Tasks (${d.structured_recommendation.tasks.length}):`;
|
|
773
|
-
d.structured_recommendation.tasks.forEach((t, i) => { text += `\n ${i+1}. ${t.title}`; });
|
|
774
|
-
}
|
|
775
|
-
return { content: [{ type: 'text', text }] };
|
|
776
|
-
}
|
|
777
|
-
);
|
|
778
|
-
|
|
779
|
-
// Tool 26: Dispatch Instruction (Step 3 — multi-channel delivery + task list creation)
|
|
780
|
-
server.registerTool(
|
|
781
|
-
'tascan_dispatch_instruction',
|
|
782
|
-
{
|
|
783
|
-
title: 'Dispatch Instruction',
|
|
784
|
-
description: 'Step 3 of the Closed-Loop. Multi-channel dispatch: issue thread, in-app message, progress feed, SMS, and optional remediation task list. Closes the loop from AI analysis to physical worker execution.',
|
|
785
|
-
inputSchema: {
|
|
786
|
-
issue_id: z.string().describe('Issue ID'),
|
|
787
|
-
instruction: z.string().describe('Actionable instruction for the worker'),
|
|
788
|
-
worker_id: z.string().optional().describe('Target worker ID (defaults to issue reporter)'),
|
|
789
|
-
remediation_tasks: z.array(z.any()).optional().describe('Tasks to create as remediation task list'),
|
|
790
|
-
recommendation_summary: z.string().optional().describe('Summary for task list description'),
|
|
791
|
-
send_sms: z.boolean().optional().describe('Send SMS (default: true if phone on file)'),
|
|
792
|
-
ai_agent: z.string().optional().describe('AI agent name (default: TaScan AI)')
|
|
793
|
-
},
|
|
794
|
-
annotations: {
|
|
795
|
-
readOnlyHint: false,
|
|
796
|
-
destructiveHint: false
|
|
797
|
-
}
|
|
798
|
-
},
|
|
799
|
-
async ({ issue_id, instruction, worker_id, remediation_tasks, recommendation_summary, send_sms, ai_agent }) => {
|
|
800
|
-
const body = { instruction };
|
|
801
|
-
if (worker_id) body.worker_id = worker_id;
|
|
802
|
-
if (remediation_tasks) body.remediation_tasks = remediation_tasks;
|
|
803
|
-
if (recommendation_summary) body.recommendation_summary = recommendation_summary;
|
|
804
|
-
if (send_sms !== undefined) body.send_sms = send_sms;
|
|
805
|
-
if (ai_agent) body.ai_agent = ai_agent;
|
|
806
|
-
const result = await apiCall('POST', `/issues/${issue_id}/dispatch`, body);
|
|
807
|
-
const d = result.data;
|
|
808
|
-
let text = `Dispatched via ${d.channels.length} channels: ${d.channels.join(', ')}\n`;
|
|
809
|
-
text += `Worker: ${d.worker_id} | Issue: ${d.issue_id} (${d.issue_status})\n`;
|
|
810
|
-
text += `SMS: ${d.sms_status}\n`;
|
|
811
|
-
if (d.remediation_task_list_id) {
|
|
812
|
-
text += `Task List: ${d.remediation_task_list_id}\nWorker URL: ${d.worker_url}\nTasks: ${d.tasks_created}\n`;
|
|
813
|
-
}
|
|
814
|
-
text += `\nInstruction:\n${d.instruction}`;
|
|
815
|
-
return { content: [{ type: 'text', text }] };
|
|
816
|
-
}
|
|
817
|
-
);
|
|
818
|
-
|
|
819
|
-
// Tool 27: Auto-Resolve (Full Closed-Loop in one call — Patent Claim 7)
|
|
820
|
-
server.registerTool(
|
|
821
|
-
'tascan_auto_resolve',
|
|
822
|
-
{
|
|
823
|
-
title: 'Auto-Resolve Issue',
|
|
824
|
-
description: 'FULL Closed-Loop Autonomous Operations Protocol in one call. Server-side AI analyzes issue, generates remediation tasks, creates task list, dispatches to worker via multi-channel delivery — all without human intervention. Patent Claim 7.',
|
|
825
|
-
inputSchema: {
|
|
826
|
-
issue_id: z.string().describe('Issue ID to auto-resolve')
|
|
827
|
-
},
|
|
828
|
-
annotations: {
|
|
829
|
-
readOnlyHint: false,
|
|
830
|
-
destructiveHint: false,
|
|
831
|
-
openWorldHint: true
|
|
832
|
-
}
|
|
833
|
-
},
|
|
834
|
-
async ({ issue_id }) => {
|
|
835
|
-
const result = await apiCall('POST', `/issues/${issue_id}/auto-resolve`);
|
|
836
|
-
const d = result.data;
|
|
837
|
-
let text = `=== CLOSED-LOOP AUTONOMOUS OPERATIONS PROTOCOL ===\n`;
|
|
838
|
-
text += `Issue: ${d.issue_id}\n\n`;
|
|
839
|
-
text += `--- ANALYSIS ---\n`;
|
|
840
|
-
text += `Summary: ${d.analysis?.summary || 'N/A'}\n`;
|
|
841
|
-
text += `Classification: ${d.analysis?.classification?.type || 'N/A'} (${d.analysis?.classification?.severity || 'N/A'})\n`;
|
|
842
|
-
text += `Urgency: ${d.analysis?.urgency_score || 'N/A'}/10\n`;
|
|
843
|
-
if (d.analysis?.root_cause_hypotheses?.length) {
|
|
844
|
-
d.analysis.root_cause_hypotheses.forEach((h, i) => {
|
|
845
|
-
text += ` ${i+1}. ${h.hypothesis} (${Math.round(h.confidence * 100)}%)\n`;
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
text += `\n--- RECOMMENDATION ---\n`;
|
|
849
|
-
text += `Fix: ${d.recommendation?.recommendation_summary || 'N/A'}\n`;
|
|
850
|
-
text += `Tasks: ${d.recommendation?.tasks?.length || 0}\n`;
|
|
851
|
-
text += `Duration: ${d.recommendation?.estimated_duration_minutes || 'N/A'} min\n`;
|
|
852
|
-
text += `\n--- DISPATCH ---\n`;
|
|
853
|
-
text += `Channels: ${d.dispatch?.channels?.join(', ') || 'none'}\n`;
|
|
854
|
-
text += `SMS: ${d.dispatch?.sms_status || 'N/A'}\n`;
|
|
855
|
-
if (d.dispatch?.worker_url) text += `Worker URL: ${d.dispatch.worker_url}\n`;
|
|
856
|
-
if (d.dispatch?.remediation_task_list_id) text += `Task List: ${d.dispatch.remediation_task_list_id}\n`;
|
|
857
|
-
text += `\nTimestamps: analyzed ${d.timestamps?.analyzed_at} | recommended ${d.timestamps?.recommended_at} | dispatched ${d.timestamps?.dispatched_at}`;
|
|
858
|
-
return { content: [{ type: 'text', text }] };
|
|
859
|
-
}
|
|
860
|
-
);
|
|
861
|
-
|
|
69
|
+
});
|
|
862
70
|
|
|
863
|
-
// ─── Start Server ──────────────────────────────────────────
|
|
864
71
|
async function main() {
|
|
865
72
|
const transport = new StdioServerTransport();
|
|
866
73
|
await server.connect(transport);
|