@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.
Files changed (50) hide show
  1. package/dist/api/client.d.ts +111 -0
  2. package/dist/api/client.d.ts.map +1 -0
  3. package/dist/api/client.js +244 -0
  4. package/dist/config/index.d.ts +89 -0
  5. package/dist/config/index.d.ts.map +1 -0
  6. package/dist/config/index.js +256 -0
  7. package/dist/crypto/index.d.ts +56 -0
  8. package/dist/crypto/index.d.ts.map +1 -0
  9. package/dist/crypto/index.js +224 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +189 -0
  13. package/dist/tools/getContext.d.ts +18 -0
  14. package/dist/tools/getContext.d.ts.map +1 -0
  15. package/dist/tools/getContext.js +87 -0
  16. package/dist/tools/getHistory.d.ts +18 -0
  17. package/dist/tools/getHistory.d.ts.map +1 -0
  18. package/dist/tools/getHistory.js +97 -0
  19. package/dist/tools/getTranscripts.d.ts +19 -0
  20. package/dist/tools/getTranscripts.d.ts.map +1 -0
  21. package/dist/tools/getTranscripts.js +129 -0
  22. package/dist/tools/index.d.ts +13 -0
  23. package/dist/tools/index.d.ts.map +1 -0
  24. package/dist/tools/index.js +37 -0
  25. package/dist/tools/logDecision.d.ts +19 -0
  26. package/dist/tools/logDecision.d.ts.map +1 -0
  27. package/dist/tools/logDecision.js +92 -0
  28. package/dist/tools/saveSession.d.ts +26 -0
  29. package/dist/tools/saveSession.d.ts.map +1 -0
  30. package/dist/tools/saveSession.js +115 -0
  31. package/dist/tools/types.d.ts +32 -0
  32. package/dist/tools/types.d.ts.map +1 -0
  33. package/dist/tools/types.js +33 -0
  34. package/dist/tools/utils.d.ts +52 -0
  35. package/dist/tools/utils.d.ts.map +1 -0
  36. package/dist/tools/utils.js +238 -0
  37. package/package.json +46 -0
  38. package/src/api/client.ts +295 -0
  39. package/src/config/index.ts +247 -0
  40. package/src/crypto/index.ts +207 -0
  41. package/src/index.ts +232 -0
  42. package/src/tools/getContext.ts +106 -0
  43. package/src/tools/getHistory.ts +118 -0
  44. package/src/tools/getTranscripts.ts +150 -0
  45. package/src/tools/index.ts +13 -0
  46. package/src/tools/logDecision.ts +118 -0
  47. package/src/tools/saveSession.ts +159 -0
  48. package/src/tools/types.ts +47 -0
  49. package/src/tools/utils.ts +226 -0
  50. package/tsconfig.json +14 -0
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Recall API Client
3
+ *
4
+ * HTTP client for communicating with the Recall backend API.
5
+ * Handles authentication, retries, and error handling.
6
+ */
7
+
8
+ import type {
9
+ SummarizeRequest,
10
+ SummarizeResponse,
11
+ SaveSessionRequest,
12
+ SaveSessionResponse,
13
+ GetContextResponse,
14
+ GetHistoryResponse,
15
+ GetTranscriptsResponse,
16
+ LogDecisionRequest,
17
+ LogDecisionResponse,
18
+ TeamKeyResponse,
19
+ RecallStatusResponse,
20
+ ListTeamsResponse,
21
+ ApiResponse,
22
+ } from '@recall_v3/shared';
23
+
24
+ // Error types for the API client
25
+ export class RecallApiError extends Error {
26
+ constructor(
27
+ message: string,
28
+ public readonly code: string,
29
+ public readonly statusCode?: number,
30
+ public readonly retryable: boolean = false
31
+ ) {
32
+ super(message);
33
+ this.name = 'RecallApiError';
34
+ }
35
+ }
36
+
37
+ export class NetworkError extends RecallApiError {
38
+ constructor(message: string, public readonly cause?: Error) {
39
+ super(message, 'NETWORK_ERROR', undefined, true);
40
+ this.name = 'NetworkError';
41
+ }
42
+ }
43
+
44
+ export class AuthenticationError extends RecallApiError {
45
+ constructor(message: string = 'Authentication failed') {
46
+ super(message, 'AUTH_ERROR', 401, false);
47
+ this.name = 'AuthenticationError';
48
+ }
49
+ }
50
+
51
+ export class TimeoutError extends RecallApiError {
52
+ constructor(message: string = 'Request timed out') {
53
+ super(message, 'TIMEOUT', undefined, true);
54
+ this.name = 'TimeoutError';
55
+ }
56
+ }
57
+
58
+ // Client configuration
59
+ interface RecallApiClientConfig {
60
+ baseUrl: string;
61
+ token: string;
62
+ timeout?: number;
63
+ maxRetries?: number;
64
+ retryBaseDelay?: number;
65
+ }
66
+
67
+ /**
68
+ * RecallApiClient - Thin HTTP client for Recall API
69
+ *
70
+ * Features:
71
+ * - Bearer token authentication
72
+ * - Exponential backoff retry on 5xx errors
73
+ * - Configurable timeout (default 30s)
74
+ * - Typed responses using @recall_v3/shared types
75
+ */
76
+ export class RecallApiClient {
77
+ private readonly baseUrl: string;
78
+ private readonly token: string;
79
+ private readonly timeout: number;
80
+ private readonly maxRetries: number;
81
+ private readonly retryBaseDelay: number;
82
+
83
+ constructor(config: RecallApiClientConfig) {
84
+ this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
85
+ this.token = config.token;
86
+ this.timeout = config.timeout ?? 30000; // 30s default
87
+ this.maxRetries = config.maxRetries ?? 3;
88
+ this.retryBaseDelay = config.retryBaseDelay ?? 1000; // 1s base delay
89
+ }
90
+
91
+ /**
92
+ * Internal fetch wrapper with timeout and retry logic
93
+ */
94
+ private async request<T>(
95
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
96
+ path: string,
97
+ body?: unknown
98
+ ): Promise<T> {
99
+ const url = `${this.baseUrl}${path}`;
100
+
101
+ let lastError: Error | undefined;
102
+
103
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
104
+ try {
105
+ const controller = new AbortController();
106
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
107
+
108
+ try {
109
+ const response = await fetch(url, {
110
+ method,
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ 'Authorization': `Bearer ${this.token}`,
114
+ },
115
+ body: body ? JSON.stringify(body) : undefined,
116
+ signal: controller.signal,
117
+ });
118
+
119
+ clearTimeout(timeoutId);
120
+
121
+ // Handle non-OK responses
122
+ if (!response.ok) {
123
+ const errorBody = await response.json().catch(() => ({})) as { error?: { code?: string; message?: string } };
124
+ const errorCode = errorBody?.error?.code ?? 'UNKNOWN_ERROR';
125
+ const errorMessage = errorBody?.error?.message ?? `HTTP ${response.status}`;
126
+
127
+ // 401 = auth error, don't retry
128
+ if (response.status === 401) {
129
+ throw new AuthenticationError(errorMessage);
130
+ }
131
+
132
+ // 5xx = server error, retry with backoff
133
+ if (response.status >= 500) {
134
+ throw new RecallApiError(errorMessage, errorCode, response.status, true);
135
+ }
136
+
137
+ // 4xx = client error, don't retry
138
+ throw new RecallApiError(errorMessage, errorCode, response.status, false);
139
+ }
140
+
141
+ // Parse successful response
142
+ const data = await response.json() as ApiResponse<T>;
143
+
144
+ if (!data.success && data.error) {
145
+ throw new RecallApiError(data.error.message, data.error.code, response.status, false);
146
+ }
147
+
148
+ return data.data as T;
149
+ } finally {
150
+ clearTimeout(timeoutId);
151
+ }
152
+ } catch (error) {
153
+ // Handle abort (timeout)
154
+ if (error instanceof Error && error.name === 'AbortError') {
155
+ lastError = new TimeoutError();
156
+ // Timeout is retryable
157
+ if (attempt < this.maxRetries) {
158
+ await this.sleep(this.calculateBackoff(attempt));
159
+ continue;
160
+ }
161
+ throw lastError;
162
+ }
163
+
164
+ // Handle network errors
165
+ if (error instanceof TypeError && error.message.includes('fetch')) {
166
+ lastError = new NetworkError('Network request failed', error);
167
+ // Network errors are retryable
168
+ if (attempt < this.maxRetries) {
169
+ await this.sleep(this.calculateBackoff(attempt));
170
+ continue;
171
+ }
172
+ throw lastError;
173
+ }
174
+
175
+ // Handle retryable API errors
176
+ if (error instanceof RecallApiError && error.retryable) {
177
+ lastError = error;
178
+ if (attempt < this.maxRetries) {
179
+ await this.sleep(this.calculateBackoff(attempt));
180
+ continue;
181
+ }
182
+ throw error;
183
+ }
184
+
185
+ // Non-retryable errors throw immediately
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ // Should not reach here, but just in case
191
+ throw lastError ?? new RecallApiError('Unknown error', 'UNKNOWN_ERROR');
192
+ }
193
+
194
+ /**
195
+ * Calculate exponential backoff delay
196
+ */
197
+ private calculateBackoff(attempt: number): number {
198
+ // Exponential backoff: 1s, 2s, 4s, 8s...
199
+ const delay = this.retryBaseDelay * Math.pow(2, attempt);
200
+ // Add jitter (0-25% of delay)
201
+ const jitter = delay * Math.random() * 0.25;
202
+ return delay + jitter;
203
+ }
204
+
205
+ /**
206
+ * Sleep for a given number of milliseconds
207
+ */
208
+ private sleep(ms: number): Promise<void> {
209
+ return new Promise(resolve => setTimeout(resolve, ms));
210
+ }
211
+
212
+ // =========================================================================
213
+ // API Methods
214
+ // =========================================================================
215
+
216
+ /**
217
+ * Summarize a session transcript
218
+ * Called by saveSession tool after reading JSONL from disk
219
+ */
220
+ async summarize(request: SummarizeRequest): Promise<SummarizeResponse> {
221
+ return this.request<SummarizeResponse>('POST', '/api/v1/summarize', request);
222
+ }
223
+
224
+ /**
225
+ * Save a session with manual input (not from transcript)
226
+ * Used when user provides summary directly via MCP tool
227
+ */
228
+ async saveSession(repoId: string, data: SaveSessionRequest): Promise<SaveSessionResponse> {
229
+ return this.request<SaveSessionResponse>('POST', `/api/v1/repos/${repoId}/sessions`, data);
230
+ }
231
+
232
+ /**
233
+ * Get context for a repository (context.md)
234
+ * Returns the distilled team brain for this repo
235
+ */
236
+ async getContext(repoId: string): Promise<GetContextResponse> {
237
+ return this.request<GetContextResponse>('GET', `/api/v1/repos/${repoId}/context`);
238
+ }
239
+
240
+ /**
241
+ * Get history for a repository (context.md + history.md)
242
+ * Returns more detail than getContext
243
+ */
244
+ async getHistory(repoId: string): Promise<GetHistoryResponse> {
245
+ return this.request<GetHistoryResponse>('GET', `/api/v1/repos/${repoId}/history`);
246
+ }
247
+
248
+ /**
249
+ * Get full transcripts for a repository
250
+ * WARNING: Can be very large, uses many tokens
251
+ */
252
+ async getTranscripts(repoId: string): Promise<GetTranscriptsResponse> {
253
+ return this.request<GetTranscriptsResponse>('GET', `/api/v1/repos/${repoId}/transcripts`);
254
+ }
255
+
256
+ /**
257
+ * Log a decision for a repository
258
+ */
259
+ async logDecision(repoId: string, data: LogDecisionRequest): Promise<LogDecisionResponse> {
260
+ return this.request<LogDecisionResponse>('POST', `/api/v1/repos/${repoId}/decisions`, data);
261
+ }
262
+
263
+ /**
264
+ * Get the encryption key for a team
265
+ * Used to decrypt content locally
266
+ */
267
+ async getTeamKey(teamId: string): Promise<TeamKeyResponse> {
268
+ return this.request<TeamKeyResponse>('GET', `/api/v1/teams/${teamId}/key`);
269
+ }
270
+
271
+ /**
272
+ * List teams the user belongs to
273
+ */
274
+ async listTeams(): Promise<ListTeamsResponse> {
275
+ return this.request<ListTeamsResponse>('GET', '/api/v1/teams');
276
+ }
277
+
278
+ /**
279
+ * Get authentication status
280
+ */
281
+ async getStatus(): Promise<RecallStatusResponse> {
282
+ return this.request<RecallStatusResponse>('GET', '/api/v1/status');
283
+ }
284
+
285
+ /**
286
+ * Lookup or create repo by GitHub repo info
287
+ * Returns the repo ID for subsequent API calls
288
+ */
289
+ async resolveRepo(fullName: string, defaultBranch?: string): Promise<{ repoId: string; teamId: string }> {
290
+ return this.request<{ repoId: string; teamId: string }>('POST', '/api/v1/repos/resolve', {
291
+ fullName,
292
+ defaultBranch,
293
+ });
294
+ }
295
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Recall Configuration Management
3
+ *
4
+ * Manages the local config file at ~/.recall/config.json.
5
+ * Handles secure storage of API tokens and team keys.
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import type { RecallConfig } from '@recall_v3/shared';
12
+
13
+ // Current config file version
14
+ const CONFIG_VERSION = 1;
15
+
16
+ // Default API base URL (v3)
17
+ const DEFAULT_API_BASE_URL = 'https://api-v3.recall.team';
18
+
19
+ /**
20
+ * Get the path to the Recall config directory
21
+ */
22
+ export function getConfigDir(): string {
23
+ return path.join(os.homedir(), '.recall');
24
+ }
25
+
26
+ /**
27
+ * Get the path to the config file
28
+ */
29
+ export function getConfigPath(): string {
30
+ return path.join(getConfigDir(), 'config.json');
31
+ }
32
+
33
+ /**
34
+ * Ensure the config directory exists with secure permissions
35
+ */
36
+ function ensureConfigDir(): void {
37
+ const configDir = getConfigDir();
38
+ if (!fs.existsSync(configDir)) {
39
+ fs.mkdirSync(configDir, { mode: 0o700, recursive: true });
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Create a default empty config
45
+ */
46
+ function createDefaultConfig(): RecallConfig {
47
+ return {
48
+ token: '',
49
+ defaultTeamId: null,
50
+ activeTeamId: null,
51
+ teamKeys: {},
52
+ user: null,
53
+ lastSyncAt: null,
54
+ version: CONFIG_VERSION,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Migrate old config format to new if necessary
60
+ */
61
+ function migrateConfig(config: Partial<RecallConfig>): RecallConfig {
62
+ const migrated = { ...createDefaultConfig(), ...config };
63
+
64
+ // Future migration logic would go here
65
+ // For now, just ensure version is set
66
+ migrated.version = CONFIG_VERSION;
67
+
68
+ return migrated;
69
+ }
70
+
71
+ /**
72
+ * Load configuration from disk
73
+ * Returns a default config if the file doesn't exist
74
+ */
75
+ export function loadConfig(): RecallConfig {
76
+ const configPath = getConfigPath();
77
+
78
+ if (!fs.existsSync(configPath)) {
79
+ return createDefaultConfig();
80
+ }
81
+
82
+ try {
83
+ const raw = fs.readFileSync(configPath, 'utf-8');
84
+ const parsed = JSON.parse(raw) as Partial<RecallConfig>;
85
+
86
+ // Migrate if necessary
87
+ if (!parsed.version || parsed.version < CONFIG_VERSION) {
88
+ const migrated = migrateConfig(parsed);
89
+ saveConfig(migrated);
90
+ return migrated;
91
+ }
92
+
93
+ return migrateConfig(parsed);
94
+ } catch (error) {
95
+ // If config is corrupted, return default
96
+ console.error('Warning: Could not load Recall config, using defaults');
97
+ return createDefaultConfig();
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Save configuration to disk with secure permissions (0600)
103
+ */
104
+ export function saveConfig(config: Partial<RecallConfig>): void {
105
+ ensureConfigDir();
106
+
107
+ const current = loadConfig();
108
+ const merged: RecallConfig = {
109
+ ...current,
110
+ ...config,
111
+ version: CONFIG_VERSION,
112
+ };
113
+
114
+ const configPath = getConfigPath();
115
+ const json = JSON.stringify(merged, null, 2);
116
+
117
+ // Write with secure permissions (0600 = owner read/write only)
118
+ fs.writeFileSync(configPath, json, { mode: 0o600 });
119
+ }
120
+
121
+ /**
122
+ * Get the API base URL from config or environment
123
+ */
124
+ export function getApiBaseUrl(): string {
125
+ // Environment variable takes precedence (for development)
126
+ const envUrl = process.env.RECALL_API_URL;
127
+ if (envUrl) {
128
+ return envUrl;
129
+ }
130
+
131
+ return DEFAULT_API_BASE_URL;
132
+ }
133
+
134
+ /**
135
+ * Get the API token from config or environment
136
+ */
137
+ export function getApiToken(): string | null {
138
+ // Environment variable takes precedence
139
+ const envToken = process.env.RECALL_API_TOKEN;
140
+ if (envToken) {
141
+ return envToken;
142
+ }
143
+
144
+ const config = loadConfig();
145
+ return config.token || null;
146
+ }
147
+
148
+ /**
149
+ * Store the API token securely
150
+ */
151
+ export function setApiToken(token: string): void {
152
+ saveConfig({ token });
153
+ }
154
+
155
+ /**
156
+ * Get the active team ID
157
+ */
158
+ export function getActiveTeamId(): string | null {
159
+ const config = loadConfig();
160
+ return config.activeTeamId ?? config.defaultTeamId ?? null;
161
+ }
162
+
163
+ /**
164
+ * Set the active team ID for this session
165
+ */
166
+ export function setActiveTeamId(teamId: string): void {
167
+ saveConfig({ activeTeamId: teamId });
168
+ }
169
+
170
+ /**
171
+ * Set the default team ID
172
+ */
173
+ export function setDefaultTeamId(teamId: string): void {
174
+ saveConfig({ defaultTeamId: teamId, activeTeamId: teamId });
175
+ }
176
+
177
+ /**
178
+ * Get a cached team encryption key
179
+ */
180
+ export function getTeamKey(teamId: string): string | null {
181
+ const config = loadConfig();
182
+ return config.teamKeys[teamId] ?? null;
183
+ }
184
+
185
+ /**
186
+ * Cache a team encryption key
187
+ */
188
+ export function setTeamKey(teamId: string, key: string): void {
189
+ const config = loadConfig();
190
+ const teamKeys = { ...config.teamKeys, [teamId]: key };
191
+ saveConfig({ teamKeys });
192
+ }
193
+
194
+ /**
195
+ * Update cached user info
196
+ */
197
+ export function setUserInfo(user: { id: string; email: string | null; githubUsername: string }): void {
198
+ saveConfig({ user });
199
+ }
200
+
201
+ /**
202
+ * Update last sync timestamp
203
+ */
204
+ export function setLastSyncAt(timestamp: string): void {
205
+ saveConfig({ lastSyncAt: timestamp });
206
+ }
207
+
208
+ /**
209
+ * Check if the user is authenticated (has a token)
210
+ */
211
+ export function isAuthenticated(): boolean {
212
+ return !!getApiToken();
213
+ }
214
+
215
+ /**
216
+ * Clear all authentication data (logout)
217
+ */
218
+ export function clearAuth(): void {
219
+ saveConfig({
220
+ token: '',
221
+ activeTeamId: null,
222
+ teamKeys: {},
223
+ user: null,
224
+ lastSyncAt: null,
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Extended config interface for the MCP client
230
+ * Adds computed properties and convenience methods
231
+ */
232
+ export interface ExtendedConfig extends RecallConfig {
233
+ apiBaseUrl: string;
234
+ isAuthenticated: boolean;
235
+ }
236
+
237
+ /**
238
+ * Get the full extended config
239
+ */
240
+ export function getExtendedConfig(): ExtendedConfig {
241
+ const config = loadConfig();
242
+ return {
243
+ ...config,
244
+ apiBaseUrl: getApiBaseUrl(),
245
+ isAuthenticated: isAuthenticated(),
246
+ };
247
+ }