tascan-mcp 3.1.0 → 3.2.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 +2 -2
- package/tools.cjs +171 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.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. 43 tools for projects, tasks, workers, QR codes, NFC tags, geofenced zones, AI condition assessment, worker passports, AI issue analysis, and autonomous remediation dispatch. 14 patents filed (435+ claims). Task. Scan. Done.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": {
|
package/tools.cjs
CHANGED
|
@@ -917,6 +917,177 @@ const TOOLS = [
|
|
|
917
917
|
});
|
|
918
918
|
return text;
|
|
919
919
|
}
|
|
920
|
+
},
|
|
921
|
+
|
|
922
|
+
// ─── Geofenced Zones (Patent 7: location-triggered delivery) ─────
|
|
923
|
+
{
|
|
924
|
+
name: 'tascan_create_zone',
|
|
925
|
+
description: 'Create a geofenced work zone. Delivery zones route workers who open the project Site Gate (geo.html?project=...) to this zone\'s task list when GPS places them inside the radius. Set enforce_on_list=true to zone-lock the task list — workers cannot start it from outside the zone.',
|
|
926
|
+
inputSchema: {
|
|
927
|
+
type: 'object',
|
|
928
|
+
properties: {
|
|
929
|
+
name: { type: 'string', description: 'Zone name (e.g. "Stage Left", "Loading Dock")' },
|
|
930
|
+
lat: { type: 'number', description: 'Zone center latitude' },
|
|
931
|
+
lng: { type: 'number', description: 'Zone center longitude' },
|
|
932
|
+
radius_m: { type: 'number', description: 'Radius in meters (default 150, min 10, max 100000)' },
|
|
933
|
+
project_id: { type: 'string', description: 'Project this zone belongs to' },
|
|
934
|
+
task_list_id: { type: 'string', description: 'Task list the Site Gate routes workers to when they are inside this zone' },
|
|
935
|
+
enforce_on_list: { type: 'boolean', description: 'Zone-lock the task list — it cannot be started from outside the radius' },
|
|
936
|
+
description: { type: 'string', description: 'Shown to workers on the Site Gate page' }
|
|
937
|
+
},
|
|
938
|
+
required: ['name', 'lat', 'lng']
|
|
939
|
+
},
|
|
940
|
+
annotations: { title: 'Create Geofenced Zone', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
941
|
+
handler: async (args, api) => {
|
|
942
|
+
const result = await api('POST', '/zones', args);
|
|
943
|
+
const z = result.data;
|
|
944
|
+
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'}`;
|
|
945
|
+
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}`;
|
|
946
|
+
return text;
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: 'tascan_list_zones',
|
|
951
|
+
description: 'List geofenced work zones, optionally filtered by project. Shows center, radius, routing target, and zone-lock status.',
|
|
952
|
+
inputSchema: {
|
|
953
|
+
type: 'object',
|
|
954
|
+
properties: { project_id: { type: 'string', description: 'Filter by project' } },
|
|
955
|
+
required: []
|
|
956
|
+
},
|
|
957
|
+
annotations: { title: 'List Geofenced Zones', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
958
|
+
handler: async (args, api) => {
|
|
959
|
+
const path = '/zones' + (args.project_id ? `?project_id=${args.project_id}` : '');
|
|
960
|
+
const result = await api('GET', path);
|
|
961
|
+
const zones = result.data || [];
|
|
962
|
+
if (!zones.length) return 'No zones configured. Use tascan_create_zone to add one.';
|
|
963
|
+
return zones.map(z =>
|
|
964
|
+
`${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'}`
|
|
965
|
+
).join('\n\n');
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: 'tascan_update_zone',
|
|
970
|
+
description: 'Update a geofenced zone — move the center, resize the radius, change the routing target, toggle zone-lock, or deactivate it (is_active=false).',
|
|
971
|
+
inputSchema: {
|
|
972
|
+
type: 'object',
|
|
973
|
+
properties: {
|
|
974
|
+
zone_id: { type: 'string', description: 'Zone ID' },
|
|
975
|
+
name: { type: 'string' }, lat: { type: 'number' }, lng: { type: 'number' },
|
|
976
|
+
radius_m: { type: 'number' }, task_list_id: { type: 'string' },
|
|
977
|
+
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' }
|
|
978
|
+
},
|
|
979
|
+
required: ['zone_id']
|
|
980
|
+
},
|
|
981
|
+
annotations: { title: 'Update Geofenced Zone', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
982
|
+
handler: async (args, api) => {
|
|
983
|
+
const { zone_id, ...updates } = args;
|
|
984
|
+
const result = await api('PUT', `/zones/${zone_id}`, updates);
|
|
985
|
+
return `Zone updated:\n\n${JSON.stringify(result.data, null, 2)}`;
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
|
|
989
|
+
// ─── Condition Ledger (Patent 12: longitudinal asset condition) ──
|
|
990
|
+
{
|
|
991
|
+
name: 'tascan_register_asset',
|
|
992
|
+
description: 'Register a physical asset (equipment, structure, vehicle, machine) in the condition ledger so it can be assessed over time. Each asset gets a longitudinal condition history with AI scoring and degradation trajectory.',
|
|
993
|
+
inputSchema: {
|
|
994
|
+
type: 'object',
|
|
995
|
+
properties: {
|
|
996
|
+
name: { type: 'string', description: 'Asset name (e.g. "LED Wall Processor #3")' },
|
|
997
|
+
serial_number: { type: 'string', description: 'Serial number or asset tag (unique per org)' },
|
|
998
|
+
asset_type: { type: 'string', description: 'Type (e.g. "LED processor", "forklift", "scaffold")' },
|
|
999
|
+
description: { type: 'string', description: 'Context the AI assessor should know' },
|
|
1000
|
+
location_description: { type: 'string' },
|
|
1001
|
+
project_id: { type: 'string' }
|
|
1002
|
+
},
|
|
1003
|
+
required: ['name']
|
|
1004
|
+
},
|
|
1005
|
+
annotations: { title: 'Register Condition Asset', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1006
|
+
handler: async (args, api) => {
|
|
1007
|
+
const result = await api('POST', '/assets', args);
|
|
1008
|
+
const a = result.data;
|
|
1009
|
+
return `Asset registered: ${a.name}\n\nID: ${a.id}${a.serial_number ? '\nSerial: ' + a.serial_number : ''}\n\nUse tascan_assess_condition with a photo URL to record its first condition assessment.`;
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: 'tascan_assess_condition',
|
|
1014
|
+
description: 'Run an AI condition assessment of an asset from a photo. The model scores 0-100 with the asset\'s full assessment history in context, so it reads degradation over time — returning the Condition Delta Score vs the previous assessment, defects, wear indicators, maintenance recommendations, and a degradation trajectory. Sensor-free predictive maintenance.',
|
|
1015
|
+
inputSchema: {
|
|
1016
|
+
type: 'object',
|
|
1017
|
+
properties: {
|
|
1018
|
+
asset_id: { type: 'string', description: 'Asset ID from tascan_register_asset / tascan_list_assets' },
|
|
1019
|
+
photo_url: { type: 'string', description: 'Public URL of the assessment photo' },
|
|
1020
|
+
worker_name: { type: 'string', description: 'Who took the photo (optional)' }
|
|
1021
|
+
},
|
|
1022
|
+
required: ['asset_id', 'photo_url']
|
|
1023
|
+
},
|
|
1024
|
+
annotations: { title: 'Assess Asset Condition', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1025
|
+
handler: async (args) => {
|
|
1026
|
+
const resp = await fetch('https://app.tascan.io/api/assess-condition', {
|
|
1027
|
+
method: 'POST',
|
|
1028
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1029
|
+
body: JSON.stringify({ asset_id: args.asset_id, photo_url: args.photo_url, worker_name: args.worker_name || null, source: 'api' })
|
|
1030
|
+
});
|
|
1031
|
+
const data = await resp.json();
|
|
1032
|
+
if (!resp.ok) return `Assessment failed: ${data.error || resp.status}`;
|
|
1033
|
+
const a = data.assessment;
|
|
1034
|
+
const f = a.findings || {};
|
|
1035
|
+
let text = `Condition Assessment — ${data.asset.name}\n\n`;
|
|
1036
|
+
text += `Score: ${a.condition_score ?? 'N/A'}/100 (${a.condition_grade || 'ungraded'})`;
|
|
1037
|
+
if (data.delta_score != null) text += ` · Delta vs previous: ${data.delta_score > 0 ? '+' : ''}${data.delta_score}`;
|
|
1038
|
+
text += `\nTrajectory: ${f.trajectory || 'unknown'}${f.trajectory_note ? ' — ' + f.trajectory_note : ''}\n`;
|
|
1039
|
+
if (f.defects?.length) text += `\nDefects:\n${f.defects.map(d => ' - ' + d).join('\n')}\n`;
|
|
1040
|
+
if (f.wear_indicators?.length) text += `\nWear indicators:\n${f.wear_indicators.map(d => ' - ' + d).join('\n')}\n`;
|
|
1041
|
+
if (f.recommendations?.length) text += `\nRecommendations:\n${f.recommendations.map(d => ' - ' + d).join('\n')}\n`;
|
|
1042
|
+
text += `\nSummary: ${a.summary || '—'}\nHistory depth: ${data.history_depth} prior assessment(s)`;
|
|
1043
|
+
return text;
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
name: 'tascan_condition_history',
|
|
1048
|
+
description: 'Get an asset\'s longitudinal condition history — score trend over time, every assessment with grade, delta, findings, and who assessed it. The per-serial-number condition ledger.',
|
|
1049
|
+
inputSchema: {
|
|
1050
|
+
type: 'object',
|
|
1051
|
+
properties: { asset_id: { type: 'string', description: 'Asset ID' } },
|
|
1052
|
+
required: ['asset_id']
|
|
1053
|
+
},
|
|
1054
|
+
annotations: { title: 'Get Condition History', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1055
|
+
handler: async (args, api) => {
|
|
1056
|
+
const result = await api('GET', `/assets/${args.asset_id}`);
|
|
1057
|
+
const a = result.data;
|
|
1058
|
+
let text = `${a.name}${a.serial_number ? ' · SN ' + a.serial_number : ''}\n`;
|
|
1059
|
+
text += `Latest score: ${a.latest_score ?? 'never assessed'}\n`;
|
|
1060
|
+
if (a.score_trend?.length > 1) text += `Trend (oldest→newest): ${a.score_trend.join(' → ')}\n`;
|
|
1061
|
+
text += `\n${(a.assessments || []).length} assessment(s):\n`;
|
|
1062
|
+
(a.assessments || []).forEach(s => {
|
|
1063
|
+
text += `\n${s.created_at.slice(0, 16).replace('T', ' ')} — ${s.condition_score ?? '?'}/100 (${s.condition_grade || '?'})`;
|
|
1064
|
+
if (s.delta_score != null) text += ` [${s.delta_score > 0 ? '+' : ''}${s.delta_score}]`;
|
|
1065
|
+
if (s.summary) text += `\n ${s.summary}`;
|
|
1066
|
+
});
|
|
1067
|
+
return text;
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
|
|
1071
|
+
// ─── Worker Passport (Patent 4 foundation) ───────────────────────
|
|
1072
|
+
{
|
|
1073
|
+
name: 'tascan_get_worker_passport',
|
|
1074
|
+
description: 'Get a worker\'s verified work passport — task counts, lists worked, photos submitted, GPS-verified hours, points, streaks, and earned merit badges, all computed from real completion data (not self-reported). Includes the shareable profile URL.',
|
|
1075
|
+
inputSchema: {
|
|
1076
|
+
type: 'object',
|
|
1077
|
+
properties: { worker_id: { type: 'string', description: 'Worker ID' } },
|
|
1078
|
+
required: ['worker_id']
|
|
1079
|
+
},
|
|
1080
|
+
annotations: { title: 'Get Worker Passport', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1081
|
+
handler: async (args, api) => {
|
|
1082
|
+
const result = await api('GET', `/workers/${args.worker_id}/passport`);
|
|
1083
|
+
const p = result.data;
|
|
1084
|
+
const s = p.stats;
|
|
1085
|
+
return `Worker Passport — ${p.worker.name}\n\n` +
|
|
1086
|
+
`Tasks completed: ${s.total_tasks}\nLists worked: ${s.total_lists}\nPhotos submitted: ${s.photos_submitted}\n` +
|
|
1087
|
+
`Verified hours: ${s.verified_hours}\nPoints: ${s.total_points} · Streak: ${s.current_streak} (best ${s.longest_streak})\n` +
|
|
1088
|
+
`Member since: ${(s.member_since || '').slice(0, 10)}\n\n` +
|
|
1089
|
+
`Badges: ${p.badges.length ? p.badges.join(' · ') : 'none yet'}\n\nShareable profile: ${p.profile_url}`;
|
|
1090
|
+
}
|
|
920
1091
|
}
|
|
921
1092
|
];
|
|
922
1093
|
|