fellow-mcp 1.0.3 → 1.0.5

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 (3) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/index.js +117 -59
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.4] - 2026-01-28
11
+
12
+ ### Fixed
13
+ - `search_meetings` now shows actual meeting times instead of "N/A" by enriching recordings with event_start from associated notes
14
+ - Automatically fetches note data from Fellow API if not in local database to get accurate meeting times
15
+ - Fixed `getNote` API method to properly parse response structure
16
+
17
+ ### Changed
18
+ - Removed confusing `created_at` field from `search_meetings` results (was showing when meeting was logged, not when it occurred)
19
+ - `search_meetings` now displays actual meeting times in local timezone for better user experience
20
+
10
21
  ## [1.0.3] - 2026-01-27
11
22
 
12
23
  ### Added
@@ -51,7 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
51
62
  - Full-text search across cached notes
52
63
  - Find meetings by participant
53
64
 
54
- [Unreleased]: https://github.com/liba2k/fellow-mcp/compare/v1.0.3...HEAD
65
+ [Unreleased]: https://github.com/liba2k/fellow-mcp/compare/v1.0.4...HEAD
66
+ [1.0.4]: https://github.com/liba2k/fellow-mcp/compare/v1.0.3...v1.0.4
55
67
  [1.0.3]: https://github.com/liba2k/fellow-mcp/compare/v1.0.2...v1.0.3
56
68
  [1.0.2]: https://github.com/liba2k/fellow-mcp/compare/v1.0.1...v1.0.2
57
69
  [1.0.1]: https://github.com/liba2k/fellow-mcp/compare/v1.0.0...v1.0.1
package/dist/index.js CHANGED
@@ -84,6 +84,40 @@ class FellowClient {
84
84
  async getRecording(recordingId) {
85
85
  return this.request("GET", `/recording/${recordingId}`);
86
86
  }
87
+ /**
88
+ * Fetch a single recording with its transcript.
89
+ * Tries GET /recording/{id} first (which may include transcript).
90
+ * If transcript is missing, falls back to POST /recordings with include.transcript
91
+ * and paginates until the recording is found.
92
+ */
93
+ async getRecordingWithTranscript(recordingId) {
94
+ // Try direct fetch first - single resource endpoints often return full object
95
+ try {
96
+ const recording = await this.getRecording(recordingId);
97
+ if (recording && recording.transcript) {
98
+ return recording;
99
+ }
100
+ // If we got the recording but no transcript, fall back to list with include
101
+ }
102
+ catch {
103
+ // Direct fetch failed, fall back to list endpoint
104
+ }
105
+ // Fall back to list with transcript include, paginating until we find it
106
+ let cursor;
107
+ do {
108
+ const resp = await this.listRecordings({
109
+ include_transcript: true,
110
+ cursor,
111
+ page_size: 50,
112
+ });
113
+ const found = resp.recordings.data.find((r) => r.id === recordingId);
114
+ if (found) {
115
+ return found;
116
+ }
117
+ cursor = resp.recordings.page_info.cursor ?? undefined;
118
+ } while (cursor);
119
+ return null;
120
+ }
87
121
  async listNotes(options) {
88
122
  const body = {};
89
123
  // Build filters
@@ -122,7 +156,8 @@ class FellowClient {
122
156
  return this.request("POST", "/notes", body);
123
157
  }
124
158
  async getNote(noteId) {
125
- return this.request("GET", `/note/${noteId}`);
159
+ const response = await this.request("GET", `/note/${noteId}`);
160
+ return response.note;
126
161
  }
127
162
  }
128
163
  // Tool definitions
@@ -589,8 +624,8 @@ function formatTranscript(transcript) {
589
624
  }
590
625
  let output = `Language: ${transcript.language_code}\n\n`;
591
626
  for (const segment of transcript.speech_segments) {
592
- const startTime = formatTime(segment.start_time);
593
- const endTime = formatTime(segment.end_time);
627
+ const startTime = formatTime(segment.start ?? segment.start_time ?? 0);
628
+ const endTime = formatTime(segment.end ?? segment.end_time ?? 0);
594
629
  output += `[${startTime} - ${endTime}] ${segment.speaker}: ${segment.text}\n`;
595
630
  }
596
631
  return output;
@@ -654,17 +689,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
654
689
  page_size: Math.min(limit ?? 20, 50),
655
690
  });
656
691
  const { subdomain } = parseArgs();
