tascan-mcp 3.5.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 +80 -12
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',
|
|
@@ -940,6 +1006,7 @@ const TOOLS = [
|
|
|
940
1006
|
auto_clock_in: { type: 'boolean', description: 'Entering the zone writes a shift_start clock-in event' },
|
|
941
1007
|
auto_clock_out: { type: 'boolean', description: 'Leaving the zone writes a shift_end clock-out event' },
|
|
942
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.' },
|
|
943
1010
|
description: { type: 'string', description: 'Shown to workers on the Site Gate page' }
|
|
944
1011
|
},
|
|
945
1012
|
required: ['name', 'lat', 'lng']
|
|
@@ -984,7 +1051,8 @@ const TOOLS = [
|
|
|
984
1051
|
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' },
|
|
985
1052
|
notify_on_enter: { type: 'boolean' }, notify_on_exit: { type: 'boolean' },
|
|
986
1053
|
auto_clock_in: { type: 'boolean' }, auto_clock_out: { type: 'boolean' },
|
|
987
|
-
notify_email: { type: 'string' }
|
|
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' }
|
|
988
1056
|
},
|
|
989
1057
|
required: ['zone_id']
|
|
990
1058
|
},
|