@recall_v3/mcp-server 0.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * getContext Tool Implementation
3
3
  *
4
- * Fetches the team brain (context.md) for the current repository.
4
+ * Fetches the team brain (context) for the current repository from Recall v3 API.
5
5
  * This is the distilled current state - loaded at every session start.
6
6
  */
7
7
 
8
8
  import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
9
9
  import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
10
- import { decryptContent } from '../crypto/index.js';
10
+ import { decryptFromApi } from '../crypto/index.js';
11
11
  import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
12
12
  import { resolveProjectPath, getRepoInfo } from './utils.js';
13
13
 
@@ -15,11 +15,100 @@ export interface GetContextArgs {
15
15
  projectPath?: string;
16
16
  }
17
17
 
18
+ interface SessionFromApi {
19
+ id: string;
20
+ encrypted_content: string;
21
+ started_at: string;
22
+ ended_at: string;
23
+ status: string;
24
+ tldr_summary: string | null;
25
+ user: {
26
+ id: string;
27
+ name: string | null;
28
+ github_username: string | null;
29
+ avatar_url: string | null;
30
+ };
31
+ }
32
+
33
+ interface ContextApiResponse {
34
+ sessions: SessionFromApi[];
35
+ cached_context: string;
36
+ updated_at: string;
37
+ session_count: number;
38
+ tier: string;
39
+ }
40
+
41
+ /**
42
+ * Build context markdown from sessions
43
+ */
44
+ function buildContextFromSessions(
45
+ sessions: SessionFromApi[],
46
+ teamKey: string,
47
+ repoName: string
48
+ ): string {
49
+ const parts: string[] = [];
50
+
51
+ parts.push(`# Team Memory - ${repoName}`);
52
+ parts.push('');
53
+ parts.push(`Last updated: ${new Date().toISOString().split('T')[0]}`);
54
+ parts.push('');
55
+
56
+ if (sessions.length === 0) {
57
+ parts.push('No sessions recorded yet. Use `recall save` to save your first session.');
58
+ return parts.join('\n');
59
+ }
60
+
61
+ parts.push('## Recent Sessions');
62
+ parts.push('');
63
+
64
+ // Show last 10 sessions for context
65
+ const recentSessions = sessions.slice(0, 10);
66
+
67
+ for (const session of recentSessions) {
68
+ const date = session.started_at.split('T')[0];
69
+ const developer = session.user.github_username || session.user.name || 'Unknown';
70
+
71
+ parts.push(`### ${date} - ${developer}`);
72
+ parts.push('');
73
+
74
+ // Use TLDR summary for quick context
75
+ if (session.tldr_summary) {
76
+ parts.push(session.tldr_summary);
77
+ parts.push('');
78
+ }
79
+
80
+ // Try to decrypt and include full content if available
81
+ if (session.encrypted_content && session.encrypted_content.startsWith('RECALL_ENCRYPTED:')) {
82
+ try {
83
+ const decrypted = decryptFromApi(session.encrypted_content, teamKey);
84
+ parts.push(decrypted);
85
+ parts.push('');
86
+ } catch (error) {
87
+ // If decryption fails, just use the TLDR
88
+ if (!session.tldr_summary) {
89
+ parts.push('(Session content encrypted)');
90
+ parts.push('');
91
+ }
92
+ }
93
+ }
94
+
95
+ parts.push('---');
96
+ parts.push('');
97
+ }
98
+
99
+ if (sessions.length > 10) {
100
+ parts.push(`*${sessions.length - 10} more sessions available. Use recall_get_history for full history.*`);
101
+ parts.push('');
102
+ }
103
+
104
+ return parts.join('\n');
105
+ }
106
+
18
107
  /**
19
108
  * Execute the getContext tool
20
109
  *
21
110
  * @param args - Tool arguments (projectPath is optional)
22
- * @returns MCP tool response with context.md content
111
+ * @returns MCP tool response with context content
23
112
  */
