imsg-mcp 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,17 +6,16 @@ import { promisify } from "node:util";
6
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { c as checkLocalAccess, f as formatAccessReport, s as shutdown, g as getImsgDbPath, a as getContactsDbPaths, b as getSlugsDbPath, d as appendLog, e as getFileLogLines, h as getLogDirectory, p as perf, i as checkMessagesAvailable, j as sendToChat, k as sendToChatId, l as sendMessageReliable, m as sendMessageAlt, n as sendAttachment, o as checkImessageAvailability, q as installShutdownHandlers, r as registerCleanup, t as enableStdinEofDetection, u as enableOrphanWatchdog, v as logStartup, w as info, x as startHeapMonitor, y as getLogs, z as setLastSendError, A as logShutdown, B as stopHeapMonitor, C as getLogFilePath, D as getLastSendError } from "./shutdown-BJifUGdo.js";
10
- import { r as resolveRecipient, d as defaultCountryFromEnv, a as rankFuzzy, b as dispatchAnalytic, i as installWatchdog, n as noteActivity, c as readWatchdogState } from "./watchdog-0RGOHpj4.js";
9
+ import { c as checkLocalAccess, f as formatAccessReport, s as shutdown, g as getImsgDbPath, a as getContactsDbPaths, b as getSlugsDbPath, d as appendLog, e as getFileLogLines, h as getLogDirectory, p as perf, i as checkMessagesAvailable, j as sendToChat, k as sendToChatId, l as sendMessageReliable, m as sendMessageAlt, n as sendAttachment, o as checkImessageAvailability, q as installShutdownHandlers, r as registerCleanup, t as enableStdinEofDetection, u as enableOrphanWatchdog, v as logStartup, w as info, x as startHeapMonitor, y as getLogs, z as setLastSendError, A as logShutdown, B as stopHeapMonitor, C as getLogFilePath, D as getLastSendError } from "./shutdown-CikCSZru.js";
10
+ import { m as minMessageId, r as resolveRecipient, d as defaultCountryFromEnv, a as rankFuzzy, b as dispatchAnalytic, i as installWatchdog, n as noteActivity, c as readWatchdogState } from "./watchdog-CnbBZnfg.js";
11
11
  import { homedir } from "node:os";
12
12
  import Database from "better-sqlite3";
13
- import { streamExport } from "./exportStream-Bu5recJ4.js";
14
- import { I as IMessageDB, h as hasNativeModule } from "./imessage-db-BYU5mxM-.js";
13
+ import { I as IMessageDB, n as normalizedPhoneVariants, h as hasNativeModule } from "./imessage-db-C7w6mu1Q.js";
14
+ import { streamExport } from "./exportStream-B86sO7Mg.js";
15
15
  import { z } from "zod";
16
16
  import { A as APP_VERSION, a as APP_NAME } from "./meta-D3NoTAjA.js";
17
17
  import { randomUUID } from "node:crypto";
18
18
  import { parseUserDate } from "./dateParse-DJXMfq3a.js";
19
- import { m as minMessageId } from "./types-Bbi1xtl-.js";
20
19
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
21
20
  let dbPath = join(homedir(), ".imsg-mcp", "analytics-cache.db");
22
21
  let db = null;
@@ -1453,7 +1452,9 @@ const WaitForReplyOutputSchema = z.object({
1453
1452
  elapsedSeconds: z.number().optional()
1454
1453
  });
1455
1454
  const ListConversationsSchema = z.object({
1456
- limit: z.number().int().min(0).default(20)
1455
+ // Coerce: some MCP hosts serialise numeric tool args as strings.
1456
+ limit: z.coerce.number().int().min(0).default(20),
1457
+ offset: z.coerce.number().int().min(0).default(0)
1457
1458
  });
