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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tools.cjs +80 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tascan-mcp",
3
- "version": "3.5.0",
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: 'Get scan event history for a registered NFC tag — timestamps, GPS coordinates, and accuracy for each scan',
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: 'Tag registry ID (from tascan_list_tags)' },
904
- limit: { type: 'number', description: 'Max results (default 50)' }
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: ['tag_id']
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
- const path = `/tags/${args.tag_id}/scans` + (args.limit ? `?limit=${args.limit}` : '');
911
- const result = await api('GET', path);
912
- const scans = result.data;
913
- if (!scans.length) return 'No scans recorded for this tag.';
914
- let text = `${scans.length} scan(s):\n\n`;
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.gps_lat && s.gps_lng) text += ` — GPS: ${s.gps_lat.toFixed(6)}, ${s.gps_lng.toFixed(6)} (±${Math.round(s.gps_accuracy || 0)}m)`;
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
  },