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.
- package/CHANGELOG.md +13 -1
- package/dist/index.js +117 -59
- 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.
|
|
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
|
-
|
|
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
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
event_start
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
//
|
|
687
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
801
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
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({
|