@hasna/contacts 0.2.8 → 0.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/mcp/index.js CHANGED
@@ -236,6 +236,85 @@ var MIGRATIONS = [
236
236
  );
237
237
 
238
238
  CREATE INDEX IF NOT EXISTS idx_contact_notes_contact ON contact_notes(contact_id);
239
+ `,
240
+ `
241
+ ALTER TABLE companies ADD COLUMN is_owned_entity INTEGER NOT NULL DEFAULT 0;
242
+ ALTER TABLE companies ADD COLUMN entity_type TEXT CHECK(entity_type IN ('operating','holding','dissolved','nonprofit','trust','branch','other'));
243
+
244
+ ALTER TABLE company_relationships ADD COLUMN start_date TEXT;
245
+ ALTER TABLE company_relationships ADD COLUMN end_date TEXT;
246
+ ALTER TABLE company_relationships ADD COLUMN is_primary INTEGER NOT NULL DEFAULT 0;
247
+ ALTER TABLE company_relationships ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','inactive','ended'));
248
+
249
+ CREATE TABLE IF NOT EXISTS org_members (
250
+ id TEXT PRIMARY KEY,
251
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
252
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
253
+ title TEXT,
254
+ specialization TEXT,
255
+ office_phone TEXT,
256
+ response_sla_hours INTEGER,
257
+ notes TEXT,
258
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
259
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
260
+ UNIQUE(company_id, contact_id)
261
+ );
262
+
263
+ CREATE TABLE IF NOT EXISTS vendor_communications (
264
+ id TEXT PRIMARY KEY,
265
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
266
+ contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
267
+ comm_date TEXT NOT NULL,
268
+ type TEXT NOT NULL DEFAULT 'email' CHECK(type IN ('email','call','meeting','invoice_request','invoice_received','payment','dispute','other')),
269
+ direction TEXT NOT NULL DEFAULT 'outbound' CHECK(direction IN ('inbound','outbound')),
270
+ subject TEXT,
271
+ body TEXT,
272
+ status TEXT NOT NULL DEFAULT 'sent' CHECK(status IN ('sent','awaiting_response','responded','no_response','resolved')),
273
+ invoice_amount REAL,
274
+ invoice_currency TEXT,
275
+ invoice_ref TEXT,
276
+ follow_up_date TEXT,
277
+ follow_up_done INTEGER NOT NULL DEFAULT 0,
278
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
279
+ );
280
+
281
+ CREATE TABLE IF NOT EXISTS contact_tasks (
282
+ id TEXT PRIMARY KEY,
283
+ title TEXT NOT NULL,
284
+ description TEXT,
285
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
286
+ assigned_by TEXT,
287
+ deadline TEXT,
288
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','awaiting_response','in_progress','completed','cancelled','escalated')),
289
+ priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low','medium','high','critical')),
290
+ entity_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
291
+ linked_todos_task_id TEXT,
292
+ escalation_rules TEXT NOT NULL DEFAULT '[]',
293
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
294
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
295
+ );
296
+
297
+ CREATE TABLE IF NOT EXISTS applications (
298
+ id TEXT PRIMARY KEY,
299
+ program_name TEXT NOT NULL,
300
+ provider_company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
301
+ type TEXT NOT NULL DEFAULT 'other' CHECK(type IN ('ai_credits','grant','startup_program','visa','trademark','tax_filing','loan','other')),
302
+ value_usd REAL,
303
+ applicant_contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
304
+ primary_contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
305
+ status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','submitted','pending','approved','rejected','follow_up_needed','expired','cancelled')),
306
+ submitted_date TEXT,
307
+ decision_date TEXT,
308
+ follow_up_date TEXT,
309
+ notes TEXT,
310
+ method TEXT CHECK(method IN ('email','form','typeform','hubspot','manual','browser','feathery','other')),
311
+ form_url TEXT,
312
+ metadata TEXT NOT NULL DEFAULT '{}',
313
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
314
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
315
+ );
316
+
317
+ ALTER TABLE contact_notes ADD COLUMN company_id TEXT REFERENCES companies(id) ON DELETE SET NULL;
239
318
  `
240
319
  ];
241
320
  var _db = null;
@@ -383,7 +462,9 @@ function rowToCompany(row) {
383
462
  ...row,
384
463
  custom_fields: JSON.parse(row.custom_fields || "{}"),
385
464
  archived: !!row.archived,
386
- project_id: row.project_id ?? null
465
+ project_id: row.project_id ?? null,
466
+ is_owned_entity: !!row.is_owned_entity,
467
+ entity_type: row.entity_type ?? null
387
468
  };
388
469
  }
389
470
  function insertEmails(db, contactId, companyId, emails) {
@@ -963,7 +1044,9 @@ function rowToCompany2(row) {
963
1044
  ...row,
964
1045
  custom_fields: JSON.parse(row.custom_fields || "{}"),
965
1046
  archived: !!row.archived,
966
- project_id: row.project_id ?? null
1047
+ project_id: row.project_id ?? null,
1048
+ is_owned_entity: !!row.is_owned_entity,
1049
+ entity_type: row.entity_type ?? null
967
1050
  };
968
1051
  }
969
1052
  function insertEmails2(db, companyId, emails) {
@@ -1027,8 +1110,8 @@ function createCompany(input, db) {
1027
1110
  const d = db || getDatabase();
1028
1111
  const id = uuid();
1029
1112
  const timestamp = now();
1030
- d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, created_at, updated_at)
1031
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1113
+ d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, is_owned_entity, entity_type, created_at, updated_at)
1114
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1032
1115
  id,
1033
1116
  input.name,
1034
1117
  input.domain ?? null,
@@ -1039,6 +1122,8 @@ function createCompany(input, db) {
1039
1122
  input.founded_year ?? null,
1040
1123
  input.notes ?? null,
1041
1124
  JSON.stringify(input.custom_fields ?? {}),
1125
+ input.is_owned_entity ? 1 : 0,
1126
+ input.entity_type ?? null,
1042
1127
  timestamp,
1043
1128
  timestamp
1044
1129
  ]);
@@ -1075,6 +1160,7 @@ function listCompanies(opts = {}, db) {
1075
1160
  tag_id,
1076
1161
  project_id,
1077
1162
  archived = false,
1163
+ is_owned_entity,
1078
1164
  order_by = "name",
1079
1165
  order_dir = "asc"
1080
1166
  } = opts;
@@ -1094,6 +1180,10 @@ function listCompanies(opts = {}, db) {
1094
1180
  conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
1095
1181
  params.push(tag_id);
1096
1182
  }
1183
+ if (is_owned_entity !== undefined) {
1184
+ conditions.push("co.is_owned_entity = ?");
1185
+ params.push(is_owned_entity ? 1 : 0);
1186
+ }
1097
1187
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1098
1188
  const validOrderBy = ["name", "created_at", "updated_at"].includes(order_by) ? order_by : "name";
1099
1189
  const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
@@ -1149,6 +1239,14 @@ function updateCompany(id, input, db) {
1149
1239
  setClauses.push("project_id = ?");
1150
1240
  params.push(input.project_id);
1151
1241
  }
1242
+ if (input.is_owned_entity !== undefined) {
1243
+ setClauses.push("is_owned_entity = ?");
1244
+ params.push(input.is_owned_entity ? 1 : 0);
1245
+ }
1246
+ if ("entity_type" in input && input.entity_type !== undefined) {
1247
+ setClauses.push("entity_type = ?");
1248
+ params.push(input.entity_type);
1249
+ }
1152
1250
  params.push(id);
1153
1251
  d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
1154
1252
  logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
@@ -1236,7 +1334,11 @@ function deleteRelationship(id, db) {
1236
1334
  function rowToCompanyRelationship(row) {
1237
1335
  return {
1238
1336
  ...row,
1239
- relationship_type: row.relationship_type
1337
+ relationship_type: row.relationship_type,
1338
+ start_date: row.start_date ?? null,
1339
+ end_date: row.end_date ?? null,
1340
+ is_primary: !!row.is_primary,
1341
+ status: row.status ?? "active"
1240
1342
  };
1241
1343
  }
1242
1344
  function createCompanyRelationship(input, db) {
@@ -1248,7 +1350,17 @@ function createCompanyRelationship(input, db) {
1248
1350
  if (!company)
1249
1351
  throw new Error(`Company ${input.company_id} not found`);
1250
1352
  const id = uuid();
1251
- d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes) VALUES (?, ?, ?, ?, ?)`, [id, input.contact_id, input.company_id, input.relationship_type, input.notes ?? null]);
1353
+ d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1354
+ id,
1355
+ input.contact_id,
1356
+ input.company_id,
1357
+ input.relationship_type,
1358
+ input.notes ?? null,
1359
+ input.start_date ?? null,
1360
+ input.end_date ?? null,
1361
+ input.is_primary ? 1 : 0,
1362
+ input.status ?? "active"
1363
+ ]);
1252
1364
  return rowToCompanyRelationship(d.query(`SELECT * FROM company_relationships WHERE id = ?`).get(id));
1253
1365
  }
