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 +68 -0
- package/dist/index.js +226 -0
- package/package.json +22 -0
- package/src/index.ts +302 -0
- package/tsconfig.json +14 -0
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
|
+
}
|