tascan-mcp 3.8.0 → 3.9.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/index.js +1 -1
- package/package.json +2 -2
- package/tools.cjs +103 -15
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"mcpName": "io.github.snowbikemike/tascan-mcp",
|
|
5
|
-
"description": "TaScan MCP Server — Closed-loop autonomous operations protocol.
|
|
5
|
+
"description": "TaScan MCP Server — Closed-loop autonomous operations protocol. 61 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, worker identity merge, AI issue analysis, and autonomous remediation dispatch. 15 patents filed (470+ claims). Task. Scan. Done.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": {
|
package/tools.cjs
CHANGED
|
@@ -159,7 +159,8 @@ const TOOLS = [
|
|
|
159
159
|
name: { type: 'string', description: 'Event name' },
|
|
160
160
|
description: { type: 'string', description: 'Event description' },
|
|
161
161
|
team_mode: { type: 'boolean', description: 'Team mode — shared completions' },
|
|
162
|
-
multi_instance: { type: 'boolean', description: 'Multi-instance — each worker gets isolated copy' }
|
|
162
|
+
multi_instance: { type: 'boolean', description: 'Multi-instance — each worker gets isolated copy' },
|
|
163
|
+
assigned_worker_ids: { type: 'array', items: { type: 'string' }, description: 'Worker UUIDs to assign to this event. Each gets a personal tap-to-open link. Note: a worker holds one event assignment per project — assigning moves them.' }
|
|
163
164
|
},
|
|
164
165
|
required: ['project_id', 'name']
|
|
165
166
|
},
|
|
@@ -168,8 +169,14 @@ const TOOLS = [
|
|
|
168
169
|
const body = { name: args.name, description: args.description };
|
|
169
170
|
if (args.team_mode !== undefined) body.team_mode = args.team_mode;
|
|
170
171
|
if (args.multi_instance !== undefined) body.multi_instance = args.multi_instance;
|
|
172
|
+
if (args.assigned_worker_ids) body.assigned_worker_ids = args.assigned_worker_ids;
|
|
171
173
|
const result = await api('POST', `/projects/${args.project_id}/lists`, body);
|
|
172
|
-
|
|
174
|
+
let text = `Event created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
175
|
+
if (result.data.assigned_workers?.length) {
|
|
176
|
+
text += '\n\nAssigned workers:';
|
|
177
|
+
for (const w of result.data.assigned_workers) text += `\n ${w.name}: ${w.worker_link}`;
|
|
178
|
+
}
|
|
179
|
+
return text;
|
|
173
180
|
}
|
|
174
181
|
},
|
|
175
182
|
{
|
|
@@ -211,7 +218,8 @@ const TOOLS = [
|
|
|
211
218
|
description: { type: 'string', description: 'New description' },
|
|
212
219
|
team_mode: { type: 'boolean', description: 'Team mode — shared completions' },
|
|
213
220
|
multi_instance: { type: 'boolean', description: 'Multi-instance — each worker gets isolated copy' },
|
|
214
|
-
timer_mode: { type: 'string', description: 'Timer mode (auto or manual)' }
|
|
221
|
+
timer_mode: { type: 'string', description: 'Timer mode (auto or manual)' },
|
|
222
|
+
assigned_worker_ids: { type: 'array', items: { type: 'string' }, description: 'Worker UUIDs to assign to this event (additive — workers not named are left alone). Each gets a personal tap-to-open link. Note: a worker holds one event assignment per project — assigning moves them.' }
|
|
215
223
|
},
|
|
216
224
|
required: ['list_id']
|
|
217
225
|
},
|
|
@@ -220,7 +228,12 @@ const TOOLS = [
|
|
|
220
228
|
const { list_id, ...updates } = args;
|
|
221
229
|
const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
|
|
222
230
|
const result = await api('PUT', `/lists/${list_id}`, body);
|
|
223
|
-
|
|
231
|
+
let text = `Event updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
232
|
+
if (result.data.assigned_workers?.length) {
|
|
233
|
+
text += '\n\nAssigned workers:';
|
|
234
|
+
for (const w of result.data.assigned_workers) text += `\n ${w.name}: ${w.worker_link}`;
|
|
235
|
+
}
|
|
236
|
+
return text;
|
|
224
237
|
}
|
|
225
238
|
},
|
|
226
239
|
{
|
|
@@ -257,6 +270,7 @@ const TOOLS = [
|
|
|
257
270
|
requires_photo: { type: 'boolean', description: 'Require photo on completion' },
|
|
258
271
|
sort_order: { type: 'number', description: 'Explicit sort position (defaults to end of list, in array order)' },
|
|
259
272
|
required_equipment: { type: 'string', description: 'Equipment this task depends on (free text, e.g. "88 lb kettlebells")' },
|
|
273
|
+
assigned_to: { type: 'string', description: 'Worker to assign this task to — pass the worker UUID (validated against your org and stored as the worker\'s name, which is how the worker portal matches "my tasks"). A plain name string is also accepted as-is.' },
|
|
260
274
|
subtasks: {
|
|
261
275
|
type: 'array',
|
|
262
276
|
items: {
|
|
@@ -407,7 +421,8 @@ const TOOLS = [
|
|
|
407
421
|
is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical flag' },
|
|
408
422
|
requires_photo: { type: 'boolean', description: 'Require photo' },
|
|
409
423
|
sort_order: { type: 'number', description: 'Sort position' },
|
|
410
|
-
required_equipment: { type: 'string', description: 'Equipment this task depends on (free text)' }
|
|
424
|
+
required_equipment: { type: 'string', description: 'Equipment this task depends on (free text)' },
|
|
425
|
+
assigned_to: { type: 'string', description: 'Worker to assign this task to — pass the worker UUID (validated against your org and stored as the worker\'s name, which is how the worker portal matches "my tasks"). A plain name string is also accepted as-is. Pass null to clear.' }
|
|
411
426
|
},
|
|
412
427
|
required: ['task_id']
|
|
413
428
|
},
|
|
@@ -610,11 +625,23 @@ const TOOLS = [
|
|
|
610
625
|
},
|
|
611
626
|
{
|
|
612
627
|
name: 'tascan_list_workers',
|
|
613
|
-
description: 'List
|
|
614
|
-
inputSchema: { type: 'object', properties: {
|
|
628
|
+
description: 'List workers (taskees) in the organization. Supports filtering by name/email/phone substring, contact-info presence, and last-activity date. Each row includes completion_count (total task completions).',
|
|
629
|
+
inputSchema: { type: 'object', properties: {
|
|
630
|
+
query: { type: 'string', description: 'Substring match on name, email, or phone' },
|
|
631
|
+
has_phone: { type: 'boolean', description: 'true = only workers with a phone on file; false = only without' },
|
|
632
|
+
has_email: { type: 'boolean', description: 'true/false — same for email' },
|
|
633
|
+
active_since: { type: 'string', description: 'ISO date/datetime — only workers with last_active_at on/after this' },
|
|
634
|
+
include_inactive: { type: 'boolean', description: 'true = also include inactive workers (e.g. tombstones left by a merge or delete) — useful for auditing right after tascan_merge_workers / tascan_delete_worker' }
|
|
635
|
+
} },
|
|
615
636
|
annotations: { title: 'List Workers', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
616
637
|
handler: async (args, api) => {
|
|
617
|
-
const
|
|
638
|
+
const qs = [];
|
|
639
|
+
if (args.query !== undefined) qs.push(`query=${encodeURIComponent(args.query)}`);
|
|
640
|
+
if (args.has_phone !== undefined) qs.push(`has_phone=${args.has_phone}`);
|
|
641
|
+
if (args.has_email !== undefined) qs.push(`has_email=${args.has_email}`);
|
|
642
|
+
if (args.active_since !== undefined) qs.push(`active_since=${encodeURIComponent(args.active_since)}`);
|
|
643
|
+
if (args.include_inactive === true) qs.push('include_inactive=true');
|
|
644
|
+
const result = await api('GET', '/workers' + (qs.length ? '?' + qs.join('&') : ''));
|
|
618
645
|
return JSON.stringify(result.data, null, 2);
|
|
619
646
|
}
|
|
620
647
|
},
|
|
@@ -983,7 +1010,8 @@ const TOOLS = [
|
|
|
983
1010
|
worker_id: { type: 'string', description: 'Worker ID — sends to their phone on file (preferred over raw phone)' },
|
|
984
1011
|
phone: { type: 'string', description: 'Raw phone number (e.g. "+17025551234") — used when no worker_id given' },
|
|
985
1012
|
message: { type: 'string', description: 'Message text. Transactional and work-related only.' },
|
|
986
|
-
list_id: { type: 'string', description: 'Optional task list ID — appends a tap-to-open worker checklist link' }
|
|
1013
|
+
list_id: { type: 'string', description: 'Optional task list ID — appends a tap-to-open worker checklist link' },
|
|
1014
|
+
include_link: { type: 'boolean', description: 'When a list_id is given, append the tap-to-open link to the SMS body (default true). Set false to send the message text alone — the link is still returned for you to share another way.' }
|
|
987
1015
|
},
|
|
988
1016
|
required: ['message']
|
|
989
1017
|
},
|
|
@@ -993,14 +1021,30 @@ const TOOLS = [
|
|
|
993
1021
|
worker_id: args.worker_id,
|
|
994
1022
|
phone: args.phone,
|
|
995
1023
|
message: args.message,
|
|
996
|
-
list_id: args.list_id
|
|
1024
|
+
list_id: args.list_id,
|
|
1025
|
+
include_link: args.include_link
|
|
997
1026
|
});
|
|
998
1027
|
const d = result.data;
|
|
999
1028
|
let text = `SMS sent!\n`;
|
|
1000
1029
|
text += ` To: ${d.worker_name ? d.worker_name + ' (' + d.to + ')' : d.to}\n`;
|
|
1001
|
-
text += ` Status: ${d.sms_status}
|
|
1002
|
-
text +=
|
|
1003
|
-
|
|
1030
|
+
text += ` Status: ${d.sms_status}`;
|
|
1031
|
+
if (d.twilio_sid) text += `\n Twilio SID: ${d.twilio_sid}`;
|
|
1032
|
+
text += `\n Message: ${d.message_body}`;
|
|
1033
|
+
if (d.short_url) text += `\n Short link: ${d.short_url}`;
|
|
1034
|
+
else if (d.worker_url) text += `\n Checklist link: ${d.worker_url}`;
|
|
1035
|
+
return text;
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
name: 'tascan_get_sms_status',
|
|
1040
|
+
description: 'Check delivery status of a previously sent TaScan SMS by its Twilio SID (returned by tascan_send_sms). Shows queued/sent/delivered/undelivered/failed plus carrier error codes.',
|
|
1041
|
+
inputSchema: { type: 'object', properties: { twilio_sid: { type: 'string', description: 'Twilio message SID (SM...) from tascan_send_sms' } }, required: ['twilio_sid'] },
|
|
1042
|
+
annotations: { title: 'Get SMS Status', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
1043
|
+
handler: async (args, api) => {
|
|
1044
|
+
const d = (await api('GET', `/sms/${args.twilio_sid}`)).data;
|
|
1045
|
+
let text = `SMS ${d.twilio_sid}\n Status: ${d.sms_status}\n To: ${d.to}\n Sent: ${d.sent_at || 'not yet'}`;
|
|
1046
|
+
if (d.num_segments) text += `\n Segments: ${d.num_segments}`;
|
|
1047
|
+
if (d.error_code) text += `\n Error ${d.error_code}: ${d.error_message || ''}`;
|
|
1004
1048
|
return text;
|
|
1005
1049
|
}
|
|
1006
1050
|
},
|
|
@@ -1503,7 +1547,8 @@ const TOOLS = [
|
|
|
1503
1547
|
properties: {
|
|
1504
1548
|
worker_id: { type: 'string', description: 'Anchor worker to find duplicates OF (preferred — enables phone + GPS signals)' },
|
|
1505
1549
|
name: { type: 'string', description: 'Or: search duplicates by display name' },
|
|
1506
|
-
threshold: { type: 'number', description: 'Min confidence 0-1 (default 0.15)' }
|
|
1550
|
+
threshold: { type: 'number', description: 'Min confidence 0-1 (default 0.15)' },
|
|
1551
|
+
include_orphans: { type: 'boolean', description: 'Also consider org-less (orphan) worker records — off by default' }
|
|
1507
1552
|
},
|
|
1508
1553
|
required: []
|
|
1509
1554
|
},
|
|
@@ -1513,15 +1558,58 @@ const TOOLS = [
|
|
|
1513
1558
|
if (args.worker_id) qs.push(`worker_id=${args.worker_id}`);
|
|
1514
1559
|
if (args.name) qs.push(`name=${encodeURIComponent(args.name)}`);
|
|
1515
1560
|
if (args.threshold != null) qs.push(`threshold=${args.threshold}`);
|
|
1561
|
+
if (args.include_orphans) qs.push('include_orphans=true');
|
|
1516
1562
|
const result = await api('GET', '/workers/duplicates?' + qs.join('&'));
|
|
1517
1563
|
const d = result.data;
|
|
1518
1564
|
if (!d.candidates.length) return `No duplicate candidates for ${d.anchor.name || d.anchor.id}.`;
|
|
1519
1565
|
let text = `Duplicate candidates for ${d.anchor.name || ''} (${d.anchor.id || 'name search'}):\n`;
|
|
1520
1566
|
d.candidates.forEach(c => {
|
|
1521
|
-
text += `\n${c.name} (${c.id}) — confidence ${c.confidence}\n signals: ${JSON.stringify(c.signals)}\n created ${String(c.created_at).slice(0, 10)} · last active ${String(c.last_active_at || '').slice(0, 10)} · ${c.total_points} pts`;
|
|
1567
|
+
text += `\n${c.name} (${c.id}) — confidence ${c.confidence}\n signals: ${JSON.stringify(c.signals)}\n created ${String(c.created_at).slice(0, 10)} · last active ${String(c.last_active_at || '').slice(0, 10)} · ${c.total_points} pts · ${c.merge_ready ? 'MERGE-READY' : 'blockers: ' + (c.blockers || []).join(',')}`;
|
|
1522
1568
|
});
|
|
1569
|
+
if (d.suggested_merge) {
|
|
1570
|
+
text += `\n\nNext step: tascan_merge_workers(primary_worker_id=${d.suggested_merge.primary_worker_id}, duplicate_worker_ids=[${d.suggested_merge.duplicate_worker_ids.join(', ')}], dry_run=true)`;
|
|
1571
|
+
}
|
|
1523
1572
|
return text;
|
|
1524
1573
|
}
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
name: 'tascan_merge_workers',
|
|
1577
|
+
description: 'Merge duplicate worker records into one canonical identity (Patent 4 identity consolidation). Reassigns every reference (completions, timer events, points, payments, rosters — 31 columns across 31 tables), backfills missing phone/email on the primary, sums points, and tombstones the duplicates (merged_into + is_active=false — NEVER hard-deletes). ALWAYS run with dry_run=true first and show Mike the counts; pass dry_run=false only after explicit confirmation.',
|
|
1578
|
+
inputSchema: {
|
|
1579
|
+
type: 'object',
|
|
1580
|
+
properties: {
|
|
1581
|
+
primary_worker_id: { type: 'string', description: 'The canonical worker that survives (usually the one with a phone)' },
|
|
1582
|
+
duplicate_worker_ids: { type: 'array', items: { type: 'string' }, description: 'Worker IDs to fold into the primary (max 20)' },
|
|
1583
|
+
dry_run: { type: 'boolean', description: 'true (default) = report reassignment counts only, change nothing. false = execute atomically.' }
|
|
1584
|
+
},
|
|
1585
|
+
required: ['primary_worker_id', 'duplicate_worker_ids']
|
|
1586
|
+
},
|
|
1587
|
+
annotations: { title: 'Merge Workers', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
1588
|
+
handler: async (args, api) => {
|
|
1589
|
+
const result = await api('POST', '/workers/merge', {
|
|
1590
|
+
primary_worker_id: args.primary_worker_id,
|
|
1591
|
+
duplicate_worker_ids: args.duplicate_worker_ids,
|
|
1592
|
+
dry_run: args.dry_run !== false
|
|
1593
|
+
});
|
|
1594
|
+
const d = result.data;
|
|
1595
|
+
const rows = Object.entries(d.counts || {}).map(([t, n]) => ` ${t}: ${n}`).join('\n') || ' (no references found)';
|
|
1596
|
+
const conf = (d.conflicts || []).length ? `\nConflicts:\n ${d.conflicts.join('\n ')}` : '';
|
|
1597
|
+
return `${d.dry_run ? 'DRY RUN — no changes made' : 'MERGE EXECUTED'}\nPrimary: ${d.primary_worker_id}\nDuplicates: ${(d.duplicate_worker_ids || []).join(', ')}\nReassignment counts:\n${rows}${conf}${d.dry_run ? '\n\nTo execute: call again with dry_run=false.' : '\nDuplicates tombstoned (merged_into set, is_active=false). Logged to worker_merge_log.'}`;
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
name: 'tascan_delete_worker',
|
|
1602
|
+
description: 'Tombstone a worker record (is_active=false). REFUSES if the worker has any task/subtask completions or payments — merge those into the real worker with tascan_merge_workers instead. Never hard-deletes.',
|
|
1603
|
+
inputSchema: {
|
|
1604
|
+
type: 'object',
|
|
1605
|
+
properties: { worker_id: { type: 'string', description: 'Worker ID to delete' } },
|
|
1606
|
+
required: ['worker_id']
|
|
1607
|
+
},
|
|
1608
|
+
annotations: { title: 'Delete Worker', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
1609
|
+
handler: async (args, api) => {
|
|
1610
|
+
const result = await api('DELETE', `/workers/${args.worker_id}`);
|
|
1611
|
+
return `Worker ${args.worker_id} tombstoned (soft delete — record retained for audit).`;
|
|
1612
|
+
}
|
|
1525
1613
|
}
|
|
1526
1614
|
];
|
|
1527
1615
|
|