1254
1366
  function listCompanyRelationships(opts = {}, db) {
@@ -1277,19 +1389,23 @@ function deleteCompanyRelationship(id, db) {
1277
1389
  }
1278
1390
 
1279
1391
  // src/db/notes.ts
1280
- function addNote(contactId, body, createdBy, db) {
1392
+ function addNote(contactId, body, createdBy, db, companyId) {
1281
1393
  const d = db || getDatabase();
1282
1394
  const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
1283
1395
  if (!contact)
1284
1396
  throw new ContactNotFoundError(contactId);
1285
1397
  const id = uuid();
1286
- d.run(`INSERT INTO contact_notes (id, contact_id, body, created_by) VALUES (?, ?, ?, ?)`, [id, contactId, body, createdBy ?? null]);
1398
+ d.run(`INSERT INTO contact_notes (id, contact_id, body, created_by, company_id) VALUES (?, ?, ?, ?, ?)`, [id, contactId, body, createdBy ?? null, companyId ?? null]);
1287
1399
  return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(id);
1288
1400
  }
1289
1401
  function listNotes(contactId, db) {
1290
1402
  const d = db || getDatabase();
1291
1403
  return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
1292
1404
  }
1405
+ function listNotesForContactAtCompany(contactId, companyId, db) {
1406
+ const d = db || getDatabase();
1407
+ return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? AND company_id = ? ORDER BY created_at ASC`).all(contactId, companyId);
1408
+ }
1293
1409
  function deleteNote(noteId, db) {
1294
1410
  const d = db || getDatabase();
1295
1411
  d.run(`DELETE FROM contact_notes WHERE id = ?`, [noteId]);
@@ -1777,6 +1893,16 @@ async function exportContacts(format, contacts) {
1777
1893
  import { join as join2 } from "path";
1778
1894
  import { existsSync as existsSync2, readFileSync } from "fs";
1779
1895
  import { homedir } from "os";
1896
+
1897
+ class ConnectorNotInstalledError extends Error {
1898
+ connectorName;
1899
+ constructor(connectorName) {
1900
+ super(`connect-${connectorName} is not installed. ` + `Run: bun install -g @hasnaxyz/connect-${connectorName}`);
1901
+ this.connectorName = connectorName;
1902
+ this.name = "ConnectorNotInstalledError";
1903
+ }
1904
+ }
1905
+
1780
1906
  class ConnectorAuthError extends Error {
1781
1907
  connectorName;
1782
1908
  constructor(connectorName, detail) {
@@ -1785,6 +1911,53 @@ class ConnectorAuthError extends Error {
1785
1911
  this.name = "ConnectorAuthError";
1786
1912
  }
1787
1913
  }
1914
+ async function runConnector(name, args, opts = {}) {
1915
+ const binary = `connect-${name}`;
1916
+ const profile = opts.profile ?? "default";
1917
+ const fullArgs = [
1918
+ "--format",
1919
+ "json",
1920
+ "--profile",
1921
+ profile,
1922
+ ...args
1923
+ ];
1924
+ let proc;
1925
+ try {
1926
+ proc = Bun.spawn([binary, ...fullArgs], {
1927
+ stdout: "pipe",
1928
+ stderr: "pipe"
1929
+ });
1930
+ } catch (err) {
1931
+ const msg = err instanceof Error ? err.message : String(err);
1932
+ if (msg.includes("not found") || msg.includes("ENOENT")) {
1933
+ throw new ConnectorNotInstalledError(name);
1934
+ }
1935
+ throw err;
1936
+ }
1937
+ const timeoutMs = opts.timeout ?? 30000;
1938
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
1939
+ try {
1940
+ const [stdout, stderr, exitCode] = await Promise.all([
1941
+ proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
1942
+ proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
1943
+ proc.exited
1944
+ ]);
1945
+ if (exitCode !== 0) {
1946
+ const errText = (stderr || stdout).trim();
1947
+ const lower = errText.toLowerCase();
1948
+ if (lower.includes("auth") || lower.includes("token") || lower.includes("401") || lower.includes("unauthorized") || lower.includes("not authenticated")) {
1949
+ throw new ConnectorAuthError(name, errText.slice(0, 200));
1950
+ }
1951
+ throw new Error(`connect-${name} exited ${exitCode}: ${errText.slice(0, 400)}`);
1952
+ }
1953
+ const text = stdout.trim();
1954
+ if (!text || text === "null")
1955
+ return null;
1956
+ return JSON.parse(text);
1957
+ } finally {
1958
+ clearTimeout(timer);
1959
+ }
1960
+ }
1788
1961
  function getConnectorTokenPath(name, profile = "default") {
1789
1962
  const bases = [
1790
1963
  join2(homedir(), ".connectors", `connect-${name}`, "profiles", profile, "tokens.json"),
@@ -1940,6 +2113,581 @@ async function extractContactsFromGmail(opts) {
1940
2113
  return Array.from(seen.values());
1941
2114
  }
1942
2115
 
2116
+ // src/lib/google-contacts.ts
2117
+ function googlePersonToContactInput(person) {
2118
+ const name = person.names?.[0];
2119
+ const primaryEmail = person.emailAddresses?.[0]?.value;
2120
+ const nameParts = name?.givenName || name?.familyName ? { first_name: name.givenName, last_name: name.familyName } : primaryEmail ? parseName(name?.displayName ?? primaryEmail.split("@")[0] ?? "") : {};
2121
+ const emails = (person.emailAddresses ?? []).filter((e) => e.value).map((e, i) => ({
2122
+ address: e.value.toLowerCase(),
2123
+ type: normalizeEmailType(e.type),
2124
+ is_primary: i === 0
2125
+ }));
2126
+ const phones = (person.phoneNumbers ?? []).filter((p) => p.value).map((p, i) => ({
2127
+ number: p.value,
2128
+ type: normalizePhoneType(p.type),
2129
+ is_primary: i === 0
2130
+ }));
2131
+ const org = person.organizations?.[0];
2132
+ const website = person.urls?.[0]?.value;
2133
+ const birthday = (() => {
2134
+ const b = person.birthdays?.[0]?.date;
2135
+ if (!b?.year || !b?.month || !b?.day)
2136
+ return;
2137
+ const mm = String(b.month).padStart(2, "0");
2138
+ const dd = String(b.day).padStart(2, "0");
2139
+ return `${b.year}-${mm}-${dd}`;
2140
+ })();
2141
+ const notes = person.biographies?.map((b) => b.value).filter(Boolean).join(`
2142
+
2143
+ `) || undefined;
2144
+ return {
2145
+ ...nameParts,
2146
+ display_name: name?.displayName ?? (nameParts.first_name ? `${nameParts.first_name} ${nameParts.last_name ?? ""}`.trim() : primaryEmail?.split("@")[0] ?? "Unknown"),
2147
+ job_title: org?.title,
2148
+ notes,
2149
+ birthday,
2150
+ website,
2151
+ source: "import",
2152
+ emails,
2153
+ phones,
2154
+ custom_fields: { google_resource_name: person.resourceName }
2155
+ };
2156
+ }
2157
+ function normalizeEmailType(type) {
2158
+ if (!type)
2159
+ return "work";
2160
+ const t = type.toLowerCase();
2161
+ if (t === "work" || t === "home" || t === "personal")
2162
+ return t === "home" ? "personal" : "work";
2163
+ return "other";
2164
+ }
2165
+ function normalizePhoneType(type) {
2166
+ if (!type)
2167
+ return "mobile";
2168
+ const t = type.toLowerCase();
2169
+ if (t === "mobile" || t === "cell")
2170
+ return "mobile";
2171
+ if (t === "work")
2172
+ return "work";
2173
+ if (t === "home")
2174
+ return "home";
2175
+ if (t === "fax")
2176
+ return "fax";
2177
+ return "other";
2178
+ }
2179
+ function contactToGoogleArgs(contact) {
2180
+ const args = [];
2181
+ const displayName = contact.display_name;
2182
+ if (displayName)
2183
+ args.push("--name", displayName);
2184
+ if (contact.first_name)
2185
+ args.push("--given-name", contact.first_name);
2186
+ if (contact.last_name)
2187
+ args.push("--family-name", contact.last_name);
2188
+ const primaryEmail = contact.emails.find((e) => e.is_primary) ?? contact.emails[0];
2189
+ if (primaryEmail)
2190
+ args.push("--email", primaryEmail.address);
2191
+ const primaryPhone = contact.phones.find((p) => p.is_primary) ?? contact.phones[0];
2192
+ if (primaryPhone)
2193
+ args.push("--phone", primaryPhone.number);
2194
+ if (contact.job_title)
2195
+ args.push("--title", contact.job_title);
2196
+ if (contact.company)
2197
+ args.push("--company", contact.company.name);
2198
+ if (contact.notes)
2199
+ args.push("--notes", contact.notes);
2200
+ if (contact.website)
2201
+ args.push("--url", contact.website);
2202
+ return args;
2203
+ }
2204
+ async function listGoogleContacts(opts = {}) {
2205
+ const args = ["contacts", "list"];
2206
+ if (opts.page_size)
2207
+ args.push("--page-size", String(opts.page_size));
2208
+ const raw = await runConnector("googlecontacts", args, opts);
2209
+ return normalizeGoogleContactsResponse(raw);
2210
+ }
2211
+ async function searchGoogleContacts(query, opts = {}) {
2212
+ const raw = await runConnector("googlecontacts", ["contacts", "search", query], opts);
2213
+ return normalizeGoogleContactsResponse(raw);
2214
+ }
2215
+ function normalizeGoogleContactsResponse(raw) {
2216
+ if (!raw)
2217
+ return [];
2218
+ if (Array.isArray(raw))
2219
+ return raw;
2220
+ const obj = raw;
2221
+ if (Array.isArray(obj["connections"]))
2222
+ return obj["connections"];
2223
+ if (Array.isArray(obj["contacts"]))
2224
+ return obj["contacts"];
2225
+ return [];
2226
+ }
2227
+ async function pushContactToGoogle(contact, opts = {}) {
2228
+ const googleResourceName = contact.custom_fields?.["google_resource_name"];
2229
+ if (googleResourceName && opts.update_existing) {
2230
+ const args2 = ["contacts", "update", googleResourceName, ...contactToGoogleArgs(contact)];
2231
+ const result2 = await runConnector("googlecontacts", args2, opts);
2232
+ return { resourceName: result2?.resourceName ?? googleResourceName, action: "updated" };
2233
+ }
2234
+ const args = ["contacts", "create", ...contactToGoogleArgs(contact)];
2235
+ const result = await runConnector("googlecontacts", args, opts);
2236
+ return { resourceName: result?.resourceName ?? "", action: "created" };
2237
+ }
2238
+ async function pullGoogleContactsAsInputs(opts = {}) {
2239
+ const people = opts.query ? await searchGoogleContacts(opts.query, opts) : await listGoogleContacts(opts);
2240
+ return people.filter((p) => p.emailAddresses?.some((e) => e.value)).map(googlePersonToContactInput);
2241
+ }
2242
+
2243
+ // src/db/org-members.ts
2244
+ function rowToOrgMember(row) {
2245
+ return {
2246
+ id: row.id,
2247
+ company_id: row.company_id,
2248
+ contact_id: row.contact_id,
2249
+ title: row.title,
2250
+ specialization: row.specialization,
2251
+ office_phone: row.office_phone,
2252
+ response_sla_hours: row.response_sla_hours,
2253
+ notes: row.notes,
2254
+ created_at: row.created_at,
2255
+ updated_at: row.updated_at
2256
+ };
2257
+ }
2258
+ function addOrgMember(input, db) {
2259
+ const d = db || getDatabase();
2260
+ const id = uuid();
2261
+ const timestamp = now();
2262
+ d.run(`INSERT INTO org_members (id, company_id, contact_id, title, specialization, office_phone, response_sla_hours, notes, created_at, updated_at)
2263
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2264
+ id,
2265
+ input.company_id,
2266
+ input.contact_id,
2267
+ input.title ?? null,
2268
+ input.specialization ?? null,
2269
+ input.office_phone ?? null,
2270
+ input.response_sla_hours ?? null,
2271
+ input.notes ?? null,
2272
+ timestamp,
2273
+ timestamp
2274
+ ]);
2275
+ return rowToOrgMember(d.query(`SELECT * FROM org_members WHERE id = ?`).get(id));
2276
+ }
2277
+ function listOrgMembers(companyId, db) {
2278
+ const d = db || getDatabase();
2279
+ const rows = d.query(`SELECT * FROM org_members WHERE company_id = ? ORDER BY created_at ASC`).all(companyId);
2280
+ return rows.map(rowToOrgMember);
2281
+ }
2282
+ function updateOrgMember(id, input, db) {
2283
+ const d = db || getDatabase();
2284
+ const setClauses = ["updated_at = ?"];
2285
+ const params = [now()];
2286
+ if ("title" in input) {
2287
+ setClauses.push("title = ?");
2288
+ params.push(input.title ?? null);
2289
+ }
2290
+ if ("specialization" in input) {
2291
+ setClauses.push("specialization = ?");
2292
+ params.push(input.specialization ?? null);
2293
+ }
2294
+ if ("office_phone" in input) {
2295
+ setClauses.push("office_phone = ?");
2296
+ params.push(input.office_phone ?? null);
2297
+ }
2298
+ if ("response_sla_hours" in input) {
2299
+ setClauses.push("response_sla_hours = ?");
2300
+ params.push(input.response_sla_hours ?? null);
2301
+ }
2302
+ if ("notes" in input) {
2303
+ setClauses.push("notes = ?");
2304
+ params.push(input.notes ?? null);
2305
+ }
2306
+ params.push(id);
2307
+ d.run(`UPDATE org_members SET ${setClauses.join(", ")} WHERE id = ?`, params);
2308
+ return rowToOrgMember(d.query(`SELECT * FROM org_members WHERE id = ?`).get(id));
2309
+ }
2310
+ function removeOrgMember(id, db) {
2311
+ const d = db || getDatabase();
2312
+ d.run(`DELETE FROM org_members WHERE id = ?`, [id]);
2313
+ }
2314
+ function listOrgMembersForContact(contactId, db) {
2315
+ const d = db || getDatabase();
2316
+ const rows = d.query(`SELECT * FROM org_members WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
2317
+ return rows.map(rowToOrgMember);
2318
+ }
2319
+
2320
+ // src/db/vendor-comms.ts
2321
+ function rowToVendorComm(row) {
2322
+ return {
2323
+ id: row.id,
2324
+ company_id: row.company_id,
2325
+ contact_id: row.contact_id,
2326
+ comm_date: row.comm_date,
2327
+ type: row.type,
2328
+ direction: row.direction,
2329
+ subject: row.subject,
2330
+ body: row.body,
2331
+ status: row.status,
2332
+ invoice_amount: row.invoice_amount,
2333
+ invoice_currency: row.invoice_currency,
2334
+ invoice_ref: row.invoice_ref,
2335
+ follow_up_date: row.follow_up_date,
2336
+ follow_up_done: !!row.follow_up_done,
2337
+ created_at: row.created_at
2338
+ };
2339
+ }
2340
+ function logVendorCommunication(input, db) {
2341
+ const d = db || getDatabase();
2342
+ const id = uuid();
2343
+ d.run(`INSERT INTO vendor_communications
2344
+ (id, company_id, contact_id, comm_date, type, direction, subject, body, status,
2345
+ invoice_amount, invoice_currency, invoice_ref, follow_up_date, follow_up_done)
2346
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2347
+ id,
2348
+ input.company_id,
2349
+ input.contact_id ?? null,
2350
+ input.comm_date,
2351
+ input.type ?? "email",
2352
+ input.direction ?? "outbound",
2353
+ input.subject ?? null,
2354
+ input.body ?? null,
2355
+ input.status ?? "sent",
2356
+ input.invoice_amount ?? null,
2357
+ input.invoice_currency ?? null,
2358
+ input.invoice_ref ?? null,
2359
+ input.follow_up_date ?? null,
2360
+ input.follow_up_done ? 1 : 0
2361
+ ]);
2362
+ return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
2363
+ }
2364
+ function listVendorCommunications(companyId, opts = {}, db) {
2365
+ const d = db || getDatabase();
2366
+ const conditions = ["company_id = ?"];
2367
+ const params = [companyId];
2368
+ if (opts.type) {
2369
+ conditions.push("type = ?");
2370
+ params.push(opts.type);
2371
+ }
2372
+ if (opts.status) {
2373
+ conditions.push("status = ?");
2374
+ params.push(opts.status);
2375
+ }
2376
+ if (opts.direction) {
2377
+ conditions.push("direction = ?");
2378
+ params.push(opts.direction);
2379
+ }
2380
+ const where = conditions.join(" AND ");
2381
+ const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
2382
+ return rows.map(rowToVendorComm);
2383
+ }
2384
+ function listPendingFollowUps(db) {
2385
+ const d = db || getDatabase();
2386
+ const today = new Date().toISOString().slice(0, 10);
2387
+ const rows = d.query(`SELECT * FROM vendor_communications
2388
+ WHERE follow_up_date <= ? AND follow_up_done = 0
2389
+ ORDER BY follow_up_date ASC`).all(today);
2390
+ return rows.map(rowToVendorComm);
2391
+ }
2392
+ function listMissingInvoices(db) {
2393
+ const d = db || getDatabase();
2394
+ const rows = d.query(`SELECT * FROM vendor_communications
2395
+ WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
2396
+ ORDER BY comm_date ASC`).all();
2397
+ return rows.map(rowToVendorComm);
2398
+ }
2399
+ function markFollowUpDone(id, db) {
2400
+ const d = db || getDatabase();
2401
+ d.run(`UPDATE vendor_communications SET follow_up_done = 1 WHERE id = ?`, [id]);
2402
+ return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
2403
+ }
2404
+
2405
+ // src/db/contact-tasks.ts
2406
+ function rowToContactTask(row) {
2407
+ return {
2408
+ id: row.id,
2409
+ title: row.title,
2410
+ description: row.description,
2411
+ contact_id: row.contact_id,
2412
+ assigned_by: row.assigned_by,
2413
+ deadline: row.deadline,
2414
+ status: row.status,
2415
+ priority: row.priority,
2416
+ entity_id: row.entity_id,
2417
+ linked_todos_task_id: row.linked_todos_task_id,
2418
+ escalation_rules: JSON.parse(row.escalation_rules || "[]"),
2419
+ created_at: row.created_at,
2420
+ updated_at: row.updated_at
2421
+ };
2422
+ }
2423
+ function createContactTask(input, db) {
2424
+ const d = db || getDatabase();
2425
+ const id = uuid();
2426
+ const timestamp = now();
2427
+ d.run(`INSERT INTO contact_tasks
2428
+ (id, title, description, contact_id, assigned_by, deadline, status, priority,
2429
+ entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
2430
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2431
+ id,
2432
+ input.title,
2433
+ input.description ?? null,
2434
+ input.contact_id,
2435
+ input.assigned_by ?? null,
2436
+ input.deadline ?? null,
2437
+ input.status ?? "pending",
2438
+ input.priority ?? "medium",
2439
+ input.entity_id ?? null,
2440
+ input.linked_todos_task_id ?? null,
2441
+ JSON.stringify(input.escalation_rules ?? []),
2442
+ timestamp,
2443
+ timestamp
2444
+ ]);
2445
+ return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
2446
+ }
2447
+ function listContactTasks(opts = {}, db) {
2448
+ const d = db || getDatabase();
2449
+ const conditions = [];
2450
+ const params = [];
2451
+ if (opts.contact_id) {
2452
+ conditions.push("contact_id = ?");
2453
+ params.push(opts.contact_id);
2454
+ }
2455
+ if (opts.entity_id) {
2456
+ conditions.push("entity_id = ?");
2457
+ params.push(opts.entity_id);
2458
+ }
2459
+ if (opts.status) {
2460
+ conditions.push("status = ?");
2461
+ params.push(opts.status);
2462
+ }
2463
+ if (opts.priority) {
2464
+ conditions.push("priority = ?");
2465
+ params.push(opts.priority);
2466
+ }
2467
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2468
+ const rows = d.query(`SELECT * FROM contact_tasks ${where} ORDER BY deadline ASC, priority DESC, created_at ASC`).all(...params);
2469
+ return rows.map(rowToContactTask);
2470
+ }
2471
+ function updateContactTask(id, input, db) {
2472
+ const d = db || getDatabase();
2473
+ const setClauses = ["updated_at = ?"];
2474
+ const params = [now()];
2475
+ if (input.title !== undefined) {
2476
+ setClauses.push("title = ?");
2477
+ params.push(input.title);
2478
+ }
2479
+ if ("description" in input) {
2480
+ setClauses.push("description = ?");
2481
+ params.push(input.description ?? null);
2482
+ }
2483
+ if ("assigned_by" in input) {
2484
+ setClauses.push("assigned_by = ?");
2485
+ params.push(input.assigned_by ?? null);
2486
+ }
2487
+ if ("deadline" in input) {
2488
+ setClauses.push("deadline = ?");
2489
+ params.push(input.deadline ?? null);
2490
+ }
2491
+ if (input.status !== undefined) {
2492
+ setClauses.push("status = ?");
2493
+ params.push(input.status);
2494
+ }
2495
+ if (input.priority !== undefined) {
2496
+ setClauses.push("priority = ?");
2497
+ params.push(input.priority);
2498
+ }
2499
+ if ("entity_id" in input) {
2500
+ setClauses.push("entity_id = ?");
2501
+ params.push(input.entity_id ?? null);
2502
+ }
2503
+ if ("linked_todos_task_id" in input) {
2504
+ setClauses.push("linked_todos_task_id = ?");
2505
+ params.push(input.linked_todos_task_id ?? null);
2506
+ }
2507
+ if (input.escalation_rules !== undefined) {
2508
+ setClauses.push("escalation_rules = ?");
2509
+ params.push(JSON.stringify(input.escalation_rules));
2510
+ }
2511
+ params.push(id);
2512
+ d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
2513
+ return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
2514
+ }
2515
+ function deleteContactTask(id, db) {
2516
+ const d = db || getDatabase();
2517
+ d.run(`DELETE FROM contact_tasks WHERE id = ?`, [id]);
2518
+ }
2519
+ function listOverdueTasks(db) {
2520
+ const d = db || getDatabase();
2521
+ const now_iso = new Date().toISOString();
2522
+ const rows = d.query(`SELECT * FROM contact_tasks
2523
+ WHERE deadline < ? AND status NOT IN ('completed','cancelled')
2524
+ ORDER BY deadline ASC`).all(now_iso);
2525
+ return rows.map(rowToContactTask);
2526
+ }
2527
+ function checkEscalations(db) {
2528
+ const overdue = listOverdueTasks(db);
2529
+ const now_ms = Date.now();
2530
+ const results = [];
2531
+ for (const task of overdue) {
2532
+ if (!task.deadline || task.escalation_rules.length === 0)
2533
+ continue;
2534
+ const deadlineMs = new Date(task.deadline).getTime();
2535
+ const daysPastDeadline = (now_ms - deadlineMs) / (1000 * 60 * 60 * 24);
2536
+ const triggered = task.escalation_rules.filter((rule) => daysPastDeadline >= rule.after_days);
2537
+ if (triggered.length > 0) {
2538
+ results.push({ task, rules_triggered: triggered });
2539
+ }
2540
+ }
2541
+ return results;
2542
+ }
2543
+
2544
+ // src/db/applications.ts
2545
+ function rowToApplication(row) {
2546
+ return {
2547
+ id: row.id,
2548
+ program_name: row.program_name,
2549
+ provider_company_id: row.provider_company_id,
2550
+ type: row.type,
2551
+ value_usd: row.value_usd,
2552
+ applicant_contact_id: row.applicant_contact_id,
2553
+ primary_contact_id: row.primary_contact_id,
2554
+ status: row.status,
2555
+ submitted_date: row.submitted_date,
2556
+ decision_date: row.decision_date,
2557
+ follow_up_date: row.follow_up_date,
2558
+ notes: row.notes,
2559
+ method: row.method ?? null,
2560
+ form_url: row.form_url,
2561
+ metadata: JSON.parse(row.metadata || "{}"),
2562
+ created_at: row.created_at,
2563
+ updated_at: row.updated_at
2564
+ };
2565
+ }
2566
+ function createApplication(input, db) {
2567
+ const d = db || getDatabase();
2568
+ const id = uuid();
2569
+ const timestamp = now();
2570
+ d.run(`INSERT INTO applications
2571
+ (id, program_name, provider_company_id, type, value_usd, applicant_contact_id, primary_contact_id,
2572
+ status, submitted_date, decision_date, follow_up_date, notes, method, form_url, metadata, created_at, updated_at)
2573
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2574
+ id,
2575
+ input.program_name,
2576
+ input.provider_company_id ?? null,
2577
+ input.type ?? "other",
2578
+ input.value_usd ?? null,
2579
+ input.applicant_contact_id ?? null,
2580
+ input.primary_contact_id ?? null,
2581
+ input.status ?? "draft",
2582
+ input.submitted_date ?? null,
2583
+ input.decision_date ?? null,
2584
+ input.follow_up_date ?? null,
2585
+ input.notes ?? null,
2586
+ input.method ?? null,
2587
+ input.form_url ?? null,
2588
+ JSON.stringify(input.metadata ?? {}),
2589
+ timestamp,
2590
+ timestamp
2591
+ ]);
2592
+ return rowToApplication(d.query(`SELECT * FROM applications WHERE id = ?`).get(id));
2593
+ }
2594
+ function listApplications(opts = {}, db) {
2595
+ const d = db || getDatabase();
2596
+ const conditions = [];
2597
+ const params = [];
2598
+ if (opts.type) {
2599
+ conditions.push("type = ?");
2600
+ params.push(opts.type);
2601
+ }
2602
+ if (opts.status) {
2603
+ conditions.push("status = ?");
2604
+ params.push(opts.status);
2605
+ }
2606
+ if (opts.provider_company_id) {
2607
+ conditions.push("provider_company_id = ?");
2608
+ params.push(opts.provider_company_id);
2609
+ }
2610
+ if (opts.applicant_contact_id) {
2611
+ conditions.push("applicant_contact_id = ?");
2612
+ params.push(opts.applicant_contact_id);
2613
+ }
2614
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2615
+ const rows = d.query(`SELECT * FROM applications ${where} ORDER BY created_at DESC`).all(...params);
2616
+ return rows.map(rowToApplication);
2617
+ }
2618
+ function updateApplication(id, input, db) {
2619
+ const d = db || getDatabase();
2620
+ const setClauses = ["updated_at = ?"];
2621
+ const params = [now()];
2622
+ if (input.program_name !== undefined) {
2623
+ setClauses.push("program_name = ?");
2624
+ params.push(input.program_name);
2625
+ }
2626
+ if ("provider_company_id" in input) {
2627
+ setClauses.push("provider_company_id = ?");
2628
+ params.push(input.provider_company_id ?? null);
2629
+ }
2630
+ if (input.type !== undefined) {
2631
+ setClauses.push("type = ?");
2632
+ params.push(input.type);
2633
+ }
2634
+ if ("value_usd" in input) {
2635
+ setClauses.push("value_usd = ?");
2636
+ params.push(input.value_usd ?? null);
2637
+ }
2638
+ if ("applicant_contact_id" in input) {
2639
+ setClauses.push("applicant_contact_id = ?");
2640
+ params.push(input.applicant_contact_id ?? null);
2641
+ }
2642
+ if ("primary_contact_id" in input) {
2643
+ setClauses.push("primary_contact_id = ?");
2644
+ params.push(input.primary_contact_id ?? null);
2645
+ }
2646
+ if (input.status !== undefined) {
2647
+ setClauses.push("status = ?");
2648
+ params.push(input.status);
2649
+ }
2650
+ if ("submitted_date" in input) {
2651
+ setClauses.push("submitted_date = ?");
2652
+ params.push(input.submitted_date ?? null);
2653
+ }
2654
+ if ("decision_date" in input) {
2655
+ setClauses.push("decision_date = ?");
2656
+ params.push(input.decision_date ?? null);
2657
+ }
2658
+ if ("follow_up_date" in input) {
2659
+ setClauses.push("follow_up_date = ?");
2660
+ params.push(input.follow_up_date ?? null);
2661
+ }
2662
+ if ("notes" in input) {
2663
+ setClauses.push("notes = ?");
2664
+ params.push(input.notes ?? null);
2665
+ }
2666
+ if ("method" in input) {
2667
+ setClauses.push("method = ?");
2668
+ params.push(input.method ?? null);
2669
+ }
2670
+ if ("form_url" in input) {
2671
+ setClauses.push("form_url = ?");
2672
+ params.push(input.form_url ?? null);
2673
+ }
2674
+ if (input.metadata !== undefined) {
2675
+ setClauses.push("metadata = ?");
2676
+ params.push(JSON.stringify(input.metadata));
2677
+ }
2678
+ params.push(id);
2679
+ d.run(`UPDATE applications SET ${setClauses.join(", ")} WHERE id = ?`, params);
2680
+ return rowToApplication(d.query(`SELECT * FROM applications WHERE id = ?`).get(id));
2681
+ }
2682
+ function listFollowUpDue(db) {
2683
+ const d = db || getDatabase();
2684
+ const today = new Date().toISOString().slice(0, 10);
2685
+ const rows = d.query(`SELECT * FROM applications
2686
+ WHERE follow_up_date <= ? AND status = 'follow_up_needed'
2687
+ ORDER BY follow_up_date ASC`).all(today);
2688
+ return rows.map(rowToApplication);
2689
+ }
2690
+
1943
2691
  // src/mcp/index.ts