1458
1459
  const ListConversationsOutputSchema = z.object({
1459
1460
  conversations: z.array(ConversationSchema),
@@ -1562,7 +1563,14 @@ const GetContactSchema = z.object({
1562
1563
  message: "Provide either `handle` or `id`."
1563
1564
  });
1564
1565
  const GetContactOutputSchema = z.object({
1565
- contact: ContactSchema.nullable()
1566
+ contact: ContactSchema.nullable(),
1567
+ /** Per-handle conversation mapping: which thread slug each handle chats under. */
1568
+ threads: z.array(
1569
+ z.object({
1570
+ handle: z.string(),
1571
+ threadSlug: z.string().nullable()
1572
+ })
1573
+ )
1566
1574
  });
1567
1575
  const ResolveHandleSchema = z.object({
1568
1576
  handle: nonEmptyString("Phone number or email to resolve to a contact name.")
@@ -1805,7 +1813,7 @@ const TOOLS = [
1805
1813
  },
1806
1814
  {
1807
1815
  name: "list_conversations",
1808
- description: "List recent conversations with thread slugs, snippets, unread counts, participants, and service metadata.",
1816
+ description: "List recent conversations (newest first) with thread slugs, snippets, unread counts, participants, and service metadata. Paginate with `offset` + the returned `nextOffset` to page past the per-call cap.",
1809
1817
  annotations: annotations.read,
1810
1818
  inputSchema: {
1811
1819
  type: "object",
@@ -1813,7 +1821,12 @@ const TOOLS = [
1813
1821
  limit: {
1814
1822
  type: "number",
1815
1823
  default: 20,
1816
- description: "Number of conversations. 0 = unlimited."
1824
+ description: "Conversations per page. 0 = as many as fit (capped at 500 per call)."
1825
+ },
1826
+ offset: {
1827
+ type: "number",
1828
+ default: 0,
1829
+ description: "Skip this many conversations (for pagination; pass the prior nextOffset)."
1817
1830
  }
1818
1831
  }
1819
1832
  },
@@ -1925,7 +1938,7 @@ const TOOLS = [
1925
1938
  },
1926
1939
  {
1927
1940
  name: "get_contact",
1928
- description: "Fetch a single contact by handle (phone/email) or by numeric id. Returns null if not found.",
1941
+ description: "Fetch a single contact by handle (phone/email) or by numeric id, including each handle's thread slug (for send_message/get_messages). Returns null if not found.",
1929
1942
  annotations: annotations.read,
1930
1943
  inputSchema: {
1931
1944
  type: "object",
@@ -2645,22 +2658,26 @@ ${formatted}${paginationLine}${perfLine}`,
2645
2658
  signal
2646
2659
  });
2647
2660
  const durMs = span.end({ format, count: result.count, sizeBytes: result.sizeBytes });
2648
- return toolText(
2649
- [
2650
- `Exported ${result.count} message(s) to ${result.savedTo}`,
2651
- `Format: ${format}`,
2652
- `Range: ${result.oldest?.toISOString() ?? "(none)"} → ${result.newest?.toISOString() ?? "(none)"}`,
2653
- `Size: ${(result.sizeBytes / 1024).toFixed(1)} KB`,
2654
- `_Took ${durMs.toFixed(0)}ms_`
2655
- ].join("\n"),
2656
- {
2657
- ...result,
2658
- format,
2659
- oldest: result.oldest?.toISOString() ?? null,
2660
- newest: result.newest?.toISOString() ?? null,
2661
- durationMs: durMs
2662
- }
2663
- );
2661
+ const lines = [
2662
+ `Exported ${result.count} message(s) to ${result.savedTo}`,
2663
+ `Format: ${format}`,
2664
+ `Range: ${result.oldest?.toISOString() ?? "(none)"} → ${result.newest?.toISOString() ?? "(none)"}`,
2665
+ `Size: ${(result.sizeBytes / 1024).toFixed(1)} KB`
2666
+ ];
2667
+ if (result.unmergedSiblings.length > 0) {
2668
+ const which = result.unmergedSiblings.map((s) => s.chatIdentifier).join(", ");
2669
+ lines.push(
2670
+ `⚠️ ${result.unmergedSiblings.length} other chat(s) (${which}) share this contact's identity but were not merged — history may be incomplete.`
2671
+ );
2672
+ }
2673
+ lines.push(`_Took ${durMs.toFixed(0)}ms_`);
2674
+ return toolText(lines.join("\n"), {
2675
+ ...result,
2676
+ format,
2677
+ oldest: result.oldest?.toISOString() ?? null,
2678
+ newest: result.newest?.toISOString() ?? null,
2679
+ durationMs: durMs
2680
+ });
2664
2681
  }
2665
2682
  async handleGetUnreadMessages(args) {
2666
2683
  const { limit } = GetUnreadMessagesSchema.parse(args ?? {});
@@ -2881,12 +2898,13 @@ ${formatted}`, {
2881
2898
  );
2882
2899
  }
2883
2900
  async handleListConversations(args) {
2884
- const { limit } = ListConversationsSchema.parse(args);
2901
+ const { limit, offset } = ListConversationsSchema.parse(args);
2885
2902
  const HARD_CAP = 500;
2886
2903
  const resolvedLimit = Math.min(resolveLimit(limit), HARD_CAP);
2887
- const limited = await this.db.listConversations(resolvedLimit + 1);
2888
- const hasMore = limited.length > resolvedLimit;
2889
- const results = limited.slice(0, resolvedLimit);
2904
+ const startAfter = offset + resolvedLimit;
2905
+ const limited = await this.db.listConversations(startAfter + 1);
2906
+ const hasMore = limited.length > startAfter;
2907
+ const results = limited.slice(offset, startAfter);
2890
2908
  if (results.length === 0) {
2891
2909
  return toolText("No conversations found.", {
2892
2910
  conversations: [],
@@ -2927,7 +2945,7 @@ ${formatted}`, {
2927
2945
  })),
2928
2946
  count: results.length,
2929
2947
  hasMore,
2930
- nextOffset: null
2948
+ nextOffset: hasMore ? startAfter : null
2931
2949
  });
2932
2950
  }
