@trylighthouse/mcp-server 0.2.15 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/build/server.js +143 -4
  2. package/package.json +2 -2
package/build/server.js CHANGED
@@ -81,11 +81,15 @@ const ADDRESS_INPUT = z.union([
81
81
  }),
82
82
  ]);
83
83
  const RECIPIENTS = z.union([ADDRESS_INPUT, z.array(ADDRESS_INPUT)]);
84
+ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
85
+ const CREATES = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
86
+ const ADDS_IF_MISSING = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
87
+ const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
84
88
  export function createServer(apiKey, options = {}) {
85
89
  const api = new LighthouseAPI(apiKey);
86
90
  const server = new McpServer({
87
91
  name: "lighthouse",
88
- version: "0.2.15",
92
+ version: "0.2.17",
89
93
  });
90
94
  const originalRegister = server.registerTool.bind(server);
91
95
  server.registerTool = (name, schemaOrConfig, handler) => {
@@ -259,6 +263,7 @@ export function createServer(apiKey, options = {}) {
259
263
  "(an outer AND of two OR groups; country codes from list_locations).";
260
264
  server.registerTool("lighthouse_crm_list_attributes", {
261
265
  title: "List CRM Attributes",
266
+ annotations: READ_ONLY,
262
267
  description: "Get a lightweight catalog of the fields for a CRM record type: each field's permalink, label, data_type, links_to, read_only, is_custom, has_options, source_field and list_id. " +
263
268
  "`source_field` names the status attribute a `duration` attribute is derived from (null on every other attribute); such attributes (time_in_stage on deals, time_in_status on tasks, time_in_<status> beside a custom status field) are read-only and computed, never written. " +
264
269
  "ALWAYS call this before building filters for search_records / list_records or creating/updating views. Use the exact permalinks it returns. " +
@@ -292,6 +297,7 @@ export function createServer(apiKey, options = {}) {
292
297
  });
293
298
  server.registerTool("lighthouse_crm_get_attribute", {
294
299
  title: "Get CRM Attribute",
300
+ annotations: READ_ONLY,
295
301
  description: "Get a single custom field definition. Response returns 'permalink', 'is_custom', 'data_type', 'links_to'. " +
296
302
  "For custom objects, use the object's permalink as record_type (e.g. 'lp').",
297
303
  inputSchema: {
@@ -309,6 +315,7 @@ export function createServer(apiKey, options = {}) {
309
315
  });
310
316
  server.registerTool("lighthouse_crm_create_attribute", {
311
317
  title: "Create CRM Attribute",
318
+ annotations: CREATES,
312
319
  description: "Create a new custom field in your CRM workspace. " +
313
320
  "Types: text, number, currency, percentage, date, datetimez, select, multi_select, status, checkbox, url, email, phone, rating, record, multi_record, user, multi_user. " +
314
321
  "Supports custom objects, use the object's permalink as record_type (e.g. 'lp'). " +
@@ -360,6 +367,7 @@ export function createServer(apiKey, options = {}) {
360
367
  });
361
368
  server.registerTool("lighthouse_crm_update_attribute", {
362
369
  title: "Update CRM Attribute",
370
+ annotations: DESTRUCTIVE,
363
371
  description: "Update a custom field's display label (the field type, linked record type, and options are fixed after creation). " +
364
372
  "For custom objects, use the object's permalink as record_type.",
365
373
  inputSchema: {
@@ -379,6 +387,7 @@ export function createServer(apiKey, options = {}) {
379
387
  });
380
388
  server.registerTool("lighthouse_crm_delete_attribute", {
381
389
  title: "Delete CRM Attribute",
390
+ annotations: DESTRUCTIVE,
382
391
  description: "Delete a custom field and all its data. Only custom fields can be deleted, not system fields. " +
383
392
  "For custom objects, use the object's permalink as record_type.",
384
393
  inputSchema: {
@@ -396,6 +405,7 @@ export function createServer(apiKey, options = {}) {
396
405
  });
397
406
  server.registerTool("lighthouse_crm_list_colors", {
398
407
  title: "List Colors",
408
+ annotations: READ_ONLY,
399
409
  description: "Get the available color palette for select/multi_select/status field options. " +
400
410
  "Returns an array of {hex_code, label} objects. " +
401
411
  "Use this to discover valid hex color codes when creating or updating select options.",
@@ -411,6 +421,7 @@ export function createServer(apiKey, options = {}) {
411
421
  });
412
422
  server.registerTool("lighthouse_crm_list_options", {
413
423
  title: "List CRM Field Options",
424
+ annotations: READ_ONLY,
414
425
  description: "Get the allowed values for a CRM select / multi_select field. Each row is { value, label, color, priority, is_default, enabled }, the same object the options[] of an attribute carries; a built-in value the workspace switched off is listed with enabled: false rather than left out, so do not offer it when setting a field. " +
415
426
  "Call this when lighthouse_crm_list_attributes shows has_options=true for a field and you need its values before filtering or setting it. " +
416
427
  "Results are paginated (max 100 per page): use `search` to narrow by label, and `limit` / `offset` to page through a field with many options. " +
@@ -434,6 +445,7 @@ export function createServer(apiKey, options = {}) {
434
445
  });
435
446
  server.registerTool("lighthouse_crm_create_option", {
436
447
  title: "Create CRM Field Option",
448
+ annotations: CREATES,
437
449
  description: "Add a new option to a select/multi_select field. The option value is auto-generated from the label. " +
438
450
  "Re-creating a system default option that was previously removed restores it (its original color and label are kept). " +
439
451
  "Returns the updated list of ALL options for the field, each row { value, label, color, priority, is_default, enabled }, with a switched-off built-in value listed as enabled: false, plus meta.total. The list comes back in the order the field offers its options, so do not read the first row as the option you just created: find it by the label you sent. " +
@@ -457,6 +469,7 @@ export function createServer(apiKey, options = {}) {
457
469
  });
458
470
  server.registerTool("lighthouse_crm_delete_option", {
459
471
  title: "Delete CRM Field Option",
472
+ annotations: DESTRUCTIVE,
460
473
  description: "Delete an option value from a select/multi_select field. Custom options are removed permanently; a system default option is disabled for the workspace instead (re-create it to restore). " +
461
474
  "For custom objects, use the object's permalink as record_type.",
462
475
  inputSchema: {
@@ -475,6 +488,7 @@ export function createServer(apiKey, options = {}) {
475
488
  });
476
489
  server.registerTool("lighthouse_crm_search_records", {
477
490
  title: "Search CRM Records",
491
+ annotations: READ_ONLY,
478
492
  description: "Search and filter records in YOUR CRM workspace (companies, people, or deals you've added). " +
479
493
  "Supports filters, sorting, pagination, and saved views. " +
480
494
  "A search by MEANING over people or company records is a semantic_search condition in filters; it needs the ready vector of a saved view (lighthouse_crm_get_view) or of a condition copied from the app, never text and never a vector you wrote (the filters description has the rule). " +
@@ -520,6 +534,7 @@ export function createServer(apiKey, options = {}) {
520
534
  });
521
535
  server.registerTool("lighthouse_crm_get_record", {
522
536
  title: "Get CRM Record",
537
+ annotations: READ_ONLY,
523
538
  description: "Get a single CRM record by ID with all fields and relationships. Custom fields are returned as top-level keys, not nested under an attributes object. " +
524
539
  "Signal fields (company_signals on companies, person_signals on people) are arrays of signal objects, one per signal: { signal (the value), picked_up_on (ISO 8601 date, the day the feed picked it up), occurrences (the number of windows or registrations behind it, 1 otherwise), and what the feed knows: " +
525
540
  "for a company measured_on, timeframe, change_percent, value (the metric's value on measured_on, from the most recently measured window), windows (every window, each its own snapshot: [{timeframe, change_percent, value, measured_on, picked_up_on}]), money_raised_usd, investment_type, announced_on, investment_stage, num_investors, pre_money_valuation_usd; " +
@@ -546,6 +561,7 @@ export function createServer(apiKey, options = {}) {
546
561
  });
547
562
  server.registerTool("lighthouse_crm_list_record_activity", {
548
563
  title: "List Record Activity",
564
+ annotations: READ_ONLY,
549
565
  description: "Read the activity feed of one CRM record, newest first: who changed what, when, and from where. Use it to answer questions about history (\"what changed on this deal last week\", \"who moved it to won\", \"which fields has nobody touched\") instead of guessing from the record's current values. " +
550
566
  "Each entry has: action (created / updated / deleted / merged), occurred_at, summary (a ready-made sentence, ending \"via <the source>\" when a workflow or an API key made the change rather than a person, since both log a human as the actor), target ({ record_type, id, name }; the feed also carries the notes and tasks attached to the record, so the target is not always the record itself), actor (null for automated changes), and source. " +
551
567
  "source always has { type, label }: type is app / mobile / api / mcp / workflow / ai_assistant / notetaker / email_sync / calendar_sync / linkedin_sync / inbound_email / inbound_whatsapp / scout / lp_portal / sync, and label names the exact origin. The four trace keys are always present and null when the origin did not record them: workflow is { id, name, run_id, node_id } when a workflow made the change (tracing an entry back to the exact run and step), api_key_id / mcp_tool identify the key or AI tool behind an API change, and trigger is automatic or manual where the origin recorded whether a person asked for the change (meeting notes written on their own and notes a member asked for both log that member as the actor, so trigger is what tells them apart). " +
@@ -573,6 +589,7 @@ export function createServer(apiKey, options = {}) {
573
589
  });
574
590
  server.registerTool("lighthouse_crm_list_ai_reports", {
575
591
  title: "List AI Reports on a Record",
592
+ annotations: READ_ONLY,
576
593
  description: "The AI reports generated on one CRM record (any record type: company, person, deal, or a custom object permalink), newest first: the memos, SWOTs, teardowns, pass memos and briefs a member generated from the record's AI Reports tab in Lighthouse. " +
577
594
  "WHEN TO USE: the user asks whether a memo or report already exists on a record, what the last one said, wants to compare or build on an earlier report, or asks about a company's \"memo\" / \"analysis\" / \"write-up\" (check here before writing one from scratch). Not for dashboards or charts (those are lighthouse_reports_*), and nothing here generates a report: that happens in the app. " +
578
595
  "Each row: id, name, status (processing / success / error), depth (short / standard / extensive), template, partial (true when the run hit a cap), summary (one paragraph of the report's own prose), created_by ({ id, first_name, last_name, full_name, avatar_url, email }, the same member object every other row carries), created_at, completed_at. Only a success report has a document: read it with lighthouse_crm_get_ai_report. " +
@@ -596,6 +613,7 @@ export function createServer(apiKey, options = {}) {
596
613
  });
597
614
  server.registerTool("lighthouse_crm_get_ai_report", {
598
615
  title: "Get AI Report",
616
+ annotations: READ_ONLY,
599
617
  description: "One AI report by id (from lighthouse_crm_list_ai_reports): the same metadata plus record_type / record_id and content, the full document as Markdown with its section headings (citation markers removed). " +
600
618
  "WHEN TO USE: the user wants what an earlier memo or report actually says (a section, a number, a recommendation), or a comparison with it. A report can be long: quote or summarise the part the question is about rather than echoing it whole. " +
601
619
  "content is null while the report is still processing or failed. Answers 403 naming the channel when the report draws on a shared mailbox, calendar, meeting notes, LinkedIn account or document the API key's user cannot read, or on " + REPORT_ROLE_SOURCES + " (its author is never refused for its sources), and 404 for an unknown id.",
@@ -613,6 +631,7 @@ export function createServer(apiKey, options = {}) {
613
631
  });
614
632
  server.registerTool("lighthouse_crm_create_record", {
615
633
  title: "Create CRM Record",
634
+ annotations: CREATES,
616
635
  description: "Create a new CRM record. Pass all fields (standard + custom) in a single flat data object, " +
617
636
  "the API splits standard vs custom fields automatically. " +
618
637
  "BEFORE calling this tool, call lighthouse_crm_list_attributes to discover available field keys and types for the record_type. " +
@@ -647,6 +666,7 @@ export function createServer(apiKey, options = {}) {
647
666
  });
648
667
  server.registerTool("lighthouse_crm_update_record", {
649
668
  title: "Update CRM Record",
669
+ annotations: DESTRUCTIVE,
650
670
  description: "Update an existing CRM record. Only include the fields you want to change. " +
651
671
  "Standard and custom fields can be mixed in a single flat data object, the API splits them automatically. " +
652
672
  "BEFORE calling this tool, call lighthouse_crm_list_attributes to discover available field keys and types. " +
@@ -684,6 +704,7 @@ export function createServer(apiKey, options = {}) {
684
704
  });
685
705
  server.registerTool("lighthouse_crm_upsert_record", {
686
706
  title: "Upsert CRM Record",
707
+ annotations: DESTRUCTIVE,
687
708
  description: "Find-or-create a CRM record by a UNIQUE attribute: if a record already matches the value it is updated, otherwise a new one is created. " +
688
709
  "Only person and company support upsert, person matches on email or linkedin, company on domain or linkedin (set matching_attribute accordingly). " +
689
710
  "Pass the value to match on as `matching_value` (\"jane@acme.com\", \"acme.com\", a LinkedIn address). It may also sit inside data as the attribute itself (emails: [\"jane@acme.com\"], domain: [\"acme.com\"], linkedin: \"...\"), which is what this tool used to require; matching_value wins when both carry it, and a request with neither is refused. A record created this way still stores the value. " +
@@ -711,6 +732,7 @@ export function createServer(apiKey, options = {}) {
711
732
  });
712
733
  server.registerTool("lighthouse_crm_delete_record", {
713
734
  title: "Delete CRM Record",
735
+ annotations: DESTRUCTIVE,
714
736
  description: "Permanently delete a CRM record. For custom objects, use the object's permalink as record_type.",
715
737
  inputSchema: {
716
738
  record_type: z.string().describe("Record type: company, person, deal, or a custom object permalink"),
@@ -727,6 +749,7 @@ export function createServer(apiKey, options = {}) {
727
749
  });
728
750
  server.registerTool("lighthouse_crm_bulk_create_records", {
729
751
  title: "Bulk Create CRM Records",
752
+ annotations: CREATES,
730
753
  description: "Create up to 100 CRM records of the same record_type in one call. " +
731
754
  "Each item in `records` is a flat data object, the SAME shape you'd pass as `data` to lighthouse_crm_create_record. " +
732
755
  "BEFORE calling, call lighthouse_crm_list_attributes to discover field keys and types. " +
@@ -752,6 +775,7 @@ export function createServer(apiKey, options = {}) {
752
775
  });
753
776
  server.registerTool("lighthouse_crm_bulk_update_records", {
754
777
  title: "Bulk Update CRM Records",
778
+ annotations: DESTRUCTIVE,
755
779
  description: "Update up to 100 CRM records of the same record_type in one call. " +
756
780
  "Each item in `records` is { id, data }, `id` is the record UUID and `data` is a flat object of the fields to change " +
757
781
  "(same shape as lighthouse_crm_update_record). " +
@@ -779,6 +803,7 @@ export function createServer(apiKey, options = {}) {
779
803
  });
780
804
  server.registerTool("lighthouse_crm_bulk_upsert_records", {
781
805
  title: "Bulk Upsert CRM Records",
806
+ annotations: DESTRUCTIVE,
782
807
  description: "Find-or-create up to 100 records of the same record_type in one call, each matched on a unique attribute. " +
783
808
  "For each item, if a record already matches the value it is updated, otherwise a new one is created. " +
784
809
  "Person matches on email or linkedin, company on domain or linkedin (set matching_attribute for the whole batch). " +
@@ -808,6 +833,7 @@ export function createServer(apiKey, options = {}) {
808
833
  });
809
834
  server.registerTool("lighthouse_crm_bulk_delete_records", {
810
835
  title: "Bulk Delete CRM Records",
836
+ annotations: DESTRUCTIVE,
811
837
  description: "Permanently delete up to 100 CRM records of the same record_type in one call. " +
812
838
  "Pass `ids` as an array of record UUIDs. " +
813
839
  "Partial success: the response returns a `results` array (one entry per id, in order, with success/error) and a `summary` ({total, succeeded, failed}).",
@@ -826,6 +852,7 @@ export function createServer(apiKey, options = {}) {
826
852
  });
827
853
  server.registerTool("lighthouse_crm_create_list", {
828
854
  title: "Create CRM List",
855
+ annotations: CREATES,
829
856
  description: "Create a new CRM list to organize records. Supports companies, people, deals, and custom objects. " +
830
857
  "Defaults to 'private' visibility. " +
831
858
  "To share with specific teams after creation, use lighthouse_crm_share_list_with_teams. " +
@@ -847,6 +874,7 @@ export function createServer(apiKey, options = {}) {
847
874
  });
848
875
  server.registerTool("lighthouse_crm_list_lists", {
849
876
  title: "List CRM Lists",
877
+ annotations: READ_ONLY,
850
878
  description: "Get all CRM lists in your workspace. Each list's record_type is 'company', 'person', 'deal', or a custom object permalink (e.g. 'lp', 'project'), and that is the record type of everything in it. " +
851
879
  "Each row carries owner (the member who created it) and is_owner (true when that member is the API key's user), so 'my lists' and 'lists shared with me' can be told apart without a second call. " +
852
880
  "WHEN TO USE: to find a list someone names (\"the Deals Reviewed list\"), pass search with the words of its name and read the id off the matching row, instead of paging through every list; leave search out to page through all of them. meta.total counts the lists that matched.",
@@ -867,6 +895,7 @@ export function createServer(apiKey, options = {}) {
867
895
  });
868
896
  server.registerTool("lighthouse_crm_get_list", {
869
897
  title: "Get CRM List",
898
+ annotations: READ_ONLY,
870
899
  description: "Get a single CRM list by ID with its configuration. Its record_type is 'company', 'person', 'deal', or a custom object permalink (e.g. 'lp', 'project'). " +
871
900
  "is_owner says whether the API key's user owns the list, so 'my lists' and 'lists shared with me' can be told apart without a second call. A list you cannot open answers 'List not found', the same as one that does not exist.",
872
901
  inputSchema: {
@@ -884,6 +913,7 @@ export function createServer(apiKey, options = {}) {
884
913
  });
885
914
  server.registerTool("lighthouse_crm_update_list", {
886
915
  title: "Update CRM List",
916
+ annotations: DESTRUCTIVE,
887
917
  description: "Update a CRM list's name or configuration. A list you cannot open answers 'List not found', the same as one that does not exist.",
888
918
  inputSchema: {
889
919
  list_id: z.string().uuid().optional().describe("List ID"),
@@ -904,6 +934,7 @@ export function createServer(apiKey, options = {}) {
904
934
  });
905
935
  server.registerTool("lighthouse_crm_delete_list", {
906
936
  title: "Delete CRM List",
937
+ annotations: DESTRUCTIVE,
907
938
  description: "Permanently delete a CRM list. A list you cannot open answers 'List not found', the same as one that does not exist.",
908
939
  inputSchema: {
909
940
  list_id: z.string().uuid().optional().describe("List ID"),
@@ -920,6 +951,7 @@ export function createServer(apiKey, options = {}) {
920
951
  });
921
952
  server.registerTool("lighthouse_crm_list_list_records", {
922
953
  title: "List Records in a List",
954
+ annotations: READ_ONLY,
923
955
  description: "Get records in a CRM list with optional filtering, sorting, and view support. " +
924
956
  "Same filter format as lighthouse_crm_search_records. To get valid filter keys, call lighthouse_crm_list_attributes with this list's record_type, which is 'company', 'person', 'deal', or a custom object permalink. " +
925
957
  "The response's `meta` includes `total` (an integer match count), `ignored_filter_fields` (any filter key that is not a valid attribute for this list's record type, each with a did_you_mean suggestion when a close match can be computed), `invalid_operators` (a condition whose operator its field does not take, left out of the results), `invalid_logical_operators` (a group joined by something other than AND/OR, read as AND) and `invalid_sort_fields` (a sort this list cannot apply, reported rather than refused, so the records come back in their default order). The saved filters of a view_id are reported on the same way. Check every array to confirm the whole filter and the sort were applied; a value in the wrong shape is refused instead, with a 400. " +
@@ -946,6 +978,7 @@ export function createServer(apiKey, options = {}) {
946
978
  });
947
979
  server.registerTool("lighthouse_crm_add_to_list", {
948
980
  title: "Add Record to List",
981
+ annotations: ADDS_IF_MISSING,
949
982
  description: "Add a CRM record to a list. The record type is inferred from the list, so the record must be of the list's record_type: 'company', 'person', 'deal', or a custom object permalink. Answers `reason` beside the flag: the sentence \"This record was already on the list.\" / \"This record was not on the list.\" when the call changed nothing, and null when it did. Both writes succeed either way, so this key is what tells a no-op from a real change.",
950
983
  inputSchema: {
951
984
  list_id: z.string().uuid().describe("List ID"),
@@ -964,6 +997,7 @@ export function createServer(apiKey, options = {}) {
964
997
  });
965
998
  server.registerTool("lighthouse_crm_remove_from_list", {
966
999
  title: "Remove Record from List",
1000
+ annotations: DESTRUCTIVE,
967
1001
  description: "Remove a record from a CRM list. The record type is inferred from the list, whose record_type is 'company', 'person', 'deal', or a custom object permalink. Answers `reason` beside the flag: the sentence \"This record was already on the list.\" / \"This record was not on the list.\" when the call changed nothing, and null when it did. Both writes succeed either way, so this key is what tells a no-op from a real change.",
968
1002
  inputSchema: {
969
1003
  list_id: z.string().uuid().describe("List ID"),
@@ -980,6 +1014,7 @@ export function createServer(apiKey, options = {}) {
980
1014
  });
981
1015
  server.registerTool("lighthouse_crm_bulk_add_to_list", {
982
1016
  title: "Bulk Add Records to List",
1017
+ annotations: ADDS_IF_MISSING,
983
1018
  description: "Add up to 100 CRM records to a list in one call. The record type is inferred from the list, so every id must be of the list's record_type: 'company', 'person', 'deal', or a custom object permalink. " +
984
1019
  "Partial success: the response returns a `results` array (one entry per id, in order, with success/error) and a `summary` ({total, succeeded, failed}). " +
985
1020
  "A failed id does NOT prevent the others from being added. Every result entry also carries `reason`, ONE key for \"why did this record not change\": the no-op sentence on a success that wrote nothing, the refusal in words on a failure, null when the record really changed. `error` stays beside it for a client that branches on a code.",
@@ -998,6 +1033,7 @@ export function createServer(apiKey, options = {}) {
998
1033
  });
999
1034
  server.registerTool("lighthouse_crm_bulk_remove_from_list", {
1000
1035
  title: "Bulk Remove Records from List",
1036
+ annotations: DESTRUCTIVE,
1001
1037
  description: "Remove up to 100 CRM records from a list in one call. Every id must be of the list's record_type: 'company', 'person', 'deal', or a custom object permalink. " +
1002
1038
  "Partial success: the response returns a `results` array (one entry per id, in order, with success/error) and a `summary` ({total, succeeded, failed}). Every result entry also carries `reason`, ONE key for \"why did this record not change\": the no-op sentence on a success that wrote nothing, the refusal in words on a failure, null when the record really changed. `error` stays beside it for a client that branches on a code.",
1003
1039
  inputSchema: {
@@ -1015,6 +1051,7 @@ export function createServer(apiKey, options = {}) {
1015
1051
  });
1016
1052
  server.registerTool("lighthouse_crm_share_list_with_teams", {
1017
1053
  title: "Share List with Teams",
1054
+ annotations: ADDS_IF_MISSING,
1018
1055
  description: "Grant team-level access to a CRM list. Only the list owner can share. " +
1019
1056
  "Use lighthouse_workspace_list_teams to find team IDs first. " +
1020
1057
  "Requires lists.share permission. " +
@@ -1036,6 +1073,7 @@ export function createServer(apiKey, options = {}) {
1036
1073
  });
1037
1074
  server.registerTool("lighthouse_crm_revoke_list_from_teams", {
1038
1075
  title: "Revoke List Team Access",
1076
+ annotations: DESTRUCTIVE,
1039
1077
  description: "Revoke team-level access from a CRM list. Only the list owner can revoke. " +
1040
1078
  "Requires lists.share permission. " +
1041
1079
  "Returns formatted list: {id, name, record_type, sharing, color, count, owner, is_owner, shared_with_teams, created_at, updated_at}, where record_type is 'company', 'person', 'deal', or a custom object permalink and is_owner says whether the API key's user owns the list.",
@@ -1056,6 +1094,7 @@ export function createServer(apiKey, options = {}) {
1056
1094
  });
1057
1095
  server.registerTool("lighthouse_crm_update_list_sharing", {
1058
1096
  title: "Update List Sharing Status",
1097
+ annotations: DESTRUCTIVE,
1059
1098
  description: "Change a CRM list's sharing visibility. Setting to 'private' also removes all team permissions. " +
1060
1099
  "Only the list owner can change sharing. Requires lists.share permission. " +
1061
1100
  "Returns formatted list: {id, name, record_type, sharing, color, count, owner, is_owner, shared_with_teams, created_at, updated_at}, where record_type is 'company', 'person', 'deal', or a custom object permalink and is_owner says whether the API key's user owns the list.",
@@ -1076,6 +1115,7 @@ export function createServer(apiKey, options = {}) {
1076
1115
  });
1077
1116
  server.registerTool("lighthouse_crm_list_notes", {
1078
1117
  title: "List Notes",
1118
+ annotations: READ_ONLY,
1079
1119
  description: "Get notes for a CRM record. Each note in the response includes a `records` map, every record (company/person/deal/custom object) the note is currently linked to that you can view, plus a `tags` array of any workspace tags attached to the note (each with id, label, color). " +
1080
1120
  LINKED_RECORDS_LEFT_OUT + " " +
1081
1121
  "`tagged_users` is the teammates the note MENTIONS, each the member object this API answers for a person ({ id, first_name, last_name, full_name, avatar_url, email }), and [] when it mentions nobody. It is read from the mention spans in `content`, not from the ids a write sent, so a note written with tagged_users ids and no mention span reads back []: always write both, as lighthouse_crm_create_note and lighthouse_crm_update_note say. " +
@@ -1096,6 +1136,7 @@ export function createServer(apiKey, options = {}) {
1096
1136
  });
1097
1137
  server.registerTool("lighthouse_crm_search_notes", {
1098
1138
  title: "Search Notes",
1139
+ annotations: READ_ONLY,
1099
1140
  description: "Search ALL notes in the workspace (unlike List Notes, which needs one record). Combine a free-text title search with a CRM-style filter tree and sort. " +
1100
1141
  'filters: {"operator":"AND","conditions":[{"field":"tags","operator":"contains_any","value":["<tag_permalink>"]}]}: nested groups supported. ' +
1101
1142
  "Filterable fields and their operators: title (text: contains/not_contains/starts_with/ends_with/eq/neq/empty/not_empty), " +
@@ -1125,6 +1166,7 @@ export function createServer(apiKey, options = {}) {
1125
1166
  });
1126
1167
  server.registerTool("lighthouse_crm_create_note", {
1127
1168
  title: "Create Note",
1169
+ annotations: CREATES,
1128
1170
  description: "Create a note, optionally linked to one or more CRM records. Records are optional: omit them to create a standalone note that still appears in the workspace Notes section. " +
1129
1171
  "Link records via the `records` param, keyed by type. " +
1130
1172
  "The company/person/deal ids you pass MUST be EXISTING CRM record ids (the ids returned by lighthouse_crm_* tools), NOT discovery ids from lighthouse_discovery_* search results. To note on a discovery result, first add it to your CRM and use the resulting CRM record id. " +
@@ -1164,6 +1206,7 @@ export function createServer(apiKey, options = {}) {
1164
1206
  });
1165
1207
  server.registerTool("lighthouse_crm_get_note", {
1166
1208
  title: "Get Note",
1209
+ annotations: READ_ONLY,
1167
1210
  description: "Get a single note by ID. Response includes a `records` object keyed by record type (or custom object permalink) with every linked record you can view, plus a `tags` array of any workspace tags attached to the note (each with permalink, label, color). " +
1168
1211
  LINKED_RECORDS_LEFT_OUT + " " +
1169
1212
  "`tagged_users` is the teammates the note MENTIONS, each the member object this API answers for a person ({ id, first_name, last_name, full_name, avatar_url, email }), and [] when it mentions nobody. It is read from the mention spans in `content`, not from the ids a write sent, so a note written with tagged_users ids and no mention span reads back []: always write both, as lighthouse_crm_create_note and lighthouse_crm_update_note say. " +
@@ -1183,6 +1226,7 @@ export function createServer(apiKey, options = {}) {
1183
1226
  });
1184
1227
  server.registerTool("lighthouse_crm_update_note", {
1185
1228
  title: "Update Note",
1229
+ annotations: DESTRUCTIVE,
1186
1230
  description: "Change an existing note's title, content, tags or mentions by note ID. Pass only what changes: a key that is absent is left untouched. " +
1187
1231
  "content REPLACES the whole body: to ADD to a note, read it with lighthouse_crm_get_note first and pass the full new content with the addition, never the addition alone (anything you leave out, an existing @mention span included, is gone). " +
1188
1232
  "Content is HTML, the same format lighthouse_crm_create_note takes: <p> for paragraphs, <ul><li> for bullets, <h2> for a heading, <strong> for emphasis; plain text works too and is stored as one paragraph. " +
@@ -1211,6 +1255,7 @@ export function createServer(apiKey, options = {}) {
1211
1255
  });
1212
1256
  server.registerTool("lighthouse_crm_delete_note", {
1213
1257
  title: "Delete Note",
1258
+ annotations: DESTRUCTIVE,
1214
1259
  description: "Permanently delete a note and all of its record links. Meeting notes are deleted the same way, together with every version of them; the meeting's recording and transcript are not affected.",
1215
1260
  inputSchema: {
1216
1261
  note_id: z.string().uuid().optional().describe("Note ID"),
@@ -1227,6 +1272,7 @@ export function createServer(apiKey, options = {}) {
1227
1272
  });
1228
1273
  server.registerTool("lighthouse_crm_link_note_record", {
1229
1274
  title: "Link Record to Note",
1275
+ annotations: ADDS_IF_MISSING,
1230
1276
  description: "Attach a record to an existing note. Call once per record. " +
1231
1277
  "The company/person/deal record_id MUST be an EXISTING CRM record id (an id returned by lighthouse_crm_* tools), NOT a discovery id from lighthouse_discovery_* search results. To link a discovery result, first add it to your CRM and use the resulting CRM record id. " +
1232
1278
  LINK_NOT_VIEWABLE_RULE + " " +
@@ -1247,6 +1293,7 @@ export function createServer(apiKey, options = {}) {
1247
1293
  });
1248
1294
  server.registerTool("lighthouse_crm_unlink_note_record", {
1249
1295
  title: "Unlink Record from Note",
1296
+ annotations: DESTRUCTIVE,
1250
1297
  description: "Remove a record link from a note. Call once per record. " +
1251
1298
  "Removing the last linked record is allowed and leaves a standalone note (it stays in the workspace Notes section). " +
1252
1299
  UNLINK_NOT_VIEWABLE_KEPT,
@@ -1266,6 +1313,7 @@ export function createServer(apiKey, options = {}) {
1266
1313
  });
1267
1314
  server.registerTool("lighthouse_crm_list_note_tags", {
1268
1315
  title: "List Note Tags",
1316
+ annotations: READ_ONLY,
1269
1317
  description: "List the workspace's note tags: labeled, colored tags that can be attached to notes as a multi-select. " +
1270
1318
  "Returns an array of { permalink, label, color }. " +
1271
1319
  "Use this to find tag permalinks before attaching them to a note (via lighthouse_crm_create_note `tags` or lighthouse_crm_manage_note_tags). " +
@@ -1282,6 +1330,7 @@ export function createServer(apiKey, options = {}) {
1282
1330
  });
1283
1331
  server.registerTool("lighthouse_crm_create_note_tag", {
1284
1332
  title: "Create Note Tag",
1333
+ annotations: CREATES,
1285
1334
  description: "Create a new note tag in the workspace vocabulary. The tag can then be attached to notes. " +
1286
1335
  "Returns the created tag as { permalink, label, color }. " +
1287
1336
  "Color is an optional palette code (get available colors from lighthouse_crm_list_colors); omit it to auto-assign one.",
@@ -1300,6 +1349,7 @@ export function createServer(apiKey, options = {}) {
1300
1349
  });
1301
1350
  server.registerTool("lighthouse_crm_update_note_tag", {
1302
1351
  title: "Update Note Tag",
1352
+ annotations: DESTRUCTIVE,
1303
1353
  description: "Update a note tag's label and/or color. Only the fields you pass are changed. " +
1304
1354
  "Returns the updated tag as { permalink, label, color }.",
1305
1355
  inputSchema: {
@@ -1318,6 +1368,7 @@ export function createServer(apiKey, options = {}) {
1318
1368
  });
1319
1369
  server.registerTool("lighthouse_crm_delete_note_tag", {
1320
1370
  title: "Delete Note Tag",
1371
+ annotations: DESTRUCTIVE,
1321
1372
  description: "Delete a note tag from the workspace vocabulary. The tag is also detached from any notes it was attached to.",
1322
1373
  inputSchema: {
1323
1374
  permalink: z.string().describe("Tag permalink"),
@@ -1333,6 +1384,7 @@ export function createServer(apiKey, options = {}) {
1333
1384
  });
1334
1385
  server.registerTool("lighthouse_crm_manage_note_tags", {
1335
1386
  title: "Attach or Detach Note Tags",
1387
+ annotations: DESTRUCTIVE,
1336
1388
  description: "Attach or detach workspace tags on an existing note. Pass operation ADD to attach the tag permalinks, or REMOVE to detach them. " +
1337
1389
  "Find tag permalinks with lighthouse_crm_list_note_tags. Returns the updated note (including its current `tags`).",
1338
1390
  inputSchema: {
@@ -1353,6 +1405,7 @@ export function createServer(apiKey, options = {}) {
1353
1405
  });
1354
1406
  server.registerTool("lighthouse_crm_list_tasks", {
1355
1407
  title: "List Tasks",
1408
+ annotations: READ_ONLY,
1356
1409
  description: "List CRM tasks with optional filtering and sorting. " +
1357
1410
  'filters (CRM-style): {"operator":"AND","conditions":[{"field":"status","operator":"contains_any","value":["<status_value>"]}]} (status AND priority are option fields whose values are workspace-specific; fetch them with lighthouse_crm_list_options(record_type:\'task\', permalink:\'status\' | \'priority\'), and filter priority with contains_any like status, not with a numeric comparison); ' +
1358
1411
  'sort: [{"field":"due_date","direction":"asc"}]; ' +
@@ -1379,6 +1432,7 @@ export function createServer(apiKey, options = {}) {
1379
1432
  });
1380
1433
  server.registerTool("lighthouse_crm_get_task", {
1381
1434
  title: "Get Task",
1435
+ annotations: READ_ONLY,
1382
1436
  description: "Get a single CRM task by ID. " +
1383
1437
  "status and priority answer the stored VALUE and nothing else. The words your workspace puts on those values come from lighthouse_crm_list_options(record_type:'task', permalink:'status' | 'priority'), one call that covers every task you will ever read.",
1384
1438
  inputSchema: {
@@ -1395,6 +1449,7 @@ export function createServer(apiKey, options = {}) {
1395
1449
  });
1396
1450
  server.registerTool("lighthouse_crm_create_task", {
1397
1451
  title: "Create Task",
1452
+ annotations: CREATES,
1398
1453
  description: "Create a CRM task, optionally linked to records and assigned to users. " +
1399
1454
  "Status values are configured per workspace and are NOT fixed: call lighthouse_crm_list_options with record_type 'task' and permalink 'status' to get the allowed values, then pass the option's value (not its label) in `status`. " +
1400
1455
  "status and priority answer the stored VALUE and nothing else. The words your workspace puts on those values come from lighthouse_crm_list_options(record_type:'task', permalink:'status' | 'priority'), one call that covers every task you will ever read.",
@@ -1424,6 +1479,7 @@ export function createServer(apiKey, options = {}) {
1424
1479
  });
1425
1480
  server.registerTool("lighthouse_crm_update_task", {
1426
1481
  title: "Update Task",
1482
+ annotations: DESTRUCTIVE,
1427
1483
  description: "Update a CRM task. Provide only fields you want to change. " +
1428
1484
  "assigned_to replaces all current assignees. In records, " + TASK_LINKS_PER_TYPE.charAt(0).toLowerCase() + TASK_LINKS_PER_TYPE.slice(1) + " " +
1429
1485
  "Status values are configured per workspace and are NOT fixed: call lighthouse_crm_list_options with record_type 'task' and permalink 'status' to get the allowed values, then pass the option's value (not its label) in `status`. " +
@@ -1456,6 +1512,7 @@ export function createServer(apiKey, options = {}) {
1456
1512
  });
1457
1513
  server.registerTool("lighthouse_crm_delete_task", {
1458
1514
  title: "Delete Task",
1515
+ annotations: DESTRUCTIVE,
1459
1516
  description: "Permanently delete a CRM task.",
1460
1517
  inputSchema: {
1461
1518
  id: z.string().uuid().describe("Task ID"),
@@ -1471,6 +1528,7 @@ export function createServer(apiKey, options = {}) {
1471
1528
  });
1472
1529
  server.registerTool("lighthouse_documents_list", {
1473
1530
  title: "List Documents",
1531
+ annotations: READ_ONLY,
1474
1532
  description: "List folders and files at the workspace root, or inside a specific folder. " +
1475
1533
  "Returns { folders, files, breadcrumb } plus meta.folders_next_cursor / " +
1476
1534
  "meta.files_next_cursor for cursor-based pagination. Pass those cursors " +
@@ -1496,6 +1554,7 @@ export function createServer(apiKey, options = {}) {
1496
1554
  });
1497
1555
  server.registerTool("lighthouse_documents_create_folder", {
1498
1556
  title: "Create Folder",
1557
+ annotations: CREATES,
1499
1558
  description: "Create a folder. At the workspace root, sharing is required. " +
1500
1559
  "Inside a parent folder, sharing is automatically inherited from the parent. " +
1501
1560
  "The folder row comes back with owner (the member who created it, { id, first_name, last_name, full_name, avatar_url, email }), is_owner, my_role, sharing ('private' or 'workspace'), shared_with_teams[] (each { team_id, team_name, role, granted_at }), linked_records_count, created_at and updated_at.",
@@ -1518,6 +1577,7 @@ export function createServer(apiKey, options = {}) {
1518
1577
  });
1519
1578
  server.registerTool("lighthouse_documents_get_folder", {
1520
1579
  title: "Get Folder",
1580
+ annotations: READ_ONLY,
1521
1581
  description: "WHEN TO USE: the user asks about ONE folder they already know (\"who can see the Investor Updates folder\", \"what is that folder linked to\"), or you have a folder id from lighthouse_documents_list and need its full row. " +
1522
1582
  "Returns the folder itself, not its contents: name, parent_folder_id, sharing ('private' or 'workspace'), shared_with_teams[] (each { team_id, team_name, role, granted_at }), is_owner, my_role ('owner' | 'editor' | 'viewer'), owner ({ id, first_name, last_name, full_name, avatar_url, email }), linked_records_count, linked_records[] (every link you can view, each { record_type, record_id, record_name, avatar_url, created_at }), created_at and updated_at. " +
1523
1583
  "To list what is INSIDE it, call lighthouse_documents_list with this folder_id. A folder you cannot open answers the same not found as one that does not exist.",
@@ -1536,7 +1596,8 @@ export function createServer(apiKey, options = {}) {
1536
1596
  });
1537
1597
  server.registerTool("lighthouse_documents_update_folder", {
1538
1598
  title: "Update Folder",
1539
- description: "Rename, move, or change sharing on a folder. Editors can rename the folder; only owners can move it, change its sharing, or grant team access. " +
1599
+ annotations: DESTRUCTIVE,
1600
+ description: "Rename, move, or change sharing on a folder. Editors can rename the folder; only owners can move it, change its sharing, or grant team access. A folder that holds files or folders other people own can only be moved down, into a folder below where it is now that you own, with every folder in between yours too; any other move of it answers 403 forbidden. " +
1540
1601
  "Pass team_ids as the full desired list, anything not in the list is revoked. " +
1541
1602
  "Note the two meanings of folder_id across the document tools: here it is the folder being changed, while on lighthouse_documents_update_file it is the folder the file is moved INTO. " +
1542
1603
  "Returns the folder row: owner, is_owner, my_role, sharing, shared_with_teams[] (each { team_id, team_name, role, granted_at }), linked_records_count, created_at, updated_at.",
@@ -1562,6 +1623,7 @@ export function createServer(apiKey, options = {}) {
1562
1623
  });
1563
1624
  server.registerTool("lighthouse_documents_delete_folder", {
1564
1625
  title: "Delete Folder",
1626
+ annotations: DESTRUCTIVE,
1565
1627
  description: "Restricted to the folder owner. Deletes the folder and its entire subtree only when you own every descendant folder and file. If any descendant is owned by another user, the request is rejected (CANNOT_DELETE_OTHERS_CONTENT_IN_FOLDER) and nothing is deleted. Editors on the folder cannot delete it. Irreversible: there is no recycle bin and the underlying stored files are permanently removed.",
1566
1628
  inputSchema: {
1567
1629
  folder_id: z.string().uuid().describe("Folder UUID"),
@@ -1577,6 +1639,7 @@ export function createServer(apiKey, options = {}) {
1577
1639
  });
1578
1640
  server.registerTool("lighthouse_documents_link_folder_to_record", {
1579
1641
  title: "Link Folder to Record",
1642
+ annotations: ADDS_IF_MISSING,
1580
1643
  description: "Link a folder to a CRM record, associating the whole folder with the record. The folder then appears on the record (in lighthouse_documents_list_for_record's `folders`); its individual files are not flattened into the record's direct `files` list. " +
1581
1644
  "Answers THE FOLDER itself, the same row lighthouse_documents_get_folder returns (owner, is_owner, my_role, sharing, shared_with_teams[] each { team_id, team_name, role, granted_at }, linked_records_count and linked_records[] as they are after the link), so there is no second call to make.",
1582
1645
  inputSchema: {
@@ -1596,6 +1659,7 @@ export function createServer(apiKey, options = {}) {
1596
1659
  });
1597
1660
  server.registerTool("lighthouse_documents_unlink_folder_from_record", {
1598
1661
  title: "Unlink Folder from Record",
1662
+ annotations: DESTRUCTIVE,
1599
1663
  description: "Remove a folder→record link. Files inside stop appearing on the record's Files tab. Answers THE FOLDER itself, the same row lighthouse_documents_get_folder returns, with its links as they are after the call.",
1600
1664
  inputSchema: {
1601
1665
  folder_id: z.string().uuid().describe("Folder UUID"),
@@ -1614,6 +1678,7 @@ export function createServer(apiKey, options = {}) {
1614
1678
  });
1615
1679
  server.registerTool("lighthouse_documents_upload_file", {
1616
1680
  title: "Upload File",
1681
+ annotations: CREATES,
1617
1682
  description: "Reserve a file row and get a signed upload URL: " +
1618
1683
  "1) call this; 2) POST the file to upload_url as multipart/form-data, first append every key/value " +
1619
1684
  "from the returned upload_fields, then append the file last under the field name 'file'. " +
@@ -1641,6 +1706,7 @@ export function createServer(apiKey, options = {}) {
1641
1706
  });
1642
1707
  server.registerTool("lighthouse_documents_get_file", {
1643
1708
  title: "Get File",
1709
+ annotations: READ_ONLY,
1644
1710
  description: "Get file metadata. Response includes my_role ('owner' | 'editor' | 'viewer'), is_owner, owner (the member who owns it: { id, first_name, last_name, full_name, avatar_url, email }), sharing ('private' or 'workspace'), linked_records_count, linked_records[] and shared_with_teams[] (each entry has { team_id, team_name, role, granted_at }). Pass download_url=true to also receive a short-lived signed read URL (expires in 15 minutes). To read what the file SAYS, use lighthouse_documents_get_file_content.",
1645
1711
  inputSchema: {
1646
1712
  file_id: z.string().uuid().describe("File UUID"),
@@ -1658,6 +1724,7 @@ export function createServer(apiKey, options = {}) {
1658
1724
  });
1659
1725
  server.registerTool("lighthouse_documents_get_file_content", {
1660
1726
  title: "Read File",
1727
+ annotations: READ_ONLY,
1661
1728
  description: "What one stored file SAYS, as text: PDFs, Word, Excel and PowerPoint files (and their OpenDocument versions), text, CSV and HTML. " +
1662
1729
  "WHEN TO USE: the answer is inside a document the workspace keeps (\"what are the conditions in the term sheet\", \"summarise the memo on this deal\", \"what does the deck say about revenue\"). " +
1663
1730
  "Take the file id from lighthouse_documents_list or lighthouse_documents_list_for_record. " +
@@ -1691,13 +1758,14 @@ export function createServer(apiKey, options = {}) {
1691
1758
  });
1692
1759
  server.registerTool("lighthouse_documents_update_file", {
1693
1760
  title: "Update File",
1694
- description: "Rename, move, or change sharing on a file. Editors can rename and delete it. Only the owner can move it (into another folder or to the root), change sharing, or grant team access, with one exception: the owner of the folder the file is in can also move it into another folder they own. " +
1761
+ annotations: DESTRUCTIVE,
1762
+ description: "Rename, move, or change sharing on a file. Editors can rename and delete it. Only the owner can move it (into another folder or to the root), change sharing, or grant team access, with one exception: the owner of the folder the file is in can also move it down into a sub-folder of that folder, when they own that sub-folder and every folder in between. " +
1695
1763
  "team_ids replaces the current set when supplied. " +
1696
1764
  "Returns the file row: owner, is_owner, my_role, sharing, shared_with_teams[] (each { team_id, team_name, role, granted_at }), linked_records_count, size_bytes, content_type, created_at, updated_at.",
1697
1765
  inputSchema: {
1698
1766
  file_id: z.string().uuid().describe("File UUID, the file being changed"),
1699
1767
  name: z.string().optional().describe("New name"),
1700
- folder_id: z.string().uuid().optional().describe("New parent folder UUID, the folder the file is moved INTO (on lighthouse_documents_update_folder the same name means the folder being changed). The file's owner moves it into any folder they can edit; the owner of the folder it is in can also move it, but only into a folder they own; anyone else gets 403 forbidden. Naming the folder the file is already in also counts as a move, so to rename a file send only name."),
1768
+ folder_id: z.string().uuid().optional().describe("New parent folder UUID, the folder the file is moved INTO (on lighthouse_documents_update_folder the same name means the folder being changed). The file's owner moves it into any folder they can edit; the owner of the folder it is in can also move it, but only down into a sub-folder of that folder that they own, with every folder in between theirs too; anyone else gets 403 forbidden. Naming the folder the file is already in also counts as a move, so to rename a file send only name."),
1701
1769
  clear_folder: z.boolean().optional().describe("Move to workspace root (owner-only)"),
1702
1770
  sharing: z.enum(["private", "workspace"]).optional().describe("Explicit override (null clears to inherit). The same word every other sharing tool takes, and the one a file answers."),
1703
1771
  sharing_status: z.enum(["PRIVATE", "WORKSPACE"]).optional().describe("Legacy name for sharing. Kept forever; sharing wins when both are sent."),
@@ -1716,6 +1784,7 @@ export function createServer(apiKey, options = {}) {
1716
1784
  });
1717
1785
  server.registerTool("lighthouse_documents_delete_file", {
1718
1786
  title: "Delete File",
1787
+ annotations: DESTRUCTIVE,
1719
1788
  description: "Owner or editor (file-level editor OR editor-on-parent-folder) can delete, provided they also have the files.delete workspace permission. Permanently deletes the file and purges the underlying object from storage. Irreversible.",
1720
1789
  inputSchema: {
1721
1790
  file_id: z.string().uuid().describe("File UUID"),
@@ -1731,6 +1800,7 @@ export function createServer(apiKey, options = {}) {
1731
1800
  });
1732
1801
  server.registerTool("lighthouse_documents_link_file_to_record", {
1733
1802
  title: "Link File to Record",
1803
+ annotations: ADDS_IF_MISSING,
1734
1804
  description: "Make a file appear on a CRM record's Files tab. Answers THE FILE itself, the same row lighthouse_documents_get_file returns, with its links as they are after the call.",
1735
1805
  inputSchema: {
1736
1806
  file_id: z.string().uuid().describe("File UUID"),
@@ -1749,6 +1819,7 @@ export function createServer(apiKey, options = {}) {
1749
1819
  });
1750
1820
  server.registerTool("lighthouse_documents_unlink_file_from_record", {
1751
1821
  title: "Unlink File from Record",
1822
+ annotations: DESTRUCTIVE,
1752
1823
  description: "Remove a file→record link. Answers THE FILE itself, the same row lighthouse_documents_get_file returns, with its links as they are after the call.",
1753
1824
  inputSchema: {
1754
1825
  file_id: z.string().uuid().describe("File UUID"),
@@ -1767,6 +1838,7 @@ export function createServer(apiKey, options = {}) {
1767
1838
  });
1768
1839
  server.registerTool("lighthouse_documents_list_for_record", {
1769
1840
  title: "List a Record's Documents",
1841
+ annotations: READ_ONLY,
1770
1842
  description: "List folders and files linked to a CRM record. Returns { folders, files }. `files` are files linked DIRECTLY to the record; files that live inside a linked folder are NOT flattened into `files`. To read a linked folder's contents, list that folder by its id with lighthouse_documents_list. " +
1771
1843
  "Each row includes my_role ('owner' | 'editor' | 'viewer'), is_owner, owner (the member who owns it: { id, first_name, last_name, full_name, avatar_url, email }), sharing ('private' or 'workspace'), linked_records_count, linked_at, and shared_with_teams[] (each entry has { team_id, team_name, role, granted_at }). " +
1772
1844
  "Cursor-based pagination via meta.folders_next_cursor / meta.files_next_cursor; pass them back as folders_cursor / files_cursor to fetch the next page.",
@@ -1796,6 +1868,7 @@ export function createServer(apiKey, options = {}) {
1796
1868
  });
1797
1869
  server.registerTool("lighthouse_crm_list_views", {
1798
1870
  title: "List CRM Views",
1871
+ annotations: READ_ONLY,
1799
1872
  description: "Get all saved CRM views, or list views scoped to a specific list. " +
1800
1873
  "Supports built-in types and custom objects. " +
1801
1874
  "Views store filter/sort configurations that can be loaded in search_records. " +
@@ -1815,6 +1888,7 @@ export function createServer(apiKey, options = {}) {
1815
1888
  });
1816
1889
  server.registerTool("lighthouse_crm_update_view", {
1817
1890
  title: "Update CRM View",
1891
+ annotations: DESTRUCTIVE,
1818
1892
  description: "Update a saved CRM view or list view's name, filters, sort, or layout. " +
1819
1893
  "Automatically detects whether it's a CRM view or list view. " +
1820
1894
  "To change sharing settings, use lighthouse_crm_update_view_sharing instead. " +
@@ -1852,6 +1926,7 @@ export function createServer(apiKey, options = {}) {
1852
1926
  });
1853
1927
  server.registerTool("lighthouse_crm_get_view", {
1854
1928
  title: "Get CRM View",
1929
+ annotations: READ_ONLY,
1855
1930
  description: "Get a single saved CRM view or list view by ID. Automatically detects the view type. The filters come back as saved, a semantic_search condition with its vector included: reuse such a condition verbatim in lighthouse_crm_search_records or lighthouse_crm_list_list_records. Conditions on lists you cannot open are left out, and so are conditions on custom fields that belong to such a list. The row carries record_type, owner and is_owner (true when the API key's user owns the view). A kanban view's board settings come with it: hidden_kanban_groups (the values whose column is collapsed, always an array) and kanban_summary (the number at the top of each column, or null), both writable through lighthouse_crm_update_view exactly as they are read.",
1856
1931
  inputSchema: {
1857
1932
  id: z.string().uuid().describe("View ID"),
@@ -1867,6 +1942,7 @@ export function createServer(apiKey, options = {}) {
1867
1942
  });
1868
1943
  server.registerTool("lighthouse_crm_delete_view", {
1869
1944
  title: "Delete CRM View",
1945
+ annotations: DESTRUCTIVE,
1870
1946
  description: "Permanently delete a saved CRM view or list view. Automatically detects the view type.",
1871
1947
  inputSchema: {
1872
1948
  id: z.string().uuid().describe("View ID"),
@@ -1882,6 +1958,7 @@ export function createServer(apiKey, options = {}) {
1882
1958
  });
1883
1959
  server.registerTool("lighthouse_workspace_me", {
1884
1960
  title: "Get Current User",
1961
+ annotations: READ_ONLY,
1885
1962
  description: "Get the profile of the user this key acts as: id, name, email, avatar, phone_number, status, created_at, workspace_role, permissions and `teams` ([{ id, name, role, joined_at }], [] when they are on none). " +
1886
1963
  "It is the same object lighthouse_workspace_get_user answers for anyone else, so read it once to learn who you are acting as and what that role may do, rather than calling the user tool on yourself.",
1887
1964
  inputSchema: {},
@@ -1896,6 +1973,7 @@ export function createServer(apiKey, options = {}) {
1896
1973
  });
1897
1974
  server.registerTool("lighthouse_workspace_list_users", {
1898
1975
  title: "List Workspace Users",
1976
+ annotations: READ_ONLY,
1899
1977
  description: "List users in the workspace. Use this to find user IDs for task assignment, note tagging, or filtering by user. " +
1900
1978
  "Supports search by name/email.",
1901
1979
  inputSchema: {
@@ -1916,6 +1994,7 @@ export function createServer(apiKey, options = {}) {
1916
1994
  });
1917
1995
  server.registerTool("lighthouse_workspace_get_user", {
1918
1996
  title: "Get Workspace User",
1997
+ annotations: READ_ONLY,
1919
1998
  description: "Get a specific workspace user's profile by ID.",
1920
1999
  inputSchema: {
1921
2000
  id: z.string().uuid().describe("User ID"),
@@ -1931,6 +2010,7 @@ export function createServer(apiKey, options = {}) {
1931
2010
  });
1932
2011
  server.registerTool("lighthouse_workspace_list_teams", {
1933
2012
  title: "List Workspace Teams",
2013
+ annotations: READ_ONLY,
1934
2014
  description: "List all teams in the workspace.",
1935
2015
  inputSchema: {
1936
2016
  limit: z.number().min(1).max(100).default(25),
@@ -1947,6 +2027,7 @@ export function createServer(apiKey, options = {}) {
1947
2027
  });
1948
2028
  server.registerTool("lighthouse_workspace_get_team", {
1949
2029
  title: "Get Workspace Team",
2030
+ annotations: READ_ONLY,
1950
2031
  description: "Get a specific team by ID with its members.",
1951
2032
  inputSchema: {
1952
2033
  id: z.string().uuid().describe("Team ID"),
@@ -1966,6 +2047,7 @@ export function createServer(apiKey, options = {}) {
1966
2047
  const WEBHOOK_SUBSCRIPTION_WRITES = "This tool only reads: creating and deleting webhook subscriptions is done with the REST API (POST /v1/webhook-subscriptions and DELETE /v1/webhook-subscriptions/{id}) or the SDKs, with an API key, and a subscription can also be created or deleted in the app, under Settings > Developer > Webhooks. A turned-off subscription is not turned back on: delete it and create a new one.";
1967
2048
  server.registerTool("lighthouse_workspace_list_webhook_subscriptions", {
1968
2049
  title: "List Webhook Subscriptions",
2050
+ annotations: READ_ONLY,
1969
2051
  description: "List the webhook subscriptions of the user this key acts as (only their own), newest first, active and turned off alike: each one sends a signed POST to its url whenever a record of its record_type is created or updated. " +
1970
2052
  WEBHOOK_SUBSCRIPTION_SHAPE + " " +
1971
2053
  "WHEN TO USE: \"which webhooks do I have\", \"where are our company updates being sent\", \"why did our integration stop receiving changes\" (read status, disabled_reason and last_delivery). " +
@@ -1985,6 +2067,7 @@ export function createServer(apiKey, options = {}) {
1985
2067
  });
1986
2068
  server.registerTool("lighthouse_workspace_get_webhook_subscription", {
1987
2069
  title: "Get Webhook Subscription",
2070
+ annotations: READ_ONLY,
1988
2071
  description: "Get one of the webhook subscriptions of the user this key acts as, by id. " +
1989
2072
  WEBHOOK_SUBSCRIPTION_SHAPE + " " +
1990
2073
  "WHEN TO USE: \"is that webhook still on\", \"when did it last deliver\", for one subscription whose id you already have (from lighthouse_workspace_list_webhook_subscriptions). " +
@@ -2003,6 +2086,7 @@ export function createServer(apiKey, options = {}) {
2003
2086
  });
2004
2087
  server.registerTool("lighthouse_discovery_list_attributes", {
2005
2088
  title: "List Discovery Data Fields",
2089
+ annotations: READ_ONLY,
2006
2090
  description: "Get a lightweight catalog of the DATA FIELDS discovery results carry for companies or people: each field's permalink, label, data_type, links_to, read_only, and has_options. " +
2007
2091
  "These are OUTPUT/data fields, NOT filter keys. To FILTER a search you MUST use the prefixed company_* / person_* keys listed in the `filters` parameter of the discovery search tools. " +
2008
2092
  "To keep the response small it does NOT include option values: when a field has has_options=true and you need its allowed values, call lighthouse_discovery_list_options with that permalink (or the dedicated lighthouse_discovery_list_industries / lighthouse_discovery_list_locations / lighthouse_discovery_list_funding_types / lighthouse_discovery_list_tags tools). " +
@@ -2029,6 +2113,7 @@ export function createServer(apiKey, options = {}) {
2029
2113
  });
2030
2114
  server.registerTool("lighthouse_discovery_list_options", {
2031
2115
  title: "List Discovery Field Options",
2116
+ annotations: READ_ONLY,
2032
2117
  description: "Get the allowed values (value, label, color) for a single discovery select / multi_select field. " +
2033
2118
  "Call this when lighthouse_discovery_list_attributes shows has_options=true for a field and you need its valid values. " +
2034
2119
  "Results are paginated (max 100 per page): use `search` to narrow by label, and `limit` / `offset` to page through a field with many values (e.g. locations). " +
@@ -2052,6 +2137,7 @@ export function createServer(apiKey, options = {}) {
2052
2137
  });
2053
2138
  server.registerTool("lighthouse_discovery_search_companies", {
2054
2139
  title: "Search Discovery Companies",
2140
+ annotations: READ_ONLY,
2055
2141
  description: "Search Lighthouse's global company database to DISCOVER new companies. " +
2056
2142
  "This searches Lighthouse's enriched database (millions of companies), NOT your CRM. " +
2057
2143
  "WORKFLOW: 1) Build filters using ONLY the company_* filter keys listed in the `filters` parameter below. " +
@@ -2082,6 +2168,7 @@ export function createServer(apiKey, options = {}) {
2082
2168
  });
2083
2169
  server.registerTool("lighthouse_discovery_search_people", {
2084
2170
  title: "Search Discovery People",
2171
+ annotations: READ_ONLY,
2085
2172
  description: "Search Lighthouse's global people database to DISCOVER new people. " +
2086
2173
  "This searches Lighthouse's enriched database, NOT your CRM. " +
2087
2174
  "WORKFLOW: 1) Build filters using ONLY the person_* filter keys listed in the `filters` parameter below. " +
@@ -2112,6 +2199,7 @@ export function createServer(apiKey, options = {}) {
2112
2199
  });
2113
2200
  server.registerTool("lighthouse_discovery_semantic_search_companies", {
2114
2201
  title: "Semantic Search Companies",
2202
+ annotations: READ_ONLY,
2115
2203
  description: "Find companies by MEANING, not exact keywords. Describe the kind of company you are looking for in plain language (what it does, who it serves, its sector, its business model) and Lighthouse returns the most semantically similar companies from its global database (NOT your CRM), ranked by relevance. " +
2116
2204
  "USE THIS when the request is conceptual or hard to express as exact filters, e.g. 'companies like Stripe but for healthcare', 'startups building developer tools for AI agents', 'sustainable packaging for e-commerce'. " +
2117
2205
  "You CAN combine it with structured filters: pass `filters` using the same company_* keys as lighthouse_discovery_search_companies and they apply together (semantic match AND your filters), e.g. semantically-similar companies that are ALSO headquartered in Germany. " +
@@ -2146,6 +2234,7 @@ export function createServer(apiKey, options = {}) {
2146
2234
  });
2147
2235
  server.registerTool("lighthouse_discovery_semantic_search_people", {
2148
2236
  title: "Semantic Search People",
2237
+ annotations: READ_ONLY,
2149
2238
  description: "Find people by MEANING, not exact keywords. Describe the ideal person in plain language (their role, skills, the industry they work in, their background) and Lighthouse returns the most semantically similar people from its global database (NOT your CRM), ranked by relevance. " +
2150
2239
  "USE THIS when the request is conceptual or hard to express as exact filters, e.g. 'technical co-founders who worked on payments infrastructure', 'ex-FAANG ML engineers now in climate tech'. " +
2151
2240
  "You CAN combine it with structured filters: pass `filters` using the same person_* keys as lighthouse_discovery_search_people and they apply together (semantic match AND your filters), e.g. semantically-similar people who are ALSO based in the UK. " +
@@ -2180,6 +2269,7 @@ export function createServer(apiKey, options = {}) {
2180
2269
  });
2181
2270
  server.registerTool("lighthouse_discovery_lookup_by_domain", {
2182
2271
  title: "Lookup Companies by Domain",
2272
+ annotations: READ_ONLY,
2183
2273
  description: "Look up companies in Lighthouse's global database by their website domain(s). " +
2184
2274
  "Returns enriched company profiles for matching domains. Max 1000 domains per request. " +
2185
2275
  "Each returned profile is condensed (the long growth and workforce history and the activity-signal " +
@@ -2203,6 +2293,7 @@ export function createServer(apiKey, options = {}) {
2203
2293
  });
2204
2294
  server.registerTool("lighthouse_discovery_lookup_by_linkedin", {
2205
2295
  title: "Lookup Companies by LinkedIn",
2296
+ annotations: READ_ONLY,
2206
2297
  description: "Look up companies in Lighthouse's global database by LinkedIn URL(s). " +
2207
2298
  "Returns enriched company profiles for matching LinkedIn company pages. Max 1000 URLs per request. " +
2208
2299
  "Each returned profile is condensed (the long growth and workforce history and the activity-signal " +
@@ -2226,6 +2317,7 @@ export function createServer(apiKey, options = {}) {
2226
2317
  });
2227
2318
  server.registerTool("lighthouse_discovery_lookup_people_by_linkedin", {
2228
2319
  title: "Lookup People by LinkedIn",
2320
+ annotations: READ_ONLY,
2229
2321
  description: "Look up people in Lighthouse's global database by LinkedIn profile URL(s). " +
2230
2322
  "Returns enriched person profiles for matching LinkedIn pages. Max 1000 URLs per request. " +
2231
2323
  "Each returned profile is condensed (the activity-signal feed is omitted and the associated-companies " +
@@ -2249,6 +2341,7 @@ export function createServer(apiKey, options = {}) {
2249
2341
  });
2250
2342
  server.registerTool("lighthouse_discovery_list_saved_searches", {
2251
2343
  title: "List Saved Searches",
2344
+ annotations: READ_ONLY,
2252
2345
  description: "List all saved discovery searches in the workspace. " +
2253
2346
  "Returns saved searches you own and workspace-shared searches. " +
2254
2347
  "Each search includes its filters and type (company or person). " +
@@ -2265,6 +2358,7 @@ export function createServer(apiKey, options = {}) {
2265
2358
  });
2266
2359
  server.registerTool("lighthouse_discovery_list_search_results", {
2267
2360
  title: "List Saved Search Results",
2361
+ annotations: READ_ONLY,
2268
2362
  description: "Run a saved discovery search and get its results. " +
2269
2363
  "The search's saved filters are applied automatically. " +
2270
2364
  "Optionally filter by signal creation date range. " +
@@ -2288,6 +2382,7 @@ export function createServer(apiKey, options = {}) {
2288
2382
  });
2289
2383
  server.registerTool("lighthouse_discovery_list_locations", {
2290
2384
  title: "List Location Options",
2385
+ annotations: READ_ONLY,
2291
2386
  description: "Get available location options (countries and regions) for discovery filters. " +
2292
2387
  "ALWAYS call this to get valid country/region values before filtering by location fields " +
2293
2388
  "(company_hq_country, company_hq_region, person_country, person_region, company_investor_country, etc.). " +
@@ -2308,6 +2403,7 @@ export function createServer(apiKey, options = {}) {
2308
2403
  });
2309
2404
  server.registerTool("lighthouse_discovery_list_industries", {
2310
2405
  title: "List Industry & Category Options",
2406
+ annotations: READ_ONLY,
2311
2407
  description: "Get available industry and category options for discovery filters. " +
2312
2408
  "ALWAYS call this to get valid category values before filtering by company_categories, company_sdg_impact, " +
2313
2409
  "person_current_categories, person_past_categories, person_current_sdg_impact, or person_past_sdg_impact, " +
@@ -2329,6 +2425,7 @@ export function createServer(apiKey, options = {}) {
2329
2425
  });
2330
2426
  server.registerTool("lighthouse_discovery_list_tags", {
2331
2427
  title: "List Tag Options",
2428
+ annotations: READ_ONLY,
2332
2429
  description: "Get the tag options for discovery filters. " +
2333
2430
  "ALWAYS call this to get valid tag values before filtering by company_tags or person_tags. " +
2334
2431
  "Returns {company_tags: [{value, name}], person_tags: [{value, name}]}. " +
@@ -2345,6 +2442,7 @@ export function createServer(apiKey, options = {}) {
2345
2442
  });
2346
2443
  server.registerTool("lighthouse_discovery_list_funding_types", {
2347
2444
  title: "List Funding Type Options",
2445
+ annotations: READ_ONLY,
2348
2446
  description: "Get the funding round types available for the funding discovery filters " +
2349
2447
  "(company_last_funding_type, person_company_last_funding_type). " +
2350
2448
  "Returns an array of { value, label, name } ordered by stage. " +
@@ -2363,6 +2461,7 @@ export function createServer(apiKey, options = {}) {
2363
2461
  });
2364
2462
  server.registerTool("lighthouse_discovery_search_investors", {
2365
2463
  title: "Search Investors by Name",
2464
+ annotations: READ_ONLY,
2366
2465
  description: "Search for investors by name to get their UUIDs for use in discovery filters. " +
2367
2466
  "Use the returned 'id' values in company_investor filter conditions (investor_multi_select type). " +
2368
2467
  "Returns [{id, name, avatar_url}]. Pass the id (or [{id, label}] objects) as the filter value. " +
@@ -2381,6 +2480,7 @@ export function createServer(apiKey, options = {}) {
2381
2480
  });
2382
2481
  server.registerTool("lighthouse_discovery_search_organizations", {
2383
2482
  title: "Search Organizations by Name",
2483
+ annotations: READ_ONLY,
2384
2484
  description: "Search for organizations (companies) by name to get their UUIDs for use in discovery filters. " +
2385
2485
  "Use the returned 'id' values in work_experience_multi_select filter conditions " +
2386
2486
  "(company_team_past_companies, person_past_companies). " +
@@ -2401,6 +2501,7 @@ export function createServer(apiKey, options = {}) {
2401
2501
  });
2402
2502
  server.registerTool("lighthouse_discovery_search_schools", {
2403
2503
  title: "Search Schools by Name",
2504
+ annotations: READ_ONLY,
2404
2505
  description: "Search for educational institutions by name to get their UUIDs for use in discovery filters. " +
2405
2506
  "Use the returned 'id' values in education_multi_select filter conditions " +
2406
2507
  "(company_team_education, person_education). " +
@@ -2420,6 +2521,7 @@ export function createServer(apiKey, options = {}) {
2420
2521
  });
2421
2522
  server.registerTool("lighthouse_discovery_search_entities", {
2422
2523
  title: "Search Entities (companies, people, investors, schools)",
2524
+ annotations: READ_ONLY,
2423
2525
  description: "Similarity-ranked, typo-tolerant search across companies, people, investors, and schools. " +
2424
2526
  "Returns lightweight matches (id, record_type, name, avatar_url, is_discoverable, plus a few " +
2425
2527
  "disambiguators like domain/employees/location) ranked by relevance then popularity (e.g. " +
@@ -2456,6 +2558,7 @@ export function createServer(apiKey, options = {}) {
2456
2558
  });
2457
2559
  server.registerTool("lighthouse_discovery_get_company", {
2458
2560
  title: "Get Company Profile by ID",
2561
+ annotations: READ_ONLY,
2459
2562
  description: "Returns the profile for a single company by id: firmographics, funding, categories, tags, links, " +
2460
2563
  "and the first several associated people. " +
2461
2564
  "To stay within response limits, this returns a condensed view: the long growth and workforce " +
@@ -2482,6 +2585,7 @@ export function createServer(apiKey, options = {}) {
2482
2585
  });
2483
2586
  server.registerTool("lighthouse_discovery_get_person", {
2484
2587
  title: "Get Person Profile by ID",
2588
+ annotations: READ_ONLY,
2485
2589
  description: "Returns the profile for a single person by id: bio, links, founder score, person tags, " +
2486
2590
  "the first several associated companies / work history, and education. " +
2487
2591
  "To stay within response limits, this returns a condensed view: the activity-signal feed is omitted, " +
@@ -2506,6 +2610,7 @@ export function createServer(apiKey, options = {}) {
2506
2610
  });
2507
2611
  server.registerTool("lighthouse_discovery_get_company_traction", {
2508
2612
  title: "Get Company Traction History",
2613
+ annotations: READ_ONLY,
2509
2614
  description: "The traction history of one company in the global database, by company id: the two blocks lighthouse_discovery_get_company omits to stay small. " +
2510
2615
  "workforce_data is the month-by-month headcount and open-job-post history behind the traction charts (series of { date, value } points, oldest first where present); growth_metrics is a summary per tracked metric (headcount, job posts, web traffic, social following and similar, where recorded): { last_value, change_1m, change_3m, change_6m, change_1y, change_2y, change_3y, change_5y, date_last_value }. " +
2511
2616
  "WHEN TO USE: \"what was their headcount in March\", \"is this company growing\", \"when did hiring pick up\", \"how has traffic moved this year\": anything about a value over time or a rate of change. The profile tool only carries the current headcount. " +
@@ -2524,6 +2629,7 @@ export function createServer(apiKey, options = {}) {
2524
2629
  });
2525
2630
  server.registerTool("lighthouse_discovery_get_company_signals", {
2526
2631
  title: "Get Company Signals",
2632
+ annotations: READ_ONLY,
2527
2633
  description: "The detected events for one company in the global database, by company id: the activity-signal feed lighthouse_discovery_get_company omits. " +
2528
2634
  "signals is a list of { type, created_at, timeframe, percentage_change }: a funding round, a headcount or traffic jump, a hiring spike and similar, each with when it was detected and, for a change, over which timeframe and by how much. Sort by created_at yourself; the list is not ordered. " +
2529
2635
  "WHEN TO USE: \"what happened at X recently\", \"did they raise\", \"is headcount spiking\", \"any momentum signals\". For the numbers behind a signal read lighthouse_discovery_get_company_traction. " +
@@ -2542,6 +2648,7 @@ export function createServer(apiKey, options = {}) {
2542
2648
  });
2543
2649
  server.registerTool("lighthouse_discovery_get_person_signals", {
2544
2650
  title: "Get Person Signals",
2651
+ annotations: READ_ONLY,
2545
2652
  description: "The detected events for one person in the global database, by person id: the signal feed lighthouse_discovery_get_person omits. " +
2546
2653
  "signals is a list of { type, created_at, new_company, domain, org_id, predicted_areas }: a new company founded or registered, a stealth period, a departure and similar; org_id is a company id you can pass to lighthouse_discovery_get_company, domain its website. Sort by created_at yourself. " +
2547
2654
  "predicted_areas is what Lighthouse predicts the founder is building, on a stealth or out_of_stealth signal: [{ permalink, label, origin }], permalink being a value of lighthouse_discovery_list_industries → stealth_predicted_areas and origin what the prediction was read from; it is [] on every other signal and when nothing was predicted. " +
@@ -2561,6 +2668,7 @@ export function createServer(apiKey, options = {}) {
2561
2668
  });
2562
2669
  server.registerTool("lighthouse_discovery_get_entity_by_name", {
2563
2670
  title: "Get Entity Profile by Name",
2671
+ annotations: READ_ONLY,
2564
2672
  description: "DEFAULT way to get the COMPLETE profile of ONE company or person when you have its name. " +
2565
2673
  "Resolves the single best-ranked match and returns its full profile in one call (equivalent to " +
2566
2674
  "lighthouse_discovery_search_entities followed by lighthouse_discovery_get_company / get_person, combined). " +
@@ -2607,6 +2715,7 @@ export function createServer(apiKey, options = {}) {
2607
2715
  "ALWAYS call lighthouse_crm_list_attributes first to discover valid field keys for dimension, measure, and stage_field.";
2608
2716
  server.registerTool("lighthouse_reports_list_dashboards", {
2609
2717
  title: "List Dashboards",
2718
+ annotations: READ_ONLY,
2610
2719
  description: "List all report dashboards accessible to the current user. " +
2611
2720
  "Returns dashboards you own, workspace-shared dashboards, and team-shared dashboards. " +
2612
2721
  "Each dashboard includes: id, name, description, sharing (private/workspace), is_owner, " +
@@ -2623,6 +2732,7 @@ export function createServer(apiKey, options = {}) {
2623
2732
  });
2624
2733
  server.registerTool("lighthouse_reports_create_dashboard", {
2625
2734
  title: "Create Dashboard",
2735
+ annotations: CREATES,
2626
2736
  description: "Create a new report dashboard. Dashboards contain report widgets (charts, tables, KPIs). " +
2627
2737
  "Requires reports.create permission. Returns the full dashboard object (same format as get dashboard).",
2628
2738
  inputSchema: {
@@ -2648,6 +2758,7 @@ export function createServer(apiKey, options = {}) {
2648
2758
  });
2649
2759
  server.registerTool("lighthouse_reports_get_dashboard", {
2650
2760
  title: "Get Dashboard",
2761
+ annotations: READ_ONLY,
2651
2762
  description: "Get a single dashboard by ID with all its widgets. " +
2652
2763
  "Returns: id, name, description, sharing, is_owner, owner {id, first_name, last_name, full_name, avatar_url, email}, " +
2653
2764
  "widget_count, shared_with_teams, widgets [{id, name, widget_type, config, created_at, updated_at}], created_at, updated_at.",
@@ -2665,6 +2776,7 @@ export function createServer(apiKey, options = {}) {
2665
2776
  });
2666
2777
  server.registerTool("lighthouse_reports_update_dashboard", {
2667
2778
  title: "Update Dashboard",
2779
+ annotations: DESTRUCTIVE,
2668
2780
  description: "Update a dashboard's name, description, or sharing. Only the dashboard owner can update. " +
2669
2781
  "Returns the full dashboard object (same format as get dashboard), plus shared_along and audience_cannot_open. " +
2670
2782
  SHARE_ALONG_RULE,
@@ -2690,6 +2802,7 @@ export function createServer(apiKey, options = {}) {
2690
2802
  });
2691
2803
  server.registerTool("lighthouse_reports_delete_dashboard", {
2692
2804
  title: "Delete Dashboard",
2805
+ annotations: DESTRUCTIVE,
2693
2806
  description: "Permanently delete a dashboard and all its widgets. Only the dashboard owner can delete.",
2694
2807
  inputSchema: {
2695
2808
  id: z.string().uuid().describe("Dashboard ID"),
@@ -2705,6 +2818,7 @@ export function createServer(apiKey, options = {}) {
2705
2818
  });
2706
2819
  server.registerTool("lighthouse_reports_list_widgets", {
2707
2820
  title: "List Dashboard Widgets",
2821
+ annotations: READ_ONLY,
2708
2822
  description: "Get all widgets for a dashboard. " +
2709
2823
  "Each widget includes: id, name, widget_type, config, created_at, updated_at.",
2710
2824
  inputSchema: {
@@ -2722,6 +2836,7 @@ export function createServer(apiKey, options = {}) {
2722
2836
  });
2723
2837
  server.registerTool("lighthouse_reports_create_widget", {
2724
2838
  title: "Create Report Widget",
2839
+ annotations: CREATES,
2725
2840
  description: "Create a report widget (chart/table/KPI) on a dashboard. " +
2726
2841
  "WORKFLOW: 1) Call lighthouse_crm_list_attributes to discover field keys for the entity type, " +
2727
2842
  "2) Build a config object using those keys, 3) Create the widget. " +
@@ -2750,6 +2865,7 @@ export function createServer(apiKey, options = {}) {
2750
2865
  });
2751
2866
  server.registerTool("lighthouse_reports_update_widget", {
2752
2867
  title: "Rename Report Widget",
2868
+ annotations: DESTRUCTIVE,
2753
2869
  description: "Rename a report widget. Only the name can be changed, a widget's type and " +
2754
2870
  "configuration are fixed once created. To change what a widget reports, delete " +
2755
2871
  "it and create a new one. " +
@@ -2771,6 +2887,7 @@ export function createServer(apiKey, options = {}) {
2771
2887
  });
2772
2888
  server.registerTool("lighthouse_reports_delete_widget", {
2773
2889
  title: "Delete Report Widget",
2890
+ annotations: DESTRUCTIVE,
2774
2891
  description: "Permanently delete a report widget from a dashboard.",
2775
2892
  inputSchema: {
2776
2893
  dashboard_id: z.string().uuid().describe("Dashboard ID"),
@@ -2787,6 +2904,7 @@ export function createServer(apiKey, options = {}) {
2787
2904
  });
2788
2905
  server.registerTool("lighthouse_reports_get_widget_data", {
2789
2906
  title: "Get Widget Report Data",
2907
+ annotations: READ_ONLY,
2790
2908
  description: "Fetch the aggregated report data for a specific widget. " +
2791
2909
  "Returns the computed data based on the widget's config (dimensions, measures, segments, filters). " +
2792
2910
  "For standard reports: array of {label, label_is_empty, value, segment?, segment_is_empty?} rows (label = the grouped dimension value, value = the aggregated measure, segment = present only when segment_by is set). " +
@@ -2808,6 +2926,7 @@ export function createServer(apiKey, options = {}) {
2808
2926
  });
2809
2927
  server.registerTool("lighthouse_reports_share_dashboard_with_teams", {
2810
2928
  title: "Share Dashboard with Teams",
2929
+ annotations: ADDS_IF_MISSING,
2811
2930
  description: "Grant team-level access to a dashboard. Only the dashboard owner can share. " +
2812
2931
  "Use lighthouse_workspace_list_teams to find team IDs first. " +
2813
2932
  "Requires reports.edit permission. " +
@@ -2832,6 +2951,7 @@ export function createServer(apiKey, options = {}) {
2832
2951
  });
2833
2952
  server.registerTool("lighthouse_reports_revoke_dashboard_from_teams", {
2834
2953
  title: "Revoke Dashboard Team Access",
2954
+ annotations: DESTRUCTIVE,
2835
2955
  description: "Revoke team-level access from a dashboard. Only the dashboard owner can revoke. " +
2836
2956
  "Requires reports.edit permission.",
2837
2957
  inputSchema: {
@@ -2850,6 +2970,7 @@ export function createServer(apiKey, options = {}) {
2850
2970
  });
2851
2971
  server.registerTool("lighthouse_crm_share_view_with_teams", {
2852
2972
  title: "Share View with Teams",
2973
+ annotations: ADDS_IF_MISSING,
2853
2974
  description: "Grant team-level access to a CRM view or list view. Only the view owner can share. " +
2854
2975
  "Use lighthouse_workspace_list_teams to find team IDs first. " +
2855
2976
  "Requires views.share permission. " +
@@ -2875,6 +2996,7 @@ export function createServer(apiKey, options = {}) {
2875
2996
  });
2876
2997
  server.registerTool("lighthouse_crm_revoke_view_from_teams", {
2877
2998
  title: "Revoke View Team Access",
2999
+ annotations: DESTRUCTIVE,
2878
3000
  description: "Revoke team-level access from a CRM view or list view. Only the view owner can revoke. " +
2879
3001
  "Requires views.share permission. " +
2880
3002
  "Returns formatted view: {id, name, record_type, list_id, view_type, filters, sort, grouped_by, sharing, owner, is_owner, shared_with_teams, created_at}, where is_owner says whether the API key's user owns the view.",
@@ -2894,6 +3016,7 @@ export function createServer(apiKey, options = {}) {
2894
3016
  });
2895
3017
  server.registerTool("lighthouse_crm_update_view_sharing", {
2896
3018
  title: "Update View Sharing Status",
3019
+ annotations: DESTRUCTIVE,
2897
3020
  description: "Change a CRM view or list view's sharing visibility. Setting to 'private' also removes all team permissions. " +
2898
3021
  "Automatically detects view type. Only the view owner can change sharing. Requires views.share permission. " +
2899
3022
  "Returns formatted view: {id, name, record_type, list_id, view_type, filters, sort, grouped_by, sharing, owner, is_owner, shared_with_teams, created_at, shared_along, audience_cannot_open}, where is_owner says whether the API key's user owns the view. " +
@@ -2918,6 +3041,7 @@ export function createServer(apiKey, options = {}) {
2918
3041
  });
2919
3042
  server.registerTool("lighthouse_inbox_email_accounts", {
2920
3043
  title: "List Email Accounts",
3044
+ annotations: READ_ONLY,
2921
3045
  description: "List the email accounts the user can access: their own connected mailbox plus mailboxes other workspace " +
2922
3046
  "members shared with them. One account object on all three channels: { id, name (the owner's display name), " +
2923
3047
  "email (the mailbox address), is_owner (true when the account belongs to the API key's user, false for a colleague's), " +
@@ -2943,6 +3067,7 @@ export function createServer(apiKey, options = {}) {
2943
3067
  });
2944
3068
  server.registerTool("lighthouse_inbox_linkedin_accounts", {
2945
3069
  title: "List LinkedIn Accounts",
3070
+ annotations: READ_ONLY,
2946
3071
  description: "List the LinkedIn messaging accounts the user can access: their own connected account plus accounts other " +
2947
3072
  "workspace members shared with them. The same account object the email and calendar channels return: " +
2948
3073
  "{ id, name (the owner's display name), email (always null on LinkedIn), is_owner (true when the account belongs " +
@@ -2967,6 +3092,7 @@ export function createServer(apiKey, options = {}) {
2967
3092
  });
2968
3093
  server.registerTool("lighthouse_inbox_email_threads", {
2969
3094
  title: "List Email Threads",
3095
+ annotations: READ_ONLY,
2970
3096
  description: "List email threads (conversations) across every email account the user can read, newest activity first. Filters are " +
2971
3097
  "all optional and combinable: record_id + record_type ('company' or 'person' ONLY) for correspondence with " +
2972
3098
  "a CRM record (person = their email addresses, company = its domains); emails / domains (comma separated lists); " +
@@ -3007,6 +3133,7 @@ export function createServer(apiKey, options = {}) {
3007
3133
  });
3008
3134
  server.registerTool("lighthouse_inbox_email_thread", {
3009
3135
  title: "Get Email Thread",
3136
+ annotations: READ_ONLY,
3010
3137
  description: "Read one email thread: data.thread (the list's row) plus data.messages, newest first by default. Each message: " +
3011
3138
  "{ id, thread_id, date, direction (sent | received), from { name, email }, to / cc / bcc / reply_to [{ name, email }], " +
3012
3139
  "subject, snippet, text (the plain words of THIS message, with the quoted history of a reply removed and a forwarded block kept), " +
@@ -3046,6 +3173,7 @@ export function createServer(apiKey, options = {}) {
3046
3173
  });
3047
3174
  server.registerTool("lighthouse_inbox_email_attachment", {
3048
3175
  title: "Read Email Attachment",
3176
+ annotations: READ_ONLY,
3049
3177
  description: "What one email attachment SAYS, as text: PDFs, Word, Excel and PowerPoint files (and their OpenDocument versions), text, CSV and HTML. " +
3050
3178
  "WHEN TO USE: the user asks what an attached file contains (\"summarise the report Jane sent\", \"what are the numbers in the attached model\", \"check the attachment on that email\"). " +
3051
3179
  "Find the message first: lighthouse_inbox_email_threads (a subject, a sender, a date), then lighthouse_inbox_email_thread, whose messages carry attachments [{ id, message_id, filename, ... }] and account { id }. Pass that attachment's id as attachment_id, its message_id, and the message's account.id as account_id. " +
@@ -3078,6 +3206,7 @@ export function createServer(apiKey, options = {}) {
3078
3206
  });
3079
3207
  server.registerTool("lighthouse_inbox_email_create_draft", {
3080
3208
  title: "Create Email Draft",
3209
+ annotations: CREATES,
3081
3210
  description: "Save an email draft in one of the user's mailboxes. The draft is NOT sent: it lands in the mailbox for the " +
3082
3211
  "user to review, edit and send themselves, and there is no tool that can send it. Use this to prepare a reply " +
3083
3212
  "or a new email on the user's behalf. account_id is REQUIRED (from lighthouse_inbox_email_accounts) and must " +
@@ -3113,6 +3242,7 @@ export function createServer(apiKey, options = {}) {
3113
3242
  });
3114
3243
  server.registerTool("lighthouse_inbox_linkedin_chats", {
3115
3244
  title: "List LinkedIn Chats",
3245
+ annotations: READ_ONLY,
3116
3246
  description: "List LinkedIn conversations across every LinkedIn account the user can view, newest activity first, " +
3117
3247
  "paginated. Filters are all optional and combinable: record_id + record_type ('company' or 'person' ONLY, where a " +
3118
3248
  "company matches its team members and company page), person_linkedin / company_linkedin (LinkedIn URL or " +
@@ -3154,6 +3284,7 @@ export function createServer(apiKey, options = {}) {
3154
3284
  });
3155
3285
  server.registerTool("lighthouse_inbox_linkedin_chat_messages", {
3156
3286
  title: "Get LinkedIn Chat Messages",
3287
+ annotations: READ_ONLY,
3157
3288
  description: "Read one LinkedIn conversation: returns the chat object (the same object lighthouse_inbox_linkedin_chats " +
3158
3289
  "returns) plus its messages, paginated, newest first by default (order 'asc' for oldest first), both sides " +
3159
3290
  "or one (direction 'sent' = what the account sent, 'received' = what the other people sent), with optional " +
@@ -3196,6 +3327,7 @@ export function createServer(apiKey, options = {}) {
3196
3327
  });
3197
3328
  server.registerTool("lighthouse_inbox_calendar_accounts", {
3198
3329
  title: "List Calendar Accounts",
3330
+ annotations: READ_ONLY,
3199
3331
  description: "Calendars the API key's user can read: their own connected calendar (is_owner true) and colleagues' calendars shared with them. " +
3200
3332
  "The same account object the email and LinkedIn channels return: { id, name, email, is_owner, provider (google | microsoft | other), " +
3201
3333
  "status (connected | pending | disconnected), status_reason (credentials | error | stopped | other; null unless disconnected), " +
@@ -3218,6 +3350,7 @@ export function createServer(apiKey, options = {}) {
3218
3350
  });
3219
3351
  server.registerTool("lighthouse_inbox_calendar_meetings", {
3220
3352
  title: "List Meetings",
3353
+ annotations: READ_ONLY,
3221
3354
  description: "Calendar meetings across every calendar the API key's user can read (their own, plus colleagues' calendars shared with them), read live from the calendar. PAST meetings newest first by default (the last 6 months; pass months up to 36, or after / before, to move the window), or the SCHEDULED ones soonest first with upcoming=true (90 days ahead by default; an explicit before reaches further). " +
3222
3355
  "Narrow by CRM record (record_id + record_type, company or person: the record's addresses and domains are matched), by emails / domains lists (comma separated), by words in the meeting TITLE (query; never the notes), or by one calendar (account_id from lighthouse_inbox_calendar_accounts). No filter lists the user's whole calendar over the window. " +
3223
3356
  "WHEN TO USE: \"when did we last meet X\", \"what meetings do I have next week\", \"have we talked to anyone at acme.com\", \"which calls with this company were recorded\". To find what was DISCUSSED about a topic, list the meetings and read the notes of the likely ones. " +
@@ -3251,6 +3384,7 @@ export function createServer(apiKey, options = {}) {
3251
3384
  });
3252
3385
  server.registerTool("lighthouse_inbox_calendar_meeting", {
3253
3386
  title: "Get Recorded Meeting",
3387
+ annotations: READ_ONLY,
3254
3388
  description: "One recorded meeting's metadata by meeting_key (" + MEETING_KEY_SOURCES + "): { meeting_key, ical_uid, title, occurred_at, meeting_url (the video link), recording { has_recording, has_transcript, has_notes, duration_seconds, language, recorded_by } (the same object the meetings list carries; recorded_by is the recorder as a workspace member, or null), notes { id, status, created_at }: the note lighthouse_inbox_calendar_meeting_notes returns, or null when there is none the user may read }. The title and the time are the same here as on the meetings list and inside the notes. " +
3255
3389
  "WHEN TO USE: to check whether a transcript or notes exist before reading them, or to answer how long a call lasted. It never returns the transcript or the notes themselves, and the API never returns the recording itself. Not found when the meeting was not recorded or the API key's user may not read it. A recurring meeting keeps the recording of its most recently recorded occurrence. " + MEETING_ACCESS_RULE,
3256
3390
  inputSchema: {
@@ -3267,6 +3401,7 @@ export function createServer(apiKey, options = {}) {
3267
3401
  });
3268
3402
  server.registerTool("lighthouse_inbox_calendar_meeting_notes", {
3269
3403
  title: "Get Meeting Notes",
3404
+ annotations: READ_ONLY,
3270
3405
  description: "The meeting notes written from one recorded meeting, by meeting_key (" + MEETING_KEY_SOURCES + "): the note in the same shape as lighthouse_crm_get_note (type 'meeting', with the meeting object and the records it is linked to that you can view), plus content_markdown (what was generated: Meeting Context first, then the sections) and is_edited (true when a member changed the note since, in which case content is the version to trust). " +
3271
3406
  "WHEN TO USE: the user asks what was discussed, decided or agreed in a specific meeting, the next steps from a call, or who attended. Lighter than the transcript: prefer it unless the answer depends on exactly what was said. " +
3272
3407
  "Not found when the meeting has no notes yet (see has_notes on the meeting) or the API key's user may not read them; for a recurring meeting the newest occurrence the user may read is returned. " + MEETING_ACCESS_RULE,
@@ -3284,6 +3419,7 @@ export function createServer(apiKey, options = {}) {
3284
3419
  });
3285
3420
  server.registerTool("lighthouse_inbox_calendar_meeting_transcript", {
3286
3421
  title: "Get Meeting Transcript",
3422
+ annotations: READ_ONLY,
3287
3423
  description: "The transcript of one recorded meeting by meeting_key (" + MEETING_KEY_SOURCES + "), as text: one \"Speaker: text\" line per utterance, in order. A note's meeting_key opens this transcript when that note's meeting.has_transcript is true. " +
3288
3424
  "WHEN TO USE: only when the answer depends on what was actually SAID (an exact quote, a number, who said what, a nuance the notes would flatten). For what was discussed or decided, lighthouse_inbox_calendar_meeting_notes is enough and far smaller. " +
3289
3425
  `A long meeting comes back in windows of max_chars (default ${TRANSCRIPT_WINDOW_CHARS}): meta.has_more true means the meeting continues past this window, so pass meta.next_offset_chars as offset_chars to read on, and never treat a window with has_more as the end of the meeting. ` +
@@ -3306,6 +3442,7 @@ export function createServer(apiKey, options = {}) {
3306
3442
  return server;
3307
3443
  server.registerTool("lighthouse_private_equity_list_saved_searches", {
3308
3444
  title: "List Private Equity Saved Searches",
3445
+ annotations: READ_ONLY,
3309
3446
  description: "List the Private Equity saved searches the user can see in this workspace (their own, the ones shared with the workspace, and the ones shared with one of their teams), ordered by name. " +
3310
3447
  "Each search carries id, name, sharing (private | workspace), shared_with_teams, is_owner, owner, created_at, updated_at, last_run_at and last_run: the stored run its results come from ({ id, created_at, last_fetched_at, result_count, match_count, created_by }, where result_count is how many companies are stored and match_count how many the search matched in the company database, or null), or null when it never ran. The search's filters are not returned. " +
3311
3448
  "READ ONLY: this reads what the workspace already stored. It never runs a search, never fetches new companies and never spends credits; a Private Equity search is run, and paid for, only in the Lighthouse app. " +
@@ -3326,6 +3463,7 @@ export function createServer(apiKey, options = {}) {
3326
3463
  });
3327
3464
  server.registerTool("lighthouse_private_equity_list_saved_search_results", {
3328
3465
  title: "List Private Equity Saved Search Results",
3466
+ annotations: READ_ONLY,
3329
3467
  description: "Read one page of the companies a Private Equity saved search's LAST run stored, in the order that run returned them. " +
3330
3468
  "Each row is a stored company: id, name, domain, website_url, linkedin_url, short_description, avatar_url, hq ({ country, country_code, region, city }) and hq_label, industry, category, sector, naics ({ code, label }), business_models, employees and revenue_usd ({ value, estimated }), founded_year, company_type, num_locations, crm_id (the id of this workspace's CRM company for it, or null), lighthouse_company (the matching company in Lighthouse's own data, or null), from_lighthouse (the keys whose value was filled from Lighthouse's own data), fetched_at, profile_updated_at, and match ({ snippets, headline }: why a keyword or product search matched it, or null). " +
3331
3469
  "meta carries total, limit, offset, has_more, has_results and run (the stored run the page is read from). A search that never ran, or whose last run is no longer stored, answers an empty page with meta.has_results false. " +
@@ -3348,6 +3486,7 @@ export function createServer(apiKey, options = {}) {
3348
3486
  });
3349
3487
  server.registerTool("lighthouse_private_equity_get_company", {
3350
3488
  title: "Get Private Equity Company",
3489
+ annotations: READ_ONLY,
3351
3490
  description: "Read one company's stored Private Equity profile by company_id: the id of a row returned by lighthouse_private_equity_list_saved_search_results (a text id, not a UUID, and not a Discovery or CRM id). " +
3352
3491
  "Returns the keys of a results row (without match) plus the full profile: legal_names, commercial_names, long_description, long_description_extracted, locations, classification ({ naics_secondary, nace, sic, isic }), target_markets, core_offerings, technology_focus, certification_focus, supply_chain_focus, business_tags, technologies and technologies_total, contact ({ primary_phone, phone_numbers, primary_email, emails }), socials, website_language, profile_created_at, is_fresh and run_count. " +
3353
3492
  "READ ONLY: the profile as a Private Equity search of this workspace stored it, never refreshed. It never fetches the company again and never spends credits. " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trylighthouse/mcp-server",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "MCP server for the Lighthouse CRM API",
5
5
  "homepage": "https://trylighthouse.vc",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "scripts": {
21
- "build": "rm -rf build && tsc && chmod 755 build/index.js && node scripts/check-no-comments.mjs",
21
+ "build": "rm -rf build && tsc && chmod 755 build/index.js && node scripts/check-no-comments.mjs && node scripts/check-tool-annotations.mjs",
22
22
  "dev": "tsc --watch",
23
23
  "test": "node --test test/*.test.mjs",
24
24
  "prepublishOnly": "npm run build"