tascan-mcp 3.0.1 → 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 +191 -20
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.0
|
|
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
|
@@ -282,7 +282,9 @@ const TOOLS = [
|
|
|
282
282
|
},
|
|
283
283
|
annotations: { title: 'Dispatch to AI Agent', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
284
284
|
handler: async (args, api) => {
|
|
285
|
-
|
|
285
|
+
let registered = [];
|
|
286
|
+
try { registered = (await api('GET', '/agents')).data || []; } catch (e) { /* registry unreachable — fall back to seeds */ }
|
|
287
|
+
const allAgents = [...AGENT_REGISTRY, ...dynamicAgents, ...registered.filter(r => !AGENT_REGISTRY.some(s => s.id === r.id))];
|
|
286
288
|
const agentQuery = (args.agent || 'claude-code-local').toLowerCase();
|
|
287
289
|
const agent = allAgents.find(a => a.id === agentQuery || a.name.toLowerCase() === agentQuery) || allAgents[0];
|
|
288
290
|
const title = args.priority === 'urgent' ? `🔴 ${args.task}` : args.task;
|
|
@@ -299,8 +301,10 @@ const TOOLS = [
|
|
|
299
301
|
required: []
|
|
300
302
|
},
|
|
301
303
|
annotations: { title: 'List AI Agents', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
302
|
-
handler: async () => {
|
|
303
|
-
|
|
304
|
+
handler: async (args, api) => {
|
|
305
|
+
let registered = [];
|
|
306
|
+
try { registered = (await api('GET', '/agents')).data || []; } catch (e) { /* registry unreachable — fall back to seeds */ }
|
|
307
|
+
const allAgents = [...AGENT_REGISTRY, ...dynamicAgents, ...registered.filter(r => !AGENT_REGISTRY.some(s => s.id === r.id))];
|
|
304
308
|
const summary = allAgents.map(a =>
|
|
305
309
|
`${a.name} (${a.id})\n Type: ${a.type} | Model: ${a.model}\n Worker ID: ${a.worker_id}\n Inbox: ${a.inbox_id}\n Capabilities: ${a.capabilities.join(', ')}\n Location: ${a.location}\n Status: ${a.status}\n ${a.description}`
|
|
306
310
|
).join('\n\n');
|
|
@@ -326,26 +330,22 @@ const TOOLS = [
|
|
|
326
330
|
required: ['id', 'name', 'type', 'inbox_id', 'capabilities']
|
|
327
331
|
},
|
|
328
332
|
annotations: { title: 'Register AI Agent', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
329
|
-
handler: async (args) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
return `Agent "${args.name}" (${args.id}) updated in registry.\n\n${JSON.stringify(existing, null, 2)}`;
|
|
334
|
-
}
|
|
335
|
-
const agent = {
|
|
333
|
+
handler: async (args, api) => {
|
|
334
|
+
// Durable registration (Patent 10 Cl. 2): persists to the agent_registry
|
|
335
|
+
// table via the API — survives cold starts and spans both transports
|
|
336
|
+
const result = await api('POST', '/agents', {
|
|
336
337
|
id: args.id,
|
|
337
338
|
name: args.name,
|
|
338
339
|
type: args.type,
|
|
339
|
-
model: args.model
|
|
340
|
-
worker_id: args.worker_id
|
|
340
|
+
model: args.model,
|
|
341
|
+
worker_id: args.worker_id,
|
|
341
342
|
inbox_id: args.inbox_id,
|
|
342
|
-
capabilities: args.capabilities
|
|
343
|
-
location: args.location
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return `Agent "${args.name}" registered!\n\nID: ${agent.id}\nInbox: ${agent.inbox_id}\nCapabilities: ${agent.capabilities.join(', ')}\nStatus: active\n\nUse tascan_dispatch_to_agent with agent="${agent.id}" to send tasks.\n\n${JSON.stringify(agent, null, 2)}`;
|
|
343
|
+
capabilities: args.capabilities,
|
|
344
|
+
location: args.location,
|
|
345
|
+
description: args.description
|
|
346
|
+
});
|
|
347
|
+
const agent = result.data;
|
|
348
|
+
return `Agent "${agent.name}" registered (durable)!\n\nID: ${agent.id}\nInbox: ${agent.inbox_id}\nCapabilities: ${agent.capabilities.join(', ')}\nStatus: ${agent.status}\n\nUse tascan_dispatch_to_agent with agent="${agent.id}" to send tasks.\n\n${JSON.stringify(agent, null, 2)}`;
|
|
349
349
|
}
|
|
350
350
|
},
|
|
351
351
|
{
|
|
@@ -776,7 +776,7 @@ const TOOLS = [
|
|
|
776
776
|
text += `--- ANALYSIS ---\n`;
|
|
777
777
|
text += `Summary: ${d.analysis?.summary || 'N/A'}\n`;
|
|
778
778
|
text += `Classification: ${d.analysis?.classification?.type || 'N/A'} (${d.analysis?.classification?.severity || 'N/A'})\n`;
|
|
779
|
-
text += `Urgency: ${d.analysis?.urgency_score
|
|
779
|
+
text += `Urgency: ${d.analysis?.urgency_score ?? 'N/A'}/100\n`;
|
|
780
780
|
if (d.analysis?.root_cause_hypotheses?.length) {
|
|
781
781
|
text += `Root Causes:\n`;
|
|
782
782
|
d.analysis.root_cause_hypotheses.forEach((h, i) => {
|
|
@@ -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
|
|