@recall_v3/mcp-server 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/dist/api/client.d.ts +111 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +244 -0
- package/dist/config/index.d.ts +89 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +256 -0
- package/dist/crypto/index.d.ts +56 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +224 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189 -0
- package/dist/tools/getContext.d.ts +18 -0
- package/dist/tools/getContext.d.ts.map +1 -0
- package/dist/tools/getContext.js +87 -0
- package/dist/tools/getHistory.d.ts +18 -0
- package/dist/tools/getHistory.d.ts.map +1 -0
- package/dist/tools/getHistory.js +97 -0
- package/dist/tools/getTranscripts.d.ts +19 -0
- package/dist/tools/getTranscripts.d.ts.map +1 -0
- package/dist/tools/getTranscripts.js +129 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/logDecision.d.ts +19 -0
- package/dist/tools/logDecision.d.ts.map +1 -0
- package/dist/tools/logDecision.js +92 -0
- package/dist/tools/saveSession.d.ts +26 -0
- package/dist/tools/saveSession.d.ts.map +1 -0
- package/dist/tools/saveSession.js +115 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +33 -0
- package/dist/tools/utils.d.ts +52 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +238 -0
- package/package.json +46 -0
- package/src/api/client.ts +295 -0
- package/src/config/index.ts +247 -0
- package/src/crypto/index.ts +207 -0
- package/src/index.ts +232 -0
- package/src/tools/getContext.ts +106 -0
- package/src/tools/getHistory.ts +118 -0
- package/src/tools/getTranscripts.ts +150 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/logDecision.ts +118 -0
- package/src/tools/saveSession.ts +159 -0
- package/src/tools/types.ts +47 -0
- package/src/tools/utils.ts +226 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall Encryption Utilities
|
|
3
|
+
*
|
|
4
|
+
* AES-256-GCM encryption/decryption compatible with Web Crypto API.
|
|
5
|
+
* Uses Node.js crypto module for server-side operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
import type { EncryptedPayload } from '@recall_v3/shared';
|
|
10
|
+
|
|
11
|
+
// AES-256-GCM constants
|
|
12
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
13
|
+
const IV_LENGTH = 12; // 96 bits for GCM
|
|
14
|
+
const TAG_LENGTH = 16; // 128 bits for GCM auth tag
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a cryptographically secure random IV
|
|
18
|
+
*/
|
|
19
|
+
export function generateIV(): Uint8Array {
|
|
20
|
+
return crypto.randomBytes(IV_LENGTH);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a new AES-256 encryption key
|
|
25
|
+
* Returns base64-encoded key
|
|
26
|
+
*/
|
|
27
|
+
export function generateKey(): string {
|
|
28
|
+
const key = crypto.randomBytes(32); // 256 bits
|
|
29
|
+
return key.toString('base64');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encrypt plaintext using AES-256-GCM
|
|
34
|
+
*
|
|
35
|
+
* @param plaintext - The string to encrypt
|
|
36
|
+
* @param keyBase64 - Base64-encoded 256-bit key
|
|
37
|
+
* @returns EncryptedPayload with base64-encoded ciphertext, iv, and tag
|
|
38
|
+
*/
|
|
39
|
+
export function encrypt(plaintext: string, keyBase64: string): EncryptedPayload {
|
|
40
|
+
// Decode the key
|
|
41
|
+
const key = Buffer.from(keyBase64, 'base64');
|
|
42
|
+
|
|
43
|
+
if (key.length !== 32) {
|
|
44
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Generate a random IV
|
|
48
|
+
const iv = generateIV();
|
|
49
|
+
|
|
50
|
+
// Create cipher
|
|
51
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
52
|
+
authTagLength: TAG_LENGTH,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Encrypt
|
|
56
|
+
const encrypted = Buffer.concat([
|
|
57
|
+
cipher.update(plaintext, 'utf8'),
|
|
58
|
+
cipher.final(),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// Get auth tag
|
|
62
|
+
const tag = cipher.getAuthTag();
|
|
63
|
+
|
|
64
|
+
// Return base64-encoded values
|
|
65
|
+
return {
|
|
66
|
+
ciphertext: encrypted.toString('base64'),
|
|
67
|
+
iv: Buffer.from(iv).toString('base64'),
|
|
68
|
+
tag: tag.toString('base64'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Decrypt an encrypted payload using AES-256-GCM
|
|
74
|
+
*
|
|
75
|
+
* @param payload - EncryptedPayload with base64-encoded values
|
|
76
|
+
* @param keyBase64 - Base64-encoded 256-bit key
|
|
77
|
+
* @returns Decrypted plaintext string
|
|
78
|
+
*/
|
|
79
|
+
export function decrypt(payload: EncryptedPayload, keyBase64: string): string {
|
|
80
|
+
// Decode all components
|
|
81
|
+
const key = Buffer.from(keyBase64, 'base64');
|
|
82
|
+
const iv = Buffer.from(payload.iv, 'base64');
|
|
83
|
+
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
|
|
84
|
+
const tag = Buffer.from(payload.tag, 'base64');
|
|
85
|
+
|
|
86
|
+
if (key.length !== 32) {
|
|
87
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (iv.length !== IV_LENGTH) {
|
|
91
|
+
throw new Error(`Invalid IV length: expected ${IV_LENGTH} bytes, got ${iv.length}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (tag.length !== TAG_LENGTH) {
|
|
95
|
+
throw new Error(`Invalid tag length: expected ${TAG_LENGTH} bytes, got ${tag.length}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create decipher
|
|
99
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
100
|
+
authTagLength: TAG_LENGTH,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Set auth tag
|
|
104
|
+
decipher.setAuthTag(tag);
|
|
105
|
+
|
|
106
|
+
// Decrypt
|
|
107
|
+
try {
|
|
108
|
+
const decrypted = Buffer.concat([
|
|
109
|
+
decipher.update(ciphertext),
|
|
110
|
+
decipher.final(),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
return decrypted.toString('utf8');
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// GCM authentication failure
|
|
116
|
+
if (error instanceof Error && error.message.includes('Unsupported state')) {
|
|
117
|
+
throw new Error('Decryption failed: authentication tag mismatch');
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse an encrypted string into an EncryptedPayload
|
|
125
|
+
* The format is: base64(iv):base64(tag):base64(ciphertext)
|
|
126
|
+
* This is an alternative compact format for storage
|
|
127
|
+
*/
|
|
128
|
+
export function parseEncryptedString(encrypted: string): EncryptedPayload {
|
|
129
|
+
const parts = encrypted.split(':');
|
|
130
|
+
if (parts.length !== 3) {
|
|
131
|
+
throw new Error('Invalid encrypted string format');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
iv: parts[0],
|
|
136
|
+
tag: parts[1],
|
|
137
|
+
ciphertext: parts[2],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Serialize an EncryptedPayload to a compact string
|
|
143
|
+
* Format: base64(iv):base64(tag):base64(ciphertext)
|
|
144
|
+
*/
|
|
145
|
+
export function serializeEncrypted(payload: EncryptedPayload): string {
|
|
146
|
+
return `${payload.iv}:${payload.tag}:${payload.ciphertext}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a string looks like an encrypted payload
|
|
151
|
+
*/
|
|
152
|
+
export function isEncryptedString(str: string): boolean {
|
|
153
|
+
// Check for compact format (iv:tag:ciphertext)
|
|
154
|
+
if (str.includes(':')) {
|
|
155
|
+
const parts = str.split(':');
|
|
156
|
+
if (parts.length === 3) {
|
|
157
|
+
// Each part should be valid base64
|
|
158
|
+
return parts.every(part => {
|
|
159
|
+
try {
|
|
160
|
+
Buffer.from(part, 'base64');
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check for JSON format
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(str) as unknown;
|
|
172
|
+
return (
|
|
173
|
+
typeof parsed === 'object' &&
|
|
174
|
+
parsed !== null &&
|
|
175
|
+
'ciphertext' in parsed &&
|
|
176
|
+
'iv' in parsed &&
|
|
177
|
+
'tag' in parsed
|
|
178
|
+
);
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Decrypt content that might be in either format (JSON or compact string)
|
|
186
|
+
*/
|
|
187
|
+
export function decryptContent(encrypted: string, keyBase64: string): string {
|
|
188
|
+
let payload: EncryptedPayload;
|
|
189
|
+
|
|
190
|
+
// Try JSON format first
|
|
191
|
+
try {
|
|
192
|
+
payload = JSON.parse(encrypted) as EncryptedPayload;
|
|
193
|
+
} catch {
|
|
194
|
+
// Try compact string format
|
|
195
|
+
payload = parseEncryptedString(encrypted);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return decrypt(payload, keyBase64);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Encrypt content and return as JSON string
|
|
203
|
+
*/
|
|
204
|
+
export function encryptContent(plaintext: string, keyBase64: string): string {
|
|
205
|
+
const payload = encrypt(plaintext, keyBase64);
|
|
206
|
+
return JSON.stringify(payload);
|
|
207
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recall MCP Server v3
|
|
5
|
+
* Local MCP server for AI coding assistants
|
|
6
|
+
*
|
|
7
|
+
* This package provides the local MCP server that connects
|
|
8
|
+
* to the Recall API for team memory synchronization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import {
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
type CallToolResult,
|
|
17
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
+
|
|
19
|
+
// Tool implementations
|
|
20
|
+
import {
|
|
21
|
+
getContext,
|
|
22
|
+
getHistory,
|
|
23
|
+
getTranscripts,
|
|
24
|
+
saveSession,
|
|
25
|
+
logDecision,
|
|
26
|
+
type GetContextArgs,
|
|
27
|
+
type GetHistoryArgs,
|
|
28
|
+
type GetTranscriptsArgs,
|
|
29
|
+
type SaveSessionArgs,
|
|
30
|
+
type LogDecisionArgs,
|
|
31
|
+
} from './tools/index.js';
|
|
32
|
+
|
|
33
|
+
// Tool definitions
|
|
34
|
+
const TOOLS = [
|
|
35
|
+
{
|
|
36
|
+
name: 'recall_get_context',
|
|
37
|
+
description:
|
|
38
|
+
"Get team brain (context.md) for the current repository. This is the distilled current state - loads automatically at every session start. Use recall_get_history for the full encyclopedia.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
projectPath: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description:
|
|
45
|
+
'Optional: explicit path to the project root. If not provided, uses current working directory.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'recall_get_history',
|
|
52
|
+
description:
|
|
53
|
+
"Get detailed session history (context.md + recent sessions). This includes more context than recall_get_context but uses more tokens.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
projectPath: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description:
|
|
60
|
+
'Path to the project root. REQUIRED to ensure correct repo context. Use the absolute path to the project you are working in.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['projectPath'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'recall_get_transcripts',
|
|
68
|
+
description:
|
|
69
|
+
"Get full session transcripts (context.md + history.md). WARNING: This can be very large and use many tokens. Only use when you need complete historical details.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
projectPath: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description:
|
|
76
|
+
'Path to the project root. REQUIRED to ensure correct repo context. Use the absolute path to the project you are working in.',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ['projectPath'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'recall_save_session',
|
|
84
|
+
description:
|
|
85
|
+
"Save a summary of what was accomplished in this coding session. This updates the team memory files.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
summary: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description:
|
|
92
|
+
'What was accomplished in this session - list all items worked on, kept short',
|
|
93
|
+
},
|
|
94
|
+
decisions: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
description: 'Key decisions made',
|
|
97
|
+
items: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
what: { type: 'string', description: 'What was decided' },
|
|
101
|
+
why: { type: 'string', description: 'Why this decision was made' },
|
|
102
|
+
},
|
|
103
|
+
required: ['what', 'why'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
mistakes: {
|
|
107
|
+
type: 'array',
|
|
108
|
+
description:
|
|
109
|
+
'Mistakes or gotchas discovered - things the team should not repeat',
|
|
110
|
+
items: { type: 'string' },
|
|
111
|
+
},
|
|
112
|
+
filesChanged: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
description: 'Files that were modified',
|
|
115
|
+
items: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
nextSteps: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'What should be done next',
|
|
120
|
+
},
|
|
121
|
+
blockers: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Any blockers encountered',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
required: ['summary'],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'recall_log_decision',
|
|
131
|
+
description:
|
|
132
|
+
"Log an important decision made during coding. Quick way to capture why something was done.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
decision: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'What was decided',
|
|
139
|
+
},
|
|
140
|
+
reasoning: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
description: 'Why this decision was made',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ['decision', 'reasoning'],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
class RecallMCPServer {
|
|
151
|
+
private server: Server;
|
|
152
|
+
|
|
153
|
+
constructor() {
|
|
154
|
+
this.server = new Server(
|
|
155
|
+
{
|
|
156
|
+
name: 'recall-mcp-v3',
|
|
157
|
+
version: '0.1.0',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
capabilities: {
|
|
161
|
+
tools: {},
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
this.setupHandlers();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private setupHandlers(): void {
|
|
170
|
+
// List available tools
|
|
171
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
172
|
+
return { tools: TOOLS };
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Handle tool calls
|
|
176
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
|
|
177
|
+
const { name, arguments: args = {} } = request.params;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
let result;
|
|
181
|
+
switch (name) {
|
|
182
|
+
case 'recall_get_context':
|
|
183
|
+
result = await getContext(args as unknown as GetContextArgs);
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case 'recall_get_history':
|
|
187
|
+
result = await getHistory(args as unknown as GetHistoryArgs);
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case 'recall_get_transcripts':
|
|
191
|
+
result = await getTranscripts(args as unknown as GetTranscriptsArgs);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'recall_save_session':
|
|
195
|
+
result = await saveSession(args as unknown as SaveSessionArgs);
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case 'recall_log_decision':
|
|
199
|
+
result = await logDecision(args as unknown as LogDecisionArgs);
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
default:
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
205
|
+
isError: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: result.content.map(c => ({ type: 'text' as const, text: c.text })),
|
|
211
|
+
isError: result.isError,
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async run(): Promise<void> {
|
|
224
|
+
const transport = new StdioServerTransport();
|
|
225
|
+
await this.server.connect(transport);
|
|
226
|
+
console.error('Recall MCP v3 server running on stdio');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Start server
|
|
231
|
+
const server = new RecallMCPServer();
|
|
232
|
+
server.run().catch(console.error);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* getContext Tool Implementation
|
|
3
|
+
*
|
|
4
|
+
* Fetches the team brain (context.md) for the current repository.
|
|
5
|
+
* This is the distilled current state - loaded at every session start.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
|
|
9
|
+
import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
|
|
10
|
+
import { decryptContent } from '../crypto/index.js';
|
|
11
|
+
import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
|
|
12
|
+
import { resolveProjectPath, getRepoInfo } from './utils.js';
|
|
13
|
+
|
|
14
|
+
export interface GetContextArgs {
|
|
15
|
+
projectPath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute the getContext tool
|
|
20
|
+
*
|
|
21
|
+
* @param args - Tool arguments (projectPath is optional)
|
|
22
|
+
* @returns MCP tool response with context.md content
|
|
23
|
+
*/
|
|
24
|
+
export async function getContext(args: GetContextArgs): Promise<ToolResponse> {
|
|
25
|
+
try {
|
|
26
|
+
// Get API token
|
|
27
|
+
const token = getApiToken();
|
|
28
|
+
if (!token) {
|
|
29
|
+
return errorResponse(
|
|
30
|
+
'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve project path
|
|
35
|
+
const projectPath = resolveProjectPath(args.projectPath);
|
|
36
|
+
|
|
37
|
+
// Get repo info from git
|
|
38
|
+
const repoInfo = await getRepoInfo(projectPath);
|
|
39
|
+
if (!repoInfo) {
|
|
40
|
+
return errorResponse(
|
|
41
|
+
`Could not determine repository info for: ${projectPath}\n` +
|
|
42
|
+
'Make sure this is a git repository with a remote origin.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create API client
|
|
47
|
+
const client = new RecallApiClient({
|
|
48
|
+
baseUrl: getApiBaseUrl(),
|
|
49
|
+
token,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Resolve repo to get repoId and teamId
|
|
53
|
+
const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
|
|
54
|
+
|
|
55
|
+
// Get team key (fetch from API if not cached)
|
|
56
|
+
let teamKey = getTeamKey(teamId);
|
|
57
|
+
if (!teamKey) {
|
|
58
|
+
const keyResponse = await client.getTeamKey(teamId);
|
|
59
|
+
teamKey = keyResponse.encryptionKey;
|
|
60
|
+
setTeamKey(teamId, teamKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fetch context from API
|
|
64
|
+
const response = await client.getContext(repoId);
|
|
65
|
+
|
|
66
|
+
// The contextMd comes encrypted from the API, decrypt it
|
|
67
|
+
let contextMd: string;
|
|
68
|
+
try {
|
|
69
|
+
// Check if content is encrypted
|
|
70
|
+
if (response.contextMd.startsWith('{') || response.contextMd.includes(':')) {
|
|
71
|
+
contextMd = decryptContent(response.contextMd, teamKey);
|
|
72
|
+
} else {
|
|
73
|
+
// Already plaintext (shouldn't happen, but handle gracefully)
|
|
74
|
+
contextMd = response.contextMd;
|
|
75
|
+
}
|
|
76
|
+
} catch (decryptError) {
|
|
77
|
+
// If decryption fails, it might already be plaintext
|
|
78
|
+
contextMd = response.contextMd;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Format output with metadata
|
|
82
|
+
const header = `Reading from: ${projectPath}/.recall`;
|
|
83
|
+
return formattedResponse(header, contextMd);
|
|
84
|
+
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof AuthenticationError) {
|
|
87
|
+
return errorResponse(
|
|
88
|
+
'Authentication failed. Your token may have expired.\n' +
|
|
89
|
+
'Run `recall auth` to reconnect your account.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (error instanceof RecallApiError) {
|
|
94
|
+
if (error.code === 'REPO_NOT_FOUND') {
|
|
95
|
+
return errorResponse(
|
|
96
|
+
'This repository is not connected to Recall.\n' +
|
|
97
|
+
'Run `recall init` to set up team memory for this repo.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return errorResponse(`API Error: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
104
|
+
return errorResponse(message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* getHistory Tool Implementation
|
|
3
|
+
*
|
|
4
|
+
* Fetches context.md + recent session history for the repository.
|
|
5
|
+
* This provides more detail than getContext but uses more tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
|
|
9
|
+
import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
|
|
10
|
+
import { decryptContent } from '../crypto/index.js';
|
|
11
|
+
import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
|
|
12
|
+
import { resolveProjectPath, getRepoInfo } from './utils.js';
|
|
13
|
+
|
|
14
|
+
export interface GetHistoryArgs {
|
|
15
|
+
projectPath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute the getHistory tool
|
|
20
|
+
*
|
|
21
|
+
* @param args - Tool arguments (projectPath is required)
|
|
22
|
+
* @returns MCP tool response with context.md + history.md content
|
|
23
|
+
*/
|
|
24
|
+
export async function getHistory(args: GetHistoryArgs): Promise<ToolResponse> {
|
|
25
|
+
try {
|
|
26
|
+
// Get API token
|
|
27
|
+
const token = getApiToken();
|
|
28
|
+
if (!token) {
|
|
29
|
+
return errorResponse(
|
|
30
|
+
'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve project path
|
|
35
|
+
const projectPath = resolveProjectPath(args.projectPath);
|
|
36
|
+
|
|
37
|
+
// Get repo info from git
|
|
38
|
+
const repoInfo = await getRepoInfo(projectPath);
|
|
39
|
+
if (!repoInfo) {
|
|
40
|
+
return errorResponse(
|
|
41
|
+
`Could not determine repository info for: ${projectPath}\n` +
|
|
42
|
+
'Make sure this is a git repository with a remote origin.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create API client
|
|
47
|
+
const client = new RecallApiClient({
|
|
48
|
+
baseUrl: getApiBaseUrl(),
|
|
49
|
+
token,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Resolve repo to get repoId and teamId
|
|
53
|
+
const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
|
|
54
|
+
|
|
55
|
+
// Get team key (fetch from API if not cached)
|
|
56
|
+
let teamKey = getTeamKey(teamId);
|
|
57
|
+
if (!teamKey) {
|
|
58
|
+
const keyResponse = await client.getTeamKey(teamId);
|
|
59
|
+
teamKey = keyResponse.encryptionKey;
|
|
60
|
+
setTeamKey(teamId, teamKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fetch history from API
|
|
64
|
+
const response = await client.getHistory(repoId);
|
|
65
|
+
|
|
66
|
+
// Decrypt content
|
|
67
|
+
let contextMd: string;
|
|
68
|
+
let historyMd: string;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Decrypt context
|
|
72
|
+
if (response.contextMd.startsWith('{') || response.contextMd.includes(':')) {
|
|
73
|
+
contextMd = decryptContent(response.contextMd, teamKey);
|
|
74
|
+
} else {
|
|
75
|
+
contextMd = response.contextMd;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Decrypt history
|
|
79
|
+
if (response.historyMd.startsWith('{') || response.historyMd.includes(':')) {
|
|
80
|
+
historyMd = decryptContent(response.historyMd, teamKey);
|
|
81
|
+
} else {
|
|
82
|
+
historyMd = response.historyMd;
|
|
83
|
+
}
|
|
84
|
+
} catch (decryptError) {
|
|
85
|
+
// If decryption fails, use as-is
|
|
86
|
+
contextMd = response.contextMd;
|
|
87
|
+
historyMd = response.historyMd;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Combine context and history
|
|
91
|
+
const combinedContent = `# Recall Context\n\n${contextMd}\n\n---\n\n# Session History\n\n${historyMd}`;
|
|
92
|
+
|
|
93
|
+
// Format output with metadata
|
|
94
|
+
const header = `Reading history from: ${projectPath}/.recall (${response.sessionCount} sessions)`;
|
|
95
|
+
return formattedResponse(header, combinedContent);
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof AuthenticationError) {
|
|
99
|
+
return errorResponse(
|
|
100
|
+
'Authentication failed. Your token may have expired.\n' +
|
|
101
|
+
'Run `recall auth` to reconnect your account.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (error instanceof RecallApiError) {
|
|
106
|
+
if (error.code === 'REPO_NOT_FOUND') {
|
|
107
|
+
return errorResponse(
|
|
108
|
+
'This repository is not connected to Recall.\n' +
|
|
109
|
+
'Run `recall init` to set up team memory for this repo.'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return errorResponse(`API Error: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
116
|
+
return errorResponse(message);
|
|
117
|
+
}
|
|
118
|
+
}
|