tascan-mcp 3.6.0 → 3.7.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 (2) hide show
  1. package/package.json +1 -1
  2. package/tools.cjs +175 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tascan-mcp",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "mcpName": "io.github.snowbikemike/tascan-mcp",
5
5
  "description": "TaScan MCP Server — Closed-loop autonomous operations protocol. 50 tools for projects, tasks, workers, QR codes, NFC tags, geofenced zones, AI condition assessment, worker passports, verification-gated gig payments, cross-entity search, duplicate-worker detection, AI issue analysis, and autonomous remediation dispatch. 14 patents filed (435+ claims). Task. Scan. Done.",
6
6
  "type": "module",
package/tools.cjs CHANGED
@@ -252,8 +252,25 @@ const TOOLS = [
252
252
  title: { type: 'string', description: 'Task title' },
253
253
  description: { type: 'string', description: 'Task description' },
254
254
  response_type: { type: 'string', enum: ['checkbox', 'photo', 'text', 'number', 'date', 'choice'], description: 'CRITICAL: "text" for names, phones, emails, notes, addresses, any free-form input. "photo" for tasks needing photographic proof (inspections, serial numbers, packed cases). "checkbox" ONLY for simple yes/no confirmations. "number" for numeric values. "date" for dates. "choice" for multiple-choice (needs response_config.options). Most info-collection tasks should be "text", most verification tasks should be "photo".' },
255
+ response_config: { type: 'object', description: 'Response configuration. For "choice": {options: ["A","B","C"]}. For "text": {placeholder, multiline}. For "number": {placeholder}. For "date": {label, include_text}.' },
255
256
  is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical task flag' },
256
- requires_photo: { type: 'boolean', description: 'Require photo on completion' }
257
+ requires_photo: { type: 'boolean', description: 'Require photo on completion' },
258
+ sort_order: { type: 'number', description: 'Explicit sort position (defaults to end of list, in array order)' },
259
+ required_equipment: { type: 'string', description: 'Equipment this task depends on (free text, e.g. "88 lb kettlebells")' },
260
+ subtasks: {
261
+ type: 'array',
262
+ items: {
263
+ type: 'object',
264
+ properties: {
265
+ title: { type: 'string', description: 'Subtask title (e.g. "Set 1")' },
266
+ response_type: { type: 'string', enum: ['checkbox', 'number', 'text', 'choice'], description: 'Typed subtask response. "number" for per-set values (reps/weight), "text" for notes, "choice" needs response_config.options. Default checkbox.' },
267
+ response_config: { type: 'object', description: 'Same shape as task response_config' },
268
+ requires_photo: { type: 'boolean' }
269
+ },
270
+ required: ['title']
271
+ },
272
+ description: 'Subtasks created with the task. Example set logging: [{title:"Set 1",response_type:"number"},{title:"Set 2",response_type:"number"},{title:"Set 3",response_type:"number"}]'
273
+ }
257
274
  },
258
275
  required: ['title']
259
276
  },
@@ -386,9 +403,11 @@ const TOOLS = [
386
403
  title: { type: 'string', description: 'New title' },
387
404
  description: { type: 'string', description: 'New description' },
388
405
  response_type: { type: 'string', enum: ['checkbox', 'photo', 'text', 'number', 'date', 'choice'], description: 'See tascan_add_tasks for guidance. "text" for info collection, "photo" for visual proof, "checkbox" for yes/no only.' },
406
+ response_config: { type: 'object', description: 'Response configuration. For "choice": {options: [...]}. See tascan_add_tasks.' },
389
407
  is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical flag' },
390
408
  requires_photo: { type: 'boolean', description: 'Require photo' },
391
- sort_order: { type: 'number', description: 'Sort position' }
409
+ sort_order: { type: 'number', description: 'Sort position' },
410
+ required_equipment: { type: 'string', description: 'Equipment this task depends on (free text)' }
392
411
  },
393
412
  required: ['task_id']
394
413
  },
@@ -400,6 +419,157 @@ const TOOLS = [
400
419
  return `Task updated: ${result.data.title}\n\n${JSON.stringify(result.data, null, 2)}`;
401
420
  }
402
421
  },
