rewind-mcp-server 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -11
- package/dist/__tests__/helpers.test.d.ts +1 -0
- package/dist/__tests__/helpers.test.js +131 -0
- package/dist/__tests__/helpers.test.js.map +1 -0
- package/dist/__tests__/image-block.live.d.ts +1 -0
- package/dist/__tests__/image-block.live.js +41 -0
- package/dist/__tests__/image-block.live.js.map +1 -0
- package/dist/__tests__/server.test.js +713 -16
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/stdio-e2e.live.d.ts +1 -0
- package/dist/__tests__/stdio-e2e.live.js +60 -0
- package/dist/__tests__/stdio-e2e.live.js.map +1 -0
- package/dist/__tests__/ui-resource.live.d.ts +1 -0
- package/dist/__tests__/ui-resource.live.js +71 -0
- package/dist/__tests__/ui-resource.live.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/prompts.js +132 -0
- package/dist/prompts.js.map +1 -1
- package/dist/resources/ui.d.ts +33 -0
- package/dist/resources/ui.js +14 -0
- package/dist/resources/ui.js.map +1 -0
- package/dist/resources.js +144 -0
- package/dist/resources.js.map +1 -1
- package/dist/server.js +55 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/collecting.js +78 -37
- package/dist/tools/collecting.js.map +1 -1
- package/dist/tools/cross-domain.js +147 -19
- package/dist/tools/cross-domain.js.map +1 -1
- package/dist/tools/debug.d.ts +6 -0
- package/dist/tools/debug.js +26 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/helpers.d.ts +81 -5
- package/dist/tools/helpers.js +138 -4
- package/dist/tools/helpers.js.map +1 -1
- package/dist/tools/listening.js +223 -52
- package/dist/tools/listening.js.map +1 -1
- package/dist/tools/reading.js +122 -22
- package/dist/tools/reading.js.map +1 -1
- package/dist/tools/running.js +87 -43
- package/dist/tools/running.js.map +1 -1
- package/dist/tools/watching.js +199 -32
- package/dist/tools/watching.js.map +1 -1
- package/dist/ui-bundles.d.ts +3 -0
- package/dist/ui-bundles.js +8 -0
- package/dist/ui-bundles.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +55 -30
- package/dist/worker.js.map +1 -1
- package/package.json +13 -2
|
@@ -13,6 +13,12 @@ async function createTestClient() {
|
|
|
13
13
|
vi.spyOn(rewindClient, 'get').mockImplementation(async (path) => {
|
|
14
14
|
return getMockResponse(path);
|
|
15
15
|
});
|
|
16
|
+
// Mock image fetches -- return a tiny fake JPEG payload so imageBlock()
|
|
17
|
+
// produces a base64 image content block in tool responses.
|
|
18
|
+
vi.spyOn(rewindClient, 'getBinaryFromUrl').mockResolvedValue({
|
|
19
|
+
bytes: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]),
|
|
20
|
+
mimeType: 'image/jpeg',
|
|
21
|
+
});
|
|
16
22
|
const server = createServer(rewindClient);
|
|
17
23
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
18
24
|
const client = new Client({
|
|
@@ -29,9 +35,27 @@ function getMockResponse(path) {
|
|
|
29
35
|
if (path === '/listening/now-playing') {
|
|
30
36
|
return {
|
|
31
37
|
is_playing: true,
|
|
32
|
-
track: {
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
track: {
|
|
39
|
+
name: 'Sabotage',
|
|
40
|
+
artist: {
|
|
41
|
+
id: 10,
|
|
42
|
+
name: 'Beastie Boys',
|
|
43
|
+
apple_music_url: 'https://music.apple.com/us/artist/beastie-boys/12',
|
|
44
|
+
},
|
|
45
|
+
album: {
|
|
46
|
+
id: 20,
|
|
47
|
+
name: 'Ill Communication',
|
|
48
|
+
image: {
|
|
49
|
+
cdn_url: 'https://cdn.rewind.rest/listening/albums/20/original.jpg',
|
|
50
|
+
thumbhash: 'x',
|
|
51
|
+
dominant_color: '#111',
|
|
52
|
+
accent_color: '#222',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
url: 'https://www.last.fm/music/Beastie+Boys/_/Sabotage',
|
|
56
|
+
apple_music_url: 'https://music.apple.com/us/album/sabotage/30',
|
|
57
|
+
preview_url: null,
|
|
58
|
+
},
|
|
35
59
|
scrobbled_at: new Date().toISOString(),
|
|
36
60
|
};
|
|
37
61
|
}
|
|
@@ -39,9 +63,24 @@ function getMockResponse(path) {
|
|
|
39
63
|
return {
|
|
40
64
|
data: [
|
|
41
65
|
{
|
|
42
|
-
track: {
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
track: {
|
|
67
|
+
id: 1,
|
|
68
|
+
name: 'Sabotage',
|
|
69
|
+
url: 'https://www.last.fm/x',
|
|
70
|
+
apple_music_url: 'https://music.apple.com/us/album/sabotage/30?i=40',
|
|
71
|
+
preview_url: null,
|
|
72
|
+
},
|
|
73
|
+
artist: { id: 10, name: 'Beastie Boys' },
|
|
74
|
+
album: {
|
|
75
|
+
id: 20,
|
|
76
|
+
name: 'Ill Communication',
|
|
77
|
+
image: {
|
|
78
|
+
cdn_url: 'https://cdn.rewind.rest/listening/albums/20/original.jpg',
|
|
79
|
+
thumbhash: 'x',
|
|
80
|
+
dominant_color: '#111',
|
|
81
|
+
accent_color: '#222',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
45
84
|
scrobbled_at: new Date().toISOString(),
|
|
46
85
|
},
|
|
47
86
|
],
|
|
@@ -53,15 +92,31 @@ function getMockResponse(path) {
|
|
|
53
92
|
unique_artists: 2100,
|
|
54
93
|
unique_albums: 3800,
|
|
55
94
|
unique_tracks: 12000,
|
|
56
|
-
|
|
95
|
+
registered_date: '2008-01-01',
|
|
57
96
|
years_tracking: 18,
|
|
97
|
+
scrobbles_per_day: 12.3,
|
|
58
98
|
};
|
|
59
99
|
}
|
|
60
100
|
if (path.startsWith('/listening/top/')) {
|
|
61
101
|
return {
|
|
62
102
|
period: '1month',
|
|
63
103
|
data: [
|
|
64
|
-
{
|
|
104
|
+
{
|
|
105
|
+
rank: 1,
|
|
106
|
+
id: 10,
|
|
107
|
+
name: 'Beastie Boys',
|
|
108
|
+
detail: 'Artist',
|
|
109
|
+
playcount: 45,
|
|
110
|
+
image: {
|
|
111
|
+
cdn_url: 'https://cdn.rewind.rest/listening/artists/10/original.jpg',
|
|
112
|
+
thumbhash: 'x',
|
|
113
|
+
dominant_color: '#111',
|
|
114
|
+
accent_color: '#222',
|
|
115
|
+
},
|
|
116
|
+
url: 'https://www.last.fm/music/Beastie+Boys',
|
|
117
|
+
apple_music_url: 'https://music.apple.com/us/artist/beastie-boys/12',
|
|
118
|
+
preview_url: null,
|
|
119
|
+
},
|
|
65
120
|
],
|
|
66
121
|
};
|
|
67
122
|
}
|
|
@@ -76,22 +131,74 @@ function getMockResponse(path) {
|
|
|
76
131
|
},
|
|
77
132
|
};
|
|
78
133
|
}
|
|
134
|
+
if (path === '/listening/genres') {
|
|
135
|
+
return {
|
|
136
|
+
data: [
|
|
137
|
+
{ period: '2026-03', genres: { Rock: 120, Pop: 80 }, total: 200 },
|
|
138
|
+
{ period: '2026-04', genres: { Rock: 90, 'Hip-Hop': 60 }, total: 150 },
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
79
142
|
if (path.match(/\/listening\/artists\/\d+/)) {
|
|
80
143
|
return {
|
|
144
|
+
id: 10,
|
|
81
145
|
name: 'Beastie Boys',
|
|
146
|
+
mbid: null,
|
|
147
|
+
url: 'https://www.last.fm/music/Beastie+Boys',
|
|
148
|
+
apple_music_url: 'https://music.apple.com/us/artist/beastie-boys/12',
|
|
82
149
|
playcount: 500,
|
|
83
150
|
scrobble_count: 500,
|
|
84
151
|
genre: 'Hip Hop',
|
|
85
|
-
|
|
86
|
-
|
|
152
|
+
image: {
|
|
153
|
+
cdn_url: 'https://cdn.rewind.rest/listening/artists/10/original.jpg',
|
|
154
|
+
thumbhash: 'x',
|
|
155
|
+
dominant_color: '#111',
|
|
156
|
+
accent_color: '#222',
|
|
157
|
+
},
|
|
158
|
+
top_albums: [
|
|
159
|
+
{
|
|
160
|
+
id: 20,
|
|
161
|
+
name: "Paul's Boutique",
|
|
162
|
+
playcount: 100,
|
|
163
|
+
apple_music_url: null,
|
|
164
|
+
image: null,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
top_tracks: [
|
|
168
|
+
{
|
|
169
|
+
id: 30,
|
|
170
|
+
name: 'Sabotage',
|
|
171
|
+
scrobble_count: 50,
|
|
172
|
+
apple_music_url: null,
|
|
173
|
+
preview_url: null,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
87
176
|
};
|
|
88
177
|
}
|
|
89
178
|
if (path.match(/\/listening\/albums\/\d+/)) {
|
|
90
179
|
return {
|
|
180
|
+
id: 20,
|
|
91
181
|
name: "Paul's Boutique",
|
|
92
|
-
|
|
182
|
+
mbid: null,
|
|
183
|
+
url: 'https://www.last.fm/music/Beastie+Boys/Paul%27s+Boutique',
|
|
184
|
+
apple_music_url: 'https://music.apple.com/us/album/pauls-boutique/40',
|
|
93
185
|
playcount: 100,
|
|
94
|
-
|
|
186
|
+
image: {
|
|
187
|
+
cdn_url: 'https://cdn.rewind.rest/listening/albums/20/original.jpg',
|
|
188
|
+
thumbhash: 'x',
|
|
189
|
+
dominant_color: '#111',
|
|
190
|
+
accent_color: '#222',
|
|
191
|
+
},
|
|
192
|
+
artist: { id: 10, name: 'Beastie Boys' },
|
|
193
|
+
tracks: [
|
|
194
|
+
{
|
|
195
|
+
id: 30,
|
|
196
|
+
name: 'Shake Your Rump',
|
|
197
|
+
scrobble_count: 20,
|
|
198
|
+
apple_music_url: null,
|
|
199
|
+
preview_url: null,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
95
202
|
};
|
|
96
203
|
}
|
|
97
204
|
// Running
|
|
@@ -123,6 +230,33 @@ function getMockResponse(path) {
|
|
|
123
230
|
city: 'Austin',
|
|
124
231
|
state: 'TX',
|
|
125
232
|
is_race: false,
|
|
233
|
+
strava_url: 'https://www.strava.com/activities/17956091264',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (path === '/running/stats/years') {
|
|
239
|
+
return {
|
|
240
|
+
data: [
|
|
241
|
+
{
|
|
242
|
+
year: 2026,
|
|
243
|
+
total_runs: 30,
|
|
244
|
+
total_distance_mi: 120,
|
|
245
|
+
total_elevation_ft: 4000,
|
|
246
|
+
total_duration_s: 54000,
|
|
247
|
+
avg_pace: '8:00/mi',
|
|
248
|
+
longest_run_mi: 13.1,
|
|
249
|
+
race_count: 1,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
year: 2025,
|
|
253
|
+
total_runs: 120,
|
|
254
|
+
total_distance_mi: 540,
|
|
255
|
+
total_elevation_ft: 18000,
|
|
256
|
+
total_duration_s: 240000,
|
|
257
|
+
avg_pace: '7:55/mi',
|
|
258
|
+
longest_run_mi: 13.1,
|
|
259
|
+
race_count: 3,
|
|
126
260
|
},
|
|
127
261
|
],
|
|
128
262
|
};
|
|
@@ -190,20 +324,31 @@ function getMockResponse(path) {
|
|
|
190
324
|
data: [
|
|
191
325
|
{
|
|
192
326
|
movie: {
|
|
327
|
+
id: 1,
|
|
193
328
|
title: 'The Royal Tenenbaums',
|
|
194
329
|
year: 2001,
|
|
195
330
|
director: 'Wes Anderson',
|
|
331
|
+
tmdb_id: 9428,
|
|
332
|
+
image: {
|
|
333
|
+
url: 'https://cdn.rewind.rest/watching/movies/1/original.jpg',
|
|
334
|
+
thumbhash: 'xhash',
|
|
335
|
+
dominant_color: '#222',
|
|
336
|
+
accent_color: '#c83',
|
|
337
|
+
},
|
|
196
338
|
},
|
|
197
339
|
watched_at: new Date().toISOString(),
|
|
198
340
|
user_rating: 9,
|
|
199
341
|
rewatch: false,
|
|
200
342
|
source: 'plex',
|
|
343
|
+
review: null,
|
|
344
|
+
review_url: 'https://letterboxd.com/patdugan/film/the-royal-tenenbaums/',
|
|
201
345
|
},
|
|
202
346
|
],
|
|
203
347
|
};
|
|
204
348
|
}
|
|
205
349
|
if (path.match(/\/watching\/movies\/\d+/)) {
|
|
206
350
|
return {
|
|
351
|
+
id: 1,
|
|
207
352
|
title: 'The Royal Tenenbaums',
|
|
208
353
|
year: 2001,
|
|
209
354
|
director: 'Wes Anderson',
|
|
@@ -211,11 +356,27 @@ function getMockResponse(path) {
|
|
|
211
356
|
genres: ['Comedy', 'Drama'],
|
|
212
357
|
duration_min: 109,
|
|
213
358
|
rating: 'R',
|
|
359
|
+
tmdb_id: 9428,
|
|
214
360
|
tmdb_rating: 7.6,
|
|
215
361
|
tagline: "Family isn't a word. It's a sentence.",
|
|
216
362
|
summary: 'A family drama.',
|
|
217
363
|
imdb_id: 'tt0265666',
|
|
218
|
-
|
|
364
|
+
image: {
|
|
365
|
+
url: 'https://cdn.rewind.rest/watching/movies/1/original.jpg',
|
|
366
|
+
thumbhash: 'xhash',
|
|
367
|
+
dominant_color: '#222',
|
|
368
|
+
accent_color: '#c83',
|
|
369
|
+
},
|
|
370
|
+
watch_history: [
|
|
371
|
+
{
|
|
372
|
+
watched_at: '2026-03-10T02:30:00.000Z',
|
|
373
|
+
user_rating: 9,
|
|
374
|
+
rewatch: false,
|
|
375
|
+
review: null,
|
|
376
|
+
review_url: 'https://letterboxd.com/patdugan/film/the-royal-tenenbaums/',
|
|
377
|
+
source: 'plex',
|
|
378
|
+
},
|
|
379
|
+
],
|
|
219
380
|
};
|
|
220
381
|
}
|
|
221
382
|
if (path === '/watching/stats') {
|
|
@@ -234,6 +395,31 @@ function getMockResponse(path) {
|
|
|
234
395
|
},
|
|
235
396
|
};
|
|
236
397
|
}
|
|
398
|
+
if (path === '/watching/stats/genres') {
|
|
399
|
+
return {
|
|
400
|
+
data: [
|
|
401
|
+
{ name: 'Drama', count: 120, percentage: 38.5 },
|
|
402
|
+
{ name: 'Comedy', count: 80, percentage: 25.6 },
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (path === '/watching/stats/decades') {
|
|
407
|
+
return {
|
|
408
|
+
data: [
|
|
409
|
+
{ decade: 2020, count: 45 },
|
|
410
|
+
{ decade: 2010, count: 90 },
|
|
411
|
+
{ decade: 2000, count: 60 },
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (path === '/watching/stats/directors') {
|
|
416
|
+
return {
|
|
417
|
+
data: [
|
|
418
|
+
{ name: 'Wes Anderson', count: 8 },
|
|
419
|
+
{ name: 'Christopher Nolan', count: 6 },
|
|
420
|
+
],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
237
423
|
if (path === '/watching/movies') {
|
|
238
424
|
return {
|
|
239
425
|
data: [
|
|
@@ -242,9 +428,18 @@ function getMockResponse(path) {
|
|
|
242
428
|
title: 'The Royal Tenenbaums',
|
|
243
429
|
year: 2001,
|
|
244
430
|
director: 'Wes Anderson',
|
|
431
|
+
directors: ['Wes Anderson'],
|
|
245
432
|
genres: ['Comedy'],
|
|
246
433
|
duration_min: 109,
|
|
434
|
+
rating: 'R',
|
|
435
|
+
tmdb_id: 9428,
|
|
247
436
|
tmdb_rating: 7.6,
|
|
437
|
+
image: {
|
|
438
|
+
url: 'https://cdn.rewind.rest/watching/movies/1/original.jpg',
|
|
439
|
+
thumbhash: 'xhash',
|
|
440
|
+
dominant_color: '#222',
|
|
441
|
+
accent_color: '#c83',
|
|
442
|
+
},
|
|
248
443
|
},
|
|
249
444
|
],
|
|
250
445
|
pagination: { page: 1, limit: 10, total: 1, total_pages: 1 },
|
|
@@ -256,6 +451,7 @@ function getMockResponse(path) {
|
|
|
256
451
|
data: [
|
|
257
452
|
{
|
|
258
453
|
id: 1,
|
|
454
|
+
discogs_id: 100,
|
|
259
455
|
title: 'Hello Nasty',
|
|
260
456
|
artists: ['Beastie Boys'],
|
|
261
457
|
year: 2009,
|
|
@@ -263,12 +459,42 @@ function getMockResponse(path) {
|
|
|
263
459
|
format_detail: 'LP, Reissue',
|
|
264
460
|
label: 'Capitol Records',
|
|
265
461
|
genres: ['Hip Hop'],
|
|
462
|
+
styles: [],
|
|
463
|
+
image: {
|
|
464
|
+
cdn_url: 'https://cdn.rewind.rest/collecting/releases/1/original.jpg',
|
|
465
|
+
thumbhash: 'x',
|
|
466
|
+
dominant_color: '#111',
|
|
467
|
+
accent_color: '#222',
|
|
468
|
+
},
|
|
266
469
|
date_added: '2024-01-15',
|
|
470
|
+
rating: null,
|
|
471
|
+
discogs_url: 'https://www.discogs.com/release/100',
|
|
267
472
|
},
|
|
268
473
|
],
|
|
269
474
|
pagination: { page: 1, limit: 10, total: 1, total_pages: 1 },
|
|
270
475
|
};
|
|
271
476
|
}
|
|
477
|
+
if (path.match(/\/collecting\/vinyl\/\d+/)) {
|
|
478
|
+
return {
|
|
479
|
+
id: 1,
|
|
480
|
+
discogs_id: 100,
|
|
481
|
+
title: 'Hello Nasty',
|
|
482
|
+
artists: ['Beastie Boys'],
|
|
483
|
+
year: 2009,
|
|
484
|
+
format: 'Vinyl',
|
|
485
|
+
format_detail: 'LP, Reissue',
|
|
486
|
+
label: 'Capitol Records',
|
|
487
|
+
genres: ['Hip Hop'],
|
|
488
|
+
styles: [],
|
|
489
|
+
image: {
|
|
490
|
+
cdn_url: 'https://cdn.rewind.rest/collecting/releases/1/original.jpg',
|
|
491
|
+
},
|
|
492
|
+
date_added: '2024-01-15',
|
|
493
|
+
rating: null,
|
|
494
|
+
discogs_url: 'https://www.discogs.com/release/100',
|
|
495
|
+
tracklist: [],
|
|
496
|
+
};
|
|
497
|
+
}
|
|
272
498
|
if (path === '/collecting/stats') {
|
|
273
499
|
return {
|
|
274
500
|
data: {
|
|
@@ -276,9 +502,11 @@ function getMockResponse(path) {
|
|
|
276
502
|
by_format: { vinyl: 120, cd: 25, cassette: 5 },
|
|
277
503
|
wantlist_count: 30,
|
|
278
504
|
unique_artists: 95,
|
|
505
|
+
estimated_value: null,
|
|
279
506
|
top_genre: 'Rock',
|
|
280
507
|
oldest_release_year: 1967,
|
|
281
508
|
newest_release_year: 2025,
|
|
509
|
+
most_collected_artist: null,
|
|
282
510
|
added_this_year: 12,
|
|
283
511
|
},
|
|
284
512
|
};
|
|
@@ -290,14 +518,48 @@ function getMockResponse(path) {
|
|
|
290
518
|
id: 1,
|
|
291
519
|
title: 'The Killing',
|
|
292
520
|
year: 1956,
|
|
293
|
-
|
|
521
|
+
tmdb_id: 100,
|
|
522
|
+
imdb_id: null,
|
|
523
|
+
image: {
|
|
524
|
+
cdn_url: 'https://cdn.rewind.rest/collecting/releases/1/original.jpg',
|
|
525
|
+
},
|
|
526
|
+
runtime: null,
|
|
294
527
|
tmdb_rating: 7.6,
|
|
528
|
+
media_type: 'bluray',
|
|
529
|
+
resolution: null,
|
|
530
|
+
hdr: null,
|
|
531
|
+
audio: null,
|
|
532
|
+
audio_channels: null,
|
|
295
533
|
collected_at: '2026-03-26',
|
|
296
534
|
},
|
|
297
535
|
],
|
|
298
536
|
pagination: { page: 1, limit: 10, total: 1, total_pages: 1 },
|
|
299
537
|
};
|
|
300
538
|
}
|
|
539
|
+
if (path.match(/\/collecting\/media\/\d+$/)) {
|
|
540
|
+
return {
|
|
541
|
+
id: 1,
|
|
542
|
+
title: 'The Killing',
|
|
543
|
+
year: 1956,
|
|
544
|
+
tmdb_id: 100,
|
|
545
|
+
imdb_id: null,
|
|
546
|
+
tagline: null,
|
|
547
|
+
summary: null,
|
|
548
|
+
image: {
|
|
549
|
+
cdn_url: 'https://cdn.rewind.rest/collecting/releases/1/original.jpg',
|
|
550
|
+
},
|
|
551
|
+
runtime: null,
|
|
552
|
+
tmdb_rating: 7.6,
|
|
553
|
+
content_rating: null,
|
|
554
|
+
media_type: 'bluray',
|
|
555
|
+
resolution: null,
|
|
556
|
+
hdr: null,
|
|
557
|
+
audio: null,
|
|
558
|
+
audio_channels: null,
|
|
559
|
+
collected_at: '2026-03-26',
|
|
560
|
+
watch_history: [],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
301
563
|
if (path === '/collecting/media/formats') {
|
|
302
564
|
return {
|
|
303
565
|
data: [
|
|
@@ -315,15 +577,37 @@ function getMockResponse(path) {
|
|
|
315
577
|
id: 1,
|
|
316
578
|
title: 'How to Build an MCP Server',
|
|
317
579
|
author: 'Anthropic',
|
|
580
|
+
url: 'https://www.anthropic.com/blog/how-to-build-an-mcp-server',
|
|
318
581
|
domain: 'anthropic.com',
|
|
319
582
|
estimated_read_min: 12,
|
|
320
583
|
status: 'archived',
|
|
321
584
|
progress: 1,
|
|
585
|
+
image: {
|
|
586
|
+
cdn_url: 'https://cdn.rewind.rest/reading/articles/1/original.jpg',
|
|
587
|
+
thumbhash: 'x',
|
|
588
|
+
dominant_color: '#111',
|
|
589
|
+
accent_color: '#222',
|
|
590
|
+
},
|
|
322
591
|
saved_at: new Date().toISOString(),
|
|
323
592
|
},
|
|
324
593
|
],
|
|
325
594
|
};
|
|
326
595
|
}
|
|
596
|
+
if (path.match(/\/reading\/articles\/\d+/)) {
|
|
597
|
+
return {
|
|
598
|
+
id: 1,
|
|
599
|
+
title: 'How to Build an MCP Server',
|
|
600
|
+
author: 'Anthropic',
|
|
601
|
+
url: 'https://www.anthropic.com/blog/how-to-build-an-mcp-server',
|
|
602
|
+
domain: 'anthropic.com',
|
|
603
|
+
estimated_read_min: 12,
|
|
604
|
+
status: 'archived',
|
|
605
|
+
progress: 1,
|
|
606
|
+
image: null,
|
|
607
|
+
highlights: [],
|
|
608
|
+
saved_at: new Date().toISOString(),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
327
611
|
if (path === '/reading/highlights') {
|
|
328
612
|
return {
|
|
329
613
|
data: [
|
|
@@ -332,9 +616,11 @@ function getMockResponse(path) {
|
|
|
332
616
|
note: null,
|
|
333
617
|
created_at: '2026-01-15',
|
|
334
618
|
article: {
|
|
619
|
+
id: 99,
|
|
335
620
|
title: 'Less is More',
|
|
336
621
|
author: 'Jeff Atwood',
|
|
337
622
|
domain: 'blog.codinghorror.com',
|
|
623
|
+
url: 'https://blog.codinghorror.com/less-is-more/',
|
|
338
624
|
},
|
|
339
625
|
},
|
|
340
626
|
],
|
|
@@ -347,9 +633,11 @@ function getMockResponse(path) {
|
|
|
347
633
|
note: 'Leonardo da Vinci',
|
|
348
634
|
created_at: '2025-06-01',
|
|
349
635
|
article: {
|
|
636
|
+
id: 100,
|
|
350
637
|
title: 'Design Principles',
|
|
351
638
|
author: null,
|
|
352
639
|
domain: 'example.com',
|
|
640
|
+
url: 'https://example.com/design-principles',
|
|
353
641
|
},
|
|
354
642
|
};
|
|
355
643
|
}
|
|
@@ -404,11 +692,19 @@ function getMockResponse(path) {
|
|
|
404
692
|
{
|
|
405
693
|
domain: 'listening',
|
|
406
694
|
entity_type: 'artist',
|
|
695
|
+
entity_id: '10',
|
|
407
696
|
title: 'Beastie Boys',
|
|
408
697
|
subtitle: null,
|
|
409
698
|
},
|
|
699
|
+
{
|
|
700
|
+
domain: 'watching',
|
|
701
|
+
entity_type: 'movie',
|
|
702
|
+
entity_id: '1',
|
|
703
|
+
title: 'The Royal Tenenbaums',
|
|
704
|
+
subtitle: '2001',
|
|
705
|
+
},
|
|
410
706
|
],
|
|
411
|
-
pagination: { total:
|
|
707
|
+
pagination: { total: 2 },
|
|
412
708
|
};
|
|
413
709
|
}
|
|
414
710
|
// Health
|
|
@@ -457,6 +753,8 @@ describe('MCP Server', () => {
|
|
|
457
753
|
expect(names).toContain('get_vinyl_collection');
|
|
458
754
|
expect(names).toContain('get_physical_media');
|
|
459
755
|
expect(names).toContain('search');
|
|
756
|
+
expect(names).toContain('semantic_search');
|
|
757
|
+
expect(names).toContain('find_similar_articles');
|
|
460
758
|
expect(names).toContain('get_feed');
|
|
461
759
|
});
|
|
462
760
|
it('all tools have readOnlyHint annotation', async () => {
|
|
@@ -472,11 +770,24 @@ describe('MCP Server', () => {
|
|
|
472
770
|
});
|
|
473
771
|
it('lists prompts', async () => {
|
|
474
772
|
const { prompts } = await client.listPrompts();
|
|
475
|
-
expect(prompts.length).toBe(
|
|
773
|
+
expect(prompts.length).toBe(7);
|
|
476
774
|
const names = prompts.map((p) => p.name);
|
|
477
775
|
expect(names).toContain('weekly-summary');
|
|
478
776
|
expect(names).toContain('year-in-review');
|
|
479
777
|
expect(names).toContain('compare-periods');
|
|
778
|
+
expect(names).toContain('letterboxd-review-draft');
|
|
779
|
+
expect(names).toContain('training-report');
|
|
780
|
+
expect(names).toContain('film-diet');
|
|
781
|
+
expect(names).toContain('find-article');
|
|
782
|
+
});
|
|
783
|
+
it('exposes server instructions', () => {
|
|
784
|
+
const instructions = client.getInstructions();
|
|
785
|
+
expect(instructions).toBeTruthy();
|
|
786
|
+
expect(instructions).toContain('Rewind');
|
|
787
|
+
expect(instructions).toContain('listening');
|
|
788
|
+
expect(instructions).toContain('resource link');
|
|
789
|
+
// Keep under 2KB (Claude Code truncates above that)
|
|
790
|
+
expect(instructions.length).toBeLessThan(2048);
|
|
480
791
|
});
|
|
481
792
|
});
|
|
482
793
|
describe('tool responses are text, not JSON', () => {
|
|
@@ -610,5 +921,391 @@ describe('MCP Server', () => {
|
|
|
610
921
|
expect(text).toContain('2025');
|
|
611
922
|
});
|
|
612
923
|
});
|
|
924
|
+
describe('Phase 1 -- watching rich responses', () => {
|
|
925
|
+
it('lists the three new aggregate watching tools', async () => {
|
|
926
|
+
const { tools } = await client.listTools();
|
|
927
|
+
const names = tools.map((t) => t.name);
|
|
928
|
+
expect(names).toContain('get_watching_genres');
|
|
929
|
+
expect(names).toContain('get_watching_decades');
|
|
930
|
+
expect(names).toContain('get_watching_directors');
|
|
931
|
+
});
|
|
932
|
+
it('get_movie_details returns text, image, resource_link, and structuredContent', async () => {
|
|
933
|
+
const result = await client.callTool({
|
|
934
|
+
name: 'get_movie_details',
|
|
935
|
+
arguments: { id: 1 },
|
|
936
|
+
});
|
|
937
|
+
const content = result.content;
|
|
938
|
+
const textBlock = content.find((b) => b.type === 'text');
|
|
939
|
+
const image = content.find((b) => b.type === 'image');
|
|
940
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
941
|
+
expect(textBlock?.text).toContain('The Royal Tenenbaums');
|
|
942
|
+
expect(image?.mimeType).toBe('image/jpeg');
|
|
943
|
+
expect(image?.data).toBeTruthy();
|
|
944
|
+
expect(link?.uri).toContain('letterboxd.com');
|
|
945
|
+
const structured = result
|
|
946
|
+
.structuredContent;
|
|
947
|
+
expect(structured?.id).toBe(1);
|
|
948
|
+
});
|
|
949
|
+
it('get_movie_details with include_images=false omits image blocks', async () => {
|
|
950
|
+
const result = await client.callTool({
|
|
951
|
+
name: 'get_movie_details',
|
|
952
|
+
arguments: { id: 1, include_images: false },
|
|
953
|
+
});
|
|
954
|
+
const content = result.content;
|
|
955
|
+
expect(content.find((b) => b.type === 'image')).toBeUndefined();
|
|
956
|
+
// resource_link should still be present
|
|
957
|
+
expect(content.find((b) => b.type === 'resource_link')).toBeDefined();
|
|
958
|
+
});
|
|
959
|
+
it('get_recent_watches emits posters and Letterboxd links', async () => {
|
|
960
|
+
const result = await client.callTool({
|
|
961
|
+
name: 'get_recent_watches',
|
|
962
|
+
arguments: {},
|
|
963
|
+
});
|
|
964
|
+
const content = result.content;
|
|
965
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
966
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
967
|
+
expect(link?.uri).toContain('letterboxd.com');
|
|
968
|
+
expect(link?.name).toContain('Letterboxd');
|
|
969
|
+
});
|
|
970
|
+
it('browse_movies emits posters and structuredContent with pagination', async () => {
|
|
971
|
+
const result = await client.callTool({
|
|
972
|
+
name: 'browse_movies',
|
|
973
|
+
arguments: {},
|
|
974
|
+
});
|
|
975
|
+
const content = result.content;
|
|
976
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
977
|
+
const structured = result.structuredContent;
|
|
978
|
+
expect(structured?.pagination?.total).toBe(1);
|
|
979
|
+
});
|
|
980
|
+
it('get_watching_stats returns structuredContent mirroring the API', async () => {
|
|
981
|
+
const result = await client.callTool({
|
|
982
|
+
name: 'get_watching_stats',
|
|
983
|
+
arguments: {},
|
|
984
|
+
});
|
|
985
|
+
const structured = result.structuredContent;
|
|
986
|
+
expect(structured?.total_movies).toBe(312);
|
|
987
|
+
expect(structured?.top_director).toBe('Wes Anderson');
|
|
988
|
+
// No image blocks on stats tools
|
|
989
|
+
const content = result.content;
|
|
990
|
+
expect(content.find((b) => b.type === 'image')).toBeUndefined();
|
|
991
|
+
});
|
|
992
|
+
it('get_watching_genres returns percentages and structuredContent', async () => {
|
|
993
|
+
const result = await client.callTool({
|
|
994
|
+
name: 'get_watching_genres',
|
|
995
|
+
arguments: {},
|
|
996
|
+
});
|
|
997
|
+
const text = result.content[0].text;
|
|
998
|
+
expect(text).toContain('Drama');
|
|
999
|
+
expect(text).toContain('38.5%');
|
|
1000
|
+
const structured = result.structuredContent;
|
|
1001
|
+
expect(structured?.items[0].name).toBe('Drama');
|
|
1002
|
+
});
|
|
1003
|
+
it('get_watching_decades returns decade breakdown', async () => {
|
|
1004
|
+
const result = await client.callTool({
|
|
1005
|
+
name: 'get_watching_decades',
|
|
1006
|
+
arguments: {},
|
|
1007
|
+
});
|
|
1008
|
+
const text = result.content[0].text;
|
|
1009
|
+
expect(text).toContain('2020s');
|
|
1010
|
+
expect(text).toContain('2010s');
|
|
1011
|
+
});
|
|
1012
|
+
it('get_watching_directors returns ranked directors', async () => {
|
|
1013
|
+
const result = await client.callTool({
|
|
1014
|
+
name: 'get_watching_directors',
|
|
1015
|
+
arguments: {},
|
|
1016
|
+
});
|
|
1017
|
+
const text = result.content[0].text;
|
|
1018
|
+
expect(text).toContain('Wes Anderson');
|
|
1019
|
+
expect(text).toContain('1. Wes Anderson');
|
|
1020
|
+
});
|
|
1021
|
+
it('registers movie and show entity resource templates', async () => {
|
|
1022
|
+
const { resourceTemplates } = await client.listResourceTemplates();
|
|
1023
|
+
const uris = resourceTemplates.map((t) => t.uriTemplate);
|
|
1024
|
+
expect(uris).toContain('rewind://movie/{id}');
|
|
1025
|
+
expect(uris).toContain('rewind://show/{id}');
|
|
1026
|
+
});
|
|
1027
|
+
it('reads a movie entity via rewind://movie/{id}', async () => {
|
|
1028
|
+
const result = await client.readResource({ uri: 'rewind://movie/1' });
|
|
1029
|
+
expect(result.contents.length).toBe(1);
|
|
1030
|
+
const content = result.contents[0];
|
|
1031
|
+
expect(content.mimeType).toBe('application/json');
|
|
1032
|
+
const data = JSON.parse(content.text);
|
|
1033
|
+
expect(data.title).toBe('The Royal Tenenbaums');
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
describe('Phase 2 -- listening rich responses', () => {
|
|
1037
|
+
it('lists get_listening_genres tool', async () => {
|
|
1038
|
+
const { tools } = await client.listTools();
|
|
1039
|
+
expect(tools.map((t) => t.name)).toContain('get_listening_genres');
|
|
1040
|
+
});
|
|
1041
|
+
it('get_now_playing returns text, album cover image, and Apple Music links', async () => {
|
|
1042
|
+
const result = await client.callTool({
|
|
1043
|
+
name: 'get_now_playing',
|
|
1044
|
+
arguments: {},
|
|
1045
|
+
});
|
|
1046
|
+
const content = result.content;
|
|
1047
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
1048
|
+
const links = content.filter((b) => b.type === 'resource_link');
|
|
1049
|
+
expect(links.length).toBeGreaterThanOrEqual(2);
|
|
1050
|
+
expect(links.some((b) => b.name?.includes('Apple Music'))).toBe(true);
|
|
1051
|
+
});
|
|
1052
|
+
it('get_album_details returns cover, Apple Music link, and structuredContent', async () => {
|
|
1053
|
+
const result = await client.callTool({
|
|
1054
|
+
name: 'get_album_details',
|
|
1055
|
+
arguments: { id: 20 },
|
|
1056
|
+
});
|
|
1057
|
+
const content = result.content;
|
|
1058
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
1059
|
+
expect(content.find((b) => b.type === 'resource_link' && b.name?.includes('Apple Music'))).toBeDefined();
|
|
1060
|
+
const structured = result
|
|
1061
|
+
.structuredContent;
|
|
1062
|
+
expect(structured?.id).toBe(20);
|
|
1063
|
+
});
|
|
1064
|
+
it('get_artist_details returns artist image and links', async () => {
|
|
1065
|
+
const result = await client.callTool({
|
|
1066
|
+
name: 'get_artist_details',
|
|
1067
|
+
arguments: { id: 10 },
|
|
1068
|
+
});
|
|
1069
|
+
const content = result.content;
|
|
1070
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
1071
|
+
expect(content.find((b) => b.type === 'resource_link' && b.name?.includes('Apple Music'))).toBeDefined();
|
|
1072
|
+
});
|
|
1073
|
+
it('get_recent_listens emits top-N covers and Apple Music links', async () => {
|
|
1074
|
+
const result = await client.callTool({
|
|
1075
|
+
name: 'get_recent_listens',
|
|
1076
|
+
arguments: {},
|
|
1077
|
+
});
|
|
1078
|
+
const content = result.content;
|
|
1079
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
1080
|
+
expect(content.find((b) => b.type === 'resource_link')).toBeDefined();
|
|
1081
|
+
});
|
|
1082
|
+
it('get_top_artists includes structuredContent mirroring API shape', async () => {
|
|
1083
|
+
const result = await client.callTool({
|
|
1084
|
+
name: 'get_top_artists',
|
|
1085
|
+
arguments: {},
|
|
1086
|
+
});
|
|
1087
|
+
const structured = result.structuredContent;
|
|
1088
|
+
expect(structured?.period).toBe('1month');
|
|
1089
|
+
expect(structured?.data[0].id).toBe(10);
|
|
1090
|
+
});
|
|
1091
|
+
it('get_listening_genres returns period breakdown and structuredContent', async () => {
|
|
1092
|
+
const result = await client.callTool({
|
|
1093
|
+
name: 'get_listening_genres',
|
|
1094
|
+
arguments: {},
|
|
1095
|
+
});
|
|
1096
|
+
const text = result.content[0].text;
|
|
1097
|
+
expect(text).toContain('Rock');
|
|
1098
|
+
const structured = result.structuredContent;
|
|
1099
|
+
expect(structured?.items[0].period).toBe('2026-03');
|
|
1100
|
+
});
|
|
1101
|
+
it('registers album and artist entity resource templates', async () => {
|
|
1102
|
+
const { resourceTemplates } = await client.listResourceTemplates();
|
|
1103
|
+
const uris = resourceTemplates.map((t) => t.uriTemplate);
|
|
1104
|
+
expect(uris).toContain('rewind://album/{id}');
|
|
1105
|
+
expect(uris).toContain('rewind://artist/{id}');
|
|
1106
|
+
});
|
|
1107
|
+
it('reads an album entity via rewind://album/{id}', async () => {
|
|
1108
|
+
const result = await client.readResource({ uri: 'rewind://album/20' });
|
|
1109
|
+
const content = result.contents[0];
|
|
1110
|
+
expect(content.mimeType).toBe('application/json');
|
|
1111
|
+
const data = JSON.parse(content.text);
|
|
1112
|
+
expect(data.name).toBe("Paul's Boutique");
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
describe('Phase 3 -- collecting rich responses', () => {
|
|
1116
|
+
it('get_vinyl_collection emits top-N covers + Discogs resource_links', async () => {
|
|
1117
|
+
const result = await client.callTool({
|
|
1118
|
+
name: 'get_vinyl_collection',
|
|
1119
|
+
arguments: {},
|
|
1120
|
+
});
|
|
1121
|
+
const content = result.content;
|
|
1122
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
1123
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
1124
|
+
expect(link?.uri).toContain('discogs.com');
|
|
1125
|
+
expect(link?.name).toContain('Discogs');
|
|
1126
|
+
});
|
|
1127
|
+
it('get_collecting_stats includes structuredContent mirroring the API', async () => {
|
|
1128
|
+
const result = await client.callTool({
|
|
1129
|
+
name: 'get_collecting_stats',
|
|
1130
|
+
arguments: {},
|
|
1131
|
+
});
|
|
1132
|
+
const structured = result.structuredContent;
|
|
1133
|
+
expect(structured?.total_items).toBe(150);
|
|
1134
|
+
expect(structured?.by_format.vinyl).toBe(120);
|
|
1135
|
+
});
|
|
1136
|
+
it('get_physical_media emits covers and pagination', async () => {
|
|
1137
|
+
const result = await client.callTool({
|
|
1138
|
+
name: 'get_physical_media',
|
|
1139
|
+
arguments: {},
|
|
1140
|
+
});
|
|
1141
|
+
const content = result.content;
|
|
1142
|
+
expect(content.find((b) => b.type === 'image')).toBeDefined();
|
|
1143
|
+
const structured = result.structuredContent;
|
|
1144
|
+
expect(structured?.pagination?.total).toBe(1);
|
|
1145
|
+
});
|
|
1146
|
+
it('get_physical_media_stats returns total + per-format structuredContent', async () => {
|
|
1147
|
+
const result = await client.callTool({
|
|
1148
|
+
name: 'get_physical_media_stats',
|
|
1149
|
+
arguments: {},
|
|
1150
|
+
});
|
|
1151
|
+
const structured = result.structuredContent;
|
|
1152
|
+
expect(structured?.total).toBe(97);
|
|
1153
|
+
expect(structured?.formats.length).toBe(3);
|
|
1154
|
+
});
|
|
1155
|
+
it('registers vinyl and physical-media entity templates', async () => {
|
|
1156
|
+
const { resourceTemplates } = await client.listResourceTemplates();
|
|
1157
|
+
const uris = resourceTemplates.map((t) => t.uriTemplate);
|
|
1158
|
+
expect(uris).toContain('rewind://vinyl/{id}');
|
|
1159
|
+
expect(uris).toContain('rewind://physical-media/{id}');
|
|
1160
|
+
});
|
|
1161
|
+
it('reads a vinyl entity via rewind://vinyl/{id}', async () => {
|
|
1162
|
+
const result = await client.readResource({ uri: 'rewind://vinyl/1' });
|
|
1163
|
+
const content = result.contents[0];
|
|
1164
|
+
expect(content.mimeType).toBe('application/json');
|
|
1165
|
+
const data = JSON.parse(content.text);
|
|
1166
|
+
expect(data.title).toBe('Hello Nasty');
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
describe('Phase 4 -- reading rich responses', () => {
|
|
1170
|
+
it('get_recent_reads emits article URL resource_links and structuredContent', async () => {
|
|
1171
|
+
const result = await client.callTool({
|
|
1172
|
+
name: 'get_recent_reads',
|
|
1173
|
+
arguments: {},
|
|
1174
|
+
});
|
|
1175
|
+
const content = result.content;
|
|
1176
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
1177
|
+
expect(link?.uri).toContain('anthropic.com');
|
|
1178
|
+
const structured = result.structuredContent;
|
|
1179
|
+
expect(structured?.items[0].id).toBe(1);
|
|
1180
|
+
});
|
|
1181
|
+
it('get_reading_highlights emits article URLs as resource_links', async () => {
|
|
1182
|
+
const result = await client.callTool({
|
|
1183
|
+
name: 'get_reading_highlights',
|
|
1184
|
+
arguments: {},
|
|
1185
|
+
});
|
|
1186
|
+
const content = result.content;
|
|
1187
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
1188
|
+
expect(link?.uri).toContain('codinghorror.com');
|
|
1189
|
+
});
|
|
1190
|
+
it('get_random_highlight emits article URL resource_link', async () => {
|
|
1191
|
+
const result = await client.callTool({
|
|
1192
|
+
name: 'get_random_highlight',
|
|
1193
|
+
arguments: {},
|
|
1194
|
+
});
|
|
1195
|
+
const content = result.content;
|
|
1196
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
1197
|
+
expect(link?.uri).toContain('example.com');
|
|
1198
|
+
});
|
|
1199
|
+
it('get_reading_stats returns structuredContent mirroring API', async () => {
|
|
1200
|
+
const result = await client.callTool({
|
|
1201
|
+
name: 'get_reading_stats',
|
|
1202
|
+
arguments: {},
|
|
1203
|
+
});
|
|
1204
|
+
const structured = result.structuredContent;
|
|
1205
|
+
expect(structured?.total_articles).toBe(350);
|
|
1206
|
+
expect(structured?.total_highlights).toBe(420);
|
|
1207
|
+
});
|
|
1208
|
+
it('registers article entity template', async () => {
|
|
1209
|
+
const { resourceTemplates } = await client.listResourceTemplates();
|
|
1210
|
+
const uris = resourceTemplates.map((t) => t.uriTemplate);
|
|
1211
|
+
expect(uris).toContain('rewind://article/{id}');
|
|
1212
|
+
});
|
|
1213
|
+
it('reads an article entity via rewind://article/{id}', async () => {
|
|
1214
|
+
const result = await client.readResource({ uri: 'rewind://article/1' });
|
|
1215
|
+
const content = result.contents[0];
|
|
1216
|
+
expect(content.mimeType).toBe('application/json');
|
|
1217
|
+
const data = JSON.parse(content.text);
|
|
1218
|
+
expect(data.title).toBe('How to Build an MCP Server');
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
describe('Phase 5 -- running rich responses', () => {
|
|
1222
|
+
it('lists the new get_running_years tool', async () => {
|
|
1223
|
+
const { tools } = await client.listTools();
|
|
1224
|
+
expect(tools.map((t) => t.name)).toContain('get_running_years');
|
|
1225
|
+
});
|
|
1226
|
+
it('get_recent_runs emits Strava resource_links', async () => {
|
|
1227
|
+
const result = await client.callTool({
|
|
1228
|
+
name: 'get_recent_runs',
|
|
1229
|
+
arguments: {},
|
|
1230
|
+
});
|
|
1231
|
+
const content = result.content;
|
|
1232
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
1233
|
+
expect(link?.uri).toContain('strava.com');
|
|
1234
|
+
});
|
|
1235
|
+
it('get_activity_details emits Strava resource_link and structuredContent', async () => {
|
|
1236
|
+
const result = await client.callTool({
|
|
1237
|
+
name: 'get_activity_details',
|
|
1238
|
+
arguments: { id: 123 },
|
|
1239
|
+
});
|
|
1240
|
+
const content = result.content;
|
|
1241
|
+
const link = content.find((b) => b.type === 'resource_link');
|
|
1242
|
+
expect(link?.uri).toContain('strava.com');
|
|
1243
|
+
expect(link?.name).toContain('Strava');
|
|
1244
|
+
const structured = result
|
|
1245
|
+
.structuredContent;
|
|
1246
|
+
expect(structured?.name).toBe('Thursday Afternoon Run');
|
|
1247
|
+
});
|
|
1248
|
+
it('get_running_stats returns structuredContent mirroring API', async () => {
|
|
1249
|
+
const result = await client.callTool({
|
|
1250
|
+
name: 'get_running_stats',
|
|
1251
|
+
arguments: {},
|
|
1252
|
+
});
|
|
1253
|
+
const structured = result.structuredContent;
|
|
1254
|
+
expect(structured?.total_runs).toBe(423);
|
|
1255
|
+
expect(structured?.eddington_number).toBe(12);
|
|
1256
|
+
});
|
|
1257
|
+
it('get_running_years returns per-year breakdown', async () => {
|
|
1258
|
+
const result = await client.callTool({
|
|
1259
|
+
name: 'get_running_years',
|
|
1260
|
+
arguments: {},
|
|
1261
|
+
});
|
|
1262
|
+
const structured = result.structuredContent;
|
|
1263
|
+
expect(structured?.items.length).toBe(2);
|
|
1264
|
+
expect(structured?.items[0].year).toBe(2026);
|
|
1265
|
+
});
|
|
1266
|
+
it('registers activity entity template', async () => {
|
|
1267
|
+
const { resourceTemplates } = await client.listResourceTemplates();
|
|
1268
|
+
const uris = resourceTemplates.map((t) => t.uriTemplate);
|
|
1269
|
+
expect(uris).toContain('rewind://activity/{id}');
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
describe('Phase 6 -- cross-domain rich responses', () => {
|
|
1273
|
+
it('search emits rewind:// resource_links per match', async () => {
|
|
1274
|
+
const result = await client.callTool({
|
|
1275
|
+
name: 'search',
|
|
1276
|
+
arguments: { query: 'beastie' },
|
|
1277
|
+
});
|
|
1278
|
+
const content = result.content;
|
|
1279
|
+
const links = content.filter((b) => b.type === 'resource_link');
|
|
1280
|
+
expect(links.length).toBeGreaterThanOrEqual(2);
|
|
1281
|
+
expect(links.map((l) => l.uri)).toContain('rewind://artist/10');
|
|
1282
|
+
expect(links.map((l) => l.uri)).toContain('rewind://movie/1');
|
|
1283
|
+
});
|
|
1284
|
+
it('search structuredContent mirrors API', async () => {
|
|
1285
|
+
const result = await client.callTool({
|
|
1286
|
+
name: 'search',
|
|
1287
|
+
arguments: { query: 'beastie' },
|
|
1288
|
+
});
|
|
1289
|
+
const structured = result.structuredContent;
|
|
1290
|
+
expect(structured?.items.length).toBe(2);
|
|
1291
|
+
expect(structured?.pagination.total).toBe(2);
|
|
1292
|
+
});
|
|
1293
|
+
it('get_feed returns structuredContent with items + pagination', async () => {
|
|
1294
|
+
const result = await client.callTool({
|
|
1295
|
+
name: 'get_feed',
|
|
1296
|
+
arguments: {},
|
|
1297
|
+
});
|
|
1298
|
+
const structured = result.structuredContent;
|
|
1299
|
+
expect(structured?.items[0].domain).toBe('listening');
|
|
1300
|
+
});
|
|
1301
|
+
it('get_on_this_day returns structuredContent grouped by year', async () => {
|
|
1302
|
+
const result = await client.callTool({
|
|
1303
|
+
name: 'get_on_this_day',
|
|
1304
|
+
arguments: {},
|
|
1305
|
+
});
|
|
1306
|
+
const structured = result.structuredContent;
|
|
1307
|
+
expect(structured?.years[0].year).toBe(2025);
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
613
1310
|
});
|
|
614
1311
|
//# sourceMappingURL=server.test.js.map
|