context-chest-mcp 0.1.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 +39 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +49 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +150 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +154 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/crypto.d.ts +8 -0
- package/dist/crypto.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +160 -0
- package/dist/summarizer.d.ts +4 -0
- package/dist/summarizer.js +36 -0
- package/dist/tools/browse.d.ts +11 -0
- package/dist/tools/browse.js +13 -0
- package/dist/tools/forget.d.ts +11 -0
- package/dist/tools/forget.js +13 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +16 -0
- package/dist/tools/recall.d.ts +14 -0
- package/dist/tools/recall.js +18 -0
- package/dist/tools/remember.d.ts +20 -0
- package/dist/tools/remember.js +21 -0
- package/dist/tools/session-append.d.ts +17 -0
- package/dist/tools/session-append.js +25 -0
- package/dist/tools/session-save.d.ts +32 -0
- package/dist/tools/session-save.js +25 -0
- package/dist/tools/session-start.d.ts +2 -0
- package/dist/tools/session-start.js +8 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @context-chest/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server that gives AI agents encrypted, persistent memory.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add to your Claude Code or Cursor MCP config:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"context-chest": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["@context-chest/mcp-server"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
| Tool | Description |
|
|
23
|
+
|------|-------------|
|
|
24
|
+
| `context-chest_remember` | Store encrypted memory |
|
|
25
|
+
| `context-chest_recall` | Search memories |
|
|
26
|
+
| `context-chest_read` | Read decrypted content |
|
|
27
|
+
| `context-chest_forget` | Delete a memory |
|
|
28
|
+
| `context-chest_browse` | Browse memory tree |
|
|
29
|
+
| `context-chest_session-start` | Start tracking a conversation |
|
|
30
|
+
| `context-chest_session-append` | Add message to session |
|
|
31
|
+
| `context-chest_session-save` | Extract memories and close session |
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
Content is encrypted client-side with AES-256-GCM before leaving your machine. The server stores only ciphertext. Keys are derived via HKDF from your credentials.
|
|
36
|
+
|
|
37
|
+
## More info
|
|
38
|
+
|
|
39
|
+
See [Context Chest](https://github.com/fuckupic/context-chest) for full documentation.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
jwt: string;
|
|
3
|
+
refreshToken?: string;
|
|
4
|
+
wrappedMasterKey: string;
|
|
5
|
+
exportKey: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadCredentials(): Credentials | null;
|
|
10
|
+
export declare function saveCredentials(credentials: Credentials): void;
|
|
11
|
+
export declare function clearCredentials(): void;
|
|
12
|
+
export declare function isTokenExpired(jwt: string): boolean;
|
|
13
|
+
export declare function tokenExpiresWithin(jwt: string, marginMs: number): boolean;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadCredentials = loadCredentials;
|
|
4
|
+
exports.saveCredentials = saveCredentials;
|
|
5
|
+
exports.clearCredentials = clearCredentials;
|
|
6
|
+
exports.isTokenExpired = isTokenExpired;
|
|
7
|
+
exports.tokenExpiresWithin = tokenExpiresWithin;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const os_1 = require("os");
|
|
11
|
+
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.context-chest');
|
|
12
|
+
const CREDENTIALS_FILE = (0, path_1.join)(CONFIG_DIR, 'credentials.json');
|
|
13
|
+
function loadCredentials() {
|
|
14
|
+
if (!(0, fs_1.existsSync)(CREDENTIALS_FILE)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const raw = (0, fs_1.readFileSync)(CREDENTIALS_FILE, 'utf-8');
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
function saveCredentials(credentials) {
|
|
21
|
+
if (!(0, fs_1.existsSync)(CONFIG_DIR)) {
|
|
22
|
+
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
(0, fs_1.writeFileSync)(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
function clearCredentials() {
|
|
27
|
+
if ((0, fs_1.existsSync)(CREDENTIALS_FILE)) {
|
|
28
|
+
(0, fs_1.writeFileSync)(CREDENTIALS_FILE, '', { mode: 0o600 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isTokenExpired(jwt) {
|
|
32
|
+
try {
|
|
33
|
+
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString());
|
|
34
|
+
return Date.now() >= payload.exp * 1000;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function tokenExpiresWithin(jwt, marginMs) {
|
|
41
|
+
try {
|
|
42
|
+
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString());
|
|
43
|
+
return Date.now() >= (payload.exp * 1000) - marginMs;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const readline_1 = require("readline");
|
|
5
|
+
const auth_1 = require("./auth");
|
|
6
|
+
const crypto_1 = require("./crypto");
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
function prompt(question, hidden = false) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
11
|
+
if (hidden) {
|
|
12
|
+
process.stdout.write(question);
|
|
13
|
+
let input = '';
|
|
14
|
+
process.stdin.setRawMode?.(true);
|
|
15
|
+
process.stdin.resume();
|
|
16
|
+
process.stdin.setEncoding('utf-8');
|
|
17
|
+
const onData = (char) => {
|
|
18
|
+
if (char === '\n' || char === '\r') {
|
|
19
|
+
process.stdin.setRawMode?.(false);
|
|
20
|
+
process.stdin.removeListener('data', onData);
|
|
21
|
+
process.stdout.write('\n');
|
|
22
|
+
rl.close();
|
|
23
|
+
resolve(input);
|
|
24
|
+
}
|
|
25
|
+
else if (char === '\u0003') {
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
else if (char === '\u007F') {
|
|
29
|
+
input = input.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
input += char;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
process.stdin.on('data', onData);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
rl.question(question, (answer) => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer.trim());
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function login() {
|
|
46
|
+
console.log('\n Context Chest — Login\n');
|
|
47
|
+
const apiUrl = await prompt(` API URL [${config_1.DEFAULT_API_URL}]: `);
|
|
48
|
+
const baseUrl = apiUrl || config_1.DEFAULT_API_URL;
|
|
49
|
+
const email = await prompt(' Email: ');
|
|
50
|
+
const password = await prompt(' Password: ', true);
|
|
51
|
+
if (!email || !password) {
|
|
52
|
+
console.error(' Email and password required.');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
console.log('\n Authenticating...');
|
|
56
|
+
const loginRes = await fetch(`${baseUrl}/v1/auth/login`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({ email, password }),
|
|
60
|
+
});
|
|
61
|
+
if (!loginRes.ok) {
|
|
62
|
+
const err = await loginRes.json().catch(() => ({ message: `HTTP ${loginRes.status}` }));
|
|
63
|
+
console.error(` Login failed: ${err.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const loginData = (await loginRes.json());
|
|
67
|
+
// Fetch or create master key
|
|
68
|
+
const authedHeaders = {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
Authorization: `Bearer ${loginData.token}`,
|
|
71
|
+
};
|
|
72
|
+
const mkRes = await fetch(`${baseUrl}/v1/auth/master-key`, { headers: authedHeaders });
|
|
73
|
+
let masterKeyHex;
|
|
74
|
+
if (mkRes.ok) {
|
|
75
|
+
// Existing user — unwrap master key to verify it works
|
|
76
|
+
const mkData = (await mkRes.json());
|
|
77
|
+
const exportKeyBuf = Buffer.from(loginData.exportKey, 'hex');
|
|
78
|
+
const wrappingKey = (0, crypto_1.deriveWrappingKey)(exportKeyBuf, loginData.userId);
|
|
79
|
+
try {
|
|
80
|
+
const mk = (0, crypto_1.unwrapMasterKey)(mkData.encryptedMasterKey, wrappingKey);
|
|
81
|
+
masterKeyHex = mk.toString('hex');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
console.error(' Failed to unwrap master key. Wrong password?');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// New user — generate and upload master key
|
|
90
|
+
console.log(' Generating master key...');
|
|
91
|
+
const mk = (0, crypto_1.generateMasterKey)();
|
|
92
|
+
const exportKeyBuf = Buffer.from(loginData.exportKey, 'hex');
|
|
93
|
+
const wrappingKey = (0, crypto_1.deriveWrappingKey)(exportKeyBuf, loginData.userId);
|
|
94
|
+
const wrapped = (0, crypto_1.wrapMasterKey)(mk, wrappingKey);
|
|
95
|
+
const putRes = await fetch(`${baseUrl}/v1/auth/master-key`, {
|
|
96
|
+
method: 'PUT',
|
|
97
|
+
headers: authedHeaders,
|
|
98
|
+
body: JSON.stringify({ encryptedMasterKey: wrapped }),
|
|
99
|
+
});
|
|
100
|
+
if (!putRes.ok) {
|
|
101
|
+
console.error(' Failed to store master key.');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
masterKeyHex = mk.toString('hex');
|
|
105
|
+
}
|
|
106
|
+
// Fetch wrapped MK for storage
|
|
107
|
+
const wrappedRes = await fetch(`${baseUrl}/v1/auth/master-key`, { headers: authedHeaders });
|
|
108
|
+
const wrappedData = (await wrappedRes.json());
|
|
109
|
+
(0, auth_1.saveCredentials)({
|
|
110
|
+
jwt: loginData.token,
|
|
111
|
+
refreshToken: loginData.refreshToken,
|
|
112
|
+
wrappedMasterKey: wrappedData.encryptedMasterKey,
|
|
113
|
+
exportKey: loginData.exportKey,
|
|
114
|
+
userId: loginData.userId,
|
|
115
|
+
apiUrl: baseUrl,
|
|
116
|
+
});
|
|
117
|
+
console.log(` Logged in as ${email}`);
|
|
118
|
+
console.log(` Credentials saved to ~/.context-chest/credentials.json`);
|
|
119
|
+
console.log(` API: ${baseUrl}\n`);
|
|
120
|
+
console.log(' Now add to your Claude Code / Cursor MCP config:');
|
|
121
|
+
console.log(`
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"context-chest": {
|
|
125
|
+
"command": "npx",
|
|
126
|
+
"args": ["@context-chest/mcp-server"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
const command = process.argv[2];
|
|
133
|
+
if (command === 'login') {
|
|
134
|
+
login().catch((err) => {
|
|
135
|
+
console.error(` Error: ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(`
|
|
141
|
+
Context Chest CLI
|
|
142
|
+
|
|
143
|
+
Commands:
|
|
144
|
+
context-chest login Authenticate and save credentials
|
|
145
|
+
|
|
146
|
+
Usage:
|
|
147
|
+
npx @context-chest/mcp-server login
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
interface ClientConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
}
|
|
6
|
+
interface RememberInput {
|
|
7
|
+
uri: string;
|
|
8
|
+
l0: string;
|
|
9
|
+
l1: string;
|
|
10
|
+
encryptedL2: string;
|
|
11
|
+
sha256: string;
|
|
12
|
+
}
|
|
13
|
+
interface RecallResult {
|
|
14
|
+
data: Array<{
|
|
15
|
+
uri: string;
|
|
16
|
+
l0: string;
|
|
17
|
+
l1: string;
|
|
18
|
+
score: number;
|
|
19
|
+
}>;
|
|
20
|
+
meta: {
|
|
21
|
+
total: number;
|
|
22
|
+
page: number;
|
|
23
|
+
limit: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface SessionMemory {
|
|
27
|
+
uri: string;
|
|
28
|
+
l0: string;
|
|
29
|
+
l1: string;
|
|
30
|
+
encryptedL2: string;
|
|
31
|
+
sha256: string;
|
|
32
|
+
}
|
|
33
|
+
export declare class ContextChestClient {
|
|
34
|
+
private readonly baseUrl;
|
|
35
|
+
private token;
|
|
36
|
+
private refreshToken;
|
|
37
|
+
private refreshing;
|
|
38
|
+
constructor(config: ClientConfig);
|
|
39
|
+
setToken(token: string): void;
|
|
40
|
+
setRefreshToken(refreshToken: string): void;
|
|
41
|
+
private headers;
|
|
42
|
+
private ensureFreshToken;
|
|
43
|
+
private doRefresh;
|
|
44
|
+
private request;
|
|
45
|
+
private requestBinary;
|
|
46
|
+
remember(input: RememberInput): Promise<{
|
|
47
|
+
success: boolean;
|
|
48
|
+
data: {
|
|
49
|
+
uri: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
};
|
|
52
|
+
}>;
|
|
53
|
+
recall(query: string, limit: number, offset: number): Promise<{
|
|
54
|
+
success: boolean;
|
|
55
|
+
} & RecallResult>;
|
|
56
|
+
getContent(uri: string): Promise<Buffer>;
|
|
57
|
+
forget(uri: string): Promise<void>;
|
|
58
|
+
browse(path?: string, depth?: number): Promise<{
|
|
59
|
+
success: boolean;
|
|
60
|
+
data: {
|
|
61
|
+
tree: unknown[];
|
|
62
|
+
};
|
|
63
|
+
}>;
|
|
64
|
+
createSession(clientId?: string): Promise<{
|
|
65
|
+
success: boolean;
|
|
66
|
+
data: {
|
|
67
|
+
id: string;
|
|
68
|
+
};
|
|
69
|
+
}>;
|
|
70
|
+
appendMessage(sessionId: string, input: {
|
|
71
|
+
role: string;
|
|
72
|
+
encryptedContent: string;
|
|
73
|
+
l0Summary: string;
|
|
74
|
+
sha256: string;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
success: boolean;
|
|
77
|
+
data: {
|
|
78
|
+
messageIndex: number;
|
|
79
|
+
};
|
|
80
|
+
}>;
|
|
81
|
+
closeSession(sessionId: string, memories: SessionMemory[]): Promise<{
|
|
82
|
+
success: boolean;
|
|
83
|
+
data: {
|
|
84
|
+
memoriesExtracted: number;
|
|
85
|
+
};
|
|
86
|
+
}>;
|
|
87
|
+
putMasterKey(encryptedMasterKey: string): Promise<{
|
|
88
|
+
success: boolean;
|
|
89
|
+
}>;
|
|
90
|
+
getMasterKey(): Promise<string>;
|
|
91
|
+
}
|
|
92
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContextChestClient = void 0;
|
|
4
|
+
const auth_1 = require("./auth");
|
|
5
|
+
const REFRESH_MARGIN_MS = 5 * 60 * 1000; // refresh 5 min before expiry
|
|
6
|
+
class ContextChestClient {
|
|
7
|
+
baseUrl;
|
|
8
|
+
token;
|
|
9
|
+
refreshToken;
|
|
10
|
+
refreshing = null;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.baseUrl = config.baseUrl;
|
|
13
|
+
this.token = config.token;
|
|
14
|
+
this.refreshToken = config.refreshToken;
|
|
15
|
+
}
|
|
16
|
+
setToken(token) {
|
|
17
|
+
this.token = token;
|
|
18
|
+
}
|
|
19
|
+
setRefreshToken(refreshToken) {
|
|
20
|
+
this.refreshToken = refreshToken;
|
|
21
|
+
}
|
|
22
|
+
headers() {
|
|
23
|
+
return {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
Authorization: `Bearer ${this.token}`,
|
|
26
|
+
'X-Agent-Name': 'Claude Code',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async ensureFreshToken() {
|
|
30
|
+
if (!this.refreshToken)
|
|
31
|
+
return;
|
|
32
|
+
if (!(0, auth_1.tokenExpiresWithin)(this.token, REFRESH_MARGIN_MS))
|
|
33
|
+
return;
|
|
34
|
+
// Deduplicate concurrent refresh calls
|
|
35
|
+
if (this.refreshing) {
|
|
36
|
+
await this.refreshing;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.refreshing = this.doRefresh();
|
|
40
|
+
try {
|
|
41
|
+
await this.refreshing;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
this.refreshing = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async doRefresh() {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(`${this.baseUrl}/v1/auth/refresh`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ refreshToken: this.refreshToken }),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
process.stderr.write(`[context-chest] Token refresh failed: HTTP ${response.status}\n`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const data = (await response.json());
|
|
59
|
+
this.token = data.token;
|
|
60
|
+
this.refreshToken = data.refreshToken;
|
|
61
|
+
// Persist the new tokens to credentials file
|
|
62
|
+
const creds = (0, auth_1.loadCredentials)();
|
|
63
|
+
if (creds) {
|
|
64
|
+
(0, auth_1.saveCredentials)({
|
|
65
|
+
...creds,
|
|
66
|
+
jwt: data.token,
|
|
67
|
+
refreshToken: data.refreshToken,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
process.stderr.write('[context-chest] Token refreshed successfully\n');
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
process.stderr.write(`[context-chest] Token refresh error: ${err.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async request(method, path, body) {
|
|
77
|
+
await this.ensureFreshToken();
|
|
78
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
79
|
+
method,
|
|
80
|
+
headers: this.headers(),
|
|
81
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
82
|
+
});
|
|
83
|
+
// If we get a 401, try one refresh and retry
|
|
84
|
+
if (response.status === 401 && this.refreshToken) {
|
|
85
|
+
await this.doRefresh();
|
|
86
|
+
const retry = await fetch(`${this.baseUrl}${path}`, {
|
|
87
|
+
method,
|
|
88
|
+
headers: this.headers(),
|
|
89
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
90
|
+
});
|
|
91
|
+
if (!retry.ok) {
|
|
92
|
+
const error = await retry.json().catch(() => ({ code: 'UNKNOWN', message: `HTTP ${retry.status}` }));
|
|
93
|
+
throw new Error(error.code ?? `HTTP ${retry.status}`);
|
|
94
|
+
}
|
|
95
|
+
if (retry.status === 204)
|
|
96
|
+
return undefined;
|
|
97
|
+
return retry.json();
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const error = await response.json().catch(() => ({ code: 'UNKNOWN', message: `HTTP ${response.status}` }));
|
|
101
|
+
throw new Error(error.code ?? `HTTP ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
if (response.status === 204) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return response.json();
|
|
107
|
+
}
|
|
108
|
+
async requestBinary(path) {
|
|
109
|
+
await this.ensureFreshToken();
|
|
110
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
111
|
+
method: 'GET',
|
|
112
|
+
headers: { Authorization: `Bearer ${this.token}`, 'X-Agent-Name': 'Claude Code' },
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const error = await response.json().catch(() => ({ code: 'UNKNOWN' }));
|
|
116
|
+
throw new Error(error.code ?? `HTTP ${response.status}`);
|
|
117
|
+
}
|
|
118
|
+
const arrayBuf = await response.arrayBuffer();
|
|
119
|
+
return Buffer.from(arrayBuf);
|
|
120
|
+
}
|
|
121
|
+
async remember(input) {
|
|
122
|
+
return this.request('POST', '/v1/memory/remember', input);
|
|
123
|
+
}
|
|
124
|
+
async recall(query, limit, offset) {
|
|
125
|
+
return this.request('POST', '/v1/memory/recall', { query, limit, offset });
|
|
126
|
+
}
|
|
127
|
+
async getContent(uri) {
|
|
128
|
+
return this.requestBinary(`/v1/memory/content/${uri}`);
|
|
129
|
+
}
|
|
130
|
+
async forget(uri) {
|
|
131
|
+
return this.request('DELETE', `/v1/memory/forget/${uri}`);
|
|
132
|
+
}
|
|
133
|
+
async browse(path = '', depth = 2) {
|
|
134
|
+
return this.request('GET', `/v1/memory/browse?path=${encodeURIComponent(path)}&depth=${depth}`);
|
|
135
|
+
}
|
|
136
|
+
async createSession(clientId) {
|
|
137
|
+
return this.request('POST', '/v1/sessions', clientId ? { clientId } : {});
|
|
138
|
+
}
|
|
139
|
+
async appendMessage(sessionId, input) {
|
|
140
|
+
return this.request('POST', `/v1/sessions/${sessionId}/messages`, input);
|
|
141
|
+
}
|
|
142
|
+
async closeSession(sessionId, memories) {
|
|
143
|
+
return this.request('POST', `/v1/sessions/${sessionId}/close`, { memories });
|
|
144
|
+
}
|
|
145
|
+
async putMasterKey(encryptedMasterKey) {
|
|
146
|
+
return this.request('PUT', '/v1/auth/master-key', { encryptedMasterKey });
|
|
147
|
+
}
|
|
148
|
+
async getMasterKey() {
|
|
149
|
+
const result = await this.request('GET', '/v1/auth/master-key');
|
|
150
|
+
return result.encryptedMasterKey;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.ContextChestClient = ContextChestClient;
|
|
154
|
+
//# sourceMappingURL=client.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFAULT_API_URL = "https://api-production-e2cd6.up.railway.app";
|
package/dist/config.js
ADDED
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function generateMasterKey(): Buffer;
|
|
2
|
+
export declare function deriveWrappingKey(exportKey: Buffer, userId: string): Buffer;
|
|
3
|
+
export declare function deriveItemKey(masterKey: Buffer, uri: string): Buffer;
|
|
4
|
+
export declare function wrapMasterKey(masterKey: Buffer, wrappingKey: Buffer): string;
|
|
5
|
+
export declare function unwrapMasterKey(wrapped: string, wrappingKey: Buffer): Buffer;
|
|
6
|
+
export declare function encryptL2(masterKey: Buffer, uri: string, plaintext: Buffer): string;
|
|
7
|
+
export declare function decryptL2(masterKey: Buffer, uri: string, encryptedBase64: string): Buffer;
|
|
8
|
+
export declare function sha256(data: Buffer): string;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateMasterKey = generateMasterKey;
|
|
4
|
+
exports.deriveWrappingKey = deriveWrappingKey;
|
|
5
|
+
exports.deriveItemKey = deriveItemKey;
|
|
6
|
+
exports.wrapMasterKey = wrapMasterKey;
|
|
7
|
+
exports.unwrapMasterKey = unwrapMasterKey;
|
|
8
|
+
exports.encryptL2 = encryptL2;
|
|
9
|
+
exports.decryptL2 = decryptL2;
|
|
10
|
+
exports.sha256 = sha256;
|
|
11
|
+
const crypto_1 = require("crypto");
|
|
12
|
+
const HKDF_HASH = 'sha256';
|
|
13
|
+
const KEY_LENGTH = 32;
|
|
14
|
+
const IV_LENGTH = 12;
|
|
15
|
+
const AUTH_TAG_LENGTH = 16;
|
|
16
|
+
function generateMasterKey() {
|
|
17
|
+
return (0, crypto_1.randomBytes)(KEY_LENGTH);
|
|
18
|
+
}
|
|
19
|
+
function deriveWrappingKey(exportKey, userId) {
|
|
20
|
+
return Buffer.from((0, crypto_1.hkdfSync)(HKDF_HASH, exportKey, userId, 'context-chest-mk-wrap', KEY_LENGTH));
|
|
21
|
+
}
|
|
22
|
+
function deriveItemKey(masterKey, uri) {
|
|
23
|
+
return Buffer.from((0, crypto_1.hkdfSync)(HKDF_HASH, masterKey, uri, 'context-chest-l2', KEY_LENGTH));
|
|
24
|
+
}
|
|
25
|
+
function wrapMasterKey(masterKey, wrappingKey) {
|
|
26
|
+
const iv = (0, crypto_1.randomBytes)(IV_LENGTH);
|
|
27
|
+
const cipher = (0, crypto_1.createCipheriv)('aes-256-gcm', wrappingKey, iv);
|
|
28
|
+
const encrypted = Buffer.concat([cipher.update(masterKey), cipher.final()]);
|
|
29
|
+
const authTag = cipher.getAuthTag();
|
|
30
|
+
return Buffer.concat([iv, encrypted, authTag]).toString('base64');
|
|
31
|
+
}
|
|
32
|
+
function unwrapMasterKey(wrapped, wrappingKey) {
|
|
33
|
+
const data = Buffer.from(wrapped, 'base64');
|
|
34
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
35
|
+
const authTag = data.subarray(data.length - AUTH_TAG_LENGTH);
|
|
36
|
+
const ciphertext = data.subarray(IV_LENGTH, data.length - AUTH_TAG_LENGTH);
|
|
37
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-256-gcm', wrappingKey, iv);
|
|
38
|
+
decipher.setAuthTag(authTag);
|
|
39
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
40
|
+
}
|
|
41
|
+
function encryptL2(masterKey, uri, plaintext) {
|
|
42
|
+
const itemKey = deriveItemKey(masterKey, uri);
|
|
43
|
+
const iv = (0, crypto_1.randomBytes)(IV_LENGTH);
|
|
44
|
+
const cipher = (0, crypto_1.createCipheriv)('aes-256-gcm', itemKey, iv);
|
|
45
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
46
|
+
const authTag = cipher.getAuthTag();
|
|
47
|
+
return Buffer.concat([iv, encrypted, authTag]).toString('base64');
|
|
48
|
+
}
|
|
49
|
+
function decryptL2(masterKey, uri, encryptedBase64) {
|
|
50
|
+
const itemKey = deriveItemKey(masterKey, uri);
|
|
51
|
+
const data = Buffer.from(encryptedBase64, 'base64');
|
|
52
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
53
|
+
const authTag = data.subarray(data.length - AUTH_TAG_LENGTH);
|
|
54
|
+
const ciphertext = data.subarray(IV_LENGTH, data.length - AUTH_TAG_LENGTH);
|
|
55
|
+
const decipher = (0, crypto_1.createDecipheriv)('aes-256-gcm', itemKey, iv);
|
|
56
|
+
decipher.setAuthTag(authTag);
|
|
57
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
58
|
+
}
|
|
59
|
+
function sha256(data) {
|
|
60
|
+
return (0, crypto_1.createHash)('sha256').update(data).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=crypto.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
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 client_1 = require("./client");
|
|
7
|
+
const auth_1 = require("./auth");
|
|
8
|
+
const crypto_1 = require("./crypto");
|
|
9
|
+
const summarizer_1 = require("./summarizer");
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
const remember_1 = require("./tools/remember");
|
|
12
|
+
const recall_1 = require("./tools/recall");
|
|
13
|
+
const read_1 = require("./tools/read");
|
|
14
|
+
const forget_1 = require("./tools/forget");
|
|
15
|
+
const browse_1 = require("./tools/browse");
|
|
16
|
+
const session_start_1 = require("./tools/session-start");
|
|
17
|
+
const session_append_1 = require("./tools/session-append");
|
|
18
|
+
const session_save_1 = require("./tools/session-save");
|
|
19
|
+
const server = new mcp_js_1.McpServer({
|
|
20
|
+
name: 'context-chest',
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
});
|
|
23
|
+
let client = null;
|
|
24
|
+
let masterKey = null;
|
|
25
|
+
function ensureInitialized() {
|
|
26
|
+
if (!client || !masterKey) {
|
|
27
|
+
throw new Error('Not authenticated. Run `context-chest login` first, or set credentials in ~/.context-chest/credentials.json');
|
|
28
|
+
}
|
|
29
|
+
return { client, masterKey };
|
|
30
|
+
}
|
|
31
|
+
async function generateSummaries(content, uri) {
|
|
32
|
+
// Generate safe, vague labels — never leak actual content
|
|
33
|
+
const path = uri ?? 'memory';
|
|
34
|
+
const segments = path.split('/').filter(Boolean);
|
|
35
|
+
const category = segments[0] ?? 'general';
|
|
36
|
+
const topic = segments.slice(1).join(' / ') || 'item';
|
|
37
|
+
const wordCount = content.split(/\s+/).length;
|
|
38
|
+
const l0 = (0, summarizer_1.parseL0Response)(`${category}: ${topic}`);
|
|
39
|
+
const l1 = (0, summarizer_1.parseL1Response)(`Category: ${category}\nTopic: ${topic}\nSize: ~${wordCount} words\nType: encrypted memory`);
|
|
40
|
+
return { l0, l1 };
|
|
41
|
+
}
|
|
42
|
+
async function generateL0(content, uri) {
|
|
43
|
+
const { l0 } = await generateSummaries(content, uri);
|
|
44
|
+
return l0;
|
|
45
|
+
}
|
|
46
|
+
server.tool('context-chest_remember', 'Store a memory in your encrypted vault', remember_1.rememberSchema.shape, async (params) => {
|
|
47
|
+
const ctx = ensureInitialized();
|
|
48
|
+
const result = await (0, remember_1.handleRemember)(params, ctx.client, ctx.masterKey, generateSummaries);
|
|
49
|
+
return { content: [{ type: 'text', text: result }] };
|
|
50
|
+
});
|
|
51
|
+
server.tool('context-chest_recall', 'Search your memories', recall_1.recallSchema.shape, async (params) => {
|
|
52
|
+
const ctx = ensureInitialized();
|
|
53
|
+
const result = await (0, recall_1.handleRecall)(params, ctx.client);
|
|
54
|
+
return { content: [{ type: 'text', text: result }] };
|
|
55
|
+
});
|
|
56
|
+
server.tool('context-chest_read', 'Read full content of a memory', read_1.readSchema.shape, async (params) => {
|
|
57
|
+
const ctx = ensureInitialized();
|
|
58
|
+
const result = await (0, read_1.handleRead)(params, ctx.client, ctx.masterKey);
|
|
59
|
+
return { content: [{ type: 'text', text: result }] };
|
|
60
|
+
});
|
|
61
|
+
server.tool('context-chest_forget', 'Delete a memory', forget_1.forgetSchema.shape, async (params) => {
|
|
62
|
+
const ctx = ensureInitialized();
|
|
63
|
+
const result = await (0, forget_1.handleForget)(params, ctx.client);
|
|
64
|
+
return { content: [{ type: 'text', text: result }] };
|
|
65
|
+
});
|
|
66
|
+
server.tool('context-chest_browse', 'Browse your memory directory', browse_1.browseSchema.shape, async (params) => {
|
|
67
|
+
const ctx = ensureInitialized();
|
|
68
|
+
const result = await (0, browse_1.handleBrowse)(params, ctx.client);
|
|
69
|
+
return { content: [{ type: 'text', text: result }] };
|
|
70
|
+
});
|
|
71
|
+
server.tool('context-chest_session-start', 'Start tracking this conversation', {}, async () => {
|
|
72
|
+
const ctx = ensureInitialized();
|
|
73
|
+
const result = await (0, session_start_1.handleSessionStart)(ctx.client);
|
|
74
|
+
return { content: [{ type: 'text', text: result }] };
|
|
75
|
+
});
|
|
76
|
+
server.tool('context-chest_session-append', 'Add a message to current session', session_append_1.sessionAppendSchema.shape, async (params) => {
|
|
77
|
+
const ctx = ensureInitialized();
|
|
78
|
+
const result = await (0, session_append_1.handleSessionAppend)(params, ctx.client, ctx.masterKey, generateL0);
|
|
79
|
+
return { content: [{ type: 'text', text: result }] };
|
|
80
|
+
});
|
|
81
|
+
server.tool('context-chest_session-save', 'Extract memories and close session', session_save_1.sessionSaveSchema.shape, async (params) => {
|
|
82
|
+
const ctx = ensureInitialized();
|
|
83
|
+
const result = await (0, session_save_1.handleSessionSave)(params, ctx.client, ctx.masterKey, generateSummaries);
|
|
84
|
+
return { content: [{ type: 'text', text: result }] };
|
|
85
|
+
});
|
|
86
|
+
async function refreshAndInit(apiUrl, refreshToken, creds) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${apiUrl}/v1/auth/refresh`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ refreshToken }),
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
process.stderr.write(`[context-chest] Refresh failed: HTTP ${response.status}\n`);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const data = (await response.json());
|
|
98
|
+
// Update credentials on disk
|
|
99
|
+
if (creds) {
|
|
100
|
+
(0, auth_1.saveCredentials)({
|
|
101
|
+
...creds,
|
|
102
|
+
jwt: data.token,
|
|
103
|
+
refreshToken: data.refreshToken,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
client = new client_1.ContextChestClient({
|
|
107
|
+
baseUrl: apiUrl,
|
|
108
|
+
token: data.token,
|
|
109
|
+
refreshToken: data.refreshToken,
|
|
110
|
+
});
|
|
111
|
+
process.stderr.write('[context-chest] Token refreshed on startup\n');
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
process.stderr.write(`[context-chest] Refresh error: ${err.message}\n`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function main() {
|
|
120
|
+
const creds = (0, auth_1.loadCredentials)();
|
|
121
|
+
if (creds) {
|
|
122
|
+
if (!(0, auth_1.isTokenExpired)(creds.jwt)) {
|
|
123
|
+
// Token is still valid
|
|
124
|
+
client = new client_1.ContextChestClient({
|
|
125
|
+
baseUrl: creds.apiUrl || config_1.DEFAULT_API_URL,
|
|
126
|
+
token: creds.jwt,
|
|
127
|
+
refreshToken: creds.refreshToken,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else if (creds.refreshToken) {
|
|
131
|
+
// JWT expired but we have a refresh token — try to refresh
|
|
132
|
+
const ok = await refreshAndInit(creds.apiUrl || config_1.DEFAULT_API_URL, creds.refreshToken, creds);
|
|
133
|
+
if (!ok) {
|
|
134
|
+
process.stderr.write('[context-chest] Could not refresh token. Run context-chest login.\n');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
process.stderr.write('[context-chest] Token expired and no refresh token. Run context-chest login.\n');
|
|
139
|
+
}
|
|
140
|
+
// Unwrap master key if client is ready
|
|
141
|
+
if (client && creds.wrappedMasterKey && creds.exportKey && creds.userId) {
|
|
142
|
+
try {
|
|
143
|
+
const wrappedMK = await client.getMasterKey();
|
|
144
|
+
const exportKeyBuf = Buffer.from(creds.exportKey, 'hex');
|
|
145
|
+
const wrappingKey = (0, crypto_1.deriveWrappingKey)(exportKeyBuf, creds.userId);
|
|
146
|
+
masterKey = (0, crypto_1.unwrapMasterKey)(wrappedMK, wrappingKey);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
process.stderr.write(`[context-chest] MK unwrap failed: ${err.message}\n`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
process.stderr.write('[context-chest] No credentials found. Run context-chest login first.\n');
|
|
155
|
+
}
|
|
156
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
157
|
+
await server.connect(transport);
|
|
158
|
+
}
|
|
159
|
+
main().catch(console.error);
|
|
160
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildL0Prompt = buildL0Prompt;
|
|
4
|
+
exports.buildL1Prompt = buildL1Prompt;
|
|
5
|
+
exports.parseL0Response = parseL0Response;
|
|
6
|
+
exports.parseL1Response = parseL1Response;
|
|
7
|
+
const MAX_INPUT_LENGTH = 5000;
|
|
8
|
+
const MAX_L0_LENGTH = 500;
|
|
9
|
+
const MAX_L1_LENGTH = 10000;
|
|
10
|
+
function buildL0Prompt(content) {
|
|
11
|
+
const truncated = content.slice(0, MAX_INPUT_LENGTH);
|
|
12
|
+
return `Summarize the following content in one sentence. Be generic — no secrets, no PII, no specific values, no API keys, no passwords. Focus on topic and type only.
|
|
13
|
+
|
|
14
|
+
Content:
|
|
15
|
+
${truncated}
|
|
16
|
+
|
|
17
|
+
Respond with ONLY the one-sentence summary, nothing else.`;
|
|
18
|
+
}
|
|
19
|
+
function buildL1Prompt(content) {
|
|
20
|
+
const truncated = content.slice(0, MAX_INPUT_LENGTH);
|
|
21
|
+
return `Create a structured overview of the following content. Include: topic, key concepts, entities mentioned (generic names OK), and type of content. Do NOT include specific values, credentials, code secrets, or personal data.
|
|
22
|
+
|
|
23
|
+
Keep it under 2000 tokens. Use markdown formatting.
|
|
24
|
+
|
|
25
|
+
Content:
|
|
26
|
+
${truncated}
|
|
27
|
+
|
|
28
|
+
Respond with ONLY the structured overview, nothing else.`;
|
|
29
|
+
}
|
|
30
|
+
function parseL0Response(response) {
|
|
31
|
+
return response.trim().slice(0, MAX_L0_LENGTH);
|
|
32
|
+
}
|
|
33
|
+
function parseL1Response(response) {
|
|
34
|
+
return response.trim().slice(0, MAX_L1_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=summarizer.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const browseSchema: z.ZodObject<{
|
|
4
|
+
path: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
path: string;
|
|
7
|
+
}, {
|
|
8
|
+
path?: string | undefined;
|
|
9
|
+
}>;
|
|
10
|
+
export type BrowseInput = z.infer<typeof browseSchema>;
|
|
11
|
+
export declare function handleBrowse(input: BrowseInput, client: ContextChestClient): Promise<string>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.browseSchema = void 0;
|
|
4
|
+
exports.handleBrowse = handleBrowse;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
exports.browseSchema = zod_1.z.object({
|
|
7
|
+
path: zod_1.z.string().optional().default('').describe('Directory path to browse'),
|
|
8
|
+
});
|
|
9
|
+
async function handleBrowse(input, client) {
|
|
10
|
+
const result = await client.browse(input.path);
|
|
11
|
+
return JSON.stringify(result.data.tree, null, 2);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=browse.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const forgetSchema: z.ZodObject<{
|
|
4
|
+
uri: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
uri: string;
|
|
7
|
+
}, {
|
|
8
|
+
uri: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ForgetInput = z.infer<typeof forgetSchema>;
|
|
11
|
+
export declare function handleForget(input: ForgetInput, client: ContextChestClient): Promise<string>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forgetSchema = void 0;
|
|
4
|
+
exports.handleForget = handleForget;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
exports.forgetSchema = zod_1.z.object({
|
|
7
|
+
uri: zod_1.z.string().min(1).describe('Memory URI to delete'),
|
|
8
|
+
});
|
|
9
|
+
async function handleForget(input, client) {
|
|
10
|
+
await client.forget(input.uri);
|
|
11
|
+
return `Deleted memory at ${input.uri}`;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=forget.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const readSchema: z.ZodObject<{
|
|
4
|
+
uri: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
uri: string;
|
|
7
|
+
}, {
|
|
8
|
+
uri: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type ReadInput = z.infer<typeof readSchema>;
|
|
11
|
+
export declare function handleRead(input: ReadInput, client: ContextChestClient, masterKey: Buffer): Promise<string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readSchema = void 0;
|
|
4
|
+
exports.handleRead = handleRead;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const crypto_1 = require("../crypto");
|
|
7
|
+
exports.readSchema = zod_1.z.object({
|
|
8
|
+
uri: zod_1.z.string().min(1).describe('Memory URI to read'),
|
|
9
|
+
});
|
|
10
|
+
async function handleRead(input, client, masterKey) {
|
|
11
|
+
const encrypted = await client.getContent(input.uri);
|
|
12
|
+
const encryptedBase64 = encrypted.toString('base64');
|
|
13
|
+
const decrypted = (0, crypto_1.decryptL2)(masterKey, input.uri, encryptedBase64);
|
|
14
|
+
return decrypted.toString('utf-8');
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const recallSchema: z.ZodObject<{
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
query: string;
|
|
8
|
+
limit: number;
|
|
9
|
+
}, {
|
|
10
|
+
query: string;
|
|
11
|
+
limit?: number | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
export type RecallInput = z.infer<typeof recallSchema>;
|
|
14
|
+
export declare function handleRecall(input: RecallInput, client: ContextChestClient): Promise<string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recallSchema = void 0;
|
|
4
|
+
exports.handleRecall = handleRecall;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
exports.recallSchema = zod_1.z.object({
|
|
7
|
+
query: zod_1.z.string().min(1).describe('What to search for'),
|
|
8
|
+
limit: zod_1.z.number().optional().default(5).describe('Max results'),
|
|
9
|
+
});
|
|
10
|
+
async function handleRecall(input, client) {
|
|
11
|
+
const result = await client.recall(input.query, input.limit, 0);
|
|
12
|
+
if (result.data.length === 0) {
|
|
13
|
+
return 'No memories found matching your query.';
|
|
14
|
+
}
|
|
15
|
+
const lines = result.data.map((r, i) => `${i + 1}. [${r.uri}] (score: ${r.score.toFixed(2)})\n ${r.l0}\n ${r.l1}`);
|
|
16
|
+
return `Found ${result.meta.total} memories:\n\n${lines.join('\n\n')}`;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=recall.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const rememberSchema: z.ZodObject<{
|
|
4
|
+
content: z.ZodString;
|
|
5
|
+
path: z.ZodOptional<z.ZodString>;
|
|
6
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
content: string;
|
|
9
|
+
path?: string | undefined;
|
|
10
|
+
tags?: string[] | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
content: string;
|
|
13
|
+
path?: string | undefined;
|
|
14
|
+
tags?: string[] | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export type RememberInput = z.infer<typeof rememberSchema>;
|
|
17
|
+
export declare function handleRemember(input: RememberInput, client: ContextChestClient, masterKey: Buffer, generateSummaries: (content: string, uri?: string) => Promise<{
|
|
18
|
+
l0: string;
|
|
19
|
+
l1: string;
|
|
20
|
+
}>): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rememberSchema = void 0;
|
|
4
|
+
exports.handleRemember = handleRemember;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const crypto_1 = require("../crypto");
|
|
7
|
+
exports.rememberSchema = zod_1.z.object({
|
|
8
|
+
content: zod_1.z.string().min(1).describe('The content to remember'),
|
|
9
|
+
path: zod_1.z.string().optional().describe('Memory path (e.g., "preferences/editor")'),
|
|
10
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe('Tags for categorization'),
|
|
11
|
+
});
|
|
12
|
+
async function handleRemember(input, client, masterKey, generateSummaries) {
|
|
13
|
+
const uri = input.path ?? `auto/${Date.now()}`;
|
|
14
|
+
const { l0, l1 } = await generateSummaries(input.content, uri);
|
|
15
|
+
const plaintext = Buffer.from(input.content, 'utf-8');
|
|
16
|
+
const encryptedL2 = (0, crypto_1.encryptL2)(masterKey, uri, plaintext);
|
|
17
|
+
const hash = (0, crypto_1.sha256)(Buffer.from(encryptedL2, 'base64'));
|
|
18
|
+
const result = await client.remember({ uri, l0, l1, encryptedL2, sha256: hash });
|
|
19
|
+
return `Remembered at ${result.data.uri}`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=remember.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const sessionAppendSchema: z.ZodObject<{
|
|
4
|
+
sessionId: z.ZodString;
|
|
5
|
+
role: z.ZodString;
|
|
6
|
+
content: z.ZodString;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
content: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
role: string;
|
|
11
|
+
}, {
|
|
12
|
+
content: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
role: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type SessionAppendInput = z.infer<typeof sessionAppendSchema>;
|
|
17
|
+
export declare function handleSessionAppend(input: SessionAppendInput, client: ContextChestClient, masterKey: Buffer, generateL0: (content: string, uri?: string) => Promise<string>): Promise<string>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionAppendSchema = void 0;
|
|
4
|
+
exports.handleSessionAppend = handleSessionAppend;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const crypto_1 = require("../crypto");
|
|
7
|
+
exports.sessionAppendSchema = zod_1.z.object({
|
|
8
|
+
sessionId: zod_1.z.string().min(1).describe('Session ID'),
|
|
9
|
+
role: zod_1.z.string().min(1).describe('Message role (user/assistant)'),
|
|
10
|
+
content: zod_1.z.string().min(1).describe('Message content'),
|
|
11
|
+
});
|
|
12
|
+
async function handleSessionAppend(input, client, masterKey, generateL0) {
|
|
13
|
+
const l0Summary = await generateL0(input.content, `session/${input.sessionId}`);
|
|
14
|
+
const plaintext = Buffer.from(input.content, 'utf-8');
|
|
15
|
+
const encryptedContent = (0, crypto_1.encryptL2)(masterKey, `session-msg-${Date.now()}`, plaintext);
|
|
16
|
+
const hash = (0, crypto_1.sha256)(Buffer.from(encryptedContent, 'base64'));
|
|
17
|
+
const result = await client.appendMessage(input.sessionId, {
|
|
18
|
+
role: input.role,
|
|
19
|
+
encryptedContent,
|
|
20
|
+
l0Summary,
|
|
21
|
+
sha256: hash,
|
|
22
|
+
});
|
|
23
|
+
return `Message ${result.data.messageIndex} added to session ${input.sessionId}`;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=session-append.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ContextChestClient } from '../client';
|
|
3
|
+
export declare const sessionSaveSchema: z.ZodObject<{
|
|
4
|
+
sessionId: z.ZodString;
|
|
5
|
+
memories: z.ZodArray<z.ZodObject<{
|
|
6
|
+
content: z.ZodString;
|
|
7
|
+
path: z.ZodString;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
content: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}, {
|
|
12
|
+
content: string;
|
|
13
|
+
path: string;
|
|
14
|
+
}>, "many">;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
memories: {
|
|
18
|
+
content: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}[];
|
|
21
|
+
}, {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
memories: {
|
|
24
|
+
content: string;
|
|
25
|
+
path: string;
|
|
26
|
+
}[];
|
|
27
|
+
}>;
|
|
28
|
+
export type SessionSaveInput = z.infer<typeof sessionSaveSchema>;
|
|
29
|
+
export declare function handleSessionSave(input: SessionSaveInput, client: ContextChestClient, masterKey: Buffer, generateSummaries: (content: string, uri?: string) => Promise<{
|
|
30
|
+
l0: string;
|
|
31
|
+
l1: string;
|
|
32
|
+
}>): Promise<string>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionSaveSchema = void 0;
|
|
4
|
+
exports.handleSessionSave = handleSessionSave;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const crypto_1 = require("../crypto");
|
|
7
|
+
exports.sessionSaveSchema = zod_1.z.object({
|
|
8
|
+
sessionId: zod_1.z.string().min(1).describe('Session ID to close'),
|
|
9
|
+
memories: zod_1.z.array(zod_1.z.object({
|
|
10
|
+
content: zod_1.z.string().min(1),
|
|
11
|
+
path: zod_1.z.string().min(1),
|
|
12
|
+
})).describe('Memories extracted from the conversation'),
|
|
13
|
+
});
|
|
14
|
+
async function handleSessionSave(input, client, masterKey, generateSummaries) {
|
|
15
|
+
const preparedMemories = await Promise.all(input.memories.map(async (m) => {
|
|
16
|
+
const { l0, l1 } = await generateSummaries(m.content, m.path);
|
|
17
|
+
const plaintext = Buffer.from(m.content, 'utf-8');
|
|
18
|
+
const encryptedL2 = (0, crypto_1.encryptL2)(masterKey, m.path, plaintext);
|
|
19
|
+
const hash = (0, crypto_1.sha256)(Buffer.from(encryptedL2, 'base64'));
|
|
20
|
+
return { uri: m.path, l0, l1, encryptedL2, sha256: hash };
|
|
21
|
+
}));
|
|
22
|
+
const result = await client.closeSession(input.sessionId, preparedMemories);
|
|
23
|
+
return `Session closed. ${result.data.memoriesExtracted} memories extracted.`;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=session-save.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleSessionStart = handleSessionStart;
|
|
4
|
+
async function handleSessionStart(client) {
|
|
5
|
+
const result = await client.createSession();
|
|
6
|
+
return `Session started: ${result.data.id}`;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=session-start.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "context-chest-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for encrypted AI agent memory — remember, recall, browse across sessions",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"context-chest-mcp": "dist/index.js",
|
|
8
|
+
"context-chest": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"ai-memory",
|
|
18
|
+
"claude",
|
|
19
|
+
"cursor",
|
|
20
|
+
"encrypted",
|
|
21
|
+
"context",
|
|
22
|
+
"model-context-protocol"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/fuckupic/context-chest.git",
|
|
27
|
+
"directory": "packages/mcp-server"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "ts-node src/index.ts",
|
|
33
|
+
"test": "jest --no-coverage",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
39
|
+
"zod": "^3.22.4"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/jest": "^29.5.11",
|
|
43
|
+
"@types/node": "^20.10.5",
|
|
44
|
+
"jest": "^29.7.0",
|
|
45
|
+
"ts-jest": "^29.1.1",
|
|
46
|
+
"ts-node": "^10.9.2",
|
|
47
|
+
"typescript": "^5.3.3"
|
|
48
|
+
}
|
|
49
|
+
}
|