indelible-mcp 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 +99 -0
- package/index.js +211 -0
- package/lib/api.js +83 -0
- package/lib/config.js +57 -0
- package/lib/crypto.js +64 -0
- package/package.json +34 -0
- package/tools/load_context.js +124 -0
- package/tools/save_session.js +165 -0
- package/tools/setup_wallet.js +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Indelible MCP Server
|
|
2
|
+
|
|
3
|
+
Blockchain-backed memory for Claude Code. Save your AI conversations permanently on BSV.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @indelible/mcp-server
|
|
9
|
+
indelible-mcp --show-config
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Copy the config output and paste it into your Claude Code settings, then restart Claude Code.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
### 1. Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @indelible/mcp-server
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Configure Claude Code
|
|
23
|
+
|
|
24
|
+
Run:
|
|
25
|
+
```bash
|
|
26
|
+
indelible-mcp --show-config
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This outputs the config to add to your Claude Code settings:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"indelible": {
|
|
35
|
+
"command": "indelible-mcp"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Restart Claude Code
|
|
42
|
+
|
|
43
|
+
Close and reopen VS Code (or your Claude Code client).
|
|
44
|
+
|
|
45
|
+
### 4. Set Up Your Wallet
|
|
46
|
+
|
|
47
|
+
In Claude Code, say:
|
|
48
|
+
|
|
49
|
+
> "Set up my wallet"
|
|
50
|
+
|
|
51
|
+
This generates a new BSV keypair and saves it to `~/.indelible/config.json`.
|
|
52
|
+
|
|
53
|
+
**Important:** Save your WIF (private key) somewhere safe. This is your identity.
|
|
54
|
+
|
|
55
|
+
### 5. Fund Your Wallet
|
|
56
|
+
|
|
57
|
+
Send a small amount of BSV (~$0.01) to your address for transaction fees.
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
### `setup_wallet`
|
|
62
|
+
Generate a new BSV wallet. Only needed once.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
"Set up my wallet"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `save_session`
|
|
69
|
+
Save the current conversation to the blockchain.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
"Save this session to blockchain"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `load_context`
|
|
76
|
+
Load previous sessions from the blockchain.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
"Load my previous context"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How It Works
|
|
83
|
+
|
|
84
|
+
1. Your conversations are encrypted with AES-256-GCM using your WIF-derived key
|
|
85
|
+
2. Encrypted data is stored on the BSV blockchain via OP_RETURN
|
|
86
|
+
3. Only you can decrypt your data (using your WIF)
|
|
87
|
+
4. Sessions are chained together for continuity
|
|
88
|
+
|
|
89
|
+
## View Your Data
|
|
90
|
+
|
|
91
|
+
Visit [indelible.one](https://indelible.one) and log in with your WIF to see all your saved sessions and code.
|
|
92
|
+
|
|
93
|
+
## Cost
|
|
94
|
+
|
|
95
|
+
~1 satoshi per byte. A typical session costs less than $0.01.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Indelible MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides blockchain-backed memory for Claude Code sessions.
|
|
6
|
+
* Communicates via stdio using JSON-RPC (MCP protocol).
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - setup_wallet: Generate BSV keypair, save to ~/.indelible/config.json
|
|
10
|
+
* - save_session: Read transcript, encrypt, commit to chain
|
|
11
|
+
* - load_context: Fetch latest checkpoint, decrypt, return summary
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Handle CLI flags before loading MCP
|
|
15
|
+
if (process.argv.includes('--show-config')) {
|
|
16
|
+
console.log(`
|
|
17
|
+
Add this to your Claude Code settings.json:
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"indelible": {
|
|
22
|
+
"command": "indelible-mcp"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Then restart Claude Code and say "set up my wallet" to get started.
|
|
28
|
+
`)
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
33
|
+
console.log(`
|
|
34
|
+
Indelible MCP Server - Blockchain memory for Claude Code
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
indelible-mcp Start MCP server (used by Claude Code)
|
|
38
|
+
indelible-mcp --show-config Show Claude Code configuration
|
|
39
|
+
indelible-mcp --help Show this help message
|
|
40
|
+
|
|
41
|
+
After installing, run 'indelible-mcp --show-config' and paste
|
|
42
|
+
the output into your Claude Code settings.
|
|
43
|
+
|
|
44
|
+
Learn more: https://indelible.one
|
|
45
|
+
`)
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
import { createInterface } from 'readline'
|
|
50
|
+
import { setupWallet } from './tools/setup_wallet.js'
|
|
51
|
+
import { saveSession } from './tools/save_session.js'
|
|
52
|
+
import { loadContext } from './tools/load_context.js'
|
|
53
|
+
|
|
54
|
+
// MCP Server metadata
|
|
55
|
+
const SERVER_INFO = {
|
|
56
|
+
name: 'indelible-mcp',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
description: 'Blockchain-backed memory for Claude Code'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Available tools
|
|
62
|
+
const TOOLS = [
|
|
63
|
+
{
|
|
64
|
+
name: 'setup_wallet',
|
|
65
|
+
description: 'Generate a new BSV wallet for Indelible AI. Creates keypair and saves to ~/.indelible/config.json. Only run once per machine.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
api_url: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Indelible API URL (default: https://indelible.one)'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
required: []
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'save_session',
|
|
79
|
+
description: 'Save the current Claude Code session to the blockchain. Encrypts transcript and commits to BSV chain.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
transcript_path: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Path to the JSONL transcript file'
|
|
86
|
+
},
|
|
87
|
+
summary: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Brief summary of what was accomplished in this session'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: ['transcript_path']
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'load_context',
|
|
97
|
+
description: 'Load previous session context from the blockchain. Returns summary of past sessions to restore memory.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
num_sessions: {
|
|
102
|
+
type: 'number',
|
|
103
|
+
description: 'Number of recent sessions to load (default: 3)'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
required: []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
// Handle MCP requests
|
|
112
|
+
async function handleRequest(request) {
|
|
113
|
+
const { method, params, id } = request
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
switch (method) {
|
|
117
|
+
case 'initialize':
|
|
118
|
+
return {
|
|
119
|
+
jsonrpc: '2.0',
|
|
120
|
+
id,
|
|
121
|
+
result: {
|
|
122
|
+
protocolVersion: '2024-11-05',
|
|
123
|
+
serverInfo: SERVER_INFO,
|
|
124
|
+
capabilities: {
|
|
125
|
+
tools: {}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case 'tools/list':
|
|
131
|
+
return {
|
|
132
|
+
jsonrpc: '2.0',
|
|
133
|
+
id,
|
|
134
|
+
result: { tools: TOOLS }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'tools/call':
|
|
138
|
+
const { name, arguments: args } = params
|
|
139
|
+
let result
|
|
140
|
+
|
|
141
|
+
switch (name) {
|
|
142
|
+
case 'setup_wallet':
|
|
143
|
+
result = await setupWallet(args?.api_url)
|
|
144
|
+
break
|
|
145
|
+
case 'save_session':
|
|
146
|
+
result = await saveSession(args?.transcript_path, args?.summary)
|
|
147
|
+
break
|
|
148
|
+
case 'load_context':
|
|
149
|
+
result = await loadContext(args?.num_sessions)
|
|
150
|
+
break
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
jsonrpc: '2.0',
|
|
157
|
+
id,
|
|
158
|
+
result: {
|
|
159
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'notifications/initialized':
|
|
164
|
+
// Client acknowledged initialization - no response needed
|
|
165
|
+
return null
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
return {
|
|
169
|
+
jsonrpc: '2.0',
|
|
170
|
+
id,
|
|
171
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
jsonrpc: '2.0',
|
|
177
|
+
id,
|
|
178
|
+
error: { code: -32000, message: error.message }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Main: stdio JSON-RPC loop
|
|
184
|
+
async function main() {
|
|
185
|
+
const rl = createInterface({
|
|
186
|
+
input: process.stdin,
|
|
187
|
+
output: process.stdout,
|
|
188
|
+
terminal: false
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
for await (const line of rl) {
|
|
192
|
+
if (!line.trim()) continue
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const request = JSON.parse(line)
|
|
196
|
+
const response = await handleRequest(request)
|
|
197
|
+
|
|
198
|
+
if (response) {
|
|
199
|
+
console.log(JSON.stringify(response))
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
jsonrpc: '2.0',
|
|
204
|
+
id: null,
|
|
205
|
+
error: { code: -32700, message: 'Parse error' }
|
|
206
|
+
}))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main().catch(console.error)
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indelible API client for MCP Server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch'
|
|
6
|
+
import { loadConfig } from './config.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API_URL = 'https://indelible.one'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get API base URL from config
|
|
12
|
+
*/
|
|
13
|
+
async function getApiUrl() {
|
|
14
|
+
const config = await loadConfig()
|
|
15
|
+
return config?.api_url || DEFAULT_API_URL
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Commit a session to the blockchain
|
|
20
|
+
* @param {Object} session - Session data to commit
|
|
21
|
+
* @param {string} wif - WIF for signing/encryption
|
|
22
|
+
* @returns {Promise<{success: boolean, txId?: string, error?: string}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function commitSession(session, wif) {
|
|
25
|
+
const apiUrl = await getApiUrl()
|
|
26
|
+
|
|
27
|
+
const response = await fetch(`${apiUrl}/api/claude-code/commit`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({ session, wif })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const error = await response.text()
|
|
37
|
+
throw new Error(error || `Commit failed: ${response.status}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response.json()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get latest sessions for an address
|
|
45
|
+
* @param {string} address - BSV address
|
|
46
|
+
* @param {string} wif - WIF for decryption
|
|
47
|
+
* @param {number} limit - Number of sessions to fetch
|
|
48
|
+
* @returns {Promise<{sessions: Array}>}
|
|
49
|
+
*/
|
|
50
|
+
export async function getLatestSessions(address, wif, limit = 3) {
|
|
51
|
+
const apiUrl = await getApiUrl()
|
|
52
|
+
|
|
53
|
+
const response = await fetch(`${apiUrl}/api/claude-code/latest`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json'
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({ address, wif, limit })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
if (response.status === 404) {
|
|
63
|
+
return { sessions: [] }
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Failed to fetch sessions: ${response.status}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return response.json()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if API is reachable
|
|
73
|
+
* @returns {Promise<boolean>}
|
|
74
|
+
*/
|
|
75
|
+
export async function checkConnection() {
|
|
76
|
+
try {
|
|
77
|
+
const apiUrl = await getApiUrl()
|
|
78
|
+
const response = await fetch(`${apiUrl}/api/health`, { method: 'GET' })
|
|
79
|
+
return response.ok
|
|
80
|
+
} catch {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config management for Indelible MCP Server
|
|
3
|
+
* Stores WIF and settings in ~/.indelible/config.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, writeFile, mkdir } from 'fs/promises'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
8
|
+
import { homedir } from 'os'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.indelible')
|
|
12
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get config file path
|
|
16
|
+
*/
|
|
17
|
+
export function getConfigPath() {
|
|
18
|
+
return CONFIG_FILE
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load config from disk
|
|
23
|
+
* @returns {Promise<Object|null>} Config object or null if not found
|
|
24
|
+
*/
|
|
25
|
+
export async function loadConfig() {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
const data = await readFile(CONFIG_FILE, 'utf-8')
|
|
31
|
+
return JSON.parse(data)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Save config to disk
|
|
39
|
+
* @param {Object} config - Config object to save
|
|
40
|
+
*/
|
|
41
|
+
export async function saveConfig(config) {
|
|
42
|
+
// Ensure directory exists
|
|
43
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
44
|
+
await mkdir(CONFIG_DIR, { recursive: true })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if wallet is configured
|
|
52
|
+
* @returns {Promise<boolean>}
|
|
53
|
+
*/
|
|
54
|
+
export async function hasWallet() {
|
|
55
|
+
const config = await loadConfig()
|
|
56
|
+
return config?.wif != null
|
|
57
|
+
}
|
package/lib/crypto.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto utilities for Indelible MCP Server
|
|
3
|
+
* WIF-derived AES-256-GCM encryption
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Derive encryption key from WIF
|
|
10
|
+
* @param {string} wif - Wallet Import Format private key
|
|
11
|
+
* @returns {Buffer} 32-byte key
|
|
12
|
+
*/
|
|
13
|
+
export function deriveKey(wif) {
|
|
14
|
+
return createHash('sha256').update(wif).digest()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Encrypt data with WIF-derived key
|
|
19
|
+
* @param {string} plaintext - Data to encrypt
|
|
20
|
+
* @param {string} wif - WIF for key derivation
|
|
21
|
+
* @returns {string} Base64 encoded encrypted data (iv:tag:ciphertext)
|
|
22
|
+
*/
|
|
23
|
+
export function encrypt(plaintext, wif) {
|
|
24
|
+
const key = deriveKey(wif)
|
|
25
|
+
const iv = randomBytes(12)
|
|
26
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
27
|
+
|
|
28
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'base64')
|
|
29
|
+
encrypted += cipher.final('base64')
|
|
30
|
+
const tag = cipher.getAuthTag()
|
|
31
|
+
|
|
32
|
+
// Format: iv:tag:ciphertext (all base64)
|
|
33
|
+
return `${iv.toString('base64')}:${tag.toString('base64')}:${encrypted}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decrypt data with WIF-derived key
|
|
38
|
+
* @param {string} encrypted - Base64 encoded encrypted data (iv:tag:ciphertext)
|
|
39
|
+
* @param {string} wif - WIF for key derivation
|
|
40
|
+
* @returns {string} Decrypted plaintext
|
|
41
|
+
*/
|
|
42
|
+
export function decrypt(encrypted, wif) {
|
|
43
|
+
const [ivB64, tagB64, ciphertext] = encrypted.split(':')
|
|
44
|
+
const key = deriveKey(wif)
|
|
45
|
+
const iv = Buffer.from(ivB64, 'base64')
|
|
46
|
+
const tag = Buffer.from(tagB64, 'base64')
|
|
47
|
+
|
|
48
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
49
|
+
decipher.setAuthTag(tag)
|
|
50
|
+
|
|
51
|
+
let decrypted = decipher.update(ciphertext, 'base64', 'utf8')
|
|
52
|
+
decrypted += decipher.final('utf8')
|
|
53
|
+
|
|
54
|
+
return decrypted
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hash data with SHA256
|
|
59
|
+
* @param {string} data - Data to hash
|
|
60
|
+
* @returns {string} Hex-encoded hash
|
|
61
|
+
*/
|
|
62
|
+
export function sha256(data) {
|
|
63
|
+
return createHash('sha256').update(data).digest('hex')
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "indelible-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Indelible AI - blockchain-backed memory for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"indelible-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"lib/",
|
|
13
|
+
"tools/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"claude",
|
|
22
|
+
"indelible",
|
|
23
|
+
"blockchain",
|
|
24
|
+
"bsv",
|
|
25
|
+
"ai-memory"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
31
|
+
"@bsv/sdk": "^1.1.23",
|
|
32
|
+
"node-fetch": "^3.3.2"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* load_context tool
|
|
3
|
+
* Fetch latest checkpoints from blockchain, decrypt, and return context summary
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { loadConfig } from '../lib/config.js'
|
|
7
|
+
import { decrypt } from '../lib/crypto.js'
|
|
8
|
+
import { getLatestSessions } from '../lib/api.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format session for context injection
|
|
12
|
+
* @param {Object} session - Decrypted session object
|
|
13
|
+
* @returns {string} Formatted session summary
|
|
14
|
+
*/
|
|
15
|
+
function formatSession(session) {
|
|
16
|
+
const date = new Date(session.created_at).toLocaleDateString()
|
|
17
|
+
const lines = [
|
|
18
|
+
`## Session: ${date}`,
|
|
19
|
+
`Summary: ${session.summary}`,
|
|
20
|
+
`Messages: ${session.message_count}`,
|
|
21
|
+
''
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// Include key exchanges (first 3 user/assistant pairs)
|
|
25
|
+
if (session.messages?.length > 0) {
|
|
26
|
+
lines.push('### Key points:')
|
|
27
|
+
let count = 0
|
|
28
|
+
for (const msg of session.messages) {
|
|
29
|
+
if (count >= 6) break // 3 pairs max
|
|
30
|
+
const role = msg.role === 'user' ? 'User' : 'Assistant'
|
|
31
|
+
const content = typeof msg.content === 'string'
|
|
32
|
+
? msg.content.slice(0, 150)
|
|
33
|
+
: JSON.stringify(msg.content).slice(0, 150)
|
|
34
|
+
lines.push(`- ${role}: ${content}${content.length >= 150 ? '...' : ''}`)
|
|
35
|
+
count++
|
|
36
|
+
}
|
|
37
|
+
lines.push('')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return lines.join('\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load previous session context from blockchain
|
|
45
|
+
* @param {number} numSessions - Number of recent sessions to load (default: 3)
|
|
46
|
+
* @returns {Promise<Object>} Result with context and status
|
|
47
|
+
*/
|
|
48
|
+
export async function loadContext(numSessions = 3) {
|
|
49
|
+
// Load config
|
|
50
|
+
const config = await loadConfig()
|
|
51
|
+
if (!config?.wif) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: 'Wallet not configured. Run setup_wallet first.',
|
|
55
|
+
context: null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Fetch sessions from API
|
|
61
|
+
const { sessions } = await getLatestSessions(config.address, config.wif, numSessions)
|
|
62
|
+
|
|
63
|
+
if (!sessions || sessions.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
context: null,
|
|
67
|
+
message: 'No previous sessions found. This is a fresh start!',
|
|
68
|
+
sessionCount: 0
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Decrypt and format each session
|
|
73
|
+
const decrypted = []
|
|
74
|
+
for (const session of sessions) {
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(decrypt(session.encrypted, config.wif))
|
|
77
|
+
decrypted.push(data)
|
|
78
|
+
} catch {
|
|
79
|
+
// Skip sessions that fail to decrypt
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (decrypted.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
context: null,
|
|
87
|
+
message: 'Found sessions but could not decrypt. Key mismatch?',
|
|
88
|
+
sessionCount: sessions.length
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build context string
|
|
93
|
+
const contextParts = [
|
|
94
|
+
'# Previous Session Context',
|
|
95
|
+
`Loaded ${decrypted.length} recent session(s) from blockchain.`,
|
|
96
|
+
''
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for (const session of decrypted) {
|
|
100
|
+
contextParts.push(formatSession(session))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const context = contextParts.join('\n')
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
context,
|
|
108
|
+
sessionCount: decrypted.length,
|
|
109
|
+
sessions: decrypted.map(s => ({
|
|
110
|
+
id: s.id,
|
|
111
|
+
summary: s.summary,
|
|
112
|
+
date: s.created_at,
|
|
113
|
+
messageCount: s.message_count
|
|
114
|
+
})),
|
|
115
|
+
message: `Loaded ${decrypted.length} session(s) from blockchain memory.`
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: `Failed to load context: ${error.message}`,
|
|
121
|
+
context: null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* save_session tool
|
|
3
|
+
* Read transcript JSONL, encrypt, and commit to blockchain
|
|
4
|
+
* Supports session chaining via prev_session_id
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
8
|
+
import { existsSync } from 'fs'
|
|
9
|
+
import { loadConfig, saveConfig } from '../lib/config.js'
|
|
10
|
+
import { encrypt, sha256 } from '../lib/crypto.js'
|
|
11
|
+
import { commitSession } from '../lib/api.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse JSONL transcript file
|
|
15
|
+
* @param {string} path - Path to JSONL file
|
|
16
|
+
* @returns {Promise<Array>} Array of message objects
|
|
17
|
+
*/
|
|
18
|
+
async function parseTranscript(path) {
|
|
19
|
+
const content = await readFile(path, 'utf-8')
|
|
20
|
+
const lines = content.trim().split('\n')
|
|
21
|
+
const messages = []
|
|
22
|
+
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (!line.trim()) continue
|
|
25
|
+
try {
|
|
26
|
+
const entry = JSON.parse(line)
|
|
27
|
+
// Extract relevant fields from Claude Code transcript format
|
|
28
|
+
// Claude Code uses type: "user" and type: "assistant"
|
|
29
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
30
|
+
// Extract content from message object
|
|
31
|
+
let content = ''
|
|
32
|
+
if (entry.message?.content) {
|
|
33
|
+
// Content can be array of content blocks or string
|
|
34
|
+
if (Array.isArray(entry.message.content)) {
|
|
35
|
+
content = entry.message.content
|
|
36
|
+
.filter(c => c.type === 'text')
|
|
37
|
+
.map(c => c.text)
|
|
38
|
+
.join('\n')
|
|
39
|
+
} else {
|
|
40
|
+
content = entry.message.content
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (content) {
|
|
45
|
+
messages.push({
|
|
46
|
+
role: entry.type === 'user' ? 'user' : 'assistant',
|
|
47
|
+
content,
|
|
48
|
+
timestamp: entry.timestamp || new Date().toISOString()
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Skip malformed lines
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return messages
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create session summary from messages
|
|
62
|
+
* @param {Array} messages - Array of messages
|
|
63
|
+
* @param {string} userSummary - Optional user-provided summary
|
|
64
|
+
* @returns {string} Session summary
|
|
65
|
+
*/
|
|
66
|
+
function createSummary(messages, userSummary) {
|
|
67
|
+
if (userSummary) return userSummary
|
|
68
|
+
|
|
69
|
+
// Auto-generate summary from first user message
|
|
70
|
+
const firstUser = messages.find(m => m.role === 'user')
|
|
71
|
+
if (firstUser) {
|
|
72
|
+
const preview = typeof firstUser.content === 'string'
|
|
73
|
+
? firstUser.content.slice(0, 200)
|
|
74
|
+
: JSON.stringify(firstUser.content).slice(0, 200)
|
|
75
|
+
return preview + (preview.length >= 200 ? '...' : '')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return 'Claude Code session'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Save current session to blockchain
|
|
83
|
+
* @param {string} transcriptPath - Path to JSONL transcript
|
|
84
|
+
* @param {string} summary - Optional session summary
|
|
85
|
+
* @returns {Promise<Object>} Result with txId and status
|
|
86
|
+
*/
|
|
87
|
+
export async function saveSession(transcriptPath, summary) {
|
|
88
|
+
// Load config
|
|
89
|
+
const config = await loadConfig()
|
|
90
|
+
if (!config?.wif) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: 'Wallet not configured. Run setup_wallet first.'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check transcript exists
|
|
98
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: `Transcript not found: ${transcriptPath}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse transcript
|
|
106
|
+
const messages = await parseTranscript(transcriptPath)
|
|
107
|
+
if (messages.length === 0) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: 'No messages found in transcript'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get previous session ID for chaining
|
|
115
|
+
const prevSessionId = config.last_session_id || null
|
|
116
|
+
|
|
117
|
+
// Create session object
|
|
118
|
+
const sessionId = sha256(transcriptPath + Date.now())
|
|
119
|
+
const session = {
|
|
120
|
+
id: sessionId,
|
|
121
|
+
type: 'claude-code',
|
|
122
|
+
prev_session_id: prevSessionId,
|
|
123
|
+
summary: createSummary(messages, summary),
|
|
124
|
+
message_count: messages.length,
|
|
125
|
+
created_at: new Date().toISOString(),
|
|
126
|
+
messages: messages
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Encrypt session data
|
|
130
|
+
const encrypted = encrypt(JSON.stringify(session), config.wif)
|
|
131
|
+
|
|
132
|
+
// Commit to chain
|
|
133
|
+
try {
|
|
134
|
+
const result = await commitSession({
|
|
135
|
+
address: config.address,
|
|
136
|
+
encrypted,
|
|
137
|
+
summary: session.summary,
|
|
138
|
+
message_count: session.message_count,
|
|
139
|
+
session_id: sessionId,
|
|
140
|
+
prev_session_id: prevSessionId
|
|
141
|
+
}, config.wif)
|
|
142
|
+
|
|
143
|
+
// Update config with new session ID for chaining
|
|
144
|
+
await saveConfig({
|
|
145
|
+
...config,
|
|
146
|
+
last_session_id: sessionId,
|
|
147
|
+
last_tx_id: result.txId
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
txId: result.txId,
|
|
153
|
+
sessionId: sessionId,
|
|
154
|
+
prevSessionId: prevSessionId,
|
|
155
|
+
messageCount: messages.length,
|
|
156
|
+
summary: session.summary,
|
|
157
|
+
message: `Session saved! ${messages.length} messages committed to blockchain.${prevSessionId ? ' Linked to previous session.' : ' (First session)'}`
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: `Failed to commit: ${error.message}`
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup_wallet tool
|
|
3
|
+
* Generate BSV keypair and save to ~/.indelible/config.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PrivateKey } from '@bsv/sdk'
|
|
7
|
+
import { loadConfig, saveConfig, getConfigPath } from '../lib/config.js'
|
|
8
|
+
import { checkConnection } from '../lib/api.js'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_API_URL = 'https://indelible.one'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a new BSV wallet for Indelible AI
|
|
14
|
+
* @param {string} apiUrl - Optional API URL override
|
|
15
|
+
* @returns {Promise<Object>} Result with address and status
|
|
16
|
+
*/
|
|
17
|
+
export async function setupWallet(apiUrl = DEFAULT_API_URL) {
|
|
18
|
+
// Check if wallet already exists
|
|
19
|
+
const existing = await loadConfig()
|
|
20
|
+
if (existing?.wif) {
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: 'Wallet already configured',
|
|
24
|
+
address: existing.address,
|
|
25
|
+
configPath: getConfigPath(),
|
|
26
|
+
hint: 'Delete ~/.indelible/config.json to reset'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generate new keypair
|
|
31
|
+
const privateKey = PrivateKey.fromRandom()
|
|
32
|
+
const wif = privateKey.toWif()
|
|
33
|
+
const address = privateKey.toPublicKey().toAddress()
|
|
34
|
+
|
|
35
|
+
// Save config
|
|
36
|
+
const config = {
|
|
37
|
+
wif,
|
|
38
|
+
address,
|
|
39
|
+
api_url: apiUrl,
|
|
40
|
+
created_at: new Date().toISOString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await saveConfig(config)
|
|
44
|
+
|
|
45
|
+
// Check API connection
|
|
46
|
+
const connected = await checkConnection()
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
address,
|
|
51
|
+
configPath: getConfigPath(),
|
|
52
|
+
apiConnected: connected,
|
|
53
|
+
message: connected
|
|
54
|
+
? `Wallet created! Address: ${address}. Connected to ${apiUrl}`
|
|
55
|
+
: `Wallet created! Address: ${address}. Warning: Could not connect to ${apiUrl}`
|
|
56
|
+
}
|
|
57
|
+
}
|