palmier 0.7.0 → 0.7.3

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/src/mcp-tools.ts CHANGED
@@ -2,6 +2,7 @@ import { StringCodec, type NatsConnection } from "nats";
2
2
  import { registerPending } from "./pending-requests.js";
3
3
  import { getLocationDevice } from "./location-device.js";
4
4
  import { getNotifications } from "./notification-store.js";
5
+ import { getSmsMessages } from "./sms-store.js";
5
6
  import type { HostConfig } from "./types.js";
6
7
 
7
8
  export class ToolError extends Error {
@@ -203,7 +204,442 @@ const deviceGeolocationTool: ToolDefinition = {
203
204
  },
204
205
  };
205
206
 
206
- export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
207
+ const readContactsTool: ToolDefinition = {
208
+ name: "read-contacts",
209
+ description: [
210
+ "Read the contact list from the user's mobile device.",
211
+ "Blocks until the device responds (up to 30 seconds).",
212
+ 'Response: `{"contacts": [{"id": ..., "name": ..., "phone": ...}]}` on success, or `{"error": "..."}` on failure.',
213
+ ],
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {},
217
+ },
218
+ async handler(_args, ctx) {
219
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
220
+
221
+ const sc = StringCodec();
222
+
223
+ const ackReply = await ctx.nc.request(
224
+ `host.${ctx.config.hostId}.fcm.contacts`,
225
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, action: "read" })),
226
+ { timeout: 5_000 },
227
+ );
228
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
229
+ if (ack.error) throw new ToolError(ack.error, 502);
230
+
231
+ const responsePromise = new Promise<string>((resolve, reject) => {
232
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.contacts.${ctx.sessionId}`, { max: 1 });
233
+ const timer = setTimeout(() => {
234
+ sub.unsubscribe();
235
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
236
+ }, 30_000);
237
+
238
+ (async () => {
239
+ for await (const msg of sub) {
240
+ clearTimeout(timer);
241
+ resolve(sc.decode(msg.data));
242
+ }
243
+ })();
244
+ });
245
+
246
+ const result = JSON.parse(await responsePromise);
247
+ if (result.error) return { error: result.error };
248
+ return result;
249
+ },
250
+ };
251
+
252
+ const createContactTool: ToolDefinition = {
253
+ name: "create-contact",
254
+ description: [
255
+ "Create a new contact on the user's mobile device.",
256
+ "Blocks until the device responds (up to 30 seconds).",
257
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
258
+ ],
259
+ inputSchema: {
260
+ type: "object",
261
+ properties: {
262
+ name: { type: "string", description: "Contact display name" },
263
+ phone: { type: "string", description: "Phone number" },
264
+ email: { type: "string", description: "Email address" },
265
+ },
266
+ required: ["name"],
267
+ },
268
+ async handler(args, ctx) {
269
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
270
+
271
+ const { name, phone, email } = args as { name: string; phone?: string; email?: string };
272
+ if (!name) throw new ToolError("name is required", 400);
273
+
274
+ const sc = StringCodec();
275
+
276
+ const ackReply = await ctx.nc.request(
277
+ `host.${ctx.config.hostId}.fcm.contacts`,
278
+ sc.encode(JSON.stringify({
279
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
280
+ action: "create", name, phone, email,
281
+ })),
282
+ { timeout: 5_000 },
283
+ );
284
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
285
+ if (ack.error) throw new ToolError(ack.error, 502);
286
+
287
+ const responsePromise = new Promise<string>((resolve, reject) => {
288
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.contacts.${ctx.sessionId}`, { max: 1 });
289
+ const timer = setTimeout(() => {
290
+ sub.unsubscribe();
291
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
292
+ }, 30_000);
293
+
294
+ (async () => {
295
+ for await (const msg of sub) {
296
+ clearTimeout(timer);
297
+ resolve(sc.decode(msg.data));
298
+ }
299
+ })();
300
+ });
301
+
302
+ const result = JSON.parse(await responsePromise);
303
+ if (result.error) return { error: result.error };
304
+ return result;
305
+ },
306
+ };
307
+
308
+ const readCalendarTool: ToolDefinition = {
309
+ name: "read-calendar",
310
+ description: [
311
+ "Read calendar events from the user's mobile device.",
312
+ "Blocks until the device responds (up to 30 seconds).",
313
+ "Pass startDate and endDate as Unix timestamps in milliseconds. Defaults to next 7 days.",
314
+ 'Response: `{"events": [{"id": ..., "title": ..., "startTime": ..., "endTime": ..., "location": ..., "description": ..., "allDay": ..., "calendar": ...}]}` on success.',
315
+ ],
316
+ inputSchema: {
317
+ type: "object",
318
+ properties: {
319
+ startDate: { type: "number", description: "Start of range (Unix ms). Defaults to now." },
320
+ endDate: { type: "number", description: "End of range (Unix ms). Defaults to 7 days from start." },
321
+ },
322
+ },
323
+ async handler(args, ctx) {
324
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
325
+
326
+ const { startDate, endDate } = args as { startDate?: number; endDate?: number };
327
+ const sc = StringCodec();
328
+
329
+ const ackReply = await ctx.nc.request(
330
+ `host.${ctx.config.hostId}.fcm.calendar`,
331
+ sc.encode(JSON.stringify({
332
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
333
+ action: "read",
334
+ ...(startDate ? { startDate: String(startDate) } : {}),
335
+ ...(endDate ? { endDate: String(endDate) } : {}),
336
+ })),
337
+ { timeout: 5_000 },
338
+ );
339
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
340
+ if (ack.error) throw new ToolError(ack.error, 502);
341
+
342
+ const responsePromise = new Promise<string>((resolve, reject) => {
343
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.calendar.${ctx.sessionId}`, { max: 1 });
344
+ const timer = setTimeout(() => {
345
+ sub.unsubscribe();
346
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
347
+ }, 30_000);
348
+
349
+ (async () => {
350
+ for await (const msg of sub) {
351
+ clearTimeout(timer);
352
+ resolve(sc.decode(msg.data));
353
+ }
354
+ })();
355
+ });
356
+
357
+ const result = JSON.parse(await responsePromise);
358
+ if (result.error) return { error: result.error };
359
+ return result;
360
+ },
361
+ };
362
+
363
+ const createCalendarEventTool: ToolDefinition = {
364
+ name: "create-calendar-event",
365
+ description: [
366
+ "Create a calendar event on the user's mobile device.",
367
+ "Blocks until the device responds (up to 30 seconds).",
368
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
369
+ ],
370
+ inputSchema: {
371
+ type: "object",
372
+ properties: {
373
+ title: { type: "string", description: "Event title" },
374
+ startTime: { type: "number", description: "Start time (Unix ms)" },
375
+ endTime: { type: "number", description: "End time (Unix ms)" },
376
+ location: { type: "string", description: "Event location" },
377
+ description: { type: "string", description: "Event description" },
378
+ },
379
+ required: ["title", "startTime", "endTime"],
380
+ },
381
+ async handler(args, ctx) {
382
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
383
+
384
+ const { title, startTime, endTime, location, description } = args as {
385
+ title: string; startTime: number; endTime: number; location?: string; description?: string;
386
+ };
387
+ if (!title || !startTime || !endTime) throw new ToolError("title, startTime, and endTime are required", 400);
388
+
389
+ const sc = StringCodec();
390
+
391
+ const ackReply = await ctx.nc.request(
392
+ `host.${ctx.config.hostId}.fcm.calendar`,
393
+ sc.encode(JSON.stringify({
394
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
395
+ action: "create",
396
+ title, startTime: String(startTime), endTime: String(endTime),
397
+ ...(location ? { location } : {}),
398
+ ...(description ? { description } : {}),
399
+ })),
400
+ { timeout: 5_000 },
401
+ );
402
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
403
+ if (ack.error) throw new ToolError(ack.error, 502);
404
+
405
+ const responsePromise = new Promise<string>((resolve, reject) => {
406
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.calendar.${ctx.sessionId}`, { max: 1 });
407
+ const timer = setTimeout(() => {
408
+ sub.unsubscribe();
409
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
410
+ }, 30_000);
411
+
412
+ (async () => {
413
+ for await (const msg of sub) {
414
+ clearTimeout(timer);
415
+ resolve(sc.decode(msg.data));
416
+ }
417
+ })();
418
+ });
419
+
420
+ const result = JSON.parse(await responsePromise);
421
+ if (result.error) return { error: result.error };
422
+ return result;
423
+ },
424
+ };
425
+
426
+ const sendSmsTool: ToolDefinition = {
427
+ name: "send-sms",
428
+ description: [
429
+ "Send an SMS message from the user's mobile device.",
430
+ "Blocks until the device responds (up to 30 seconds).",
431
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
432
+ ],
433
+ inputSchema: {
434
+ type: "object",
435
+ properties: {
436
+ to: { type: "string", description: "Recipient phone number" },
437
+ body: { type: "string", description: "Message text" },
438
+ },
439
+ required: ["to", "body"],
440
+ },
441
+ async handler(args, ctx) {
442
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
443
+
444
+ const { to, body } = args as { to: string; body: string };
445
+ if (!to || !body) throw new ToolError("to and body are required", 400);
446
+
447
+ const sc = StringCodec();
448
+
449
+ const ackReply = await ctx.nc.request(
450
+ `host.${ctx.config.hostId}.fcm.sms`,
451
+ sc.encode(JSON.stringify({
452
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
453
+ action: "send", to, body,
454
+ })),
455
+ { timeout: 5_000 },
456
+ );
457
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
458
+ if (ack.error) throw new ToolError(ack.error, 502);
459
+
460
+ const responsePromise = new Promise<string>((resolve, reject) => {
461
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.sms.${ctx.sessionId}`, { max: 1 });
462
+ const timer = setTimeout(() => {
463
+ sub.unsubscribe();
464
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
465
+ }, 30_000);
466
+
467
+ (async () => {
468
+ for await (const msg of sub) {
469
+ clearTimeout(timer);
470
+ resolve(sc.decode(msg.data));
471
+ }
472
+ })();
473
+ });
474
+
475
+ const result = JSON.parse(await responsePromise);
476
+ if (result.error) return { error: result.error };
477
+ return result;
478
+ },
479
+ };
480
+
481
+ const setAlarmTool: ToolDefinition = {
482
+ name: "set-alarm",
483
+ description: [
484
+ "Set an alarm on the user's mobile device.",
485
+ "Blocks until the device responds (up to 30 seconds).",
486
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
487
+ ],
488
+ inputSchema: {
489
+ type: "object",
490
+ properties: {
491
+ hour: { type: "number", description: "Hour (0-23)" },
492
+ minutes: { type: "number", description: "Minutes (0-59)" },
493
+ label: { type: "string", description: "Alarm label" },
494
+ days: {
495
+ type: "array",
496
+ items: { type: "number" },
497
+ description: "Recurring days (1=Sun, 2=Mon, ..., 7=Sat). Omit for one-time.",
498
+ },
499
+ },
500
+ required: ["hour", "minutes"],
501
+ },
502
+ async handler(args, ctx) {
503
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
504
+
505
+ const { hour, minutes, label, days } = args as { hour: number; minutes: number; label?: string; days?: number[] };
506
+ if (hour == null || minutes == null) throw new ToolError("hour and minutes are required", 400);
507
+
508
+ const sc = StringCodec();
509
+
510
+ const payload: Record<string, unknown> = {
511
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
512
+ action: "set", hour: String(hour), minutes: String(minutes),
513
+ };
514
+ if (label) payload.label = label;
515
+ if (days?.length) payload.days = days.join(",");
516
+
517
+ const ackReply = await ctx.nc.request(
518
+ `host.${ctx.config.hostId}.fcm.alarm`,
519
+ sc.encode(JSON.stringify(payload)),
520
+ { timeout: 5_000 },
521
+ );
522
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
523
+ if (ack.error) throw new ToolError(ack.error, 502);
524
+
525
+ const responsePromise = new Promise<string>((resolve, reject) => {
526
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.alarm.${ctx.sessionId}`, { max: 1 });
527
+ const timer = setTimeout(() => {
528
+ sub.unsubscribe();
529
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
530
+ }, 30_000);
531
+
532
+ (async () => {
533
+ for await (const msg of sub) {
534
+ clearTimeout(timer);
535
+ resolve(sc.decode(msg.data));
536
+ }
537
+ })();
538
+ });
539
+
540
+ const result = JSON.parse(await responsePromise);
541
+ if (result.error) return { error: result.error };
542
+ return result;
543
+ },
544
+ };
545
+
546
+ const readBatteryTool: ToolDefinition = {
547
+ name: "read-battery",
548
+ description: [
549
+ "Get the battery level and charging status of the user's mobile device.",
550
+ "Blocks until the device responds (up to 30 seconds).",
551
+ 'Response: `{"level": 85, "charging": true}` on success, or `{"error": "..."}` on failure.',
552
+ ],
553
+ inputSchema: {
554
+ type: "object",
555
+ properties: {},
556
+ },
557
+ async handler(_args, ctx) {
558
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
559
+
560
+ const sc = StringCodec();
561
+
562
+ const ackReply = await ctx.nc.request(
563
+ `host.${ctx.config.hostId}.fcm.battery`,
564
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId })),
565
+ { timeout: 5_000 },
566
+ );
567
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
568
+ if (ack.error) throw new ToolError(ack.error, 502);
569
+
570
+ const responsePromise = new Promise<string>((resolve, reject) => {
571
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.battery.${ctx.sessionId}`, { max: 1 });
572
+ const timer = setTimeout(() => {
573
+ sub.unsubscribe();
574
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
575
+ }, 30_000);
576
+
577
+ (async () => {
578
+ for await (const msg of sub) {
579
+ clearTimeout(timer);
580
+ resolve(sc.decode(msg.data));
581
+ }
582
+ })();
583
+ });
584
+
585
+ const result = JSON.parse(await responsePromise);
586
+ if (result.error) return { error: result.error };
587
+ return result;
588
+ },
589
+ };
590
+
591
+ const setRingerModeTool: ToolDefinition = {
592
+ name: "set-ringer-mode",
593
+ description: [
594
+ "Set the phone's ringer mode. Requires Do Not Disturb access on the device.",
595
+ "Blocks until the device responds (up to 30 seconds).",
596
+ 'Response: `{"ok": true, "mode": "silent"}` on success, or `{"error": "..."}` on failure.',
597
+ ],
598
+ inputSchema: {
599
+ type: "object",
600
+ properties: {
601
+ mode: { type: "string", description: "Ringer mode: 'normal', 'vibrate', or 'silent'" },
602
+ },
603
+ required: ["mode"],
604
+ },
605
+ async handler(args, ctx) {
606
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
607
+
608
+ const { mode } = args as { mode: string };
609
+ if (!["normal", "vibrate", "silent"].includes(mode)) throw new ToolError("mode must be 'normal', 'vibrate', or 'silent'", 400);
610
+
611
+ const sc = StringCodec();
612
+
613
+ const ackReply = await ctx.nc.request(
614
+ `host.${ctx.config.hostId}.fcm.ringer`,
615
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, mode })),
616
+ { timeout: 5_000 },
617
+ );
618
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
619
+ if (ack.error) throw new ToolError(ack.error, 502);
620
+
621
+ const responsePromise = new Promise<string>((resolve, reject) => {
622
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.ringer.${ctx.sessionId}`, { max: 1 });
623
+ const timer = setTimeout(() => {
624
+ sub.unsubscribe();
625
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
626
+ }, 30_000);
627
+
628
+ (async () => {
629
+ for await (const msg of sub) {
630
+ clearTimeout(timer);
631
+ resolve(sc.decode(msg.data));
632
+ }
633
+ })();
634
+ });
635
+
636
+ const result = JSON.parse(await responsePromise);
637
+ if (result.error) return { error: result.error };
638
+ return result;
639
+ },
640
+ };
641
+
642
+ export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, setAlarmTool, readBatteryTool, setRingerModeTool];
207
643
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
208
644
 
