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.
Files changed (4) hide show
  1. package/README.md +3 -2
  2. package/index.js +32 -913
  3. package/package.json +3 -2
  4. 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
- // Wraps TaScan REST API v1 for Claude and other AI agents
6
- // Transport: stdio (for Claude Desktop / Claude Code)
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 { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
- import { z } from 'zod';
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
- // ─── API Helper ────────────────────────────────────────────
22
- async function apiCall(method, path, body) {
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
- // ─── Server Setup ──────────────────────────────────────────
47
- const server = new McpServer({
48
- name: 'tascan',
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
- // 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
- );
52
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
53
+ tools: TOOLS.map(({ name, description, inputSchema, annotations }) => ({
54
+ name, description, inputSchema, annotations
55
+ }))
56
+ }));
744
57
 
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 }] };
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
- // 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 }] };
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);