tascan-mcp 3.4.0 → 3.6.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/package.json +1 -1
- package/tools.cjs +90 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.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
|
@@ -896,31 +896,97 @@ const TOOLS = [
|
|
|
896
896
|
},
|
|
897
897
|
{
|
|
898
898
|
name: 'tascan_get_scan_history',
|
|
899
|
-
description: '
|
|
899
|
+
description: 'Scan accountability data. Two modes: (1) tag_id — scans of a registered NFC tag; (2) task_list_id or project_id — every QR/link page-open stamp: when the code was scanned, GPS + IP + channel (qr/nfc/sms/email/link), who the scanner turned out to be, and the scan→start delta (how long between scanning and actually identifying + starting work — the sign-in-and-vanish metric).',
|
|
900
900
|
inputSchema: {
|
|
901
901
|
type: 'object',
|
|
902
902
|
properties: {
|
|
903
|
-
tag_id: { type: 'string', description: '
|
|
904
|
-
|
|
903
|
+
tag_id: { type: 'string', description: 'NFC tag registry ID (from tascan_list_tags) — tag mode' },
|
|
904
|
+
task_list_id: { type: 'string', description: 'Task list ID — page-scan mode' },
|
|
905
|
+
project_id: { type: 'string', description: 'Project ID — page-scan mode, all lists in the project' },
|
|
906
|
+
source: { type: 'string', description: 'Filter page scans by channel: qr | nfc | sms | email | link | unknown' },
|
|
907
|
+
since: { type: 'string', description: 'ISO timestamp — only scans after this' },
|
|
908
|
+
limit: { type: 'number', description: 'Max results (default 50, max 200)' }
|
|
905
909
|
},
|
|
906
|
-
required: [
|
|
910
|
+
required: []
|
|
907
911
|
},
|
|
908
912
|
annotations: { title: 'Get Scan History', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
909
913
|
handler: async (args, api) => {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
914
|
+
if (args.tag_id) {
|
|
915
|
+
const path = `/tags/${args.tag_id}/scans` + (args.limit ? `?limit=${args.limit}` : '');
|
|
916
|
+
const result = await api('GET', path);
|
|
917
|
+
const scans = result.data;
|
|
918
|
+
if (!scans.length) return 'No scans recorded for this tag.';
|
|
919
|
+
let text = `${scans.length} tag scan(s):\n\n`;
|
|
920
|
+
scans.forEach((s, i) => {
|
|
921
|
+
text += `${i + 1}. ${s.scanned_at}`;
|
|
922
|
+
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)`;
|
|
923
|
+
text += '\n';
|
|
924
|
+
});
|
|
925
|
+
return text;
|
|
926
|
+
}
|
|
927
|
+
if (!args.task_list_id && !args.project_id) {
|
|
928
|
+
return 'Provide tag_id (NFC tag mode) or task_list_id / project_id (page-scan mode).';
|
|
929
|
+
}
|
|
930
|
+
const qs = new URLSearchParams();
|
|
931
|
+
if (args.task_list_id) qs.set('task_list_id', args.task_list_id);
|
|
932
|
+
else qs.set('project_id', args.project_id);
|
|
933
|
+
if (args.source) qs.set('source', args.source);
|
|
934
|
+
if (args.since) qs.set('since', args.since);
|
|
935
|
+
if (args.limit) qs.set('limit', String(args.limit));
|
|
936
|
+
const result = await api('GET', '/scans?' + qs.toString());
|
|
937
|
+
const scans = result.data || [];
|
|
938
|
+
if (!scans.length) return 'No page scans recorded yet for this scope.';
|
|
939
|
+
const converted = scans.filter(s => s.converted);
|
|
940
|
+
const deltas = converted.map(s => s.scan_to_start_secs).filter(n => n != null);
|
|
941
|
+
const median = deltas.length ? deltas.sort((a, b) => a - b)[Math.floor(deltas.length / 2)] : null;
|
|
942
|
+
let text = `${scans.length} scan(s) — ${converted.length} converted to a working session (${Math.round(converted.length / scans.length * 100)}%)`;
|
|
943
|
+
if (median != null) text += ` · median scan→start ${median}s`;
|
|
944
|
+
text += '\n\n';
|
|
915
945
|
scans.forEach((s, i) => {
|
|
916
|
-
text += `${i + 1}. ${s.scanned_at}`;
|
|
917
|
-
if (s.
|
|
946
|
+
text += `${i + 1}. ${s.scanned_at} · ${s.scan_source || 'unknown'}`;
|
|
947
|
+
if (s.worker_name) text += ` · ${s.worker_name}`;
|
|
948
|
+
text += s.converted ? ` · started +${s.scan_to_start_secs}s` : ' · NEVER STARTED';
|
|
949
|
+
if (s.ip) text += ` · ip ${s.ip}`;
|
|
950
|
+
if (s.gps_lat && s.gps_lng) text += ` · GPS ${s.gps_lat.toFixed(5)}, ${s.gps_lng.toFixed(5)}`;
|
|
918
951
|
text += '\n';
|
|
919
952
|
});
|
|
920
953
|
return text;
|
|
921
954
|
}
|
|
922
955
|
},
|
|
923
956
|
|
|
957
|
+
// ─── Worker Marketplace (Patent 4: portable reputation) ──────────
|
|
958
|
+
{
|
|
959
|
+
name: 'tascan_search_marketplace',
|
|
960
|
+
description: 'Search the cross-org Worker Marketplace: workers who opted in (discoverable=true on their passport), ranked by passkey trust tier + verified completion volume. Skills are AI-inferred from REAL completed work, not resumes — each carries a verified_task_count and a civilian_equivalent job title. Returns sanitized public cards only (first name + last initial, skills, stats, passport URL) — never phone, email, or org membership.',
|
|
961
|
+
inputSchema: {
|
|
962
|
+
type: 'object',
|
|
963
|
+
properties: {
|
|
964
|
+
q: { type: 'string', description: 'Skill, category, name, or civilian job title — e.g. "forklift", "LED wall", "AV technician"' },
|
|
965
|
+
min_completions: { type: 'number', description: 'Only workers with at least this many verified completions' },
|
|
966
|
+
limit: { type: 'number', description: 'Max cards (default 25, max 50)' }
|
|
967
|
+
},
|
|
968
|
+
required: []
|
|
969
|
+
},
|
|
970
|
+
annotations: { title: 'Search Worker Marketplace', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
971
|
+
handler: async (args, api) => {
|
|
972
|
+
const qs = new URLSearchParams();
|
|
973
|
+
if (args.q) qs.set('q', args.q);
|
|
974
|
+
if (args.min_completions) qs.set('min_completions', String(args.min_completions));
|
|
975
|
+
if (args.limit) qs.set('limit', String(args.limit));
|
|
976
|
+
const result = await api('GET', '/marketplace' + (qs.toString() ? '?' + qs.toString() : ''));
|
|
977
|
+
const cards = result.data || [];
|
|
978
|
+
if (!cards.length) return 'No discoverable workers match. Workers appear after opting in on their own passport (profile page → "List me").';
|
|
979
|
+
return cards.map(w => {
|
|
980
|
+
let t = `${w.name}${w.passkey_verified ? ' ✓ID-VERIFIED' : ''} — ${w.completions} verified completions · ${w.points} pts · since ${w.member_since}`;
|
|
981
|
+
if (w.skills.length) t += '\n Skills: ' + w.skills.map(s => `${s.skill} (${s.verified_task_count}×)`).join(', ');
|
|
982
|
+
const civ = [...new Set(w.skills.map(s => s.civilian_equivalent).filter(Boolean))];
|
|
983
|
+
if (civ.length) t += '\n Civilian equivalent: ' + civ.join(' · ');
|
|
984
|
+
t += '\n Passport: ' + w.passport_url;
|
|
985
|
+
return t;
|
|
986
|
+
}).join('\n\n');
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
|
|
924
990
|
// ─── Geofenced Zones (Patent 7: location-triggered delivery) ─────
|
|
925
991
|
{
|
|
926
992
|
name: 'tascan_create_zone',
|
|
@@ -935,6 +1001,12 @@ const TOOLS = [
|
|
|
935
1001
|
project_id: { type: 'string', description: 'Project this zone belongs to' },
|
|
936
1002
|
task_list_id: { type: 'string', description: 'Task list the Site Gate routes workers to when they are inside this zone' },
|
|
937
1003
|
enforce_on_list: { type: 'boolean', description: 'Zone-lock the task list — it cannot be started from outside the radius' },
|
|
1004
|
+
notify_on_enter: { type: 'boolean', description: 'Email the manager when a worker enters this zone (danger areas)' },
|
|
1005
|
+
notify_on_exit: { type: 'boolean', description: 'Email the manager when a worker leaves this zone (accountability — sign in then disappear)' },
|
|
1006
|
+
auto_clock_in: { type: 'boolean', description: 'Entering the zone writes a shift_start clock-in event' },
|
|
1007
|
+
auto_clock_out: { type: 'boolean', description: 'Leaving the zone writes a shift_end clock-out event' },
|
|
1008
|
+
notify_email: { type: 'string', description: 'Alert recipient override — defaults to all org admins' },
|
|
1009
|
+
polygon: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Polygon/rectangle zone instead of a circle: vertices as [[lat,lng], ...], at least 3. lat/lng/radius_m are then computed (centroid + bounding radius) — still pass lat/lng but they are overridden.' },
|
|
938
1010
|
description: { type: 'string', description: 'Shown to workers on the Site Gate page' }
|
|
939
1011
|
},
|
|
940
1012
|
required: ['name', 'lat', 'lng']
|
|
@@ -943,7 +1015,7 @@ const TOOLS = [
|
|
|
943
1015
|
handler: async (args, api) => {
|
|
944
1016
|
const result = await api('POST', '/zones', args);
|
|
945
1017
|
const z = result.data;
|
|
946
|
-
let text = `Zone created: ${z.name}\n\nID: ${z.id}\nCenter: ${z.lat}, ${z.lng}\nRadius: ${z.radius_m}m\nRoutes to list: ${z.task_list_id || '(none — set task_list_id to enable Site Gate routing)'}\nZone-locked: ${z.enforce_on_list ? 'YES — list cannot start outside the zone' : 'no'}`;
|
|
1018
|
+
let text = `Zone created: ${z.name}\n\nID: ${z.id}\nCenter: ${z.lat}, ${z.lng}\nRadius: ${z.radius_m}m\nRoutes to list: ${z.task_list_id || '(none — set task_list_id to enable Site Gate routing)'}\nZone-locked: ${z.enforce_on_list ? 'YES — list cannot start outside the zone' : 'no'}\nAlerts: enter=${z.notify_on_enter ? 'ON' : 'off'} exit=${z.notify_on_exit ? 'ON' : 'off'} · Auto-clock: in=${z.auto_clock_in ? 'ON' : 'off'} out=${z.auto_clock_out ? 'ON' : 'off'}`;
|
|
947
1019
|
if (z.project_id) text += `\n\nSite Gate URL (print this QR at the site entrance):\nhttps://app.tascan.io/geo.html?project=${z.project_id}`;
|
|
948
1020
|
return text;
|
|
949
1021
|
}
|
|
@@ -963,7 +1035,7 @@ const TOOLS = [
|
|
|
963
1035
|
const zones = result.data || [];
|
|
964
1036
|
if (!zones.length) return 'No zones configured. Use tascan_create_zone to add one.';
|
|
965
1037
|
return zones.map(z =>
|
|
966
|
-
`${z.name} (${z.id})\n Center: ${z.lat}, ${z.lng} · Radius: ${z.radius_m}m · ${z.is_active ? 'active' : 'INACTIVE'}\n Routes to: ${z.task_list_id || '—'} · Zone-locked: ${z.enforce_on_list ? 'yes' : 'no'}`
|
|
1038
|
+
`${z.name} (${z.id})\n Center: ${z.lat}, ${z.lng} · Radius: ${z.radius_m}m · ${z.is_active ? 'active' : 'INACTIVE'}\n Routes to: ${z.task_list_id || '—'} · Zone-locked: ${z.enforce_on_list ? 'yes' : 'no'}\n Alerts: enter=${z.notify_on_enter ? 'ON' : 'off'} exit=${z.notify_on_exit ? 'ON' : 'off'} · Auto-clock: in=${z.auto_clock_in ? 'ON' : 'off'} out=${z.auto_clock_out ? 'ON' : 'off'}`
|
|
967
1039
|
).join('\n\n');
|
|
968
1040
|
}
|
|
969
1041
|
},
|
|
@@ -976,7 +1048,11 @@ const TOOLS = [
|
|
|
976
1048
|
zone_id: { type: 'string', description: 'Zone ID' },
|
|
977
1049
|
name: { type: 'string' }, lat: { type: 'number' }, lng: { type: 'number' },
|
|
978
1050
|
radius_m: { type: 'number' }, task_list_id: { type: 'string' },
|
|
979
|
-
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' }
|
|
1051
|
+
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' },
|
|
1052
|
+
notify_on_enter: { type: 'boolean' }, notify_on_exit: { type: 'boolean' },
|
|
1053
|
+
auto_clock_in: { type: 'boolean' }, auto_clock_out: { type: 'boolean' },
|
|
1054
|
+
notify_email: { type: 'string' },
|
|
1055
|
+
polygon: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Replace geometry with a polygon ([[lat,lng],...], ≥3 vertices); pass null to revert to a circle' }
|
|
980
1056
|
},
|
|
981
1057
|
required: ['zone_id']
|
|
982
1058
|
},
|