tascan-mcp 2.4.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 -913
- 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,927 +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.4.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
|
-
}
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// ─── Tool: Create Project ──────────────────────────────────
|
|
78
|
-
server.registerTool(
|
|
79
|
-
'tascan_create_project',
|
|
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
|
-
};
|
|
100
|
-
}
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// ─── Tool: Create Event (Task List) ────────────────────────
|
|
105
|
-
server.registerTool(
|
|
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
|
-
};
|
|
431
|
-
}
|
|
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
|
-
}
|
|
47
|
+
const server = new Server(
|
|
48
|
+
{ name: 'tascan', version: '3.0.0' },
|
|
49
|
+
{ capabilities: { tools: {} } }
|
|
694
50
|
);
|
|
695
51
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
);
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
53
|
+
tools: TOOLS.map(({ name, description, inputSchema, annotations }) => ({
|
|
54
|
+
name, description, inputSchema, annotations
|
|
55
|
+
}))
|
|
56
|
+
}));
|
|
744
57
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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 }] };
|
|
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 };
|
|
776
62
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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 }] };
|
|
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 };
|
|
816
68
|
}
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
// ─── NFC Tag Registry ─────────────────────────────────────
|
|
864
|
-
|
|
865
|
-
// Tool 28: Register NFC Tag
|
|
866
|
-
server.registerTool(
|
|
867
|
-
'tascan_register_tag',
|
|
868
|
-
{
|
|
869
|
-
title: 'Register NFC Tag',
|
|
870
|
-
description: 'Register a physical NFC tag to a project, task list, or specific task. When someone taps the tag, TaScan routes them to the linked resource. Tags use NTAG215 chips and are programmed with NFC Tools Pro.',
|
|
871
|
-
inputSchema: {
|
|
872
|
-
tag_hardware_id: z.string().describe('NFC tag hardware serial number (e.g., "04:CB:6C:51:CE:2A:81")'),
|
|
873
|
-
tag_name: z.string().describe('Friendly name (e.g., "Ballroom A Door", "Breaker Panel 3")'),
|
|
874
|
-
target_type: z.enum(['project', 'task_list', 'task']).describe('What this tag points to'),
|
|
875
|
-
project_id: z.string().optional().describe('Project ID'),
|
|
876
|
-
task_list_id: z.string().optional().describe('Task list ID (required for task_list/task targets)'),
|
|
877
|
-
task_id: z.string().optional().describe('Task ID (required for task targets)'),
|
|
878
|
-
location_description: z.string().optional().describe('Physical location of the tag')
|
|
879
|
-
},
|
|
880
|
-
annotations: { readOnlyHint: false, destructiveHint: false }
|
|
881
|
-
},
|
|
882
|
-
async ({ tag_hardware_id, tag_name, target_type, project_id, task_list_id, task_id, location_description }) => {
|
|
883
|
-
const result = await apiCall('POST', '/tags', {
|
|
884
|
-
tag_hardware_id, tag_name, target_type, project_id, task_list_id, task_id, location_description
|
|
885
|
-
});
|
|
886
|
-
const d = result.data;
|
|
887
|
-
const nfcUrl = 'https://app.tascan.io/scan?tag={TAG-ID}&time={DATETIME}';
|
|
888
|
-
return {
|
|
889
|
-
content: [{
|
|
890
|
-
type: 'text',
|
|
891
|
-
text: `Tag registered: "${d.tag_name}" (${d.tag_hardware_id})\nTarget: ${d.target_type}\nID: ${d.id}\n\nProgram the NFC tag with this URL:\n${nfcUrl}\n\n${JSON.stringify(d, null, 2)}`
|
|
892
|
-
}]
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
// Tool 29: List Tags
|
|
898
|
-
server.registerTool(
|
|
899
|
-
'tascan_list_tags',
|
|
900
|
-
{
|
|
901
|
-
title: 'List NFC Tags',
|
|
902
|
-
description: 'List all registered NFC tags in the organization with their linked projects/task lists and scan counts',
|
|
903
|
-
inputSchema: {},
|
|
904
|
-
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
905
|
-
},
|
|
906
|
-
async () => {
|
|
907
|
-
const result = await apiCall('GET', '/tags');
|
|
908
|
-
const tags = result.data;
|
|
909
|
-
if (!tags.length) return { content: [{ type: 'text', text: 'No NFC tags registered.' }] };
|
|
910
|
-
let text = `${tags.length} tag(s) registered:\n\n`;
|
|
911
|
-
tags.forEach((t, i) => {
|
|
912
|
-
text += `${i+1}. "${t.tag_name}" — ${t.tag_hardware_id}\n`;
|
|
913
|
-
text += ` Target: ${t.target_type} | Scans: ${t.scan_count} | Active: ${t.is_active}\n`;
|
|
914
|
-
if (t.projects) text += ` Project: ${t.projects.name}\n`;
|
|
915
|
-
if (t.task_lists) text += ` Task List: ${t.task_lists.name}\n`;
|
|
916
|
-
if (t.location_description) text += ` Location: ${t.location_description}\n`;
|
|
917
|
-
text += '\n';
|
|
918
|
-
});
|
|
919
|
-
return { content: [{ type: 'text', text }] };
|
|
920
|
-
}
|
|
921
|
-
);
|
|
922
|
-
|
|
923
|
-
// Tool 30: Get Scan History
|
|
924
|
-
server.registerTool(
|
|
925
|
-
'tascan_get_scan_history',
|
|
926
|
-
{
|
|
927
|
-
title: 'Get Scan History',
|
|
928
|
-
description: 'Get scan event history for a registered NFC tag — timestamps, GPS coordinates, and accuracy for each scan',
|
|
929
|
-
inputSchema: {
|
|
930
|
-
tag_id: z.string().describe('Tag registry ID (from tascan_list_tags)'),
|
|
931
|
-
limit: z.number().optional().describe('Max results (default 50)')
|
|
932
|
-
},
|
|
933
|
-
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
934
|
-
},
|
|
935
|
-
async ({ tag_id, limit }) => {
|
|
936
|
-
const path = `/tags/${tag_id}/scans` + (limit ? `?limit=${limit}` : '');
|
|
937
|
-
const result = await apiCall('GET', path);
|
|
938
|
-
const scans = result.data;
|
|
939
|
-
if (!scans.length) return { content: [{ type: 'text', text: 'No scans recorded for this tag.' }] };
|
|
940
|
-
let text = `${scans.length} scan(s):\n\n`;
|
|
941
|
-
scans.forEach((s, i) => {
|
|
942
|
-
text += `${i+1}. ${s.scanned_at}`;
|
|
943
|
-
if (s.gps_lat && s.gps_lng) text += ` — GPS: ${s.gps_lat.toFixed(6)}, ${s.gps_lng.toFixed(6)} (±${Math.round(s.gps_accuracy || 0)}m)`;
|
|
944
|
-
text += '\n';
|
|
945
|
-
});
|
|
946
|
-
return { content: [{ type: 'text', text }] };
|
|
947
|
-
}
|
|
948
|
-
);
|
|
949
|
-
|
|
69
|
+
});
|
|
950
70
|
|
|
951
|
-
// ─── Start Server ──────────────────────────────────────────
|
|
952
71
|
async function main() {
|
|
953
72
|
const transport = new StdioServerTransport();
|
|
954
73
|
await server.connect(transport);
|