tascan-mcp 3.3.0 → 3.4.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 +2 -2
  2. package/tools.cjs +89 -2
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "tascan-mcp",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "mcpName": "io.github.snowbikemike/tascan-mcp",
5
- "description": "TaScan MCP Server — Closed-loop autonomous operations protocol. 46 tools for projects, tasks, workers, QR codes, NFC tags, geofenced zones, AI condition assessment, worker passports, verification-gated gig payments, AI issue analysis, and autonomous remediation dispatch. 14 patents filed (435+ claims). Task. Scan. Done.",
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}`;
@@ -1156,6 +1158,91 @@ const TOOLS = [
1156
1158
  `Member since: ${(s.member_since || '').slice(0, 10)}\n\n` +
1157
1159
  `Badges: ${p.badges.length ? p.badges.join(' · ') : 'none yet'}\n\nShareable profile: ${p.profile_url}`;
1158
1160
  }
1161
+ },
1162
+
1163
+ // ─── Read-layer integrity (from the 2026-08-18 MCP audit) ────────
1164
+ {
1165
+ name: 'tascan_server_info',
1166
+ 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.',
1167
+ inputSchema: { type: 'object', properties: {}, required: [] },
1168
+ annotations: { title: 'Server Info', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1169
+ handler: async (args, api) => {
1170
+ let pkgVersion = 'unknown';
1171
+ try { pkgVersion = require('./package.json').version; } catch (e) {}
1172
+ let meta = null;
1173
+ try { meta = (await api('GET', '/meta')).data; } catch (e) { meta = { error: String(e.message || e) }; }
1174
+ return `TaScan MCP Server Info\n\n` +
1175
+ `tascan-mcp package: v${pkgVersion} (${TOOLS.length} tools)\n` +
1176
+ `API service: ${meta?.service || 'UNREACHABLE'}\n` +
1177
+ `Host: ${meta?.host || '?'} · Environment: ${meta?.environment || '?'}\n` +
1178
+ `Org: ${meta?.org_id || '?'}\n` +
1179
+ `DB schema: ${meta?.schema_version || '?'}\n` +
1180
+ `Server time: ${meta?.server_time || '?'}` +
1181
+ (meta?.error ? `\nAPI error: ${meta.error}` : '');
1182
+ }
1183
+ },
1184
+ {
1185
+ name: 'tascan_find',
1186
+ 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.',
1187
+ inputSchema: {
1188
+ type: 'object',
1189
+ properties: {
1190
+ query: { type: 'string', description: 'Search text (min 2 chars, case-insensitive substring)' },
1191
+ type: { type: 'string', enum: ['project', 'list', 'task', 'worker', 'asset'], description: 'Optional: restrict to one entity type' }
1192
+ },
1193
+ required: ['query']
1194
+ },
1195
+ annotations: { title: 'Find Anything', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1196
+ handler: async (args, api) => {
1197
+ let path = `/find?q=${encodeURIComponent(args.query)}`;
1198
+ if (args.type) path += `&type=${args.type}`;
1199
+ const result = await api('GET', path);
1200
+ const rows = result.data || [];
1201
+ if (!rows.length) return `No matches for "${args.query}".`;
1202
+ 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');
1203
+ }
1204
+ },
1205
+ {
1206
+ name: 'tascan_get_worker',
1207
+ 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.)',
1208
+ inputSchema: {
1209
+ type: 'object',
1210
+ properties: { worker_id: { type: 'string', description: 'Worker ID' } },
1211
+ required: ['worker_id']
1212
+ },
1213
+ annotations: { title: 'Get Worker', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1214
+ handler: async (args, api) => {
1215
+ const result = await api('GET', `/workers/${args.worker_id}`);
1216
+ return JSON.stringify(result.data, null, 2);
1217
+ }
1218
+ },
1219
+ {
1220
+ name: 'tascan_find_duplicate_workers',
1221
+ 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.',
1222
+ inputSchema: {
1223
+ type: 'object',
1224
+ properties: {
1225
+ worker_id: { type: 'string', description: 'Anchor worker to find duplicates OF (preferred — enables phone + GPS signals)' },
1226
+ name: { type: 'string', description: 'Or: search duplicates by display name' },
1227
+ threshold: { type: 'number', description: 'Min confidence 0-1 (default 0.15)' }
1228
+ },
1229
+ required: []
1230
+ },
1231
+ annotations: { title: 'Find Duplicate Workers', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1232
+ handler: async (args, api) => {
1233
+ const qs = [];
1234
+ if (args.worker_id) qs.push(`worker_id=${args.worker_id}`);
1235
+ if (args.name) qs.push(`name=${encodeURIComponent(args.name)}`);
1236
+ if (args.threshold != null) qs.push(`threshold=${args.threshold}`);
1237
+ const result = await api('GET', '/workers/duplicates?' + qs.join('&'));
1238
+ const d = result.data;
1239
+ if (!d.candidates.length) return `No duplicate candidates for ${d.anchor.name || d.anchor.id}.`;
1240
+ let text = `Duplicate candidates for ${d.anchor.name || ''} (${d.anchor.id || 'name search'}):\n`;
1241
+ d.candidates.forEach(c => {
1242
+ 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`;
1243
+ });
1244
+ return text;
1245
+ }
1159
1246
  }
1160
1247
  ];
1161
1248