422
+ {
423
+ name: 'tascan_reply_with_list',
424
+ description: 'Reply to a task list WITH a task list — the two-way tasking primitive. Creates a new list linked into the parent\'s thread, aimed back at whoever sent the original (e.g. "Grant access — pick a window" with response_type date, or an info request with response_type text). The org gets pinged; the thread shows in both the worker portal and Simple Mode. Use tascan_get_thread-style follow-up via tascan_list_projects/tascan_get_report to read answers.',
425
+ inputSchema: {
426
+ type: 'object',
427
+ properties: {
428
+ parent_list_id: { type: 'string', description: 'The list being replied to' },
429
+ title: { type: 'string', description: 'Reply list title (e.g. "Before I can start...")' },
430
+ author_name: { type: 'string', description: 'Who is replying (shown in the thread)' },
431
+ tasks: {
432
+ type: 'array',
433
+ items: {
434
+ type: 'object',
435
+ properties: {
436
+ title: { type: 'string' },
437
+ description: { type: 'string' },
438
+ response_type: { type: 'string', enum: ['checkbox', 'text', 'number', 'date', 'choice', 'photo'], description: '"date" for scheduling windows, "text" for info requests, "choice" needs response_config.options' },
439
+ response_config: { type: 'object' }
440
+ },
441
+ required: ['title']
442
+ },
443
+ description: 'Items the recipient answers — each becomes a typed task'
444
+ }
445
+ },
446
+ required: ['parent_list_id', 'title', 'tasks']
447
+ },
448
+ annotations: { title: 'Reply With a List', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
449
+ handler: async (args) => {
450
+ const resp = await fetch('https://app.tascan.io/.netlify/functions/list-reply', {
451
+ method: 'POST',
452
+ headers: { 'Content-Type': 'application/json' },
453
+ body: JSON.stringify({
454
+ parent_list_id: args.parent_list_id,
455
+ title: args.title,
456
+ author_name: args.author_name,
457
+ tasks: args.tasks
458
+ })
459
+ });
460
+ const data = await resp.json();
461
+ if (!resp.ok) throw new Error(data.error || 'Reply failed');
462
+ return `Reply list created and linked into the thread!\n List: ${args.title} (${data.list_id})\n Thread: ${data.thread_id}\n Tasks: ${data.task_count}\n Worker link: ${data.worker_url}\nThe org has been pinged via app_messages.`;
463
+ }
464
+ },
465
+ {
466
+ name: 'tascan_add_subtasks',
467
+ description: 'Add one or more subtasks to a task (bulk). Subtasks support typed responses: "number" for per-set data (reps, weight, distance), "text" for notes, "choice" for options, "checkbox" for simple steps. Set-logging example: task "Bench Press" with subtasks Set 1/Set 2/Set 3 each response_type "number" — each completed set stores its value and timestamp, giving per-set timing for progression tracking.',
468
+ inputSchema: {
469
+ type: 'object',
470
+ properties: {
471
+ task_id: { type: 'string', description: 'Parent task ID' },
472
+ subtasks: {
473
+ type: 'array',
474
+ items: {
475
+ type: 'object',
476
+ properties: {
477
+ title: { type: 'string', description: 'Subtask title (e.g. "Set 1")' },
478
+ description: { type: 'string', description: 'Optional detail' },
479
+ response_type: { type: 'string', enum: ['checkbox', 'number', 'text', 'choice'], description: 'Default checkbox. "number" for per-set values. "choice" needs response_config.options.' },
480
+ response_config: { type: 'object', description: 'For "choice": {options: [...]}. For "number"/"text": {placeholder}.' },
481
+ requires_photo: { type: 'boolean', description: 'Require photo on completion' },
482
+ sort_order: { type: 'number', description: 'Explicit position (defaults to end, in array order)' }
483
+ },
484
+ required: ['title']
485
+ },
486
+ description: 'Array of subtasks to create'
487
+ }
488
+ },
489
+ required: ['task_id', 'subtasks']
490
+ },
491
+ annotations: { title: 'Add Subtasks', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
492
+ handler: async (args, api) => {
493
+ const result = await api('POST', `/tasks/${args.task_id}/subtasks`, args.subtasks);
494
+ return `${result.count} subtask(s) created on task ${args.task_id}\n\n${JSON.stringify(result.data, null, 2)}`;
495
+ }
496
+ },
497
+ {
498
+ name: 'tascan_list_subtasks',
499
+ description: 'List the subtasks of a task, including completion state, stored response values, and completion timestamps (per-set timing).',
500
+ inputSchema: {
501
+ type: 'object',
502
+ properties: { task_id: { type: 'string', description: 'Parent task ID' } },
503
+ required: ['task_id']
504
+ },
505
+ annotations: { title: 'List Subtasks', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
506
+ handler: async (args, api) => {
507
+ const result = await api('GET', `/tasks/${args.task_id}/subtasks`);
508
+ return JSON.stringify(result.data, null, 2);
509
+ }
510
+ },
511
+ {
512
+ name: 'tascan_update_subtask',
513
+ description: 'Update a subtask (title, description, response_type, response_config, requires_photo, sort_order).',
514
+ inputSchema: {
515
+ type: 'object',
516
+ properties: {
517
+ subtask_id: { type: 'string', description: 'Subtask ID' },
518
+ title: { type: 'string' },
519
+ description: { type: 'string' },
520
+ response_type: { type: 'string', enum: ['checkbox', 'number', 'text', 'choice'] },
521
+ response_config: { type: 'object' },
522
+ requires_photo: { type: 'boolean' },
523
+ sort_order: { type: 'number' }
524
+ },
525
+ required: ['subtask_id']
526
+ },
527
+ annotations: { title: 'Update Subtask', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
528
+ handler: async (args, api) => {
529
+ const { subtask_id, ...updates } = args;
530
+ const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
531
+ const result = await api('PUT', `/subtasks/${subtask_id}`, body);
532
+ return `Subtask updated: ${result.data.title}\n\n${JSON.stringify(result.data, null, 2)}`;
533
+ }
534
+ },
535
+ {
536
+ name: 'tascan_complete_subtask',
537
+ description: 'Complete a subtask, optionally recording a typed response_value (e.g. the weight or reps for that set). Each completion is timestamped, so consecutive set completions yield per-set durations. Returns progress including all_subtasks_complete — when true, complete the parent task with tascan_complete_task.',
538
+ inputSchema: {
539
+ type: 'object',
540
+ properties: {
541
+ subtask_id: { type: 'string', description: 'Subtask ID to complete' },
542
+ worker_id: { type: 'string', description: 'Worker performing the completion (optional)' },
543
+ response_value: { type: 'string', description: 'Typed response value (for number/text/choice subtasks), e.g. "165"' },
544
+ notes: { type: 'string', description: 'Optional notes' }
545
+ },
546
+ required: ['subtask_id']
547
+ },
548
+ annotations: { title: 'Complete Subtask', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
549
+ handler: async (args, api) => {
550
+ const result = await api('POST', `/subtasks/${args.subtask_id}/complete`, {
551
+ worker_id: args.worker_id,
552
+ response_value: args.response_value,
553
+ notes: args.notes
554
+ });
555
+ const d = result.data;
556
+ return `Subtask completed!\n ${d.subtask_title}${d.response_value ? ' = ' + d.response_value : ''}\n Task: ${d.task_title}\n Progress: ${d.subtasks_done}/${d.subtasks_total}${d.all_subtasks_complete ? '\n ALL SUBTASKS COMPLETE — parent task is ready to complete.' : ''}`;
557
+ }
558
+ },
559
+ {
560
+ name: 'tascan_delete_subtask',
561
+ description: 'Delete a subtask and its completions. This action is irreversible.',
562
+ inputSchema: {
563
+ type: 'object',
564
+ properties: { subtask_id: { type: 'string', description: 'Subtask ID to delete' } },
565
+ required: ['subtask_id']
566
+ },
567
+ annotations: { title: 'Delete Subtask', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
568
+ handler: async (args, api) => {
569
+ await api('DELETE', `/subtasks/${args.subtask_id}`);
570
+ return `Subtask ${args.subtask_id} deleted successfully.`;
571
+ }
572
+ },
403
573
  {
404
574
  name: 'tascan_delete_task',
405
575
  description: 'Delete a specific task and its completions. This action is irreversible.',
@@ -571,7 +741,7 @@ const TOOLS = [
571
741
  },
572
742
  {
573
743
  name: 'tascan_query_responses',
574
- description: "Query one task's submitted responses across every list in a project — e.g. the same exercise repeated across many workout lists returns one chronological progression series instead of N report lookups. Match by task title pattern or exact task ID.",
744
+ description: "Query one task's submitted responses across every list in a project — e.g. the same exercise repeated across many workout lists returns one chronological progression series instead of N report lookups. Match by task title pattern or exact task ID. Subtask completions interleave into the same series labeled 'Task › Subtask' (e.g. per-set values Set 1/2/3 with their own timestamps), so set-level progression chains across lists automatically.",
575
745
  inputSchema: {
576
746
  type: 'object',
577
747
  properties: {
@@ -592,7 +762,8 @@ const TOOLS = [
592
762
  const { value: v0, note } = fmtResponse(resp.response_value, resp.notes);
593
763
  const value = (v0 == null && resp.photo_url) ? '[photo submitted]' : (v0 != null && resp.photo_url) ? v0 + ' [+photo]' : v0;
594
764
  const when = (resp.completed_at || '').slice(0, 16).replace('T', ' ');
595
- text += `${when} · ${resp.list_name} · ${resp.task_title}: ${value != null ? value : '(no value)'}`;
765
+ const label = resp.subtask_title ? `${resp.task_title} › ${resp.subtask_title}` : resp.task_title;
766
+ text += `${when} · ${resp.list_name} · ${label}: ${value != null ? value : '(no value)'}`;
596
767
  if (note) text += ` — ${note}`;
597
768
  text += ` (${resp.worker_name})\n`;
598
769
  }