1944
2692
  var server = new Server({ name: "contacts", version: "0.1.0" }, { capabilities: { tools: {} } });
1945
2693
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -2376,23 +3124,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2376
3124
  },
2377
3125
  {
2378
3126
  name: "add_note",
2379
- description: "Add a structured timestamped note to a contact. Returns the note object with id, body, created_at.",
3127
+ description: "Add a structured timestamped note to a contact. Returns the note object with id, body, created_at. Optionally scope the note to a specific company context.",
2380
3128
  inputSchema: {
2381
3129
  type: "object",
2382
3130
  properties: {
2383
3131
  contact_id: { type: "string" },
2384
3132
  note: { type: "string", description: "Note text/body" },
2385
- created_by: { type: "string", description: "Agent or user who created the note (optional)" }
3133
+ created_by: { type: "string", description: "Agent or user who created the note (optional)" },
3134
+ company_id: { type: "string", description: "Optional company context \u2014 scope this note to a specific contact-company pair" }
2386
3135
  },
2387
3136
  required: ["contact_id", "note"]
2388
3137
  }
2389
3138
  },
2390
3139
  {
2391
3140
  name: "list_notes",
2392
- description: "List all structured notes for a contact, ordered by date ascending.",
3141
+ description: "List all structured notes for a contact, ordered by date ascending. Optionally filter by company_id to show only notes scoped to that contact-company pair.",
2393
3142
  inputSchema: {
2394
3143
  type: "object",
2395
- properties: { contact_id: { type: "string" } },
3144
+ properties: {
3145
+ contact_id: { type: "string" },
3146
+ company_id: { type: "string", description: "Optional company ID \u2014 if provided, only returns notes scoped to this contact-company pair" }
3147
+ },
2396
3148
  required: ["contact_id"]
2397
3149
  }
2398
3150
  },
