burn-mcp-server 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +423 -122
- package/package.json +1 -1
- package/src/index.ts +523 -154
package/dist/index.js
CHANGED
|
@@ -10,174 +10,474 @@ const zod_1 = require("zod");
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
const SUPABASE_URL = process.env.BURN_SUPABASE_URL || 'https://juqtxylquemiuvvmgbej.supabase.co';
|
|
12
12
|
const SUPABASE_ANON_KEY = process.env.BURN_SUPABASE_ANON_KEY || 'sb_publishable_reVgmmCC6ndIo6jFRMM2LQ_wujj5FrO';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// Support both old JWT token (BURN_SUPABASE_TOKEN) and new long-lived MCP token (BURN_MCP_TOKEN)
|
|
14
|
+
const MCP_TOKEN = process.env.BURN_MCP_TOKEN;
|
|
15
|
+
const LEGACY_JWT = process.env.BURN_SUPABASE_TOKEN;
|
|
16
|
+
if (!MCP_TOKEN && !LEGACY_JWT) {
|
|
17
|
+
console.error('Error: BURN_MCP_TOKEN environment variable is required.');
|
|
18
|
+
console.error('Get your token from: Burn App → Settings → MCP Server → Generate Token');
|
|
17
19
|
process.exit(1);
|
|
18
20
|
}
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
|
-
// Supabase client
|
|
22
|
+
// Supabase client — bootstrapped with anon key, session set after auth below
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
23
25
|
auth: {
|
|
24
26
|
persistSession: false,
|
|
25
|
-
autoRefreshToken:
|
|
26
|
-
},
|
|
27
|
-
global: {
|
|
28
|
-
headers: {
|
|
29
|
-
Authorization: `Bearer ${SUPABASE_TOKEN}`,
|
|
30
|
-
},
|
|
27
|
+
autoRefreshToken: true,
|
|
31
28
|
},
|
|
29
|
+
...(LEGACY_JWT ? { global: { headers: { Authorization: `Bearer ${LEGACY_JWT}` } } } : {}),
|
|
32
30
|
});
|
|
33
31
|
// ---------------------------------------------------------------------------
|
|
32
|
+
// Auth: exchange MCP token for a real Supabase session (auto-refreshes)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
async function initAuth() {
|
|
35
|
+
if (LEGACY_JWT)
|
|
36
|
+
return; // legacy mode: JWT already set in headers above
|
|
37
|
+
// Call SECURITY DEFINER function — works with anon key, no JWT needed
|
|
38
|
+
const { data, error } = await supabase.rpc('get_mcp_session', { p_token: MCP_TOKEN });
|
|
39
|
+
if (error || !data || data.length === 0) {
|
|
40
|
+
console.error('Error: Invalid or revoked BURN_MCP_TOKEN.');
|
|
41
|
+
console.error('Generate a new token in Burn App → Settings → MCP Server → Generate Token');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const { refresh_token } = data[0];
|
|
45
|
+
const { error: refreshError } = await supabase.auth.refreshSession({ refresh_token });
|
|
46
|
+
if (refreshError) {
|
|
47
|
+
console.error('Error: Failed to refresh session with stored token.', refreshError.message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
// supabase client now has a valid session and will auto-refresh going forward
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
34
53
|
// MCP Server
|
|
35
54
|
// ---------------------------------------------------------------------------
|
|
36
55
|
const server = new mcp_js_1.McpServer({
|
|
37
56
|
name: 'burn-mcp-server',
|
|
38
|
-
version: '1.
|
|
57
|
+
version: '1.2.0',
|
|
39
58
|
});
|
|
40
59
|
// ---------------------------------------------------------------------------
|
|
41
|
-
//
|
|
60
|
+
// Helper: standard text result
|
|
42
61
|
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.or(`title.ilike.%${query}%,ai_takeaway.cs.{${query}},tags.cs.{${query}}`)
|
|
52
|
-
.order('vaulted_at', { ascending: false })
|
|
53
|
-
.limit(limit || 10);
|
|
54
|
-
if (error) {
|
|
55
|
-
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
56
|
-
}
|
|
62
|
+
function textResult(text) {
|
|
63
|
+
return { content: [{ type: 'text', text }] };
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Helper: extract fields from content_metadata JSONB
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function meta(row) {
|
|
69
|
+
const m = row.content_metadata || {};
|
|
57
70
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
id: row.id,
|
|
72
|
+
url: row.url,
|
|
73
|
+
title: row.title,
|
|
74
|
+
author: m.author || row.author || null,
|
|
75
|
+
platform: row.platform,
|
|
76
|
+
status: row.status,
|
|
77
|
+
tags: m.tags || [],
|
|
78
|
+
thumbnail: m.thumbnail || null,
|
|
79
|
+
vaultCategory: m.vault_category || null,
|
|
80
|
+
vaultedAt: m.vaulted_at || null,
|
|
81
|
+
aiPositioning: m.ai_positioning || null,
|
|
82
|
+
aiDensity: m.ai_density || null,
|
|
83
|
+
aiMinutes: m.ai_minutes || null,
|
|
84
|
+
aiTakeaway: m.ai_takeaway || [],
|
|
85
|
+
aiStrategyReason: m.ai_strategy_reason || null,
|
|
86
|
+
aiHowToRead: m.ai_how_to_read || null,
|
|
87
|
+
aiOverlap: m.ai_overlap || null,
|
|
88
|
+
aiVerdict: m.ai_verdict || null,
|
|
89
|
+
aiSummary: m.ai_summary || null,
|
|
90
|
+
sparkInsight: m.spark_insight || null,
|
|
91
|
+
extractedContent: m.extracted_content || null,
|
|
92
|
+
externalURL: m.external_url || null,
|
|
93
|
+
aiRelevance: m.ai_relevance || null,
|
|
94
|
+
aiNovelty: m.ai_novelty || null,
|
|
95
|
+
createdAt: row.created_at,
|
|
96
|
+
countdownExpiresAt: row.countdown_expires_at,
|
|
97
|
+
readAt: row.read_at,
|
|
62
98
|
};
|
|
63
|
-
}
|
|
99
|
+
}
|
|
100
|
+
/** Compact summary for list views (no extracted content) */
|
|
101
|
+
function metaSummary(row) {
|
|
102
|
+
const m = row.content_metadata || {};
|
|
103
|
+
return {
|
|
104
|
+
id: row.id,
|
|
105
|
+
url: row.url,
|
|
106
|
+
title: row.title,
|
|
107
|
+
author: m.author || null,
|
|
108
|
+
platform: row.platform,
|
|
109
|
+
tags: m.tags || [],
|
|
110
|
+
vaultCategory: m.vault_category || null,
|
|
111
|
+
vaultedAt: m.vaulted_at || null,
|
|
112
|
+
aiPositioning: m.ai_positioning || null,
|
|
113
|
+
aiTakeaway: m.ai_takeaway || [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
64
116
|
// ---------------------------------------------------------------------------
|
|
65
|
-
// Tool
|
|
117
|
+
// Tool handlers
|
|
66
118
|
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}, async ({ id }) => {
|
|
119
|
+
async function handleSearchVault(args) {
|
|
120
|
+
const { query, limit } = args;
|
|
70
121
|
const { data, error } = await supabase
|
|
71
122
|
.from('bookmarks')
|
|
72
123
|
.select('*')
|
|
73
|
-
.eq('
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
.eq('status', 'absorbed')
|
|
125
|
+
.ilike('title', `%${query}%`)
|
|
126
|
+
.order('created_at', { ascending: false })
|
|
127
|
+
.limit(limit || 10);
|
|
128
|
+
if (error)
|
|
129
|
+
return textResult(`Error: ${error.message}`);
|
|
130
|
+
// Also search in content_metadata tags and takeaway
|
|
131
|
+
let results = (data || []).map(metaSummary);
|
|
132
|
+
// If title search returned few results, also search by tag
|
|
133
|
+
if (results.length < (limit || 10)) {
|
|
134
|
+
const { data: tagData } = await supabase
|
|
135
|
+
.from('bookmarks')
|
|
136
|
+
.select('*')
|
|
137
|
+
.eq('status', 'absorbed')
|
|
138
|
+
.order('created_at', { ascending: false })
|
|
139
|
+
.limit(50);
|
|
140
|
+
if (tagData) {
|
|
141
|
+
const existingIds = new Set(results.map((r) => r.id));
|
|
142
|
+
const tagMatches = tagData
|
|
143
|
+
.filter((row) => {
|
|
144
|
+
if (existingIds.has(row.id))
|
|
145
|
+
return false;
|
|
146
|
+
const m = row.content_metadata || {};
|
|
147
|
+
const tags = (m.tags || []);
|
|
148
|
+
const takeaway = (m.ai_takeaway || []);
|
|
149
|
+
const positioning = m.ai_positioning || '';
|
|
150
|
+
const allText = [...tags, ...takeaway, positioning].join(' ').toLowerCase();
|
|
151
|
+
return allText.includes(query.toLowerCase());
|
|
152
|
+
})
|
|
153
|
+
.map(metaSummary);
|
|
154
|
+
results = [...results, ...tagMatches].slice(0, limit || 10);
|
|
155
|
+
}
|
|
77
156
|
}
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
text: JSON.stringify(data, null, 2),
|
|
82
|
-
}],
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Tool: list_categories
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
server.tool('list_categories', 'List all Vault categories with article counts', {}, async () => {
|
|
157
|
+
return textResult(JSON.stringify(results, null, 2));
|
|
158
|
+
}
|
|
159
|
+
async function handleGetBookmark(args) {
|
|
89
160
|
const { data, error } = await supabase
|
|
90
161
|
.from('bookmarks')
|
|
91
|
-
.select('
|
|
92
|
-
.eq('
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
162
|
+
.select('*')
|
|
163
|
+
.eq('id', args.id)
|
|
164
|
+
.single();
|
|
165
|
+
if (error)
|
|
166
|
+
return textResult(`Error: ${error.message}`);
|
|
167
|
+
return textResult(JSON.stringify(meta(data), null, 2));
|
|
168
|
+
}
|
|
169
|
+
async function handleListCategories() {
|
|
170
|
+
const { data, error } = await supabase
|
|
171
|
+
.from('bookmarks')
|
|
172
|
+
.select('content_metadata')
|
|
173
|
+
.eq('status', 'absorbed');
|
|
174
|
+
if (error)
|
|
175
|
+
return textResult(`Error: ${error.message}`);
|
|
97
176
|
const counts = {};
|
|
98
177
|
for (const row of data || []) {
|
|
99
|
-
const cat = row.vault_category || 'Uncategorized';
|
|
178
|
+
const cat = row.content_metadata?.vault_category || 'Uncategorized';
|
|
100
179
|
counts[cat] = (counts[cat] || 0) + 1;
|
|
101
180
|
}
|
|
102
181
|
const categories = Object.entries(counts)
|
|
103
182
|
.map(([category, count]) => ({ category, count }))
|
|
104
183
|
.sort((a, b) => b.count - a.count);
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
text: JSON.stringify(categories, null, 2),
|
|
109
|
-
}],
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Tool: get_clusters
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
server.tool('get_clusters', 'Get AI-generated topic clusters from your Vault', {}, async () => {
|
|
184
|
+
return textResult(JSON.stringify(categories, null, 2));
|
|
185
|
+
}
|
|
186
|
+
async function handleGetCollections() {
|
|
116
187
|
const { data, error } = await supabase
|
|
117
|
-
.from('
|
|
118
|
-
.select('
|
|
188
|
+
.from('collections')
|
|
189
|
+
.select('id, name, bookmark_ids, ai_overview');
|
|
190
|
+
if (error)
|
|
191
|
+
return textResult(`Error: ${error.message}`);
|
|
192
|
+
const collections = (data || []).map((c) => ({
|
|
193
|
+
id: c.id,
|
|
194
|
+
name: c.name,
|
|
195
|
+
articleCount: Array.isArray(c.bookmark_ids) ? c.bookmark_ids.length : 0,
|
|
196
|
+
overview: c.ai_overview?.theme || null,
|
|
197
|
+
}));
|
|
198
|
+
return textResult(JSON.stringify(collections, null, 2));
|
|
199
|
+
}
|
|
200
|
+
async function handleGetCollectionOverview(args) {
|
|
201
|
+
const { data: collection, error } = await supabase
|
|
202
|
+
.from('collections')
|
|
203
|
+
.select('*')
|
|
204
|
+
.eq('name', args.name)
|
|
119
205
|
.single();
|
|
120
206
|
if (error) {
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
text: error.code === 'PGRST116'
|
|
125
|
-
? 'No clusters generated yet. Open the Vault tab in Burn to generate clusters.'
|
|
126
|
-
: `Error: ${error.message}`,
|
|
127
|
-
}],
|
|
128
|
-
};
|
|
207
|
+
return textResult(error.code === 'PGRST116'
|
|
208
|
+
? `No collection found with name "${args.name}".`
|
|
209
|
+
: `Error: ${error.message}`);
|
|
129
210
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
211
|
+
let bookmarks = [];
|
|
212
|
+
if (Array.isArray(collection.bookmark_ids) && collection.bookmark_ids.length > 0) {
|
|
213
|
+
const { data: bData, error: bError } = await supabase
|
|
214
|
+
.from('bookmarks')
|
|
215
|
+
.select('*')
|
|
216
|
+
.in('id', collection.bookmark_ids);
|
|
217
|
+
if (!bError && bData) {
|
|
218
|
+
bookmarks = bData.map(metaSummary);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return textResult(JSON.stringify({
|
|
222
|
+
id: collection.id,
|
|
223
|
+
name: collection.name,
|
|
224
|
+
articleCount: Array.isArray(collection.bookmark_ids) ? collection.bookmark_ids.length : 0,
|
|
225
|
+
aiOverview: collection.ai_overview,
|
|
226
|
+
bookmarks,
|
|
227
|
+
}, null, 2));
|
|
228
|
+
}
|
|
229
|
+
async function handleGetArticleContent(args) {
|
|
230
|
+
const { data, error } = await supabase
|
|
231
|
+
.from('bookmarks')
|
|
232
|
+
.select('*')
|
|
233
|
+
.eq('id', args.id)
|
|
234
|
+
.single();
|
|
235
|
+
if (error)
|
|
236
|
+
return textResult(`Error: ${error.message}`);
|
|
237
|
+
return textResult(JSON.stringify(meta(data), null, 2));
|
|
238
|
+
}
|
|
141
239
|
// ---------------------------------------------------------------------------
|
|
142
|
-
//
|
|
240
|
+
// Vercel API base URL for content fetching
|
|
143
241
|
// ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
242
|
+
const API_BASE = process.env.BURN_API_URL || 'https://api.burn451.cloud';
|
|
243
|
+
const API_KEY = 'burn451-2026-secret-key';
|
|
244
|
+
/** Detect platform from URL */
|
|
245
|
+
function detectPlatform(url) {
|
|
246
|
+
if (/x\.com|twitter\.com/i.test(url))
|
|
247
|
+
return 'x';
|
|
248
|
+
if (/youtube\.com|youtu\.be/i.test(url))
|
|
249
|
+
return 'youtube';
|
|
250
|
+
if (/reddit\.com|redd\.it/i.test(url))
|
|
251
|
+
return 'reddit';
|
|
252
|
+
if (/bilibili\.com|b23\.tv/i.test(url))
|
|
253
|
+
return 'bilibili';
|
|
254
|
+
if (/open\.spotify\.com/i.test(url))
|
|
255
|
+
return 'spotify';
|
|
256
|
+
if (/mp\.weixin\.qq\.com/i.test(url))
|
|
257
|
+
return 'wechat';
|
|
258
|
+
if (/xiaohongshu\.com|xhslink\.com/i.test(url))
|
|
259
|
+
return 'xhs';
|
|
260
|
+
return 'web';
|
|
261
|
+
}
|
|
262
|
+
/** Fetch content via Vercel API proxy (bypasses GFW for X.com, etc.) */
|
|
263
|
+
async function fetchViaAPI(url, platform) {
|
|
264
|
+
try {
|
|
265
|
+
let endpoint;
|
|
266
|
+
let params;
|
|
267
|
+
switch (platform) {
|
|
268
|
+
case 'x':
|
|
269
|
+
endpoint = `${API_BASE}/api/parse-x`;
|
|
270
|
+
params = `url=${encodeURIComponent(url)}`;
|
|
271
|
+
break;
|
|
272
|
+
case 'reddit':
|
|
273
|
+
endpoint = `${API_BASE}/api/parse-reddit`;
|
|
274
|
+
params = `url=${encodeURIComponent(url)}`;
|
|
275
|
+
break;
|
|
276
|
+
case 'spotify':
|
|
277
|
+
endpoint = `${API_BASE}/api/parse-meta`;
|
|
278
|
+
params = `url=${encodeURIComponent(url)}&_platform=spotify`;
|
|
279
|
+
break;
|
|
280
|
+
case 'wechat':
|
|
281
|
+
case 'xhs':
|
|
282
|
+
endpoint = `${API_BASE}/api/parse-meta`;
|
|
283
|
+
params = `url=${encodeURIComponent(url)}&_platform=${platform}`;
|
|
284
|
+
break;
|
|
285
|
+
case 'youtube':
|
|
286
|
+
// Try transcript extraction via jina-extract with youtube platform hint
|
|
287
|
+
endpoint = `${API_BASE}/api/jina-extract`;
|
|
288
|
+
params = `url=${encodeURIComponent(url)}&_platform=youtube`;
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
endpoint = `${API_BASE}/api/jina-extract`;
|
|
292
|
+
params = `url=${encodeURIComponent(url)}`;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
const resp = await fetch(`${endpoint}?${params}`, {
|
|
296
|
+
headers: {
|
|
297
|
+
'x-api-key': API_KEY,
|
|
298
|
+
'Accept': 'application/json',
|
|
299
|
+
},
|
|
300
|
+
signal: AbortSignal.timeout(30000),
|
|
301
|
+
});
|
|
302
|
+
if (!resp.ok) {
|
|
303
|
+
return { error: `API returned ${resp.status}` };
|
|
304
|
+
}
|
|
305
|
+
const data = await resp.json();
|
|
306
|
+
// Normalize response across different API endpoints
|
|
307
|
+
if (platform === 'x') {
|
|
308
|
+
const text = data.text || data.article_text || '';
|
|
309
|
+
const quoteText = data.quote ? `\n\n[Quote from @${data.quote.handle}]: ${data.quote.text}` : '';
|
|
310
|
+
return {
|
|
311
|
+
title: data.text?.slice(0, 100) || 'Tweet',
|
|
312
|
+
author: data.author ? `@${data.handle || data.author}` : undefined,
|
|
313
|
+
content: text + quoteText,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (platform === 'spotify') {
|
|
317
|
+
return {
|
|
318
|
+
title: data.title,
|
|
319
|
+
author: data.author,
|
|
320
|
+
content: data.extracted_content || data.description || null,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (platform === 'wechat') {
|
|
324
|
+
// WeChat parse-meta returns extracted_content from js_content div
|
|
325
|
+
return {
|
|
326
|
+
title: data.title,
|
|
327
|
+
author: data.author,
|
|
328
|
+
content: data.extracted_content || data.content || null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
title: data.title,
|
|
333
|
+
author: data.author,
|
|
334
|
+
content: data.content || data.extracted_content || data.text || data.transcript || null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return { error: err.message || 'Fetch failed' };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function handleFetchContent(args) {
|
|
342
|
+
const { url } = args;
|
|
343
|
+
const platform = detectPlatform(url);
|
|
344
|
+
// Step 1: Check if we already have content in Supabase
|
|
345
|
+
const { data: existing } = await supabase
|
|
346
|
+
.from('bookmarks')
|
|
347
|
+
.select('*')
|
|
348
|
+
.eq('url', url)
|
|
349
|
+
.limit(1)
|
|
350
|
+
.maybeSingle();
|
|
351
|
+
if (existing) {
|
|
352
|
+
const m = existing.content_metadata || {};
|
|
353
|
+
if (m.extracted_content && m.extracted_content.length > 50) {
|
|
354
|
+
return textResult(JSON.stringify({
|
|
355
|
+
source: 'cache',
|
|
356
|
+
url,
|
|
357
|
+
platform,
|
|
358
|
+
title: existing.title,
|
|
359
|
+
author: m.author,
|
|
360
|
+
content: m.extracted_content,
|
|
361
|
+
aiPositioning: m.ai_positioning,
|
|
362
|
+
aiTakeaway: m.ai_takeaway,
|
|
363
|
+
tags: m.tags,
|
|
364
|
+
}, null, 2));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Step 2: Fetch fresh content via Vercel API
|
|
368
|
+
const result = await fetchViaAPI(url, platform);
|
|
369
|
+
if (result.error) {
|
|
370
|
+
return textResult(JSON.stringify({
|
|
371
|
+
source: 'error',
|
|
372
|
+
url,
|
|
373
|
+
platform,
|
|
374
|
+
error: result.error,
|
|
375
|
+
hint: platform === 'x' ? 'X.com content is fetched via Vercel Edge proxy to bypass GFW' : undefined,
|
|
376
|
+
}, null, 2));
|
|
377
|
+
}
|
|
378
|
+
return textResult(JSON.stringify({
|
|
379
|
+
source: 'live',
|
|
380
|
+
url,
|
|
381
|
+
platform,
|
|
382
|
+
title: result.title,
|
|
383
|
+
author: result.author,
|
|
384
|
+
content: result.content,
|
|
385
|
+
}, null, 2));
|
|
386
|
+
}
|
|
387
|
+
async function handleListSparks(args) {
|
|
147
388
|
const { data, error } = await supabase
|
|
148
|
-
.from('
|
|
149
|
-
.select('
|
|
150
|
-
.eq('
|
|
151
|
-
.
|
|
152
|
-
|
|
389
|
+
.from('bookmarks')
|
|
390
|
+
.select('*')
|
|
391
|
+
.eq('status', 'read')
|
|
392
|
+
.order('created_at', { ascending: false })
|
|
393
|
+
.limit(args.limit || 20);
|
|
394
|
+
if (error)
|
|
395
|
+
return textResult(`Error: ${error.message}`);
|
|
396
|
+
const results = (data || []).map((row) => {
|
|
397
|
+
const s = metaSummary(row);
|
|
398
|
+
const m = row.content_metadata || {};
|
|
399
|
+
return {
|
|
400
|
+
...s,
|
|
401
|
+
sparkInsight: m.spark_insight || null,
|
|
402
|
+
sparkExpiresAt: m.spark_expires_at || null,
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
return textResult(JSON.stringify(results, null, 2));
|
|
406
|
+
}
|
|
407
|
+
async function handleSearchSparks(args) {
|
|
408
|
+
const { query, limit } = args;
|
|
409
|
+
const { data, error } = await supabase
|
|
410
|
+
.from('bookmarks')
|
|
411
|
+
.select('*')
|
|
412
|
+
.eq('status', 'read')
|
|
413
|
+
.order('created_at', { ascending: false })
|
|
414
|
+
.limit(50);
|
|
415
|
+
if (error)
|
|
416
|
+
return textResult(`Error: ${error.message}`);
|
|
417
|
+
const results = (data || [])
|
|
418
|
+
.filter((row) => {
|
|
419
|
+
const m = row.content_metadata || {};
|
|
420
|
+
const searchable = [
|
|
421
|
+
row.title || '',
|
|
422
|
+
...(m.tags || []),
|
|
423
|
+
...(m.ai_takeaway || []),
|
|
424
|
+
m.ai_positioning || '',
|
|
425
|
+
m.spark_insight || '',
|
|
426
|
+
].join(' ').toLowerCase();
|
|
427
|
+
return searchable.includes(query.toLowerCase());
|
|
428
|
+
})
|
|
429
|
+
.slice(0, limit || 10)
|
|
430
|
+
.map((row) => {
|
|
431
|
+
const s = metaSummary(row);
|
|
432
|
+
const m = row.content_metadata || {};
|
|
153
433
|
return {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
? `No digest found for cluster "${clusterName}". Open the cluster in Burn App to generate a digest first.`
|
|
158
|
-
: `Error: ${error.message}`,
|
|
159
|
-
}],
|
|
434
|
+
...s,
|
|
435
|
+
sparkInsight: m.spark_insight || null,
|
|
436
|
+
sparkExpiresAt: m.spark_expires_at || null,
|
|
160
437
|
};
|
|
438
|
+
});
|
|
439
|
+
return textResult(JSON.stringify(results, null, 2));
|
|
440
|
+
}
|
|
441
|
+
async function handleListVault(args) {
|
|
442
|
+
let query = supabase
|
|
443
|
+
.from('bookmarks')
|
|
444
|
+
.select('*')
|
|
445
|
+
.eq('status', 'absorbed')
|
|
446
|
+
.order('created_at', { ascending: false })
|
|
447
|
+
.limit(args.limit || 20);
|
|
448
|
+
const { data, error } = await query;
|
|
449
|
+
if (error)
|
|
450
|
+
return textResult(`Error: ${error.message}`);
|
|
451
|
+
let results = (data || []).map(metaSummary);
|
|
452
|
+
// Filter by category if provided
|
|
453
|
+
if (args.category) {
|
|
454
|
+
results = results.filter((r) => r.vaultCategory?.toLowerCase() === args.category.toLowerCase());
|
|
161
455
|
}
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
});
|
|
456
|
+
return textResult(JSON.stringify(results, null, 2));
|
|
457
|
+
}
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Register tools
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// @ts-expect-error — MCP SDK 1.27 TS2589: type instantiation too deep with multiple .tool() calls
|
|
462
|
+
server.tool('search_vault', 'Search your Burn Vault for bookmarks by keyword (searches title, tags, AI takeaway)', { query: zod_1.z.string().describe('Search keyword'), limit: zod_1.z.number().optional().describe('Max results (default 10)') }, handleSearchVault);
|
|
463
|
+
server.tool('list_vault', 'List bookmarks in your Vault, optionally filtered by category', { limit: zod_1.z.number().optional().describe('Max results (default 20)'), category: zod_1.z.string().optional().describe('Filter by vault category') }, handleListVault);
|
|
464
|
+
server.tool('list_sparks', 'List your Sparks (bookmarks you have read, with 30-day lifespan). Includes spark insight and expiry date.', { limit: zod_1.z.number().optional().describe('Max results (default 20)') }, handleListSparks);
|
|
465
|
+
server.tool('search_sparks', 'Search your Sparks by keyword (searches title, tags, AI takeaway, spark insight)', { query: zod_1.z.string().describe('Search keyword'), limit: zod_1.z.number().optional().describe('Max results (default 10)') }, handleSearchSparks);
|
|
466
|
+
server.tool('get_bookmark', 'Get full details of a single bookmark including AI analysis and extracted content', { id: zod_1.z.string().describe('Bookmark UUID') }, handleGetBookmark);
|
|
467
|
+
server.tool('get_article_content', 'Get full article content and AI analysis for a bookmark by ID (same as get_bookmark)', { id: zod_1.z.string().describe('Bookmark UUID') }, handleGetArticleContent);
|
|
468
|
+
server.tool('fetch_content', 'Fetch article/tweet content from a URL. Works with X.com (bypasses GFW via proxy), Reddit, YouTube, Bilibili, WeChat, and any web page. First checks Supabase cache, then fetches live.', { url: zod_1.z.string().describe('The URL to fetch content from') }, handleFetchContent);
|
|
469
|
+
server.tool('list_categories', 'List all Vault categories with article counts', {}, handleListCategories);
|
|
470
|
+
server.tool('get_collections', 'List all your Collections with article counts and AI overview themes', {}, handleGetCollections);
|
|
471
|
+
server.tool('get_collection_overview', 'Get a Collection by name with its AI overview and linked bookmarks metadata', { name: zod_1.z.string().describe('Collection name') }, handleGetCollectionOverview);
|
|
172
472
|
// ---------------------------------------------------------------------------
|
|
173
473
|
// Resource: burn://vault/bookmarks
|
|
174
474
|
// ---------------------------------------------------------------------------
|
|
175
475
|
server.resource('vault-bookmarks', 'burn://vault/bookmarks', async (uri) => {
|
|
176
476
|
const { data, error } = await supabase
|
|
177
477
|
.from('bookmarks')
|
|
178
|
-
.select('
|
|
179
|
-
.eq('status', '
|
|
180
|
-
.order('
|
|
478
|
+
.select('*')
|
|
479
|
+
.eq('status', 'absorbed')
|
|
480
|
+
.order('created_at', { ascending: false });
|
|
181
481
|
if (error) {
|
|
182
482
|
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] };
|
|
183
483
|
}
|
|
@@ -185,7 +485,7 @@ server.resource('vault-bookmarks', 'burn://vault/bookmarks', async (uri) => {
|
|
|
185
485
|
contents: [{
|
|
186
486
|
uri: uri.href,
|
|
187
487
|
mimeType: 'application/json',
|
|
188
|
-
text: JSON.stringify(data, null, 2),
|
|
488
|
+
text: JSON.stringify((data || []).map(metaSummary), null, 2),
|
|
189
489
|
}],
|
|
190
490
|
};
|
|
191
491
|
});
|
|
@@ -195,14 +495,14 @@ server.resource('vault-bookmarks', 'burn://vault/bookmarks', async (uri) => {
|
|
|
195
495
|
server.resource('vault-categories', 'burn://vault/categories', async (uri) => {
|
|
196
496
|
const { data, error } = await supabase
|
|
197
497
|
.from('bookmarks')
|
|
198
|
-
.select('
|
|
199
|
-
.eq('status', '
|
|
498
|
+
.select('content_metadata')
|
|
499
|
+
.eq('status', 'absorbed');
|
|
200
500
|
if (error) {
|
|
201
501
|
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] };
|
|
202
502
|
}
|
|
203
503
|
const counts = {};
|
|
204
504
|
for (const row of data || []) {
|
|
205
|
-
const cat = row.vault_category || 'Uncategorized';
|
|
505
|
+
const cat = row.content_metadata?.vault_category || 'Uncategorized';
|
|
206
506
|
counts[cat] = (counts[cat] || 0) + 1;
|
|
207
507
|
}
|
|
208
508
|
return {
|
|
@@ -217,6 +517,7 @@ server.resource('vault-categories', 'burn://vault/categories', async (uri) => {
|
|
|
217
517
|
// Start
|
|
218
518
|
// ---------------------------------------------------------------------------
|
|
219
519
|
async function main() {
|
|
520
|
+
await initAuth();
|
|
220
521
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
221
522
|
await server.connect(transport);
|
|
222
523
|
console.error('Burn MCP Server running on stdio');
|