2933
2951
  async handleSearchMessages(args) {
@@ -3057,7 +3075,7 @@ ${formatted}${hint}`,
3057
3075
  contact = this.db.contacts.getContact(id);
3058
3076
  }
3059
3077
  if (!contact) {
3060
- return toolText("Contact not found.", { contact: null });
3078
+ return toolText("Contact not found.", { contact: null, threads: [] });
3061
3079
  }
3062
3080
  const phones = contact.phoneNumbers.length > 0 ? `
3063
3081
  Phones: ${contact.phoneNumbers.join(", ")}` : "";
@@ -3065,9 +3083,27 @@ Phones: ${contact.phoneNumbers.join(", ")}` : "";
3065
3083
  Emails: ${contact.emails.join(", ")}` : "";
3066
3084
  const org = contact.organization ? `
3067
3085
  Organization: ${contact.organization}` : "";
3086
+ const resolveThreadSlug = async (handle2) => {
3087
+ const candidates = handle2.includes("@") ? [handle2] : normalizedPhoneVariants(handle2);
3088
+ for (const candidate of candidates) {
3089
+ const conv = await this.db.findChatByHandle(candidate);
3090
+ if (conv?.threadSlug) return conv.threadSlug;
3091
+ }
3092
+ return null;
3093
+ };
3094
+ const threads = await Promise.all(
3095
+ [...contact.phoneNumbers, ...contact.emails].map(async (h) => ({
3096
+ handle: h,
3097
+ threadSlug: await resolveThreadSlug(h)
3098
+ }))
3099
+ );
3100
+ const withThreads = threads.filter((t) => t.threadSlug);
3101
+ const threadLines = withThreads.length > 0 ? `
3102
+ Threads:
3103
+ ${withThreads.map((t) => ` ${t.handle} → ${t.threadSlug}`).join("\n")}` : "";
3068
3104
  return toolText(
3069
- `${sanitizeUserText(contact.displayName)} (id ${contact.id})${phones}${emails}${org}`,
3070
- { contact }
3105
+ `${sanitizeUserText(contact.displayName)} (id ${contact.id})${phones}${emails}${org}${threadLines}`,
3106
+ { contact, threads }
3071
3107
  );
3072
3108
  }
3073
3109
  async handleResolveHandle(args) {