burn-mcp-server 1.0.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 ADDED
@@ -0,0 +1,68 @@
1
+ # Burn MCP Server
2
+
3
+ Let Claude Desktop, Cursor, or any MCP-compatible AI tool access your Burn Vault.
4
+
5
+ ## Setup
6
+
7
+ ### 1. Get your access token
8
+
9
+ Open Burn App → Settings → MCP Server → **Copy Access Token**
10
+
11
+ ### 2. Configure Claude Desktop
12
+
13
+ Add to your `~/.config/claude/claude_desktop_config.json`:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "burn": {
19
+ "command": "npx",
20
+ "args": ["burn-mcp-server"],
21
+ "env": {
22
+ "BURN_SUPABASE_TOKEN": "<paste-your-token-here>"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### 3. Restart Claude Desktop
30
+
31
+ The Burn tools will appear in the tools menu.
32
+
33
+ ## Available Tools
34
+
35
+ | Tool | Description |
36
+ |------|-------------|
37
+ | `search_vault` | Search Vault bookmarks by keyword |
38
+ | `get_bookmark` | Get full details of a single bookmark |
39
+ | `list_categories` | List all Vault categories with counts |
40
+ | `get_clusters` | Get AI-generated topic clusters |
41
+ | `get_cluster_digest` | Get cluster digest (summary + relationships) |
42
+
43
+ ## Available Resources
44
+
45
+ | URI | Description |
46
+ |-----|-------------|
47
+ | `burn://vault/bookmarks` | All Vault bookmarks (JSON) |
48
+ | `burn://vault/categories` | Category list (JSON) |
49
+
50
+ ## Example prompts
51
+
52
+ - "What did I save about SwiftUI animations?"
53
+ - "Summarize my AI-related bookmarks"
54
+ - "Reference my Vault article about API design patterns"
55
+ - "What are the main topics in my knowledge base?"
56
+
57
+ ## Environment Variables
58
+
59
+ | Variable | Required | Description |
60
+ |----------|----------|-------------|
61
+ | `BURN_SUPABASE_TOKEN` | Yes | Your Burn access token (JWT) |
62
+ | `BURN_SUPABASE_URL` | No | Custom Supabase URL (default: production) |
63
+
64
+ ## Security
65
+
66
+ - Your token only grants access to **your own** Vault (enforced by Row Level Security)
67
+ - The MCP Server is **read-only** — it cannot modify your bookmarks
68
+ - Tokens expire; regenerate from the Burn App if needed
package/dist/index.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const supabase_js_1 = require("@supabase/supabase-js");
7
+ const zod_1 = require("zod");
8
+ // ---------------------------------------------------------------------------
9
+ // Config
10
+ // ---------------------------------------------------------------------------
11
+ const SUPABASE_URL = process.env.BURN_SUPABASE_URL || 'https://juqtxylquemiuvvmgbej.supabase.co';
12
+ const SUPABASE_TOKEN = process.env.BURN_SUPABASE_TOKEN;
13
+ if (!SUPABASE_TOKEN) {
14
+ console.error('Error: BURN_SUPABASE_TOKEN environment variable is required.');
15
+ console.error('Get your token from: Burn App → Settings → MCP Server → Copy Access Token');
16
+ process.exit(1);
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // Supabase client (authenticated as user via JWT)
20
+ // ---------------------------------------------------------------------------
21
+ const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_TOKEN, {
22
+ auth: {
23
+ persistSession: false,
24
+ autoRefreshToken: false,
25
+ },
26
+ global: {
27
+ headers: {
28
+ Authorization: `Bearer ${SUPABASE_TOKEN}`,
29
+ },
30
+ },
31
+ });
32
+ // ---------------------------------------------------------------------------
33
+ // MCP Server
34
+ // ---------------------------------------------------------------------------
35
+ const server = new mcp_js_1.McpServer({
36
+ name: 'burn-mcp-server',
37
+ version: '1.0.0',
38
+ });
39
+ // ---------------------------------------------------------------------------
40
+ // Tool: search_vault
41
+ // ---------------------------------------------------------------------------
42
+ server.tool('search_vault', 'Search your Burn Vault for bookmarks by keyword', {
43
+ query: zod_1.z.string().describe('Search keyword'),
44
+ limit: zod_1.z.number().optional().default(10).describe('Max results (default 10)'),
45
+ }, async ({ query, limit }) => {
46
+ const { data, error } = await supabase
47
+ .from('bookmarks')
48
+ .select('id, url, title, author, ai_positioning, ai_takeaway, tags, vault_category, vaulted_at')
49
+ .eq('status', 'vault')
50
+ .or(`title.ilike.%${query}%,ai_takeaway.cs.{${query}},tags.cs.{${query}}`)
51
+ .order('vaulted_at', { ascending: false })
52
+ .limit(limit || 10);
53
+ if (error) {
54
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
55
+ }
56
+ return {
57
+ content: [{
58
+ type: 'text',
59
+ text: JSON.stringify(data, null, 2),
60
+ }],
61
+ };
62
+ });
63
+ // ---------------------------------------------------------------------------
64
+ // Tool: get_bookmark
65
+ // ---------------------------------------------------------------------------
66
+ server.tool('get_bookmark', 'Get full details of a single bookmark including AI analysis and content', {
67
+ id: zod_1.z.string().describe('Bookmark UUID'),
68
+ }, async ({ id }) => {
69
+ const { data, error } = await supabase
70
+ .from('bookmarks')
71
+ .select('*')
72
+ .eq('id', id)
73
+ .single();
74
+ if (error) {
75
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
76
+ }
77
+ return {
78
+ content: [{
79
+ type: 'text',
80
+ text: JSON.stringify(data, null, 2),
81
+ }],
82
+ };
83
+ });
84
+ // ---------------------------------------------------------------------------
85
+ // Tool: list_categories
86
+ // ---------------------------------------------------------------------------
87
+ server.tool('list_categories', 'List all Vault categories with article counts', {}, async () => {
88
+ const { data, error } = await supabase
89
+ .from('bookmarks')
90
+ .select('vault_category')
91
+ .eq('status', 'vault');
92
+ if (error) {
93
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
94
+ }
95
+ // Group by category and count
96
+ const counts = {};
97
+ for (const row of data || []) {
98
+ const cat = row.vault_category || 'Uncategorized';
99
+ counts[cat] = (counts[cat] || 0) + 1;
100
+ }
101
+ const categories = Object.entries(counts)
102
+ .map(([category, count]) => ({ category, count }))
103
+ .sort((a, b) => b.count - a.count);
104
+ return {
105
+ content: [{
106
+ type: 'text',
107
+ text: JSON.stringify(categories, null, 2),
108
+ }],
109
+ };
110
+ });
111
+ // ---------------------------------------------------------------------------
112
+ // Tool: get_clusters
113
+ // ---------------------------------------------------------------------------
114
+ server.tool('get_clusters', 'Get AI-generated topic clusters from your Vault', {}, async () => {
115
+ const { data, error } = await supabase
116
+ .from('vault_clusters')
117
+ .select('clusters, generated_at, stale')
118
+ .single();
119
+ if (error) {
120
+ return {
121
+ content: [{
122
+ type: 'text',
123
+ text: error.code === 'PGRST116'
124
+ ? 'No clusters generated yet. Open the Vault tab in Burn to generate clusters.'
125
+ : `Error: ${error.message}`,
126
+ }],
127
+ };
128
+ }
129
+ return {
130
+ content: [{
131
+ type: 'text',
132
+ text: JSON.stringify({
133
+ clusters: data.clusters,
134
+ generatedAt: data.generated_at,
135
+ isStale: data.stale,
136
+ }, null, 2),
137
+ }],
138
+ };
139
+ });
140
+ // ---------------------------------------------------------------------------
141
+ // Tool: get_cluster_digest
142
+ // ---------------------------------------------------------------------------
143
+ server.tool('get_cluster_digest', 'Get the AI-generated digest (summary, insights, relationships) for a topic cluster', {
144
+ clusterName: zod_1.z.string().describe('Name of the cluster'),
145
+ }, async ({ clusterName }) => {
146
+ const { data, error } = await supabase
147
+ .from('cluster_digests')
148
+ .select('digest, generated_at')
149
+ .eq('cluster_name', clusterName)
150
+ .single();
151
+ if (error) {
152
+ return {
153
+ content: [{
154
+ type: 'text',
155
+ text: error.code === 'PGRST116'
156
+ ? `No digest found for cluster "${clusterName}". Open the cluster in Burn App to generate a digest first.`
157
+ : `Error: ${error.message}`,
158
+ }],
159
+ };
160
+ }
161
+ return {
162
+ content: [{
163
+ type: 'text',
164
+ text: JSON.stringify({
165
+ ...data.digest,
166
+ generatedAt: data.generated_at,
167
+ }, null, 2),
168
+ }],
169
+ };
170
+ });
171
+ // ---------------------------------------------------------------------------
172
+ // Resource: burn://vault/bookmarks
173
+ // ---------------------------------------------------------------------------
174
+ server.resource('vault-bookmarks', 'burn://vault/bookmarks', async (uri) => {
175
+ const { data, error } = await supabase
176
+ .from('bookmarks')
177
+ .select('id, url, title, author, ai_positioning, tags, vault_category, vaulted_at')
178
+ .eq('status', 'vault')
179
+ .order('vaulted_at', { ascending: false });
180
+ if (error) {
181
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] };
182
+ }
183
+ return {
184
+ contents: [{
185
+ uri: uri.href,
186
+ mimeType: 'application/json',
187
+ text: JSON.stringify(data, null, 2),
188
+ }],
189
+ };
190
+ });
191
+ // ---------------------------------------------------------------------------
192
+ // Resource: burn://vault/categories
193
+ // ---------------------------------------------------------------------------
194
+ server.resource('vault-categories', 'burn://vault/categories', async (uri) => {
195
+ const { data, error } = await supabase
196
+ .from('bookmarks')
197
+ .select('vault_category')
198
+ .eq('status', 'vault');
199
+ if (error) {
200
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] };
201
+ }
202
+ const counts = {};
203
+ for (const row of data || []) {
204
+ const cat = row.vault_category || 'Uncategorized';
205
+ counts[cat] = (counts[cat] || 0) + 1;
206
+ }
207
+ return {
208
+ contents: [{
209
+ uri: uri.href,
210
+ mimeType: 'application/json',
211
+ text: JSON.stringify(Object.entries(counts).map(([category, count]) => ({ category, count })), null, 2),
212
+ }],
213
+ };
214
+ });
215
+ // ---------------------------------------------------------------------------
216
+ // Start
217
+ // ---------------------------------------------------------------------------
218
+ async function main() {
219
+ const transport = new stdio_js_1.StdioServerTransport();
220
+ await server.connect(transport);
221
+ console.error('Burn MCP Server running on stdio');
222
+ }
223
+ main().catch((err) => {
224
+ console.error('Fatal error:', err);
225
+ process.exit(1);
226
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "burn-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for Burn — access your Vault from Claude/Cursor",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "burn-mcp": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js"
12
+ },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.0.0",
15
+ "@supabase/supabase-js": "^2.39.0",
16
+ "zod": "^3.22.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.3.0",
20
+ "@types/node": "^20.0.0"
21
+ }
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
+ import { createClient, SupabaseClient } from '@supabase/supabase-js'
6
+ import { z } from 'zod'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Config
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const SUPABASE_URL = process.env.BURN_SUPABASE_URL || 'https://juqtxylquemiuvvmgbej.supabase.co'
13
+ const SUPABASE_TOKEN = process.env.BURN_SUPABASE_TOKEN
14
+
15
+ if (!SUPABASE_TOKEN) {
16
+ console.error('Error: BURN_SUPABASE_TOKEN environment variable is required.')
17
+ console.error('Get your token from: Burn App → Settings → MCP Server → Copy Access Token')
18
+ process.exit(1)
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Supabase client (authenticated as user via JWT)
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const supabase: SupabaseClient = createClient(SUPABASE_URL, SUPABASE_TOKEN, {
26
+ auth: {
27
+ persistSession: false,
28
+ autoRefreshToken: false,
29
+ },
30
+ global: {
31
+ headers: {
32
+ Authorization: `Bearer ${SUPABASE_TOKEN}`,
33
+ },
34
+ },
35
+ })
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // MCP Server
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const server = new McpServer({
42
+ name: 'burn-mcp-server',
43
+ version: '1.0.0',
44
+ })
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Tool: search_vault
48
+ // ---------------------------------------------------------------------------
49
+
50
+ server.tool(
51
+ 'search_vault',
52
+ 'Search your Burn Vault for bookmarks by keyword',
53
+ {
54
+ query: z.string().describe('Search keyword'),
55
+ limit: z.number().optional().default(10).describe('Max results (default 10)'),
56
+ },
57
+ async ({ query, limit }) => {
58
+ const { data, error } = await supabase
59
+ .from('bookmarks')
60
+ .select('id, url, title, author, ai_positioning, ai_takeaway, tags, vault_category, vaulted_at')
61
+ .eq('status', 'vault')
62
+ .or(`title.ilike.%${query}%,ai_takeaway.cs.{${query}},tags.cs.{${query}}`)
63
+ .order('vaulted_at', { ascending: false })
64
+ .limit(limit || 10)
65
+
66
+ if (error) {
67
+ return { content: [{ type: 'text' as const, text: `Error: ${error.message}` }] }
68
+ }
69
+
70
+ return {
71
+ content: [{
72
+ type: 'text' as const,
73
+ text: JSON.stringify(data, null, 2),
74
+ }],
75
+ }
76
+ }
77
+ )
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Tool: get_bookmark
81
+ // ---------------------------------------------------------------------------
82
+
83
+ server.tool(
84
+ 'get_bookmark',
85
+ 'Get full details of a single bookmark including AI analysis and content',
86
+ {
87
+ id: z.string().describe('Bookmark UUID'),
88
+ },
89
+ async ({ id }) => {
90
+ const { data, error } = await supabase
91
+ .from('bookmarks')
92
+ .select('*')
93
+ .eq('id', id)
94
+ .single()
95
+
96
+ if (error) {
97
+ return { content: [{ type: 'text' as const, text: `Error: ${error.message}` }] }
98
+ }
99
+
100
+ return {
101
+ content: [{
102
+ type: 'text' as const,
103
+ text: JSON.stringify(data, null, 2),
104
+ }],
105
+ }
106
+ }
107
+ )
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Tool: list_categories
111
+ // ---------------------------------------------------------------------------
112
+
113
+ server.tool(
114
+ 'list_categories',
115
+ 'List all Vault categories with article counts',
116
+ {},
117
+ async () => {
118
+ const { data, error } = await supabase
119
+ .from('bookmarks')
120
+ .select('vault_category')
121
+ .eq('status', 'vault')
122
+
123
+ if (error) {
124
+ return { content: [{ type: 'text' as const, text: `Error: ${error.message}` }] }
125
+ }
126
+
127
+ // Group by category and count
128
+ const counts: Record<string, number> = {}
129
+ for (const row of data || []) {
130
+ const cat = row.vault_category || 'Uncategorized'
131
+ counts[cat] = (counts[cat] || 0) + 1
132
+ }
133
+
134
+ const categories = Object.entries(counts)
135
+ .map(([category, count]) => ({ category, count }))
136
+ .sort((a, b) => b.count - a.count)
137
+
138
+ return {
139
+ content: [{
140
+ type: 'text' as const,
141
+ text: JSON.stringify(categories, null, 2),
142
+ }],
143
+ }
144
+ }
145
+ )
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Tool: get_clusters
149
+ // ---------------------------------------------------------------------------
150
+
151
+ server.tool(
152
+ 'get_clusters',
153
+ 'Get AI-generated topic clusters from your Vault',
154
+ {},
155
+ async () => {
156
+ const { data, error } = await supabase
157
+ .from('vault_clusters')
158
+ .select('clusters, generated_at, stale')
159
+ .single()
160
+
161
+ if (error) {
162
+ return {
163
+ content: [{
164
+ type: 'text' as const,
165
+ text: error.code === 'PGRST116'
166
+ ? 'No clusters generated yet. Open the Vault tab in Burn to generate clusters.'
167
+ : `Error: ${error.message}`,
168
+ }],
169
+ }
170
+ }
171
+
172
+ return {
173
+ content: [{
174
+ type: 'text' as const,
175
+ text: JSON.stringify({
176
+ clusters: data.clusters,
177
+ generatedAt: data.generated_at,
178
+ isStale: data.stale,
179
+ }, null, 2),
180
+ }],
181
+ }
182
+ }
183
+ )
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Tool: get_cluster_digest
187
+ // ---------------------------------------------------------------------------
188
+
189
+ server.tool(
190
+ 'get_cluster_digest',
191
+ 'Get the AI-generated digest (summary, insights, relationships) for a topic cluster',
192
+ {
193
+ clusterName: z.string().describe('Name of the cluster'),
194
+ },
195
+ async ({ clusterName }) => {
196
+ const { data, error } = await supabase
197
+ .from('cluster_digests')
198
+ .select('digest, generated_at')
199
+ .eq('cluster_name', clusterName)
200
+ .single()
201
+
202
+ if (error) {
203
+ return {
204
+ content: [{
205
+ type: 'text' as const,
206
+ text: error.code === 'PGRST116'
207
+ ? `No digest found for cluster "${clusterName}". Open the cluster in Burn App to generate a digest first.`
208
+ : `Error: ${error.message}`,
209
+ }],
210
+ }
211
+ }
212
+
213
+ return {
214
+ content: [{
215
+ type: 'text' as const,
216
+ text: JSON.stringify({
217
+ ...data.digest,
218
+ generatedAt: data.generated_at,
219
+ }, null, 2),
220
+ }],
221
+ }
222
+ }
223
+ )
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Resource: burn://vault/bookmarks
227
+ // ---------------------------------------------------------------------------
228
+
229
+ server.resource(
230
+ 'vault-bookmarks',
231
+ 'burn://vault/bookmarks',
232
+ async (uri) => {
233
+ const { data, error } = await supabase
234
+ .from('bookmarks')
235
+ .select('id, url, title, author, ai_positioning, tags, vault_category, vaulted_at')
236
+ .eq('status', 'vault')
237
+ .order('vaulted_at', { ascending: false })
238
+
239
+ if (error) {
240
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] }
241
+ }
242
+
243
+ return {
244
+ contents: [{
245
+ uri: uri.href,
246
+ mimeType: 'application/json',
247
+ text: JSON.stringify(data, null, 2),
248
+ }],
249
+ }
250
+ }
251
+ )
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Resource: burn://vault/categories
255
+ // ---------------------------------------------------------------------------
256
+
257
+ server.resource(
258
+ 'vault-categories',
259
+ 'burn://vault/categories',
260
+ async (uri) => {
261
+ const { data, error } = await supabase
262
+ .from('bookmarks')
263
+ .select('vault_category')
264
+ .eq('status', 'vault')
265
+
266
+ if (error) {
267
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] }
268
+ }
269
+
270
+ const counts: Record<string, number> = {}
271
+ for (const row of data || []) {
272
+ const cat = row.vault_category || 'Uncategorized'
273
+ counts[cat] = (counts[cat] || 0) + 1
274
+ }
275
+
276
+ return {
277
+ contents: [{
278
+ uri: uri.href,
279
+ mimeType: 'application/json',
280
+ text: JSON.stringify(
281
+ Object.entries(counts).map(([category, count]) => ({ category, count })),
282
+ null, 2
283
+ ),
284
+ }],
285
+ }
286
+ }
287
+ )
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Start
291
+ // ---------------------------------------------------------------------------
292
+
293
+ async function main() {
294
+ const transport = new StdioServerTransport()
295
+ await server.connect(transport)
296
+ console.error('Burn MCP Server running on stdio')
297
+ }
298
+
299
+ main().catch((err) => {
300
+ console.error('Fatal error:', err)
301
+ process.exit(1)
302
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }