tascan-mcp 2.3.0 → 3.0.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 (4) hide show
  1. package/README.md +3 -2
  2. package/index.js +32 -825
  3. package/package.json +3 -2
  4. package/tools.cjs +895 -0
package/tools.cjs ADDED
@@ -0,0 +1,895 @@
1
+ // ============================================================
2
+ // TaScan MCP — canonical tool registry (single source of truth)
3
+ // ============================================================
4
+ // Consumed by BOTH transports:
5
+ // - index.js (stdio, npm package `tascan-mcp`, Claude Desktop/Code)
6
+ // - live-event/netlify/functions/mcp-endpoint.js (remote HTTP,
7
+ // https://app.tascan.io/mcp, claude.ai connector + MCP Directory)
8
+ //
9
+ // Every tool: { name, description, inputSchema (JSON Schema),
10
+ // annotations, handler(args, api) } where api(method, path, body)
11
+ // is the transport-bound REST call to app.tascan.io/api/v1.
12
+ // Handlers return a plain string; the transport wraps it in MCP
13
+ // content blocks.
14
+ // ============================================================
15
+
16
+ const AGENT_REGISTRY = [
17
+ {
18
+ id: 'claude-code-local',
19
+ name: 'Claude Code',
20
+ type: 'local',
21
+ model: 'claude-opus-4-6',
22
+ worker_id: '9d194a8e-0c38-4cba-ba62-8560e497a0c3',
23
+ inbox_id: 'f745d0fa-421a-42e5-85be-0a3f9acd28e8',
24
+ capabilities: ['CODE', 'SHELL', 'PLAN', 'WRITE', 'RESEARCH', 'DEFAULT'],
25
+ location: 'Las Vegas, NV (Mike\'s PC)',
26
+ status: 'active',
27
+ description: 'Local Claude Code agent on Mike\'s PC. Spawns claude -p for code/shell tasks. Full filesystem + tool access. Picks up tasks from AI Inbox via Supabase Realtime.'
28
+ },
29
+ {
30
+ id: 'cloud-agent',
31
+ name: 'Cloud Agent',
32
+ type: 'cloud',
33
+ model: 'claude-haiku-4-5-20251001',
34
+ worker_id: '9d194a8e-0c38-4cba-ba62-8560e497a0c3',
35
+ inbox_id: 'f745d0fa-421a-42e5-85be-0a3f9acd28e8',
36
+ capabilities: ['RESEARCH', 'WRITE', 'REVIEW', 'MCP', 'DEFAULT'],
37
+ location: 'Netlify (cloud)',
38
+ status: 'active',
39
+ description: 'Cloud agent via Netlify function (agent-execute.js). Uses Anthropic API for RESEARCH/WRITE tasks. Triggered by Supabase pg_net webhook on task insert.'
40
+ }
41
+ ];
42
+ // Dynamic registry entries (from tascan_register_agent calls within this session)
43
+ const dynamicAgents = [];
44
+
45
+ const TOOLS = [
46
+ {
47
+ name: 'tascan_list_projects',
48
+ description: 'List all TaScan projects in the organization',
49
+ inputSchema: { type: 'object', properties: {} },
50
+ annotations: { title: 'List Projects', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
51
+ handler: async (args, api) => {
52
+ const result = await api('GET', '/projects');
53
+ return JSON.stringify(result.data, null, 2);
54
+ }
55
+ },
56
+ {
57
+ name: 'tascan_create_project',
58
+ description: 'Create a new TaScan project (top-level container for events)',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ name: { type: 'string', description: 'Project name' },
63
+ location: { type: 'string', description: 'Project location / venue' }
64
+ },
65
+ required: ['name']
66
+ },
67
+ annotations: { title: 'Create Project', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
68
+ handler: async (args, api) => {
69
+ const result = await api('POST', '/projects', { name: args.name, location: args.location });
70
+ return `Project created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`;
71
+ }
72
+ },
73
+ {
74
+ name: 'tascan_get_project',
75
+ description: 'Get details of a specific project',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: { project_id: { type: 'string', description: 'Project ID' } },
79
+ required: ['project_id']
80
+ },
81
+ annotations: { title: 'Get Project', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
82
+ handler: async (args, api) => {
83
+ const result = await api('GET', `/projects/${args.project_id}`);
84
+ return JSON.stringify(result.data, null, 2);
85
+ }
86
+ },
87
+ {
88
+ name: 'tascan_update_project',
89
+ description: 'Update a project (name, location, status, dates)',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ project_id: { type: 'string', description: 'Project ID' },
94
+ name: { type: 'string', description: 'New name' },
95
+ location: { type: 'string', description: 'New location' },
96
+ status: { type: 'string', enum: ['active', 'archived'], description: 'Status' },
97
+ start_date: { type: 'string', description: 'Start date (ISO)' },
98
+ end_date: { type: 'string', description: 'End date (ISO)' }
99
+ },
100
+ required: ['project_id']
101
+ },
102
+ annotations: { title: 'Update Project', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
103
+ handler: async (args, api) => {
104
+ const { project_id, ...updates } = args;
105
+ const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
106
+ const result = await api('PUT', `/projects/${project_id}`, body);
107
+ return `Project updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`;
108
+ }
109
+ },
110
+ {
111
+ name: 'tascan_delete_project',
112
+ description: 'Delete a project and all its events, tasks, and completions. This action is irreversible.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: { project_id: { type: 'string', description: 'Project ID to delete' } },
116
+ required: ['project_id']
117
+ },
118
+ annotations: { title: 'Delete Project', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
119
+ handler: async (args, api) => {
120
+ await api('DELETE', `/projects/${args.project_id}`);
121
+ return `Project ${args.project_id} deleted successfully.`;
122
+ }
123
+ },
124
+ {
125
+ name: 'tascan_create_event',
126
+ description: 'Create a new event (task list) within a project. Supports team_mode (shared completions) and multi_instance (each worker gets isolated copy — great for surveys, onboarding, info collection). team_mode and multi_instance cannot both be true.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ project_id: { type: 'string', description: 'Project ID' },
131
+ name: { type: 'string', description: 'Event name' },
132
+ description: { type: 'string', description: 'Event description' },
133
+ team_mode: { type: 'boolean', description: 'Team mode — shared completions' },
134
+ multi_instance: { type: 'boolean', description: 'Multi-instance — each worker gets isolated copy' }
135
+ },
136
+ required: ['project_id', 'name']
137
+ },
138
+ annotations: { title: 'Create Event', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
139
+ handler: async (args, api) => {
140
+ const body = { name: args.name, description: args.description };
141
+ if (args.team_mode !== undefined) body.team_mode = args.team_mode;
142
+ if (args.multi_instance !== undefined) body.multi_instance = args.multi_instance;
143
+ const result = await api('POST', `/projects/${args.project_id}/lists`, body);
144
+ return `Event created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`;
145
+ }
146
+ },
147
+ {
148
+ name: 'tascan_list_events',
149
+ description: 'List all events (task lists) within a project',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: { project_id: { type: 'string', description: 'Project ID' } },
153
+ required: ['project_id']
154
+ },
155
+ annotations: { title: 'List Events', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
156
+ handler: async (args, api) => {
157
+ const result = await api('GET', `/projects/${args.project_id}/lists`);
158
+ return JSON.stringify(result.data, null, 2);
159
+ }
160
+ },
161
+ {
162
+ name: 'tascan_get_event',
163
+ description: 'Get details of a specific event (task list) including its tasks',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: { list_id: { type: 'string', description: 'Task list (event) ID' } },
167
+ required: ['list_id']
168
+ },
169
+ annotations: { title: 'Get Event', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
170
+ handler: async (args, api) => {
171
+ const result = await api('GET', `/lists/${args.list_id}`);
172
+ return JSON.stringify(result.data, null, 2);
173
+ }
174
+ },
175
+ {
176
+ name: 'tascan_update_event',
177
+ description: 'Update an event / task list (name, description, team_mode, multi_instance, timer_mode). team_mode and multi_instance cannot both be true.',
178
+ inputSchema: {
179
+ type: 'object',
180
+ properties: {
181
+ list_id: { type: 'string', description: 'Task list (event) ID' },
182
+ name: { type: 'string', description: 'New name' },
183
+ description: { type: 'string', description: 'New description' },
184
+ team_mode: { type: 'boolean', description: 'Team mode — shared completions' },
185
+ multi_instance: { type: 'boolean', description: 'Multi-instance — each worker gets isolated copy' },
186
+ timer_mode: { type: 'string', description: 'Timer mode (auto or manual)' }
187
+ },
188
+ required: ['list_id']
189
+ },
190
+ annotations: { title: 'Update Event', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
191
+ handler: async (args, api) => {
192
+ const { list_id, ...updates } = args;
193
+ const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
194
+ const result = await api('PUT', `/lists/${list_id}`, body);
195
+ return `Event updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`;
196
+ }
197
+ },
198
+ {
199
+ name: 'tascan_delete_event',
200
+ description: 'Delete an event (task list) and all its tasks and completions. This action is irreversible.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: { list_id: { type: 'string', description: 'Task list (event) ID to delete' } },
204
+ required: ['list_id']
205
+ },
206
+ annotations: { title: 'Delete Event', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
207
+ handler: async (args, api) => {
208
+ await api('DELETE', `/lists/${args.list_id}`);
209
+ return `Event ${args.list_id} deleted successfully.`;
210
+ }
211
+ },
212
+ {
213
+ name: 'tascan_add_tasks',
214
+ description: 'Add one or more tasks to an event (task list). Supports bulk creation. IMPORTANT: Set response_type correctly — use "text" for info collection (names, phones, emails, notes), "photo" for visual verification (inspections, serial numbers, damage checks), "checkbox" only for simple confirmations. NOTE: To dispatch tasks to the Claude Code agent running on Mike\'s PC, use tascan_dispatch_to_agent instead — it routes directly to the agent\'s inbox with zero configuration needed.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ list_id: { type: 'string', description: 'Task list (event) ID' },
219
+ tasks: {
220
+ type: 'array',
221
+ items: {
222
+ type: 'object',
223
+ properties: {
224
+ title: { type: 'string', description: 'Task title' },
225
+ description: { type: 'string', description: 'Task description' },
226
+ response_type: { type: 'string', enum: ['checkbox', 'photo', 'text', 'number', 'date', 'choice'], description: 'CRITICAL: "text" for names, phones, emails, notes, addresses, any free-form input. "photo" for tasks needing photographic proof (inspections, serial numbers, packed cases). "checkbox" ONLY for simple yes/no confirmations. "number" for numeric values. "date" for dates. "choice" for multiple-choice (needs response_config.options). Most info-collection tasks should be "text", most verification tasks should be "photo".' },
227
+ is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical task flag' },
228
+ requires_photo: { type: 'boolean', description: 'Require photo on completion' }
229
+ },
230
+ required: ['title']
231
+ },
232
+ description: 'Array of tasks to create'
233
+ }
234
+ },
235
+ required: ['list_id', 'tasks']
236
+ },
237
+ annotations: { title: 'Add Tasks', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
238
+ handler: async (args, api) => {
239
+ const result = await api('POST', `/lists/${args.list_id}/tasks`, args.tasks);
240
+ return `${result.count} task(s) created in list ${args.list_id}\n\n${JSON.stringify(result.data, null, 2)}`;
241
+ }
242
+ },
243
+ {
244
+ name: 'tascan_dispatch_to_agent',
245
+ description: 'PREFERRED tool for sending work to an AI agent. Dispatches a task to the agent\'s inbox — picked up and executed automatically. No list ID needed. Supports prefixes: CODE: SHELL: RESEARCH: WRITE: PLAN: for routing. Use "agent" param to target a specific agent (default: claude-code-local). Use tascan_list_agents to discover available agents.',
246
+ inputSchema: {
247
+ type: 'object',
248
+ properties: {
249
+ task: { type: 'string', description: 'The task description. Prefix with CODE: SHELL: RESEARCH: WRITE: PLAN: for routing, or just plain text.' },
250
+ agent: { type: 'string', description: 'Agent ID or name to dispatch to (default: claude-code-local). Use tascan_list_agents to see options.' },
251
+ priority: { type: 'string', enum: ['normal', 'urgent'], description: 'Priority level (default: normal)' }
252
+ },
253
+ required: ['task']
254
+ },
255
+ annotations: { title: 'Dispatch to AI Agent', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
256
+ handler: async (args, api) => {
257
+ const allAgents = [...AGENT_REGISTRY, ...dynamicAgents];
258
+ const agentQuery = (args.agent || 'claude-code-local').toLowerCase();
259
+ const agent = allAgents.find(a => a.id === agentQuery || a.name.toLowerCase() === agentQuery) || allAgents[0];
260
+ const title = args.priority === 'urgent' ? `🔴 ${args.task}` : args.task;
261
+ const result = await api('POST', `/lists/${agent.inbox_id}/tasks`, [{ title, response_type: 'text' }]);
262
+ return `Task dispatched to ${agent.name}!\n\nAgent: ${agent.name} (${agent.id})\nType: ${agent.type}\nLocation: ${agent.location}\nTask: ${title}\nStatus: Queued — agent will pick this up automatically.\n\n${JSON.stringify(result.data, null, 2)}`;
263
+ }
264
+ },
265
+ {
266
+ name: 'tascan_list_agents',
267
+ description: 'List all registered AI agents with their capabilities, inbox IDs, and status. Like reading input labels on a video matrix — discover which agents are available and what they can do before dispatching work.',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {},
271
+ required: []
272
+ },
273
+ annotations: { title: 'List AI Agents', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
274
+ handler: async () => {
275
+ const allAgents = [...AGENT_REGISTRY, ...dynamicAgents];
276
+ const summary = allAgents.map(a =>
277
+ `${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}`
278
+ ).join('\n\n');
279
+ return `${allAgents.length} registered agent(s):\n\n${summary}`;
280
+ }
281
+ },
282
+ {
283
+ name: 'tascan_register_agent',
284
+ description: 'Register a new AI agent in the agent registry. The agent will appear in tascan_list_agents and can receive dispatched tasks. Self-registration for AI agents joining the TaScan network.',
285
+ inputSchema: {
286
+ type: 'object',
287
+ properties: {
288
+ id: { type: 'string', description: 'Unique agent ID (e.g. "my-agent-1")' },
289
+ name: { type: 'string', description: 'Display name (e.g. "Research Bot")' },
290
+ type: { type: 'string', enum: ['local', 'cloud', 'hybrid'], description: 'Agent type' },
291
+ model: { type: 'string', description: 'Model powering this agent (e.g. "claude-sonnet-4-6")' },
292
+ worker_id: { type: 'string', description: 'TaScan worker ID for this agent' },
293
+ inbox_id: { type: 'string', description: 'Task list ID this agent monitors for new tasks' },
294
+ capabilities: { type: 'array', items: { type: 'string' }, description: 'Task type prefixes this agent handles (e.g. ["RESEARCH", "WRITE"])' },
295
+ location: { type: 'string', description: 'Where the agent runs (e.g. "AWS us-east-1")' },
296
+ description: { type: 'string', description: 'What this agent does' }
297
+ },
298
+ required: ['id', 'name', 'type', 'inbox_id', 'capabilities']
299
+ },
300
+ annotations: { title: 'Register AI Agent', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
301
+ handler: async (args) => {
302
+ const existing = [...AGENT_REGISTRY, ...dynamicAgents].find(a => a.id === args.id);
303
+ if (existing) {
304
+ Object.assign(existing, args, { status: 'active' });
305
+ return `Agent "${args.name}" (${args.id}) updated in registry.\n\n${JSON.stringify(existing, null, 2)}`;
306
+ }
307
+ const agent = {
308
+ id: args.id,
309
+ name: args.name,
310
+ type: args.type,
311
+ model: args.model || 'unknown',
312
+ worker_id: args.worker_id || 'unassigned',
313
+ inbox_id: args.inbox_id,
314
+ capabilities: args.capabilities || ['DEFAULT'],
315
+ location: args.location || 'unknown',
316
+ status: 'active',
317
+ description: args.description || `${args.name} agent`
318
+ };
319
+ dynamicAgents.push(agent);
320
+ 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)}`;
321
+ }
322
+ },
323
+ {
324
+ name: 'tascan_list_tasks',
325
+ description: 'List all tasks in an event (task list)',
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: { list_id: { type: 'string', description: 'Task list (event) ID' } },
329
+ required: ['list_id']
330
+ },
331
+ annotations: { title: 'List Tasks', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
332
+ handler: async (args, api) => {
333
+ const result = await api('GET', `/lists/${args.list_id}/tasks`);
334
+ return JSON.stringify(result.data, null, 2);
335
+ }
336
+ },
337
+ {
338
+ name: 'tascan_get_task',
339
+ description: 'Get details of a specific task including completions',
340
+ inputSchema: {
341
+ type: 'object',
342
+ properties: { task_id: { type: 'string', description: 'Task ID' } },
343
+ required: ['task_id']
344
+ },
345
+ annotations: { title: 'Get Task', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
346
+ handler: async (args, api) => {
347
+ const result = await api('GET', `/tasks/${args.task_id}`);
348
+ return JSON.stringify(result.data, null, 2);
349
+ }
350
+ },
351
+ {
352
+ name: 'tascan_update_task',
353
+ description: 'Update a task (title, description, response_type, flags, sort_order)',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {
357
+ task_id: { type: 'string', description: 'Task ID' },
358
+ title: { type: 'string', description: 'New title' },
359
+ description: { type: 'string', description: 'New description' },
360
+ response_type: { type: 'string', enum: ['checkbox', 'photo', 'text', 'number', 'date', 'choice'], description: 'See tascan_add_tasks for guidance. "text" for info collection, "photo" for visual proof, "checkbox" for yes/no only.' },
361
+ is_safety_checkpoint: { type: 'boolean', description: 'Safety-critical flag' },
362
+ requires_photo: { type: 'boolean', description: 'Require photo' },
363
+ sort_order: { type: 'number', description: 'Sort position' }
364
+ },
365
+ required: ['task_id']
366
+ },
367
+ annotations: { title: 'Update Task', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
368
+ handler: async (args, api) => {
369
+ const { task_id, ...updates } = args;
370
+ const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
371
+ const result = await api('PUT', `/tasks/${task_id}`, body);
372
+ return `Task updated: ${result.data.title}\n\n${JSON.stringify(result.data, null, 2)}`;
373
+ }
374
+ },
375
+ {
376
+ name: 'tascan_delete_task',
377
+ description: 'Delete a specific task and its completions. This action is irreversible.',
378
+ inputSchema: {
379
+ type: 'object',
380
+ properties: { task_id: { type: 'string', description: 'Task ID to delete' } },
381
+ required: ['task_id']
382
+ },
383
+ annotations: { title: 'Delete Task', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
384
+ handler: async (args, api) => {
385
+ await api('DELETE', `/tasks/${args.task_id}`);
386
+ return `Task ${args.task_id} deleted successfully.`;
387
+ }
388
+ },
389
+ {
390
+ name: 'tascan_complete_task',
391
+ description: 'Complete a task on behalf of a worker. Inserts a completion record and timer event. Use this to simulate or record task completions via the API.',
392
+ inputSchema: {
393
+ type: 'object',
394
+ properties: {
395
+ task_id: { type: 'string', description: 'Task ID to complete' },
396
+ worker_id: { type: 'string', description: 'Worker ID performing the completion' },
397
+ notes: { type: 'string', description: 'Optional completion notes' },
398
+ response_value: { type: 'string', description: 'Response value (for text/number/choice tasks)' }
399
+ },
400
+ required: ['task_id', 'worker_id']
401
+ },
402
+ annotations: { title: 'Complete Task', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
403
+ handler: async (args, api) => {
404
+ const result = await api('POST', `/tasks/${args.task_id}/complete`, {
405
+ worker_id: args.worker_id,
406
+ notes: args.notes,
407
+ response_value: args.response_value
408
+ });
409
+ const d = result.data;
410
+ return `Task completed!\n Task: ${d.task_title} (${d.task_id})\n Worker: ${d.worker_id}\n Completed: ${d.completed_at}`;
411
+ }
412
+ },
413
+ {
414
+ name: 'tascan_list_workers',
415
+ description: 'List all workers (taskees) in the organization',
416
+ inputSchema: { type: 'object', properties: {} },
417
+ annotations: { title: 'List Workers', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
418
+ handler: async (args, api) => {
419
+ const result = await api('GET', '/workers');
420
+ return JSON.stringify(result.data, null, 2);
421
+ }
422
+ },
423
+ {
424
+ name: 'tascan_create_worker',
425
+ description: 'Create a new worker (taskee) in the organization',
426
+ inputSchema: {
427
+ type: 'object',
428
+ properties: {
429
+ name: { type: 'string', description: 'Worker name' },
430
+ phone: { type: 'string', description: 'Phone number' },
431
+ email: { type: 'string', description: 'Email' }
432
+ },
433
+ required: ['name']
434
+ },
435
+ annotations: { title: 'Create Worker', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
436
+ handler: async (args, api) => {
437
+ const result = await api('POST', '/workers', { name: args.name, phone: args.phone, email: args.email });
438
+ return `Worker created: ${result.data.name} (ID: ${result.data.id})\n\n${JSON.stringify(result.data, null, 2)}`;
439
+ }
440
+ },
441
+ {
442
+ name: 'tascan_update_worker',
443
+ description: 'Update a worker profile (name, phone, email)',
444
+ inputSchema: {
445
+ type: 'object',
446
+ properties: {
447
+ worker_id: { type: 'string', description: 'Worker ID' },
448
+ name: { type: 'string', description: 'New name' },
449
+ phone: { type: 'string', description: 'New phone' },
450
+ email: { type: 'string', description: 'New email' }
451
+ },
452
+ required: ['worker_id']
453
+ },
454
+ annotations: { title: 'Update Worker', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
455
+ handler: async (args, api) => {
456
+ const { worker_id, ...updates } = args;
457
+ const body = Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined));
458
+ const result = await api('PUT', `/workers/${worker_id}`, body);
459
+ return `Worker updated: ${result.data.name}\n\n${JSON.stringify(result.data, null, 2)}`;
460
+ }
461
+ },
462
+ {
463
+ name: 'tascan_generate_qr',
464
+ description: 'Generate a QR code for a task list (event) that workers can scan to access tasks',
465
+ inputSchema: {
466
+ type: 'object',
467
+ properties: { list_id: { type: 'string', description: 'Task list (event) ID' } },
468
+ required: ['list_id']
469
+ },
470
+ annotations: { title: 'Generate QR Code', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
471
+ handler: async (args, api) => {
472
+ const result = await api('POST', `/lists/${args.list_id}/qr`);
473
+ return `QR Code generated for "${result.data.list_name}":\n- Scan URL: ${result.data.scan_url}\n- QR Image: ${result.data.qr_image_url}`;
474
+ }
475
+ },
476
+ {
477
+ name: 'tascan_apply_template',
478
+ description: 'Apply a pre-built template to a task list, adding all template tasks',
479
+ inputSchema: {
480
+ type: 'object',
481
+ properties: {
482
+ list_id: { type: 'string', description: 'Task list (event) ID' },
483
+ template_slug: { type: 'string', description: 'Template slug (e.g. "conference-load-in", "warehouse-receiving")' }
484
+ },
485
+ required: ['list_id', 'template_slug']
486
+ },
487
+ annotations: { title: 'Apply Template', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
488
+ handler: async (args, api) => {
489
+ const result = await api('POST', `/lists/${args.list_id}/apply-template`, { template_slug: args.template_slug });
490
+ return `Template applied! ${result.count} tasks added to list ${args.list_id}\n\n${JSON.stringify(result.data, null, 2)}`;
491
+ }
492
+ },
493
+ {
494
+ name: 'tascan_list_templates',
495
+ description: 'List available task templates (built-in and saved)',
496
+ inputSchema: {
497
+ type: 'object',
498
+ properties: {
499
+ category: { type: 'string', description: 'Filter by category (e.g. "live-events", "hospitality", "logistics")' }
500
+ }
501
+ },
502
+ annotations: { title: 'List Templates', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
503
+ handler: async (args, api) => {
504
+ const path = args.category ? `/templates?category=${encodeURIComponent(args.category)}` : '/templates';
505
+ const result = await api('GET', path);
506
+ return JSON.stringify(result.data, null, 2);
507
+ }
508
+ },
509
+ {
510
+ name: 'tascan_get_report',
511
+ description: 'Get completion report for a task list (event) including task status, completions, workers, and photos. Set include_responses to also return the actual submitted response data (numbers, text, choices) for each completed task.',
512
+ inputSchema: {
513
+ type: 'object',
514
+ properties: {
515
+ list_id: { type: 'string', description: 'Task list (event) ID' },
516
+ include_responses: { type: 'boolean', description: 'Include the actual submitted response values for each completed task (default false — keeps the payload light)' }
517
+ },
518
+ required: ['list_id']
519
+ },
520
+ annotations: { title: 'Get Report', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
521
+ handler: async (args, api) => {
522
+ const path = `/lists/${args.list_id}/report` + (args.include_responses ? '?include_responses=true' : '');
523
+ const result = await api('GET', path);
524
+ const r = result.data;
525
+ let text = `Report for "${r.list_name}":\nCompletion: ${r.completed_tasks}/${r.total_tasks} (${r.completion_rate}%)\n\n`;
526
+ for (const task of r.tasks) {
527
+ const status = task.completed ? '[x]' : '[ ]';
528
+ text += `${status} ${task.title}`;
529
+ if (task.completions && task.completions.length > 0) {
530
+ text += ` (by ${task.completions.map(c => c.worker_name).join(', ')})`;
531
+ }
532
+ text += '\n';
533
+ if (args.include_responses && task.responses && task.responses.length > 0) {
534
+ for (const resp of task.responses) {
535
+ const val = resp.response_value != null ? JSON.stringify(resp.response_value) : '(no value)';
536
+ text += ` -> ${val}${resp.notes ? ' — ' + resp.notes : ''} (${resp.worker_name}, ${resp.completed_at})\n`;
537
+ }
538
+ }
539
+ }
540
+ return text;
541
+ }
542
+ },
543
+ {
544
+ name: 'tascan_query_responses',
545
+ description: "Query one task's submitted responses across every list in a project — e.g. the same exercise repeated across many workout lists returns one chronological progression series instead of N report lookups. Match by task title pattern or exact task ID.",
546
+ inputSchema: {
547
+ type: 'object',
548
+ properties: {
549
+ project_id: { type: 'string', description: 'Project ID' },
550
+ task: { type: 'string', description: 'Task title pattern (case-insensitive substring) or exact task ID' },
551
+ limit: { type: 'number', description: 'Max responses to return (default 200, max 500)' }
552
+ },
553
+ required: ['project_id', 'task']
554
+ },
555
+ annotations: { title: 'Query Task Responses Across Lists', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
556
+ handler: async (args, api) => {
557
+ let path = `/projects/${args.project_id}/responses?task=${encodeURIComponent(args.task)}`;
558
+ if (args.limit) path += `&limit=${args.limit}`;
559
+ const result = await api('GET', path);
560
+ const r = result.data;
561
+ 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`;
562
+ for (const resp of r.responses) {
563
+ const val = resp.response_value != null ? JSON.stringify(resp.response_value) : '(no value)';
564
+ const when = (resp.completed_at || '').slice(0, 16).replace('T', ' ');
565
+ text += `${when} · ${resp.list_name} · ${resp.task_title}: ${val}`;
566
+ if (resp.notes) text += ` — ${resp.notes}`;
567
+ text += ` (${resp.worker_name})\n`;
568
+ }
569
+ if (r.responses.length === 0) text += '(no responses found)\n';
570
+ return text;
571
+ }
572
+ },
573
+
574
+ // ─── Closed-Loop Autonomous Operations Protocol ─────────
575
+ {
576
+ name: 'tascan_list_issues',
577
+ description: 'List all issues for a task list (event). Returns open, acknowledged, and resolved issues with severity, type, and category. Use this to discover issues that need AI analysis via tascan_analyze_issue.',
578
+ inputSchema: {
579
+ type: 'object',
580
+ properties: { list_id: { type: 'string', description: 'Task list (event) ID' } },
581
+ required: ['list_id']
582
+ },
583
+ annotations: { title: 'List Issues', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
584
+ handler: async (args, api) => {
585
+ const result = await api('GET', `/lists/${args.list_id}/issues`);
586
+ const issues = result.data;
587
+ if (!issues.length) return 'No issues reported for this task list.';
588
+ let text = `${issues.length} issue(s) found:\n\n`;
589
+ issues.forEach((iss, i) => {
590
+ text += `${i+1}. [${iss.status.toUpperCase()}] ${iss.title}\n`;
591
+ text += ` ID: ${iss.id} | Type: ${iss.type} | Category: ${iss.category} | Severity: ${iss.severity || 'unset'}\n`;
592
+ if (iss.tasks) text += ` Task: ${iss.tasks.title}\n`;
593
+ text += ` Reported: ${iss.created_at}\n\n`;
594
+ });
595
+ return text;
596
+ }
597
+ },
598
+ {
599
+ name: 'tascan_analyze_issue',
600
+ description: 'Step 1 of the Closed-Loop Autonomous Operations Protocol. Retrieves full issue context including worker info, message thread, project history, and recent similar issues. Use this data to reason about the root cause and generate a remediation plan. Also supports server-side AI analysis via POST (calls Anthropic API directly).',
601
+ inputSchema: {
602
+ type: 'object',
603
+ properties: {
604
+ issue_id: { type: 'string', description: 'Issue ID to analyze' },
605
+ server_side_ai: { type: 'boolean', description: 'If true, the server calls Anthropic API directly for AI analysis (default: false — returns raw data for MCP client to analyze)' }
606
+ },
607
+ required: ['issue_id']
608
+ },
609
+ annotations: { title: 'Analyze Issue', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
610
+ handler: async (args, api) => {
611
+ // If server_side_ai, call the /analyze endpoint (Anthropic API on server)
612
+ if (args.server_side_ai) {
613
+ const result = await api('POST', `/issues/${args.issue_id}/analyze`);
614
+ const d = result.data;
615
+ let text = `=== AI ANALYSIS (Server-Side) ===\n`;
616
+ text += `Issue: ${d.issue_title} (${d.issue_id})\n`;
617
+ text += `Analyzed at: ${d.analyzed_at}\n\n`;
618
+ text += JSON.stringify(d.analysis, null, 2);
619
+ return text;
620
+ }
621
+
622
+ // Default: return rich context for MCP client to reason about
623
+ const result = await api('GET', `/issues/${args.issue_id}`);
624
+ const d = result.data;
625
+ let text = `=== ISSUE CONTEXT FOR ANALYSIS ===\n`;
626
+ text += `Issue: ${d.description || 'No description'}\n`;
627
+ text += `Type: ${d.type} | Category: ${d.category} | Severity: ${d.severity || 'unset'} | Status: ${d.status}\n`;
628
+ text += `Description: ${d.description || 'none'}\n`;
629
+ if (d.tasks) text += `Task: ${d.tasks.title} (${d.tasks.response_type || 'checkbox'})\nTask Description: ${d.tasks.description || 'none'}\n`;
630
+ if (d.worker) text += `Worker: ${d.worker.name} (ID: ${d.worker.id})\n`;
631
+ if (d.project) text += `Project: ${d.project.name} | Location: ${d.project.location || 'unknown'}\n`;
632
+ if (d.injury_data) text += `Injury Data: ${JSON.stringify(d.injury_data)}\n`;
633
+ text += `Reported: ${d.created_at}\n`;
634
+ if (d.ai_analysis) text += `\n--- PREVIOUS AI ANALYSIS ---\n${JSON.stringify(d.ai_analysis, null, 2)}\n`;
635
+ if (d.messages && d.messages.length) {
636
+ text += `\n--- Message Thread (${d.messages.length}) ---\n`;
637
+ d.messages.forEach(m => {
638
+ text += `[${m.sender_type}/${m.sender_name}] ${m.message}\n`;
639
+ });
640
+ }
641
+ if (d.recent_issues && d.recent_issues.length) {
642
+ text += `\n--- Recent Issues on Same Project (${d.recent_issues.length}) ---\n`;
643
+ d.recent_issues.forEach(ri => {
644
+ text += `- [${ri.severity}/${ri.category}] ${(ri.description || '').substring(0, 80)} (${ri.status})\n`;
645
+ });
646
+ }
647
+ return text;
648
+ }
649
+ },
650
+ {
651
+ name: 'tascan_recommend_fix',
652
+ description: 'Step 2 of the Closed-Loop Autonomous Operations Protocol. Post an AI-generated recommendation to an issue thread. Accepts both a text recommendation and an optional structured_recommendation object with task definitions for auto-dispatch. The recommendation is persisted in the AI audit trail.',
653
+ inputSchema: {
654
+ type: 'object',
655
+ properties: {
656
+ issue_id: { type: 'string', description: 'Issue ID to recommend a fix for' },
657
+ recommendation: { type: 'string', description: 'The AI-generated recommendation text (clear, actionable instructions)' },
658
+ structured_recommendation: {
659
+ type: 'object',
660
+ description: 'Optional structured recommendation with tasks for auto-dispatch. Format: { recommendation_summary, confidence_score, tasks: [{ title, description, response_type, requires_photo, is_safety_checkpoint, sort_order }], estimated_duration_minutes, required_responder_role }'
661
+ },
662
+ ai_agent: { type: 'string', description: 'Name of the AI agent posting (default: TaScan AI)' }
663
+ },
664
+ required: ['issue_id', 'recommendation']
665
+ },
666
+ annotations: { title: 'Recommend Fix', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
667
+ handler: async (args, api) => {
668
+ const result = await api('POST', `/issues/${args.issue_id}/recommend`, {
669
+ recommendation: args.recommendation,
670
+ structured_recommendation: args.structured_recommendation,
671
+ ai_agent: args.ai_agent
672
+ });
673
+ const d = result.data;
674
+ let text = `Recommendation posted to issue "${d.issue_title}":\n`;
675
+ text += `Project: ${d.project_name} | List: ${d.list_name}\n`;
676
+ text += `Type: ${d.issue_type} | Category: ${d.issue_category}\n`;
677
+ text += `Message ID: ${d.message_id}\n\n`;
678
+ text += `Recommendation:\n${d.recommendation}`;
679
+ if (d.structured_recommendation?.tasks) {
680
+ text += `\n\nStructured Tasks (${d.structured_recommendation.tasks.length}):`;
681
+ d.structured_recommendation.tasks.forEach((t, i) => {
682
+ text += `\n ${i+1}. ${t.title}`;
683
+ });
684
+ }
685
+ return text;
686
+ }
687
+ },
688
+ {
689
+ name: 'tascan_dispatch_instruction',
690
+ description: 'Step 3 of the Closed-Loop Autonomous Operations Protocol. Dispatches remediation to the worker via MULTI-CHANNEL delivery: (1) issue thread message, (2) in-app notification, (3) progress feed update, (4) SMS if phone on file, (5) optional remediation task list creation. Closes the loop from digital AI analysis to physical worker execution.',
691
+ inputSchema: {
692
+ type: 'object',
693
+ properties: {
694
+ issue_id: { type: 'string', description: 'Issue ID this instruction relates to' },
695
+ instruction: { type: 'string', description: 'Clear, actionable instruction for the worker to execute' },
696
+ worker_id: { type: 'string', description: 'Target worker ID (defaults to the worker who reported the issue)' },
697
+ remediation_tasks: {
698
+ type: 'array',
699
+ description: 'Optional array of tasks to create as a remediation task list. Each: { title, description, response_type, requires_photo, is_safety_checkpoint, sort_order }',
700
+ items: { type: 'object' }
701
+ },
702
+ recommendation_summary: { type: 'string', description: 'One-line summary for the task list description' },
703
+ send_sms: { type: 'boolean', description: 'Send SMS to worker (default: true if phone on file)' },
704
+ ai_agent: { type: 'string', description: 'Name of the AI agent dispatching (default: TaScan AI)' }
705
+ },
706
+ required: ['issue_id', 'instruction']
707
+ },
708
+ annotations: { title: 'Dispatch Instruction', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
709
+ handler: async (args, api) => {
710
+ const result = await api('POST', `/issues/${args.issue_id}/dispatch`, {
711
+ instruction: args.instruction,
712
+ worker_id: args.worker_id,
713
+ remediation_tasks: args.remediation_tasks,
714
+ recommendation_summary: args.recommendation_summary,
715
+ send_sms: args.send_sms,
716
+ ai_agent: args.ai_agent
717
+ });
718
+ const d = result.data;
719
+ let text = `Instruction dispatched via ${d.channels.length} channels: ${d.channels.join(', ')}\n`;
720
+ text += `Worker: ${d.worker_id}\n`;
721
+ text += `Issue: ${d.issue_id} (status: ${d.issue_status})\n`;
722
+ text += `SMS: ${d.sms_status}\n`;
723
+ if (d.remediation_task_list_id) {
724
+ text += `\nRemediation Task List Created: ${d.remediation_task_list_id}\n`;
725
+ text += `Worker URL: ${d.worker_url}\n`;
726
+ text += `Tasks: ${d.tasks_created}\n`;
727
+ }
728
+ text += `\nInstruction:\n${d.instruction}`;
729
+ return text;
730
+ }
731
+ },
732
+ {
733
+ name: 'tascan_auto_resolve',
734
+ description: 'FULL Closed-Loop Autonomous Operations Protocol in one call. Server-side AI analyzes the issue, generates remediation tasks, creates a task list, and dispatches to the worker — all without human intervention. This executes Patent Claim 7: autonomous operations from issue detection through physical-world instruction delivery.',
735
+ inputSchema: {
736
+ type: 'object',
737
+ properties: {
738
+ issue_id: { type: 'string', description: 'Issue ID to auto-resolve' }
739
+ },
740
+ required: ['issue_id']
741
+ },
742
+ annotations: { title: 'Auto-Resolve Issue', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
743
+ handler: async (args, api) => {
744
+ const result = await api('POST', `/issues/${args.issue_id}/auto-resolve`);
745
+ const d = result.data;
746
+ let text = `=== CLOSED-LOOP AUTONOMOUS OPERATIONS PROTOCOL ===\n`;
747
+ text += `Issue: ${d.issue_id}\n\n`;
748
+ text += `--- ANALYSIS ---\n`;
749
+ text += `Summary: ${d.analysis?.summary || 'N/A'}\n`;
750
+ text += `Classification: ${d.analysis?.classification?.type || 'N/A'} (${d.analysis?.classification?.severity || 'N/A'})\n`;
751
+ text += `Urgency: ${d.analysis?.urgency_score || 'N/A'}/10\n`;
752
+ if (d.analysis?.root_cause_hypotheses?.length) {
753
+ text += `Root Causes:\n`;
754
+ d.analysis.root_cause_hypotheses.forEach((h, i) => {
755
+ text += ` ${i+1}. ${h.hypothesis} (${Math.round(h.confidence * 100)}%)\n`;
756
+ });
757
+ }
758
+ text += `\n--- RECOMMENDATION ---\n`;
759
+ text += `Fix: ${d.recommendation?.recommendation_summary || 'N/A'}\n`;
760
+ text += `Tasks: ${d.recommendation?.tasks?.length || 0}\n`;
761
+ text += `Est. Duration: ${d.recommendation?.estimated_duration_minutes || 'N/A'} min\n`;
762
+ text += `\n--- DISPATCH ---\n`;
763
+ text += `Channels: ${d.dispatch?.channels?.join(', ') || 'none'}\n`;
764
+ text += `SMS: ${d.dispatch?.sms_status || 'N/A'}\n`;
765
+ if (d.dispatch?.worker_url) text += `Worker URL: ${d.dispatch.worker_url}\n`;
766
+ if (d.dispatch?.remediation_task_list_id) text += `Task List ID: ${d.dispatch.remediation_task_list_id}\n`;
767
+ text += `\n--- TIMESTAMPS ---\n`;
768
+ text += `Analyzed: ${d.timestamps?.analyzed_at}\n`;
769
+ text += `Recommended: ${d.timestamps?.recommended_at}\n`;
770
+ text += `Dispatched: ${d.timestamps?.dispatched_at}\n`;
771
+ return text;
772
+ }
773
+ },
774
+
775
+ // ─── SendGrid Email Tool ──────────────────────────────────
776
+ {
777
+ name: 'tascan_send_task_email',
778
+ description: 'Send a branded TaScan task notification email via SendGrid. Can notify anyone about a specific task list or task. Includes QR code, task summary, and "Open in TaScan" button.',
779
+ inputSchema: {
780
+ type: 'object',
781
+ properties: {
782
+ to_email: { type: 'string', description: 'Recipient email address' },
783
+ to_name: { type: 'string', description: 'Recipient display name' },
784
+ list_id: { type: 'string', description: 'Task list (event) ID' },
785
+ task_id: { type: 'string', description: 'Optional specific task ID to highlight' },
786
+ subject: { type: 'string', description: 'Custom email subject (defaults to auto-generated)' },
787
+ message: { type: 'string', description: 'Optional custom message to include in the email body' },
788
+ include_qr: { type: 'boolean', description: 'Include QR code for the task list in the email (default: true)' }
789
+ },
790
+ required: ['to_email', 'list_id']
791
+ },
792
+ annotations: { title: 'Send Task Email', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
793
+ handler: async (args) => {
794
+ // Call the send-task-email function directly via HTTP
795
+ const url = 'https://app.tascan.io/.netlify/functions/send-task-email';
796
+ const resp = await fetch(url, {
797
+ method: 'POST',
798
+ headers: { 'Content-Type': 'application/json' },
799
+ body: JSON.stringify({
800
+ to_email: args.to_email,
801
+ to_name: args.to_name,
802
+ list_id: args.list_id,
803
+ task_id: args.task_id,
804
+ subject: args.subject,
805
+ message: args.message,
806
+ include_qr: args.include_qr !== false
807
+ })
808
+ });
809
+ const data = await resp.json();
810
+ if (!resp.ok || data.error) {
811
+ throw new Error(data.error || `Email send failed (${resp.status})`);
812
+ }
813
+ return `Email sent successfully!\n To: ${args.to_email}\n Subject: ${data.subject}\n Message ID: ${data.message_id || 'N/A'}`;
814
+ }
815
+ },
816
+
817
+ // ─── NFC Tag Registry (ported from the stdio server) ─────
818
+ {
819
+ name: 'tascan_register_tag',
820
+ description: 'Register a physical NFC tag to a project, task list, or specific task. When someone taps the tag, TaScan routes them to the linked resource. Tags use NTAG215 chips and are programmed with NFC Tools Pro.',
821
+ inputSchema: {
822
+ type: 'object',
823
+ properties: {
824
+ tag_hardware_id: { type: 'string', description: 'NFC tag hardware serial number (e.g., "04:CB:6C:51:CE:2A:81")' },
825
+ tag_name: { type: 'string', description: 'Friendly name (e.g., "Ballroom A Door", "Breaker Panel 3")' },
826
+ target_type: { type: 'string', enum: ['project', 'task_list', 'task'], description: 'What this tag points to' },
827
+ project_id: { type: 'string', description: 'Project ID' },
828
+ task_list_id: { type: 'string', description: 'Task list ID (required for task_list/task targets)' },
829
+ task_id: { type: 'string', description: 'Task ID (required for task targets)' },
830
+ location_description: { type: 'string', description: 'Physical location of the tag' }
831
+ },
832
+ required: ['tag_hardware_id', 'tag_name', 'target_type']
833
+ },
834
+ annotations: { title: 'Register NFC Tag', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
835
+ handler: async (args, api) => {
836
+ const result = await api('POST', '/tags', {
837
+ tag_hardware_id: args.tag_hardware_id, tag_name: args.tag_name, target_type: args.target_type,
838
+ project_id: args.project_id, task_list_id: args.task_list_id, task_id: args.task_id,
839
+ location_description: args.location_description
840
+ });
841
+ const d = result.data;
842
+ const nfcUrl = 'https://app.tascan.io/scan?tag={TAG-ID}&time={DATETIME}';
843
+ return `Tag registered: "${d.tag_name}" (${d.tag_hardware_id})\nTarget: ${d.target_type}\nID: ${d.id}\n\nProgram the NFC tag with this URL:\n${nfcUrl}\n\n${JSON.stringify(d, null, 2)}`;
844
+ }
845
+ },
846
+ {
847
+ name: 'tascan_list_tags',
848
+ description: 'List all registered NFC tags in the organization with their linked projects/task lists and scan counts',
849
+ inputSchema: { type: 'object', properties: {} },
850
+ annotations: { title: 'List NFC Tags', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
851
+ handler: async (args, api) => {
852
+ const result = await api('GET', '/tags');
853
+ const tags = result.data;
854
+ if (!tags.length) return 'No NFC tags registered.';
855
+ let text = `${tags.length} tag(s) registered:\n\n`;
856
+ tags.forEach((t, i) => {
857
+ text += `${i + 1}. "${t.tag_name}" — ${t.tag_hardware_id}\n`;
858
+ text += ` Target: ${t.target_type} | Scans: ${t.scan_count} | Active: ${t.is_active}\n`;
859
+ if (t.projects) text += ` Project: ${t.projects.name}\n`;
860
+ if (t.task_lists) text += ` Task List: ${t.task_lists.name}\n`;
861
+ if (t.location_description) text += ` Location: ${t.location_description}\n`;
862
+ text += '\n';
863
+ });
864
+ return text;
865
+ }
866
+ },
867
+ {
868
+ name: 'tascan_get_scan_history',
869
+ description: 'Get scan event history for a registered NFC tag — timestamps, GPS coordinates, and accuracy for each scan',
870
+ inputSchema: {
871
+ type: 'object',
872
+ properties: {
873
+ tag_id: { type: 'string', description: 'Tag registry ID (from tascan_list_tags)' },
874
+ limit: { type: 'number', description: 'Max results (default 50)' }
875
+ },
876
+ required: ['tag_id']
877
+ },
878
+ annotations: { title: 'Get Scan History', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
879
+ handler: async (args, api) => {
880
+ const path = `/tags/${args.tag_id}/scans` + (args.limit ? `?limit=${args.limit}` : '');
881
+ const result = await api('GET', path);
882
+ const scans = result.data;
883
+ if (!scans.length) return 'No scans recorded for this tag.';
884
+ let text = `${scans.length} scan(s):\n\n`;
885
+ scans.forEach((s, i) => {
886
+ text += `${i + 1}. ${s.scanned_at}`;
887
+ if (s.gps_lat && s.gps_lng) text += ` — GPS: ${s.gps_lat.toFixed(6)}, ${s.gps_lng.toFixed(6)} (±${Math.round(s.gps_accuracy || 0)}m)`;
888
+ text += '\n';
889
+ });
890
+ return text;
891
+ }
892
+ }
893
+ ];
894
+
895
+ module.exports = { TOOLS, AGENT_REGISTRY, dynamicAgents };