tascan-mcp 3.2.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 +158 -3
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "tascan-mcp",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "mcpName": "io.github.snowbikemike/tascan-mcp",
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.",
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}`;
@@ -1015,7 +1017,7 @@ const TOOLS = [
1015
1017
  inputSchema: {
1016
1018
  type: 'object',
1017
1019
  properties: {
1018
- asset_id: { type: 'string', description: 'Asset ID from tascan_register_asset / tascan_list_assets' },
1020
+ asset_id: { type: 'string', description: 'Asset ID (from tascan_register_asset or tascan_list_assets)' },
1019
1021
  photo_url: { type: 'string', description: 'Public URL of the assessment photo' },
1020
1022
  worker_name: { type: 'string', description: 'Who took the photo (optional)' }
1021
1023
  },
@@ -1068,6 +1070,74 @@ const TOOLS = [
1068
1070
  }
1069
1071
  },
1070
1072
 
1073
+ {
1074
+ name: 'tascan_list_assets',
1075
+ description: 'List registered condition-ledger assets with their latest condition scores. Use to recover an asset_id for tascan_assess_condition or tascan_condition_history.',
1076
+ inputSchema: {
1077
+ type: 'object',
1078
+ properties: { project_id: { type: 'string', description: 'Filter by project' } },
1079
+ required: []
1080
+ },
1081
+ annotations: { title: 'List Condition Assets', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1082
+ handler: async (args, api) => {
1083
+ const path = '/assets' + (args.project_id ? `?project_id=${args.project_id}` : '');
1084
+ const result = await api('GET', path);
1085
+ const assets = result.data || [];
1086
+ if (!assets.length) return 'No assets registered. Use tascan_register_asset to add one.';
1087
+ return assets.map(a =>
1088
+ `${a.name} (${a.id})${a.serial_number ? '\n SN: ' + a.serial_number : ''}${a.asset_type ? ' · ' + a.asset_type : ''}${a.location_description ? '\n Location: ' + a.location_description : ''}`
1089
+ ).join('\n\n');
1090
+ }
1091
+ },
1092
+
1093
+ // ─── Gig Payments (verification-gated direct payer→worker) ───────
1094
+ {
1095
+ name: 'tascan_request_payment',
1096
+ description: 'Pledge a payment on a task list: when the list is verified complete (every task done + photo evidence on photo-required tasks), the payer automatically receives a Stripe pay link that routes the money DIRECTLY to the worker (0% TaScan fee). No money moves and no card is stored at pledge time. The worker must have completed payout onboarding (Get Paid on their profile).',
1097
+ inputSchema: {
1098
+ type: 'object',
1099
+ properties: {
1100
+ task_list_id: { type: 'string', description: 'Task list the payment is tied to' },
1101
+ worker_id: { type: 'string', description: 'Worker who gets paid' },
1102
+ amount_cents: { type: 'number', description: 'Amount in cents ($1 min, $10,000 max)' },
1103
+ payer_email: { type: 'string', description: 'Who pays — receives the pay link on verification' },
1104
+ payer_name: { type: 'string' },
1105
+ memo: { type: 'string', description: 'What the payment is for (shown to the payer)' }
1106
+ },
1107
+ required: ['task_list_id', 'worker_id', 'amount_cents', 'payer_email']
1108
+ },
1109
+ annotations: { title: 'Request Verified Payment', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1110
+ handler: async (args, api) => {
1111
+ const result = await api('POST', '/payments', args);
1112
+ const p = result.data;
1113
+ return `Payment pledged: $${(p.amount_cents / 100).toFixed(2)} to worker ${p.worker_id}\n\nID: ${p.id}\nStatus: ${p.status}\nPayer: ${p.payer_email}\n\n${result.note || 'Pay link is issued automatically when the list is verified complete.'}`;
1114
+ }
1115
+ },
1116
+ {
1117
+ name: 'tascan_list_payments',
1118
+ description: 'List gig payments and their lifecycle status: awaiting_completion (pledged, work not verified yet), ready_to_pay (verified — pay link sent to payer), paid, canceled. Filter by task list or status.',
1119
+ inputSchema: {
1120
+ type: 'object',
1121
+ properties: {
1122
+ task_list_id: { type: 'string', description: 'Filter by task list' },
1123
+ status: { type: 'string', enum: ['awaiting_completion', 'ready_to_pay', 'paid', 'canceled'], description: 'Filter by status' }
1124
+ },
1125
+ required: []
1126
+ },
1127
+ annotations: { title: 'List Gig Payments', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1128
+ handler: async (args, api) => {
1129
+ const qs = [];
1130
+ if (args.task_list_id) qs.push(`task_list_id=${args.task_list_id}`);
1131
+ if (args.status) qs.push(`status=${args.status}`);
1132
+ const result = await api('GET', '/payments' + (qs.length ? '?' + qs.join('&') : ''));
1133
+ const payments = result.data || [];
1134
+ if (!payments.length) return 'No payments found.';
1135
+ return payments.map(p =>
1136
+ `$${(p.amount_cents / 100).toFixed(2)} — ${p.status.toUpperCase()}\n ID: ${p.id}\n List: ${p.task_list_id}\n Payer: ${p.payer_email}${p.memo ? '\n Memo: ' + p.memo : ''}${p.paid_at ? '\n Paid: ' + p.paid_at : p.released_at ? '\n Link issued: ' + p.released_at : ''}`
1137
+ ).join('\n\n');
1138
+ }
1139
+ },
1140
+
1071
1141
  // ─── Worker Passport (Patent 4 foundation) ───────────────────────
1072
1142
  {
1073
1143
  name: 'tascan_get_worker_passport',
@@ -1088,6 +1158,91 @@ const TOOLS = [
1088
1158
  `Member since: ${(s.member_since || '').slice(0, 10)}\n\n` +
1089
1159
  `Badges: ${p.badges.length ? p.badges.join(' · ') : 'none yet'}\n\nShareable profile: ${p.profile_url}`;
1090
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
+ }
1091
1246
  }
1092
1247
  ];
1093
1248