@@ -2706,14 +3458,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2706
3458
  },
2707
3459
  {
2708
3460
  name: "add_company_relationship",
2709
- description: "Declare a typed relationship between a contact and a company (client, vendor, partner, employee, contractor, investor, advisor, other).",
3461
+ description: "Declare a typed relationship between a contact and a company. relationship_type: client|vendor|partner|employee|contractor|investor|advisor|tax_preparer|bank_manager|attorney|registered_agent|accountant|payroll_specialist|insurance_broker|other. Supports optional start_date, end_date, is_primary, and status fields.",
2710
3462
  inputSchema: {
2711
3463
  type: "object",
2712
3464
  properties: {
2713
3465
  contact_id: { type: "string" },
2714
3466
  company_id: { type: "string" },
2715
- relationship_type: { type: "string", enum: ["client", "vendor", "partner", "employee", "contractor", "investor", "advisor", "other"] },
2716
- notes: { type: "string" }
3467
+ relationship_type: { type: "string", enum: ["client", "vendor", "partner", "employee", "contractor", "investor", "advisor", "tax_preparer", "bank_manager", "attorney", "registered_agent", "accountant", "payroll_specialist", "insurance_broker", "other"] },
3468
+ notes: { type: "string" },
3469
+ start_date: { type: "string", description: "ISO date when the relationship began (YYYY-MM-DD)" },
3470
+ end_date: { type: "string", description: "ISO date when the relationship ended (YYYY-MM-DD)" },
3471
+ is_primary: { type: "boolean", description: "Whether this is the primary contact for this relationship type at this company" },
3472
+ status: { type: "string", enum: ["active", "inactive", "ended"], description: "Relationship status (default: active)" }
2717
3473
  },
2718
3474
  required: ["contact_id", "company_id", "relationship_type"]
2719
3475
  }
@@ -2754,25 +3510,352 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2754
3510
  },
