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.
Files changed (3) hide show
  1. package/dist/index.js +423 -122
  2. package/package.json +1 -1
  3. 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
- const SUPABASE_TOKEN = process.env.BURN_SUPABASE_TOKEN;
14
- if (!SUPABASE_TOKEN) {
15
- console.error('Error: BURN_SUPABASE_TOKEN environment variable is required.');
16
- console.error('Get your token from: Burn App → Settings → MCP Server → Copy Access Token');
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 (authenticated as user via JWT)
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: false,
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.0.0',
57
+ version: '1.2.0',
39
58
  });
40
59
  // ---------------------------------------------------------------------------
41
- // Tool: search_vault
60
+ // Helper: standard text result
42
61
  // ---------------------------------------------------------------------------
43
- server.tool('search_vault', 'Search your Burn Vault for bookmarks by keyword', {
44
- query: zod_1.z.string().describe('Search keyword'),
45
- limit: zod_1.z.number().optional().default(10).describe('Max results (default 10)'),
46
- }, async ({ query, limit }) => {
47
- const { data, error } = await supabase
48
- .from('bookmarks')
49
- .select('id, url, title, author, ai_positioning, ai_takeaway, tags, vault_category, vaulted_at')
50
- .eq('status', 'vault')
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
- content: [{
59
- type: 'text',
60
- text: JSON.stringify(data, null, 2),
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: get_bookmark
117
+ // Tool handlers
66
118
  // ---------------------------------------------------------------------------
67
- server.tool('get_bookmark', 'Get full details of a single bookmark including AI analysis and content', {
68
- id: zod_1.z.string().describe('Bookmark UUID'),
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('id', id)
74
- .single();
75
- if (error) {
76
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
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
- content: [{
80
- type: 'text',
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('vault_category')
92
- .eq('status', 'vault');
93
- if (error) {
94
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
95
- }
96
- // Group by category and count
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
- content: [{
107
- type: 'text',
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('vault_clusters')
118
- .select('clusters, generated_at, stale')
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
- content: [{
123
- type: 'text',
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
- return {
131
- content: [{
132
- type: 'text',
133
- text: JSON.stringify({
134
- clusters: data.clusters,
135
- generatedAt: data.generated_at,
136
- isStale: data.stale,
137
- }, null, 2),
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
- // Tool: get_cluster_digest
240
+ // Vercel API base URL for content fetching
143
241
  // ---------------------------------------------------------------------------
144
- server.tool('get_cluster_digest', 'Get the AI-generated digest (summary, insights, relationships) for a topic cluster', {
145
- clusterName: zod_1.z.string().describe('Name of the cluster'),
146
- }, async ({ clusterName }) => {
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('cluster_digests')
149
- .select('digest, generated_at')
150
- .eq('cluster_name', clusterName)
151
- .single();
152
- if (error) {
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
- content: [{
155
- type: 'text',
156
- text: error.code === 'PGRST116'
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
- content: [{
164
- type: 'text',
165
- text: JSON.stringify({
166
- ...data.digest,
167
- generatedAt: data.generated_at,
168
- }, null, 2),
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('id, url, title, author, ai_positioning, tags, vault_category, vaulted_at')
179
- .eq('status', 'vault')
180
- .order('vaulted_at', { ascending: false });
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('vault_category')
199
- .eq('status', 'vault');
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "burn-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "MCP Server for Burn — access your Vault from Claude/Cursor",
5
5
  "main": "dist/index.js",
6
6
  "bin": {