tascan-mcp 3.3.0 → 3.5.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 +100 -5
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.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. 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",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": {
|
package/tools.cjs
CHANGED
|
@@ -560,7 +560,8 @@ const TOOLS = [
|
|
|
560
560
|
text += '\n';
|
|
561
561
|
if (args.include_responses && task.responses && task.responses.length > 0) {
|
|
562
562
|
for (const resp of task.responses) {
|
|
563
|
-
const { value, note } = fmtResponse(resp.response_value, resp.notes);
|
|
563
|
+
const { value: v0, note } = fmtResponse(resp.response_value, resp.notes);
|
|
564
|
+
const value = (v0 == null && resp.photo_url) ? '[photo submitted]' : (v0 != null && resp.photo_url) ? v0 + ' [+photo]' : v0;
|
|
564
565
|
text += ` -> ${value != null ? value : '(no value)'}${note ? ' — ' + note : ''} (${resp.worker_name}, ${resp.completed_at})\n`;
|
|
565
566
|
}
|
|
566
567
|
}
|
|
@@ -588,7 +589,8 @@ const TOOLS = [
|
|
|
588
589
|
const r = result.data;
|
|
589
590
|
let text = `Responses for "${r.task}" in project "${r.project_name}" (${r.matched_tasks} matching task(s), ${r.responses.length} response(s), oldest first):\n\n`;
|
|
590
591
|
for (const resp of r.responses) {
|
|
591
|
-
const { value, note } = fmtResponse(resp.response_value, resp.notes);
|
|
592
|
+
const { value: v0, note } = fmtResponse(resp.response_value, resp.notes);
|
|
593
|
+
const value = (v0 == null && resp.photo_url) ? '[photo submitted]' : (v0 != null && resp.photo_url) ? v0 + ' [+photo]' : v0;
|
|
592
594
|
const when = (resp.completed_at || '').slice(0, 16).replace('T', ' ');
|
|
593
595
|
text += `${when} · ${resp.list_name} · ${resp.task_title}: ${value != null ? value : '(no value)'}`;
|
|
594
596
|
if (note) text += ` — ${note}`;
|
|
@@ -933,6 +935,11 @@ const TOOLS = [
|
|
|
933
935
|
project_id: { type: 'string', description: 'Project this zone belongs to' },
|
|
934
936
|
task_list_id: { type: 'string', description: 'Task list the Site Gate routes workers to when they are inside this zone' },
|
|
935
937
|
enforce_on_list: { type: 'boolean', description: 'Zone-lock the task list — it cannot be started from outside the radius' },
|
|
938
|
+
notify_on_enter: { type: 'boolean', description: 'Email the manager when a worker enters this zone (danger areas)' },
|
|
939
|
+
notify_on_exit: { type: 'boolean', description: 'Email the manager when a worker leaves this zone (accountability — sign in then disappear)' },
|
|
940
|
+
auto_clock_in: { type: 'boolean', description: 'Entering the zone writes a shift_start clock-in event' },
|
|
941
|
+
auto_clock_out: { type: 'boolean', description: 'Leaving the zone writes a shift_end clock-out event' },
|
|
942
|
+
notify_email: { type: 'string', description: 'Alert recipient override — defaults to all org admins' },
|
|
936
943
|
description: { type: 'string', description: 'Shown to workers on the Site Gate page' }
|
|
937
944
|
},
|
|
938
945
|
required: ['name', 'lat', 'lng']
|
|
@@ -941,7 +948,7 @@ const TOOLS = [
|
|
|
941
948
|
handler: async (args, api) => {
|
|
942
949
|
const result = await api('POST', '/zones', args);
|
|
943
950
|
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'}`;
|
|
951
|
+
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'}`;
|
|
945
952
|
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
953
|
return text;
|
|
947
954
|
}
|
|
@@ -961,7 +968,7 @@ const TOOLS = [
|
|
|
961
968
|
const zones = result.data || [];
|
|
962
969
|
if (!zones.length) return 'No zones configured. Use tascan_create_zone to add one.';
|
|
963
970
|
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'}`
|
|
971
|
+
`${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'}`
|
|
965
972
|
).join('\n\n');
|
|
966
973
|
}
|
|
967
974
|
},
|
|
@@ -974,7 +981,10 @@ const TOOLS = [
|
|
|
974
981
|
zone_id: { type: 'string', description: 'Zone ID' },
|
|
975
982
|
name: { type: 'string' }, lat: { type: 'number' }, lng: { type: 'number' },
|
|
976
983
|
radius_m: { type: 'number' }, task_list_id: { type: 'string' },
|
|
977
|
-
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' }
|
|
984
|
+
enforce_on_list: { type: 'boolean' }, is_active: { type: 'boolean' },
|
|
985
|
+
notify_on_enter: { type: 'boolean' }, notify_on_exit: { type: 'boolean' },
|
|
986
|
+
auto_clock_in: { type: 'boolean' }, auto_clock_out: { type: 'boolean' },
|
|
987
|
+
notify_email: { type: 'string' }
|
|
978
988
|
},
|
|
979
989
|
required: ['zone_id']
|
|
980
990
|
},
|
|
@@ -1156,6 +1166,91 @@ const TOOLS = [
|
|
|
1156
1166
|
`Member since: ${(s.member_since || '').slice(0, 10)}\n\n` +
|
|
1157
1167
|
`Badges: ${p.badges.length ? p.badges.join(' · ') : 'none yet'}\n\nShareable profile: ${p.profile_url}`;
|
|
1158
1168
|
}
|
|
1169
|
+
},
|
|
1170
|
+
|
|
1171
|
+
// ─── Read-layer integrity (from the 2026-08-18 MCP audit) ────────
|
|
1172
|
+
{
|
|
1173
|
+
name: 'tascan_server_info',
|
|
1174
|
+
description: 'Identify exactly which TaScan server and schema this MCP session is talking to. Call this FIRST when diagnosing anything — it makes "dev server masquerading as production" and "is my fix deployed yet" one tool call instead of an inference.',
|
|
1175
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
1176
|
+
annotations: { title: 'Server Info', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1177
|
+
handler: async (args, api) => {
|
|
1178
|
+
let pkgVersion = 'unknown';
|
|
1179
|
+
try { pkgVersion = require('./package.json').version; } catch (e) {}
|
|
1180
|
+
let meta = null;
|
|
1181
|
+
try { meta = (await api('GET', '/meta')).data; } catch (e) { meta = { error: String(e.message || e) }; }
|
|
1182
|
+
return `TaScan MCP Server Info\n\n` +
|
|
1183
|
+
`tascan-mcp package: v${pkgVersion} (${TOOLS.length} tools)\n` +
|
|
1184
|
+
`API service: ${meta?.service || 'UNREACHABLE'}\n` +
|
|
1185
|
+
`Host: ${meta?.host || '?'} · Environment: ${meta?.environment || '?'}\n` +
|
|
1186
|
+
`Org: ${meta?.org_id || '?'}\n` +
|
|
1187
|
+
`DB schema: ${meta?.schema_version || '?'}\n` +
|
|
1188
|
+
`Server time: ${meta?.server_time || '?'}` +
|
|
1189
|
+
(meta?.error ? `\nAPI error: ${meta.error}` : '');
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
name: 'tascan_find',
|
|
1194
|
+
description: 'Cross-entity search: find projects, task lists, tasks, workers, or condition assets by name in one call — with ids and parent context to disambiguate. Use this instead of walking projects→lists→tasks or guessing ids from display names.',
|
|
1195
|
+
inputSchema: {
|
|
1196
|
+
type: 'object',
|
|
1197
|
+
properties: {
|
|
1198
|
+
query: { type: 'string', description: 'Search text (min 2 chars, case-insensitive substring)' },
|
|
1199
|
+
type: { type: 'string', enum: ['project', 'list', 'task', 'worker', 'asset'], description: 'Optional: restrict to one entity type' }
|
|
1200
|
+
},
|
|
1201
|
+
required: ['query']
|
|
1202
|
+
},
|
|
1203
|
+
annotations: { title: 'Find Anything', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1204
|
+
handler: async (args, api) => {
|
|
1205
|
+
let path = `/find?q=${encodeURIComponent(args.query)}`;
|
|
1206
|
+
if (args.type) path += `&type=${args.type}`;
|
|
1207
|
+
const result = await api('GET', path);
|
|
1208
|
+
const rows = result.data || [];
|
|
1209
|
+
if (!rows.length) return `No matches for "${args.query}".`;
|
|
1210
|
+
return rows.map(r => `[${r.type}] ${r.name}\n id: ${r.id}${r.parent_id ? '\n parent: ' + r.parent_id : ''}${r.context ? '\n ' + r.context : ''}`).join('\n\n');
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: 'tascan_get_worker',
|
|
1215
|
+
description: 'Ungated, plain read of one worker row: name, contact, org, points, streaks, timestamps. (tascan_get_worker_passport is the rich stats view; this is the boring lookup.)',
|
|
1216
|
+
inputSchema: {
|
|
1217
|
+
type: 'object',
|
|
1218
|
+
properties: { worker_id: { type: 'string', description: 'Worker ID' } },
|
|
1219
|
+
required: ['worker_id']
|
|
1220
|
+
},
|
|
1221
|
+
annotations: { title: 'Get Worker', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1222
|
+
handler: async (args, api) => {
|
|
1223
|
+
const result = await api('GET', `/workers/${args.worker_id}`);
|
|
1224
|
+
return JSON.stringify(result.data, null, 2);
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
name: 'tascan_find_duplicate_workers',
|
|
1229
|
+
description: 'Find candidate same-person worker records with per-signal match detail (Patent 4 §6.25(b) signals: phone reuse, name similarity, GPS pattern correlation). Turns identity fragmentation from an accidental discovery into a monitorable metric, and feeds the merge workflow its candidate list.',
|
|
1230
|
+
inputSchema: {
|
|
1231
|
+
type: 'object',
|
|
1232
|
+
properties: {
|
|
1233
|
+
worker_id: { type: 'string', description: 'Anchor worker to find duplicates OF (preferred — enables phone + GPS signals)' },
|
|
1234
|
+
name: { type: 'string', description: 'Or: search duplicates by display name' },
|
|
1235
|
+
threshold: { type: 'number', description: 'Min confidence 0-1 (default 0.15)' }
|
|
1236
|
+
},
|
|
1237
|
+
required: []
|
|
1238
|
+
},
|
|
1239
|
+
annotations: { title: 'Find Duplicate Workers', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1240
|
+
handler: async (args, api) => {
|
|
1241
|
+
const qs = [];
|
|
1242
|
+
if (args.worker_id) qs.push(`worker_id=${args.worker_id}`);
|
|
1243
|
+
if (args.name) qs.push(`name=${encodeURIComponent(args.name)}`);
|
|
1244
|
+
if (args.threshold != null) qs.push(`threshold=${args.threshold}`);
|
|
1245
|
+
const result = await api('GET', '/workers/duplicates?' + qs.join('&'));
|
|
1246
|
+
const d = result.data;
|
|
1247
|
+
if (!d.candidates.length) return `No duplicate candidates for ${d.anchor.name || d.anchor.id}.`;
|
|
1248
|
+
let text = `Duplicate candidates for ${d.anchor.name || ''} (${d.anchor.id || 'name search'}):\n`;
|
|
1249
|
+
d.candidates.forEach(c => {
|
|
1250
|
+
text += `\n${c.name} (${c.id}) — confidence ${c.confidence}\n signals: ${JSON.stringify(c.signals)}\n created ${String(c.created_at).slice(0, 10)} · last active ${String(c.last_active_at || '').slice(0, 10)} · ${c.total_points} pts`;
|
|
1251
|
+
});
|
|
1252
|
+
return text;
|
|
1253
|
+
}
|
|
1159
1254
|
}
|
|
1160
1255
|
];
|
|
1161
1256
|
|