657
- const results = recordingsResp.recordings.data.map((r) => ({
658
- id: r.id,
659
- title: r.title,
660
- note_id: r.note_id,
661
- event_start: r.event_start,
662
- event_start_local: formatDateTime(r.event_start),
663
- event_end: r.event_end,
664
- created_at: r.created_at,
665
- call_url: r.call_url,
666
- event_guid: r.event_guid,
667
- fellow_url: getFellowUrl(subdomain, r.event_guid),
692
+ const db = getDatabase();
693
+ // Enrich recordings with event_start from notes
694
+ // Use Promise.all to fetch notes from API in parallel if needed
695
+ const results = await Promise.all(recordingsResp.recordings.data.map(async (r) => {
696
+ // Try to get event_start from the associated note
697
+ let eventStart = r.event_start;
698
+ if (!eventStart && r.note_id) {
699
+ // First check local database
700
+ const localNote = db.getNote(r.note_id);
701
+ if (localNote?.event_start) {
702
+ eventStart = localNote.event_start;
703
+ }
704
+ else {
705
+ // If not in DB, fetch from API
706
+ try {
707
+ const apiNote = await client.getNote(r.note_id);
708
+ if (apiNote?.event_start) {
709
+ eventStart = apiNote.event_start;
710
+ }
711
+ }
712
+ catch (error) {
713
+ // Silently fail - we'll just show N/A for event_start
714
+ }
715
+ }
716
+ }
717
+ return {
718
+ id: r.id,
719
+ title: r.title,
720
+ note_id: r.note_id,
721
+ event_start: eventStart,
722
+ event_start_local: formatDateTime(eventStart),
723
+ event_end: r.event_end,
724
+ call_url: r.call_url,
725
+ event_guid: r.event_guid,
726
+ fellow_url: getFellowUrl(subdomain, r.event_guid),
727
+ };
668
728
  }));
669
729
  return {
670
730
  content: [
@@ -683,22 +743,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
683
743
  const { recording_id, meeting_title } = args;
684
744
  let recordingWithTranscript = null;
685
745
  if (recording_id) {
686
- // Get the specific recording with transcript
687
- const recordingsResp = await client.listRecordings({
688
- include_transcript: true,
689
- page_size: 50,
690
- });
691
- recordingWithTranscript =
692
- recordingsResp.recordings.data.find((r) => r.id === recording_id) ?? null;
693
- if (!recordingWithTranscript) {
694
- // Try fetching all to find it
695
- const allRecordingsResp = await client.listRecordings({
696
- include_transcript: true,
697
- page_size: 50,
698
- });
699
- recordingWithTranscript =
700
- allRecordingsResp.recordings.data.find((r) => r.id === recording_id) ?? null;
701
- }
746
+ // Direct fetch by ID with transcript (paginates if needed)
747
+ recordingWithTranscript = await client.getRecordingWithTranscript(recording_id);
702
748
  }
703
749
  else if (meeting_title) {
704
750
  // Search by title and get transcript
@@ -739,10 +785,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
739
785
  let targetRecordingId = recording_id;
740
786
  // If recording_id provided, get the associated note_id
741
787
  if (!noteId && recording_id) {
742
- const recordingsResp = await client.listRecordings({ page_size: 50 });
743
- const recording = recordingsResp.recordings.data.find((r) => r.id === recording_id);
744
- if (recording) {
745
- noteId = recording.note_id;
788
+ try {
789
+ const recording = await client.getRecording(recording_id);
790
+ if (recording) {
791
+ noteId = recording.note_id;
792
+ }
793
+ }
794
+ catch {
795
+ // Direct fetch failed, try list as fallback
796
+ const recordingsResp = await client.listRecordings({ page_size: 50 });
797
+ const recording = recordingsResp.recordings.data.find((r) => r.id === recording_id);
798
+ if (recording) {
799
+ noteId = recording.note_id;
800
+ }
746
801
  }
747
802
  }
748
803
  // If meeting_title provided, search for the note and recording
@@ -774,16 +829,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
774
829
  let eventStart = null;
775
830
  let eventGuid = null;
776
831
  if (noteId) {
777
- const notesResp = await client.listNotes({
778
- include_content: true,
779
- page_size: 50,
780
- });
781
- const note = notesResp.notes.data.find((n) => n.id === noteId);
782
- if (note) {
783
- noteTitle = note.title;
784
- eventStart = note.event_start ?? null;
785
- eventGuid = note.event_guid ?? null;
786
- noteContent = note.content_markdown ?? null;
832
+ // Direct fetch by ID - much more reliable than listing and filtering
833
+ try {
834
+ const note = await client.getNote(noteId);
835
+ if (note) {
836
+ noteTitle = note.title;
837
+ eventStart = note.event_start ?? null;
838
+ eventGuid = note.event_guid ?? null;
839
+ noteContent = note.content_markdown ?? null;
840
+ }
841
+ }
842
+ catch {
843
+ // Note fetch failed, continue without note content
787
844
  }
788
845
  }
789
846
  // Find recording ID if we don't have it
@@ -797,11 +854,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
797
854
  // Fetch transcript
798
855
  let transcriptText = null;
799
856
  if (targetRecordingId) {
800
- const recordingsResp = await client.listRecordings({
801
- include_transcript: true,
802
- page_size: 50,
803
- });
804
- const recording = recordingsResp.recordings.data.find((r) => r.id === targetRecordingId);
857
+ // Direct fetch by ID with transcript (paginates if needed)
858
+ const recording = await client.getRecordingWithTranscript(targetRecordingId);
805
859
  if (recording?.transcript) {
806
860
  transcriptText = formatTranscript(recording.transcript);
807
861
  if (!noteTitle || noteTitle === "Unknown Meeting") {
@@ -841,11 +895,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
841
895
  const { note_id, meeting_title } = args;
842
896
  let note = null;
843
897
  if (note_id) {
844
- const notesResp = await client.listNotes({
845
- include_content: true,
846
- page_size: 50,
847
- });
848
- note = notesResp.notes.data.find((n) => n.id === note_id) ?? null;
898
+ // Direct fetch by ID
899
+ try {
900
+ note = await client.getNote(note_id);
901
+ }
902
+ catch {
903
+ note = null;
904
+ }
849
905
  }
850
906
  else if (meeting_title) {
851
907
  const notesResp = await client.listNotes({
@@ -893,11 +949,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
893
949
  const { note_id, meeting_title } = args;
894
950
  let note = null;
895
951
  if (note_id) {
896
- const notesResp = await client.listNotes({
897
- include_attendees: true,
898
- page_size: 50,
899
- });
900
- note = notesResp.notes.data.find((n) => n.id === note_id) ?? null;
952
+ // Direct fetch by ID
953
+ try {
954
+ note = await client.getNote(note_id);
955
+ }
956
+ catch {
957
+ note = null;
958
+ }
901
959
  }
902
960
  else if (meeting_title) {
903
961
  const notesResp = await client.listNotes({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fellow-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "MCP server for Fellow.ai API - access meeting data, transcripts, summaries, and action items",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",