2755
3511
  required: ["query"]
2756
3512
  }
2757
- }
2758
- ]
2759
- }));
2760
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2761
- const { name, arguments: args } = request.params;
2762
- const a = args ?? {};
2763
- try {
2764
- switch (name) {
2765
- case "create_contact": {
2766
- const input = {
2767
- first_name: a.first_name,
2768
- last_name: a.last_name,
2769
- display_name: a.display_name,
2770
- nickname: a.nickname,
2771
- job_title: a.job_title,
2772
- company_id: a.company_id,
2773
- notes: a.notes,
2774
- birthday: a.birthday,
2775
- website: a.website,
3513
+ },
3514
+ {
3515
+ name: "sync_from_google_contacts",
3516
+ description: "Pull contacts from Google Contacts (People API) and upsert them into the local database. Skips contacts that already exist by email. Requires connect-googlecontacts auth login. Returns { imported, skipped, errors }.",
3517
+ inputSchema: {
3518
+ type: "object",
3519
+ properties: {
3520
+ query: { type: "string", description: "Optional search query to filter which Google Contacts to import" },
3521
+ page_size: { type: "number", description: "Contacts per page (default: connector default)" },
3522
+ google_profile: { type: "string", description: "connect-googlecontacts profile (default: 'default')" },
3523
+ tag_ids: { type: "array", items: { type: "string" }, description: "Tag IDs to apply to all imported contacts" },
3524
+ project_id: { type: "string", description: "Project ID to assign to all imported contacts" },
3525
+ dry_run: { type: "boolean", description: "Preview what would be imported without saving" }
3526
+ }
3527
+ }
3528
+ },
3529
+ {
3530
+ name: "push_contact_to_google",
3531
+ description: "Push a local contact to Google Contacts \u2014 creates a new Google contact (or updates if google_resource_name is stored in custom_fields). Requires connect-googlecontacts auth login.",
3532
+ inputSchema: {
3533
+ type: "object",
3534
+ properties: {
3535
+ contact_id: { type: "string", description: "Local contact ID to push" },
3536
+ google_profile: { type: "string", description: "connect-googlecontacts profile (default: 'default')" },
3537
+ update_existing: { type: "boolean", description: "If true, update existing Google contact when google_resource_name is present in custom_fields" }
3538
+ },
3539
+ required: ["contact_id"]
3540
+ }
3541
+ },
3542
+ {
3543
+ name: "search_google_contacts",
3544
+ description: "Search Google Contacts by name or email (read-only, does not import). Returns raw Google People API objects. Useful for lookup or preview before a push/sync.",
3545
+ inputSchema: {
3546
+ type: "object",
3547
+ properties: {
3548
+ query: { type: "string" },
3549
+ google_profile: { type: "string", description: "connect-googlecontacts profile (default: 'default')" }
3550
+ },
3551
+ required: ["query"]
3552
+ }
3553
+ },
3554
+ {
3555
+ name: "get_contact_workload",
3556
+ description: "Get a comprehensive workload summary for a contact: entities/companies they manage, active tasks assigned to them, overdue tasks, pending applications they're linked to, days since last contact, org memberships with specializations. Essential for checking if a key contact is overloaded or responsive.",
3557
+ inputSchema: {
3558
+ type: "object",
3559
+ properties: {
3560
+ contact_id: { type: "string" }
3561
+ },
3562
+ required: ["contact_id"]
3563
+ }
3564
+ },
3565
+ {
3566
+ name: "list_overdue_contact_tasks",
3567
+ description: "List all contact tasks that are past their deadline and not yet completed/cancelled. Includes escalation rules so agents know who to escalate to.",
3568
+ inputSchema: {
3569
+ type: "object",
3570
+ properties: {}
3571
+ }
3572
+ },
3573
+ {
3574
+ name: "check_escalations",
3575
+ description: "Check all contact tasks with escalation rules to see which ones should be escalated now based on days overdue vs escalation thresholds. Returns tasks with the contact to escalate to.",
3576
+ inputSchema: {
3577
+ type: "object",
3578
+ properties: {}
3579
+ }
3580
+ },
3581
+ {
3582
+ name: "add_org_member",
3583
+ description: "Add a contact as an org member of a company (employee, team member) with optional title, specialization, office phone, SLA hours, and notes.",
3584
+ inputSchema: {
3585
+ type: "object",
3586
+ properties: {
3587
+ company_id: { type: "string" },
3588
+ contact_id: { type: "string" },
3589
+ title: { type: "string" },
3590
+ specialization: { type: "string" },
3591
+ office_phone: { type: "string" },
3592
+ response_sla_hours: { type: "number" },
3593
+ notes: { type: "string" }
3594
+ },
3595
+ required: ["company_id", "contact_id"]
3596
+ }
3597
+ },
3598
+ {
3599
+ name: "list_org_members",
3600
+ description: "List all org members (contacts) of a company.",
3601
+ inputSchema: {
3602
+ type: "object",
3603
+ properties: {
3604
+ company_id: { type: "string" }
3605
+ },
3606
+ required: ["company_id"]
3607
+ }
3608
+ },
3609
+ {
3610
+ name: "update_org_member",
3611
+ description: "Update an org member's title, specialization, office phone, SLA hours, or notes.",
3612
+ inputSchema: {
3613
+ type: "object",
3614
+ properties: {
3615
+ id: { type: "string" },
3616
+ title: { type: "string" },
3617
+ specialization: { type: "string" },
3618
+ office_phone: { type: "string" },
3619
+ response_sla_hours: { type: "number" },
3620
+ notes: { type: "string" }
3621
+ },
3622
+ required: ["id"]
3623
+ }
3624
+ },
3625
+ {
3626
+ name: "remove_org_member",
3627
+ description: "Remove a contact from a company's org members by org member record ID.",
3628
+ inputSchema: {
3629
+ type: "object",
3630
+ properties: {
3631
+ id: { type: "string" }
3632
+ },
3633
+ required: ["id"]
3634
+ }
3635
+ },
3636
+ {
3637
+ name: "log_vendor_communication",
3638
+ description: "Log a vendor communication (email, invoice request, call, payment, etc.) for a company. Track invoice amounts, references, follow-up dates, and status.",
3639
+ inputSchema: {
3640
+ type: "object",
3641
+ properties: {
3642
+ company_id: { type: "string" },
3643
+ contact_id: { type: "string" },
3644
+ comm_date: { type: "string", description: "ISO date (defaults to today)" },
3645
+ type: { type: "string", enum: ["email", "call", "meeting", "invoice_request", "invoice_received", "payment", "dispute", "other"] },
3646
+ direction: { type: "string", enum: ["inbound", "outbound"] },
3647
+ subject: { type: "string" },
3648
+ body: { type: "string" },
3649
+ status: { type: "string", enum: ["sent", "awaiting_response", "responded", "no_response", "resolved"] },
3650
+ invoice_amount: { type: "number" },
3651
+ invoice_currency: { type: "string" },
3652
+ invoice_ref: { type: "string" },
3653
+ follow_up_date: { type: "string", description: "ISO date for follow-up (YYYY-MM-DD)" }
3654
+ },
3655
+ required: ["company_id", "type"]
3656
+ }
3657
+ },
3658
+ {
3659
+ name: "list_vendor_communications",
3660
+ description: "List vendor communications for a company, optionally filtered by type or status.",
3661
+ inputSchema: {
3662
+ type: "object",
3663
+ properties: {
3664
+ company_id: { type: "string" },
3665
+ type: { type: "string", enum: ["email", "call", "meeting", "invoice_request", "invoice_received", "payment", "dispute", "other"] },
3666
+ status: { type: "string", enum: ["sent", "awaiting_response", "responded", "no_response", "resolved"] }
3667
+ },
3668
+ required: ["company_id"]
3669
+ }
3670
+ },
3671
+ {
3672
+ name: "list_missing_invoices",
3673
+ description: "List all invoice_request communications with no_response or awaiting_response status \u2014 vendors who haven't sent invoices yet.",
3674
+ inputSchema: {
3675
+ type: "object",
3676
+ properties: {}
3677
+ }
3678
+ },
3679
+ {
3680
+ name: "list_pending_followups",
3681
+ description: "List all vendor communications with follow_up_date on or before today that haven't been marked done.",
3682
+ inputSchema: {
3683
+ type: "object",
3684
+ properties: {}
3685
+ }
3686
+ },
3687
+ {
3688
+ name: "mark_followup_done",
3689
+ description: "Mark a vendor communication follow-up as done.",
3690
+ inputSchema: {
3691
+ type: "object",
3692
+ properties: {
3693
+ id: { type: "string" }
3694
+ },
3695
+ required: ["id"]
3696
+ }
3697
+ },
3698
+ {
3699
+ name: "create_contact_task",
3700
+ description: "Create a task assigned to a contact, with optional deadline, priority, entity link, escalation rules, and todos task link.",
3701
+ inputSchema: {
3702
+ type: "object",
3703
+ properties: {
3704
+ title: { type: "string" },
3705
+ contact_id: { type: "string" },
3706
+ description: { type: "string" },
3707
+ assigned_by: { type: "string" },
3708
+ deadline: { type: "string", description: "ISO date (YYYY-MM-DD)" },
3709
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
3710
+ entity_id: { type: "string", description: "Company/entity ID this task is for" },
3711
+ escalation_rules: { type: "array", items: { type: "object", properties: { after_days: { type: "number" }, escalate_to_contact_id: { type: "string" }, method: { type: "string", enum: ["email", "note", "both"] } }, required: ["after_days", "escalate_to_contact_id", "method"] } },
3712
+ linked_todos_task_id: { type: "string" }
3713
+ },
3714
+ required: ["title", "contact_id"]
3715
+ }
3716
+ },
3717
+ {
3718
+ name: "list_contact_tasks",
3719
+ description: "List contact tasks, optionally filtered by contact, entity, status, or priority.",
3720
+ inputSchema: {
3721
+ type: "object",
3722
+ properties: {
3723
+ contact_id: { type: "string" },
3724
+ entity_id: { type: "string" },
3725
+ status: { type: "string", enum: ["pending", "awaiting_response", "in_progress", "completed", "cancelled", "escalated"] },
3726
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] }
3727
+ }
3728
+ }
3729
+ },
3730
+ {
3731
+ name: "update_contact_task",
3732
+ description: "Update a contact task's title, status, deadline, priority, description, or escalation rules.",
3733
+ inputSchema: {
3734
+ type: "object",
3735
+ properties: {
3736
+ id: { type: "string" },
3737
+ title: { type: "string" },
3738
+ status: { type: "string", enum: ["pending", "awaiting_response", "in_progress", "completed", "cancelled", "escalated"] },
3739
+ deadline: { type: "string" },
3740
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
3741
+ description: { type: "string" },
3742
+ escalation_rules: { type: "array", items: { type: "object" } }
3743
+ },
3744
+ required: ["id"]
3745
+ }
3746
+ },
3747
+ {
3748
+ name: "delete_contact_task",
3749
+ description: "Delete a contact task by ID.",
3750
+ inputSchema: {
3751
+ type: "object",
3752
+ properties: {
3753
+ id: { type: "string" }
3754
+ },
3755
+ required: ["id"]
3756
+ }
3757
+ },
3758
+ {
3759
+ name: "create_application",
3760
+ description: "Create an application record (grant, AI credits, startup program, visa, trademark, tax filing, etc.).",
3761
+ inputSchema: {
3762
+ type: "object",
3763
+ properties: {
3764
+ program_name: { type: "string" },
3765
+ type: { type: "string", enum: ["ai_credits", "grant", "startup_program", "visa", "trademark", "tax_filing", "loan", "other"] },
3766
+ value_usd: { type: "number" },
3767
+ provider_company_id: { type: "string" },
3768
+ primary_contact_id: { type: "string" },
3769
+ status: { type: "string", enum: ["draft", "submitted", "pending", "approved", "rejected", "follow_up_needed", "expired", "cancelled"] },
3770
+ submitted_date: { type: "string" },
3771
+ follow_up_date: { type: "string" },
3772
+ notes: { type: "string" },
3773
+ method: { type: "string", enum: ["email", "form", "typeform", "hubspot", "manual", "browser", "feathery", "other"] },
3774
+ form_url: { type: "string" }
3775
+ },
3776
+ required: ["program_name"]
3777
+ }
3778
+ },
3779
+ {
3780
+ name: "list_applications",
3781
+ description: "List applications, optionally filtered by type, status, or provider company.",
3782
+ inputSchema: {
3783
+ type: "object",
3784
+ properties: {
3785
+ type: { type: "string", enum: ["ai_credits", "grant", "startup_program", "visa", "trademark", "tax_filing", "loan", "other"] },
3786
+ status: { type: "string", enum: ["draft", "submitted", "pending", "approved", "rejected", "follow_up_needed", "expired", "cancelled"] },
3787
+ provider_company_id: { type: "string" }
3788
+ }
3789
+ }
3790
+ },
3791
+ {
3792
+ name: "update_application",
3793
+ description: "Update an application's program name, status, dates, value, notes, or other fields.",
3794
+ inputSchema: {
3795
+ type: "object",
3796
+ properties: {
3797
+ id: { type: "string" },
3798
+ program_name: { type: "string" },
3799
+ type: { type: "string", enum: ["ai_credits", "grant", "startup_program", "visa", "trademark", "tax_filing", "loan", "other"] },
3800
+ value_usd: { type: "number" },
3801
+ provider_company_id: { type: "string" },
3802
+ primary_contact_id: { type: "string" },
3803
+ status: { type: "string", enum: ["draft", "submitted", "pending", "approved", "rejected", "follow_up_needed", "expired", "cancelled"] },
3804
+ submitted_date: { type: "string" },
3805
+ decision_date: { type: "string" },
3806
+ follow_up_date: { type: "string" },
3807
+ notes: { type: "string" },
3808
+ method: { type: "string" },
3809
+ form_url: { type: "string" }
3810
+ },
3811
+ required: ["id"]
3812
+ }
3813
+ },
3814
+ {
3815
+ name: "get_followup_due_applications",
3816
+ description: "List applications with follow_up_date on or before today that haven't been completed or cancelled.",
3817
+ inputSchema: {
3818
+ type: "object",
3819
+ properties: {}
3820
+ }
3821
+ },
3822
+ {
3823
+ name: "list_owned_entities",
3824
+ description: "List all companies marked as owned entities (is_owned_entity=true) \u2014 your legal entities, subsidiaries, and operating companies.",
3825
+ inputSchema: {
3826
+ type: "object",
3827
+ properties: {}
3828
+ }
3829
+ },
3830
+ {
3831
+ name: "get_entity_team",
3832
+ description: "Get the full team for an owned entity \u2014 all contacts linked via company_relationships, with their roles and primary status.",
3833
+ inputSchema: {
3834
+ type: "object",
3835
+ properties: {
3836
+ company_id: { type: "string" }
3837
+ },
3838
+ required: ["company_id"]
3839
+ }
3840
+ }
3841
+ ]
3842
+ }));
3843
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3844
+ const { name, arguments: args } = request.params;
3845
+ const a = args ?? {};
3846
+ try {
3847
+ switch (name) {
3848
+ case "create_contact": {
3849
+ const input = {
3850
+ first_name: a.first_name,
3851
+ last_name: a.last_name,
3852
+ display_name: a.display_name,
3853
+ nickname: a.nickname,
3854
+ job_title: a.job_title,
3855
+ company_id: a.company_id,
3856
+ notes: a.notes,
3857
+ birthday: a.birthday,
3858
+ website: a.website,
2776
3859
  last_contacted_at: a.last_contacted_at,
2777
3860
  preferred_contact_method: a.preferred_contact_method,
2778
3861
  status: a.status,
@@ -3113,11 +4196,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3113
4196
  return { content: [{ type: "text", text: JSON.stringify({ contact: created, action: "created" }, null, 2) }] };
3114
4197
  }
3115
4198
  case "add_note": {
3116
- const noteObj = addNote(a.contact_id, a.note, a.created_by);
4199
+ const noteObj = addNote(a.contact_id, a.note, a.created_by, undefined, a.company_id);
3117
4200
  return { content: [{ type: "text", text: JSON.stringify(noteObj, null, 2) }] };
3118
4201
  }
3119
4202
  case "list_notes": {
3120
- const notes = listNotes(a.contact_id);
4203
+ const db = getDatabase();
4204
+ const companyId = a.company_id;
4205
+ const notes = companyId ? listNotesForContactAtCompany(a.contact_id, companyId, db) : listNotes(a.contact_id);
3121
4206
  return { content: [{ type: "text", text: JSON.stringify(notes, null, 2) }] };
3122
4207
  }
3123
4208
  case "delete_note": {
@@ -3320,7 +4405,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3320
4405
  contact_id: a.contact_id,
3321
4406
  company_id: a.company_id,
3322
4407
  relationship_type: a.relationship_type,
3323
- notes: a.notes
4408
+ notes: a.notes,
4409
+ start_date: a.start_date,
4410
+ end_date: a.end_date,
4411
+ is_primary: a.is_primary,
4412
+ status: a.status
3324
4413
  });
3325
4414
  return { content: [{ type: "text", text: JSON.stringify(rel, null, 2) }] };
3326
4415
  }
@@ -3384,14 +4473,300 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3384
4473
  }]