24
113
  export async function getContext(args: GetContextArgs): Promise<ToolResponse> {
25
114
  try {
@@ -61,25 +150,18 @@ export async function getContext(args: GetContextArgs): Promise<ToolResponse> {
61
150
  }
62
151
 
63
152
  // 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
- }
153
+ const response = await client.getContextV3(repoId) as ContextApiResponse;
154
+
155
+ // Build context markdown from sessions
156
+ const contextMd = buildContextFromSessions(
157
+ response.sessions,
158
+ teamKey,
159
+ repoInfo.fullName
160
+ );
80
161
 
81
162
  // Format output with metadata
82
- const header = `Reading from: ${projectPath}/.recall`;
163
+ const header = `# Team Memory (Recall)\n\nThe following is your team's shared context for this repository.\nUse this to understand what has been built, why decisions were made,\nand what mistakes to avoid.\n\n---`;
164
+
83
165
  return formattedResponse(header, contextMd);
84
166
 
85
167
  } catch (error) {
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * getHistory Tool Implementation
3
3
  *
4
- * Fetches context.md + recent session history for the repository.
4
+ * Fetches context + recent session history for the repository from Recall v3 API.
5
5
  * This provides more detail than getContext but uses more tokens.
6
6
  */
7
7
 
8
8
  import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
9
9
  import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
10
- import { decryptContent } from '../crypto/index.js';
10
+ import { decryptFromApi } from '../crypto/index.js';
11
11
  import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
12
12
  import { resolveProjectPath, getRepoInfo } from './utils.js';
13
13
 
@@ -15,6 +15,151 @@ export interface GetHistoryArgs {
15
15
  projectPath: string;
16
16
  }
17
17
 
18
+ interface SessionFromApi {
19
+ id: string;
20
+ encrypted_content: string;
21
+ started_at: string;
22
+ ended_at: string;
23
+ status: string;
24
+ tldr_summary: string | null;
25
+ user: {
26
+ id: string;
27
+ name: string | null;
28
+ github_username: string | null;
29
+ avatar_url: string | null;
30
+ };
31
+ }
32
+
33
+ interface DecisionFromApi {
34
+ id: string;
35
+ title: string;
36
+ encrypted_content: string;
37
+ created_at: string;
38
+ user: {
39
+ id: string;
40
+ name: string | null;
41
+ github_username: string | null;
42
+ avatar_url: string | null;
43
+ };
44
+ }
45
+
46
+ interface MistakeFromApi {
47
+ id: string;
48
+ title: string;
49
+ encrypted_content: string;
50
+ created_at: string;
51
+ user: {
52
+ id: string;
53
+ name: string | null;
54
+ github_username: string | null;
55
+ avatar_url: string | null;
56
+ };
57
+ }
58
+
59
+ interface HistoryApiResponse {
60
+ sessions: SessionFromApi[];
61
+ decisions: DecisionFromApi[];
62
+ mistakes: MistakeFromApi[];
63
+ token_warning?: string;
64
+ }
65
+
66
+ /**
67
+ * Build history markdown from API response
68
+ */
69
+ function buildHistoryMarkdown(
70
+ response: HistoryApiResponse,
71
+ teamKey: string,
72
+ repoName: string
73
+ ): string {
74
+ const parts: string[] = [];
75
+
76
+ parts.push(`# Team Memory History - ${repoName}`);
77
+ parts.push('');
78
+ parts.push(`Generated: ${new Date().toISOString()}`);
79
+ parts.push('');
80
+
81
+ // Decisions section
82
+ if (response.decisions.length > 0) {
83
+ parts.push('## Key Decisions');
84
+ parts.push('');
85
+ for (const decision of response.decisions) {
86
+ const date = decision.created_at.split('T')[0];
87
+ const author = decision.user.github_username || decision.user.name || 'Unknown';
88
+ parts.push(`### ${date} - ${author}`);
89
+ parts.push('');
90
+ parts.push(`**${decision.title}**`);
91
+ parts.push('');
92
+
93
+ // Try to decrypt full content
94
+ if (decision.encrypted_content && decision.encrypted_content.startsWith('RECALL_ENCRYPTED:')) {
95
+ try {
96
+ const decrypted = decryptFromApi(decision.encrypted_content, teamKey);
97
+ parts.push(decrypted);
98
+ } catch {
99
+ // Use title only
100
+ }
101
+ }
102
+ parts.push('');
103
+ }
104
+ parts.push('---');
105
+ parts.push('');
106
+ }
107
+
108
+ // Mistakes section
109
+ if (response.mistakes.length > 0) {
110
+ parts.push('## Mistakes & Gotchas');
111
+ parts.push('');
112
+ parts.push('*Things the team learned the hard way - avoid repeating these.*');
113
+ parts.push('');
114
+ for (const mistake of response.mistakes) {
115
+ const date = mistake.created_at.split('T')[0];
116
+ const author = mistake.user.github_username || mistake.user.name || 'Unknown';
117
+ parts.push(`- **${mistake.title}** (${date}, ${author})`);
118
+ }
119
+ parts.push('');
120
+ parts.push('---');
121
+ parts.push('');
122
+ }
123
+
124
+ // Sessions section
125
+ parts.push('## Session History');
126
+ parts.push('');
127
+
128
+ if (response.sessions.length === 0) {
129
+ parts.push('No sessions recorded yet.');
130
+ } else {
131
+ for (const session of response.sessions) {
132
+ const date = session.started_at.split('T')[0];
133
+ const developer = session.user.github_username || session.user.name || 'Unknown';
134
+
135
+ parts.push(`### ${date} - ${developer}`);
136
+ parts.push('');
137
+
138
+ // TLDR summary
139
+ if (session.tldr_summary) {
140
+ parts.push(`**Summary:** ${session.tldr_summary}`);
141
+ parts.push('');
142
+ }
143
+
144
+ // Full content if available
145
+ if (session.encrypted_content && session.encrypted_content.startsWith('RECALL_ENCRYPTED:')) {
146
+ try {
147
+ const decrypted = decryptFromApi(session.encrypted_content, teamKey);
148
+ parts.push(decrypted);
149
+ } catch {
150
+ // Use TLDR only
151
+ }
152
+ }
153
+
154
+ parts.push('');
155
+ parts.push('---');
156
+ parts.push('');
157
+ }
158
+ }
159
+
160
+ return parts.join('\n');
161
+ }
162
+
18
163
  /**
19
164
  * Execute the getHistory tool
20
165
  *
@@ -61,38 +206,19 @@ export async function getHistory(args: GetHistoryArgs): Promise<ToolResponse> {
61
206
  }
62
207
 
63
208
  // 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
- }
209
+ const response = await client.getHistoryV3(repoId) as HistoryApiResponse;
89
210
 
90
- // Combine context and history
91
- const combinedContent = `# Recall Context\n\n${contextMd}\n\n---\n\n# Session History\n\n${historyMd}`;
211
+ // Build history markdown
212
+ const historyMd = buildHistoryMarkdown(response, teamKey, repoInfo.fullName);
92
213
 
93
214
  // Format output with metadata
94
- const header = `Reading history from: ${projectPath}/.recall (${response.sessionCount} sessions)`;
95
- return formattedResponse(header, combinedContent);
215
+ const sessionCount = response.sessions.length;
216
+ const decisionCount = response.decisions.length;
217
+ const mistakeCount = response.mistakes.length;
218
+
219
+ const header = `# Team Memory (Recall)\n\nRepository: ${repoInfo.fullName}\nSessions: ${sessionCount} | Decisions: ${decisionCount} | Mistakes: ${mistakeCount}\n\n---`;
220
+
221
+ return formattedResponse(header, historyMd);
96
222
 
97
223
  } catch (error) {
98
224
  if (error instanceof AuthenticationError) {
@@ -123,7 +123,7 @@ export async function getTranscripts(args: GetTranscriptsArgs): Promise<ToolResp
123
123
  ].join('\n');
124
124
 
125
125
  // Format output
126
- const header = `Reading full transcripts from: ${projectPath}/.recall`;
126
+ const header = `Full session transcripts for: ${repoInfo.fullName}`;
127
127
  return formattedResponse(header, combinedContent);
128
128
 
129
129
  } catch (error) {