209
645
  // ── MCP Resources ─────────────────────────────────────────────────────
@@ -234,7 +670,19 @@ const deviceNotificationsResource: ResourceDefinition = {
234
670
  read: getNotifications,
235
671
  };
236
672
 
237
- export const agentResources: ResourceDefinition[] = [deviceNotificationsResource];
673
+ const deviceSmsResource: ResourceDefinition = {
674
+ uri: "sms://device",
675
+ name: "Device SMS",
676
+ description: [
677
+ "Get recent SMS messages from the user's Android device.",
678
+ "Response: JSON array of message objects with `id`, `sender`, `body`, `timestamp`.",
679
+ ],
680
+ mimeType: "application/json",
681
+ restPath: "/sms",
682
+ read: getSmsMessages,
683
+ };
684
+
685
+ export const agentResources: ResourceDefinition[] = [deviceNotificationsResource, deviceSmsResource];
238
686
  export const agentResourceMap = new Map<string, ResourceDefinition>(agentResources.map((r) => [r.uri, r]));
239
687
 
240
688
  /**
@@ -288,7 +736,7 @@ export function generateEndpointDocs(
288
736
 
289
737
  for (const resource of resources) {
290
738
  const [header, ...details] = resource.description;
291
- lines.push(`**\`GET ${baseUrl}${resource.restPath}\`** — ${header}`);
739
+ lines.push(`**\`GET ${baseUrl}${resource.restPath}?taskId=${taskId}\`** — ${header}`);
292
740
  for (const detail of details) {
293
741
  lines.push(`- ${detail}`);
294
742
  }
@@ -0,0 +1,28 @@
1
+ export interface SmsMessage {
2
+ id: string;
3
+ sender: string;
4
+ body: string;
5
+ timestamp: number;
6
+ receivedAt: number;
7
+ }
8
+
9
+ const MAX_MESSAGES = 50;
10
+ const messages: SmsMessage[] = [];
11
+ const listeners = new Set<() => void>();
12
+
13
+ export function addSmsMessage(m: SmsMessage): void {
14
+ messages.push(m);
15
+ if (messages.length > MAX_MESSAGES) {
16
+ messages.shift();
17
+ }
18
+ for (const cb of listeners) cb();
19
+ }
20
+
21
+ export function getSmsMessages(): SmsMessage[] {
22
+ return [...messages];
23
+ }
24
+
25
+ export function onSmsChanged(cb: () => void): () => void {
26
+ listeners.add(cb);
27
+ return () => { listeners.delete(cb); };
28
+ }
@@ -10,6 +10,7 @@ import { agentToolMap, agentResources, ToolError, type ToolContext } from "../mc
10
10
  import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
11
11
  import { getTaskDir } from "../task.js";
12
12
  import { onNotificationsChanged } from "../notification-store.js";
13
+ import { onSmsChanged } from "../sms-store.js";
13
14
 
14
15
  // ── Bundled PWA asset serving ───────────────────────────────────────────
15
16
 
@@ -124,6 +125,7 @@ export async function startHttpTransport(
124
125
 
125
126
  // Wire up resource change listeners
126
127
  onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
128
+ onSmsChanged(() => broadcastResourceUpdated("sms://device"));
127
129
 
128
130
  // If a pairing code is provided, pre-register it
129
131
  if (pairingCode) {
@@ -262,7 +264,20 @@ export async function startHttpTransport(
262
264
  const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
263
265
  if (matchedResource) {
264
266
  if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
265
- sendJson(res, 200, matchedResource.read());
267
+ const taskId = url.searchParams.get("taskId");
268
+ if (!taskId) {
269
+ sendJson(res, 400, { error: "taskId query parameter is required" });
270
+ return;
271
+ }
272
+ const taskDir = getTaskDir(config.projectRoot, taskId);
273
+ if (!fs.existsSync(taskDir)) {
274
+ sendJson(res, 404, { error: `Task not found: ${taskId}` });
275
+ return;
276
+ }
277
+ console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${matchedResource.name}`);
278
+ const result = matchedResource.read();
279
+ console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${matchedResource.name} done: ${JSON.stringify(result).slice(0, 200)}`);
280
+ sendJson(res, 200, result);
266
281
  return;
267
282
  }
268
283
 
@@ -141,7 +141,7 @@ describe("generateEndpointDocs", () => {
141
141
  "- Blocks until the device responds.",
142
142
  '- Response: `{"data": ...}` on success.',
143
143
  "",
144
- "**`GET http://localhost:9966/mock-data`** — Get mock data from the device.",
144
+ "**`GET http://localhost:9966/mock-data?taskId=test-id`** — Get mock data from the device.",
145
145
  "- Response: JSON array of data objects.",
146
146
  ].join("\n");
147
147
  assert.equal(docs, expected);