3385
4474
  };
3386
4475
  }
4476
+ case "sync_from_google_contacts": {
4477
+ const db = getDatabase();
4478
+ const googleProfile = a.google_profile;
4479
+ const inputs = await pullGoogleContactsAsInputs({
4480
+ query: a.query,
4481
+ page_size: a.page_size,
4482
+ profile: googleProfile ?? "default"
4483
+ });
4484
+ const enriched = inputs.map((inp) => ({
4485
+ ...inp,
4486
+ ...a.tag_ids ? { tag_ids: a.tag_ids } : {},
4487
+ ...a.project_id ? { project_id: a.project_id } : {}
4488
+ }));
4489
+ if (a.dry_run) {
4490
+ return {
4491
+ content: [{
4492
+ type: "text",
4493
+ text: JSON.stringify({ dry_run: true, would_import: enriched.length, contacts: enriched }, null, 2)
4494
+ }]
4495
+ };
4496
+ }
4497
+ let imported = 0;
4498
+ let skipped = 0;
4499
+ const syncErrors = [];
4500
+ for (const input of enriched) {
4501
+ const primaryEmail = input.emails?.[0]?.address;
4502
+ if (!primaryEmail) {
4503
+ skipped++;
4504
+ continue;
4505
+ }
4506
+ try {
4507
+ const existing = db.prepare(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(primaryEmail);
4508
+ if (existing) {
4509
+ skipped++;
4510
+ continue;
4511
+ }
4512
+ createContact(input);
4513
+ imported++;
4514
+ } catch (err) {
4515
+ syncErrors.push(`${primaryEmail}: ${err instanceof Error ? err.message : String(err)}`);
4516
+ }
4517
+ }
4518
+ return {
4519
+ content: [{
4520
+ type: "text",
4521
+ text: JSON.stringify({ imported, skipped, errors: syncErrors.length, error_details: syncErrors }, null, 2)
4522
+ }]
4523
+ };
4524
+ }
4525
+ case "push_contact_to_google": {
4526
+ const contact = getContact(a.contact_id);
4527
+ const result = await pushContactToGoogle(contact, {
4528
+ profile: a.google_profile ?? "default",
4529
+ update_existing: a.update_existing
4530
+ });
4531
+ return { content: [{ type: "text", text: JSON.stringify({ ...result, contact_id: contact.id }, null, 2) }] };
4532
+ }
4533
+ case "search_google_contacts": {
4534
+ const people = await searchGoogleContacts(a.query, {
4535
+ profile: a.google_profile ?? "default"
4536
+ });
4537
+ const mapped = people.map((p) => ({
4538
+ google: p,
4539
+ as_contact_input: googlePersonToContactInput(p)
4540
+ }));
4541
+ return { content: [{ type: "text", text: JSON.stringify(mapped, null, 2) }] };
4542
+ }
4543
+ case "get_contact_workload": {
4544
+ const db = getDatabase();
4545
+ const { contact_id } = a;
4546
+ const contact = getContact(contact_id);
4547
+ const companyRels = listCompanyRelationships({ contact_id }, db);
4548
+ const activeTasks = listContactTasks({ contact_id, status: "pending" }, db);
4549
+ const overdueTasks = listOverdueTasks(db).filter((t) => t.contact_id === contact_id);
4550
+ const orgMemberships = listOrgMembersForContact(contact_id, db);
4551
+ const daysSince = contact.last_contacted_at ? Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000) : null;
4552
+ return {
4553
+ content: [{
4554
+ type: "text",
4555
+ text: JSON.stringify({
4556
+ contact,
4557
+ company_relationships: companyRels,
4558
+ active_tasks: activeTasks,
4559
+ overdue_tasks: overdueTasks,
4560
+ org_memberships: orgMemberships,
4561
+ days_since_last_contact: daysSince,
4562
+ total_entities_managed: companyRels.length
4563
+ }, null, 2)
4564
+ }]
4565
+ };
4566
+ }
4567
+ case "list_overdue_contact_tasks": {
4568
+ const db = getDatabase();
4569
+ const overdue = listOverdueTasks(db);
4570
+ return { content: [{ type: "text", text: JSON.stringify(overdue, null, 2) }] };
4571
+ }
4572
+ case "check_escalations": {
4573
+ const db = getDatabase();
4574
+ const escalations = checkEscalations(db);
4575
+ return { content: [{ type: "text", text: JSON.stringify(escalations, null, 2) }] };
4576
+ }
4577
+ case "add_org_member": {
4578
+ const db = getDatabase();
4579
+ const input = {
4580
+ company_id: a.company_id,
4581
+ contact_id: a.contact_id,
4582
+ title: a.title,
4583
+ specialization: a.specialization,
4584
+ office_phone: a.office_phone,
4585
+ response_sla_hours: a.response_sla_hours,
4586
+ notes: a.notes
4587
+ };
4588
+ const member = addOrgMember(input, db);
4589
+ return { content: [{ type: "text", text: JSON.stringify(member, null, 2) }] };
4590
+ }
4591
+ case "list_org_members": {
4592
+ const db = getDatabase();
4593
+ const members = listOrgMembers(a.company_id, db);
4594
+ return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }] };
4595
+ }
4596
+ case "update_org_member": {
4597
+ const db = getDatabase();
4598
+ const { id: memberId, ...memberRest } = a;
4599
+ const input = {
4600
+ title: memberRest.title,
4601
+ specialization: memberRest.specialization,
4602
+ office_phone: memberRest.office_phone,
4603
+ response_sla_hours: memberRest.response_sla_hours,
4604
+ notes: memberRest.notes
4605
+ };
4606
+ const updated = updateOrgMember(memberId, input, db);
4607
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
4608
+ }
4609
+ case "remove_org_member": {
4610
+ const db = getDatabase();
4611
+ removeOrgMember(a.id, db);
4612
+ return { content: [{ type: "text", text: JSON.stringify({ deleted: true }) }] };
4613
+ }
4614
+ case "log_vendor_communication": {
4615
+ const db = getDatabase();
4616
+ const input = {
4617
+ company_id: a.company_id,
4618
+ contact_id: a.contact_id,
4619
+ comm_date: a.comm_date ?? new Date().toISOString().slice(0, 10),
4620
+ type: a.type,
4621
+ direction: a.direction,
4622
+ subject: a.subject,
4623
+ body: a.body,
4624
+ status: a.status,
4625
+ invoice_amount: a.invoice_amount,
4626
+ invoice_currency: a.invoice_currency,
4627
+ invoice_ref: a.invoice_ref,
4628
+ follow_up_date: a.follow_up_date
4629
+ };
4630
+ const comm = logVendorCommunication(input, db);
4631
+ return { content: [{ type: "text", text: JSON.stringify(comm, null, 2) }] };
4632
+ }
4633
+ case "list_vendor_communications": {
4634
+ const db = getDatabase();
4635
+ const comms = listVendorCommunications(a.company_id, {
4636
+ type: a.type,
4637
+ status: a.status
4638
+ }, db);
4639
+ return { content: [{ type: "text", text: JSON.stringify(comms, null, 2) }] };
4640
+ }
4641
+ case "list_missing_invoices": {
4642
+ const db = getDatabase();
4643
+ const missing = listMissingInvoices(db);
4644
+ return { content: [{ type: "text", text: JSON.stringify(missing, null, 2) }] };
4645
+ }
4646
+ case "list_pending_followups": {
4647
+ const db = getDatabase();
4648
+ const pending = listPendingFollowUps(db);
4649
+ return { content: [{ type: "text", text: JSON.stringify(pending, null, 2) }] };
4650
+ }
4651
+ case "mark_followup_done": {
4652
+ const db = getDatabase();
4653
+ const updated = markFollowUpDone(a.id, db);
4654
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
4655
+ }
4656
+ case "create_contact_task": {
4657
+ const db = getDatabase();
4658
+ const input = {
4659
+ title: a.title,
4660
+ contact_id: a.contact_id,
4661
+ description: a.description,
4662
+ assigned_by: a.assigned_by,
4663
+ deadline: a.deadline,
4664
+ priority: a.priority,
4665
+ entity_id: a.entity_id,
4666
+ escalation_rules: a.escalation_rules,
4667
+ linked_todos_task_id: a.linked_todos_task_id
4668
+ };
4669
+ const task = createContactTask(input, db);
4670
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
4671
+ }
4672
+ case "list_contact_tasks": {
4673
+ const db = getDatabase();
4674
+ const tasks = listContactTasks({
4675
+ contact_id: a.contact_id,
4676
+ entity_id: a.entity_id,
4677
+ status: a.status,
4678
+ priority: a.priority
4679
+ }, db);
4680
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
4681
+ }
4682
+ case "update_contact_task": {
4683
+ const db = getDatabase();
4684
+ const { id: taskId, ...taskRest } = a;
4685
+ const input = {
4686
+ title: taskRest.title,
4687
+ status: taskRest.status,
4688
+ deadline: taskRest.deadline,
4689
+ priority: taskRest.priority,
4690
+ description: taskRest.description,
4691
+ escalation_rules: taskRest.escalation_rules
4692
+ };
4693
+ const updated = updateContactTask(taskId, input, db);
4694
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
4695
+ }
4696
+ case "delete_contact_task": {
4697
+ const db = getDatabase();
4698
+ deleteContactTask(a.id, db);
4699
+ return { content: [{ type: "text", text: JSON.stringify({ deleted: true }) }] };
4700
+ }
4701
+ case "create_application": {
4702
+ const db = getDatabase();
4703
+ const input = {
4704
+ program_name: a.program_name,
4705
+ type: a.type,
4706
+ value_usd: a.value_usd,
4707
+ provider_company_id: a.provider_company_id,
4708
+ primary_contact_id: a.primary_contact_id,
4709
+ status: a.status,
4710
+ submitted_date: a.submitted_date,
4711
+ follow_up_date: a.follow_up_date,
4712
+ notes: a.notes,
4713
+ method: a.method,
4714
+ form_url: a.form_url
4715
+ };
4716
+ const app = createApplication(input, db);
4717
+ return { content: [{ type: "text", text: JSON.stringify(app, null, 2) }] };
4718
+ }
4719
+ case "list_applications": {
4720
+ const db = getDatabase();
4721
+ const apps = listApplications({
4722
+ type: a.type,
4723
+ status: a.status,
4724
+ provider_company_id: a.provider_company_id
4725
+ }, db);
4726
+ return { content: [{ type: "text", text: JSON.stringify(apps, null, 2) }] };
4727
+ }
4728
+ case "update_application": {
4729
+ const db = getDatabase();
4730
+ const { id: appId, ...appRest } = a;
4731
+ const input = {
4732
+ program_name: appRest.program_name,
4733
+ type: appRest.type,
4734
+ value_usd: appRest.value_usd,
4735
+ provider_company_id: appRest.provider_company_id,
4736
+ primary_contact_id: appRest.primary_contact_id,
4737
+ status: appRest.status,
4738
+ submitted_date: appRest.submitted_date,
4739
+ decision_date: appRest.decision_date,
4740
+ follow_up_date: appRest.follow_up_date,
4741
+ notes: appRest.notes,
4742
+ method: appRest.method,
4743
+ form_url: appRest.form_url
4744
+ };
4745
+ const updated = updateApplication(appId, input, db);
4746
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
4747
+ }
4748
+ case "get_followup_due_applications": {
4749
+ const db = getDatabase();
4750
+ const apps = listFollowUpDue(db);
4751
+ return { content: [{ type: "text", text: JSON.stringify(apps, null, 2) }] };
4752
+ }
4753
+ case "list_owned_entities": {
4754
+ const result = listCompanies({ limit: 200 });
4755
+ const owned = result.companies.filter((c) => c.is_owned_entity);
4756
+ return { content: [{ type: "text", text: JSON.stringify(owned, null, 2) }] };
4757
+ }
4758
+ case "get_entity_team": {
4759
+ const db = getDatabase();
4760
+ const company = getCompany(a.company_id);
4761
+ const team = listCompanyRelationships({ company_id: a.company_id }, db);
4762
+ return { content: [{ type: "text", text: JSON.stringify({ company, team }, null, 2) }] };
4763
+ }
3387
4764
  default:
3388
4765
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
3389
4766
  }
3390
4767
  } catch (err) {
3391
- return {
3392
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3393
- isError: true
3394
- };
4768
+ const msg = err instanceof ConnectorNotInstalledError || err instanceof ConnectorAuthError ? err.message : `Error: ${err instanceof Error ? err.message : String(err)}`;
4769
+ return { content: [{ type: "text", text: msg }], isError: true };
3395
4770
  }
3396
4771
  });
3397
4772
  async function main() {