@memoire-ai/mcp-runtime 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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +13 -0
- package/dist/ast-parser-inline.d.ts +49 -0
- package/dist/ast-parser-inline.d.ts.map +1 -0
- package/dist/ast-parser-inline.js +278 -0
- package/dist/ast-parser-inline.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1137 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +165 -0
- package/dist/index.test.js.map +1 -0
- package/package.json +28 -0
- package/src/ast-parser-inline.ts +277 -0
- package/src/index.test.ts +183 -0
- package/src/index.ts +1470 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { MemoireClient } from '@memoire-ai/sdk';
|
|
5
|
+
export function createMemoireMcpServer(config, deps) {
|
|
6
|
+
const client = deps?.client ?? new MemoireClient({
|
|
7
|
+
apiUrl: config.apiUrl,
|
|
8
|
+
apiKey: config.apiKey,
|
|
9
|
+
clientId: config.clientId,
|
|
10
|
+
});
|
|
11
|
+
const server = new McpServer({
|
|
12
|
+
name: 'memoire',
|
|
13
|
+
version: '0.2.0',
|
|
14
|
+
});
|
|
15
|
+
// ── Search Context ─────────────────────────────────────────────────
|
|
16
|
+
server.tool('search_context', 'Search shared project context — decisions, observations, attempts, and teammate work. Uses hybrid search (vector + fulltext + metadata). Use the detail parameter to control how much context is returned: summary (L0, cheapest), overview (L1, balanced), or full (L2, richest).', {
|
|
17
|
+
query: z.string().describe('Search query'),
|
|
18
|
+
event_types: z.array(z.string()).optional().describe('Filter by event types'),
|
|
19
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
20
|
+
strategy: z.enum(['auto', 'vector', 'fulltext', 'metadata', 'hybrid']).optional().describe('Search strategy (default: auto)'),
|
|
21
|
+
detail: z.enum(['summary', 'overview', 'full']).optional().describe('Detail level: summary (L0, ~200 tokens), overview (L1, ~1000 tokens), full (L2, complete content). Default: summary'),
|
|
22
|
+
}, async ({ query, event_types, limit, strategy, detail }) => {
|
|
23
|
+
const res = await client.searchContext({
|
|
24
|
+
org_id: config.orgId,
|
|
25
|
+
project_id: config.projectId,
|
|
26
|
+
viewer_user_id: config.userId,
|
|
27
|
+
query,
|
|
28
|
+
filters: event_types ? { event_types } : undefined,
|
|
29
|
+
limit,
|
|
30
|
+
detail: detail ?? 'summary',
|
|
31
|
+
});
|
|
32
|
+
const text = res.results
|
|
33
|
+
.map((r) => {
|
|
34
|
+
const header = `[${r.event_type}] (score: ${r.score.toFixed(2)}) ${r.summary}`;
|
|
35
|
+
const meta = ` by ${r.user_id} via ${r.client} at ${r.created_at}`;
|
|
36
|
+
const feedbackLine = r.feedback_score ? ` feedback: ${r.feedback_score > 0 ? '+' : ''}${r.feedback_score}${r.feedback_labels?.length ? ` [${r.feedback_labels.join(', ')}]` : ''}` : '';
|
|
37
|
+
const body = detail === 'full' && r.content
|
|
38
|
+
? `\n ${r.content}`
|
|
39
|
+
: detail === 'overview' && r.overview
|
|
40
|
+
? `\n ${r.overview}`
|
|
41
|
+
: '';
|
|
42
|
+
return `${header}\n${meta}${feedbackLine}${body}`;
|
|
43
|
+
})
|
|
44
|
+
.join('\n\n');
|
|
45
|
+
return { content: [{ type: 'text', text: text || 'No results found.' }] };
|
|
46
|
+
});
|
|
47
|
+
// ── Timeline Context ───────────────────────────────────────────────
|
|
48
|
+
server.tool('timeline_context', 'Get chronological project context — recent activity, branch work, session history.', {
|
|
49
|
+
branch_name: z.string().optional().describe('Filter by branch'),
|
|
50
|
+
limit: z.number().optional().describe('Max entries (default 50)'),
|
|
51
|
+
}, async ({ branch_name, limit }) => {
|
|
52
|
+
const res = await client.timelineContext({
|
|
53
|
+
org_id: config.orgId,
|
|
54
|
+
project_id: config.projectId,
|
|
55
|
+
viewer_user_id: config.userId,
|
|
56
|
+
branch_name,
|
|
57
|
+
limit,
|
|
58
|
+
});
|
|
59
|
+
const text = res.entries
|
|
60
|
+
.map((e) => `[${e.created_at}] ${e.event_type}: ${e.content.slice(0, 300)}`)
|
|
61
|
+
.join('\n\n');
|
|
62
|
+
return { content: [{ type: 'text', text: text || 'No timeline entries.' }] };
|
|
63
|
+
});
|
|
64
|
+
// ── Project Profile ────────────────────────────────────────────────
|
|
65
|
+
server.tool('project_profile', 'Get the stable shared project profile — architecture, conventions, current focus, and open threads.', {}, async () => {
|
|
66
|
+
const res = await client.projectProfile({
|
|
67
|
+
org_id: config.orgId,
|
|
68
|
+
project_id: config.projectId,
|
|
69
|
+
viewer_user_id: config.userId,
|
|
70
|
+
});
|
|
71
|
+
const sections = [];
|
|
72
|
+
sections.push(`# Project Profile`);
|
|
73
|
+
sections.push(res.project_profile.summary);
|
|
74
|
+
appendProfileSection(sections, 'Architecture', res.project_profile.architecture);
|
|
75
|
+
appendProfileSection(sections, 'Conventions', res.project_profile.conventions);
|
|
76
|
+
appendProfileSection(sections, 'Current Focus', res.project_profile.current_focus);
|
|
77
|
+
appendProfileSection(sections, 'Recent Decisions', res.project_profile.recent_decisions);
|
|
78
|
+
appendProfileSection(sections, 'Open Threads', res.project_profile.open_threads);
|
|
79
|
+
appendProfileSection(sections, 'Key Files', res.project_profile.key_files);
|
|
80
|
+
sections.push(`Source coverage: ${res.source_event_count} events across ${res.source_event_types.join(', ') || 'no event types yet'}`);
|
|
81
|
+
return { content: [{ type: 'text', text: sections.join('\n\n') }] };
|
|
82
|
+
});
|
|
83
|
+
// ── Project Facts ──────────────────────────────────────────────────
|
|
84
|
+
server.tool('project_facts', 'Get derived static and dynamic project facts distilled from shared memory.', {}, async () => {
|
|
85
|
+
const res = await client.projectFacts({
|
|
86
|
+
org_id: config.orgId,
|
|
87
|
+
project_id: config.projectId,
|
|
88
|
+
viewer_user_id: config.userId,
|
|
89
|
+
});
|
|
90
|
+
const sections = ['# Project Facts'];
|
|
91
|
+
appendFactSection(sections, 'Static Facts', res.facts.static.map((fact) => `${fact.text} (${fact.category})`));
|
|
92
|
+
appendFactSection(sections, 'Dynamic Facts', res.facts.dynamic.map((fact) => `${fact.text} (${fact.category})`));
|
|
93
|
+
sections.push(`Source coverage: ${res.source_event_count} events across ${res.source_event_types.join(', ') || 'no event types yet'}`);
|
|
94
|
+
return { content: [{ type: 'text', text: sections.join('\n\n') }] };
|
|
95
|
+
});
|
|
96
|
+
// ── Assemble Context ───────────────────────────────────────────────
|
|
97
|
+
server.tool('assemble_context', 'Get a prompt-ready context block with decisions, failed attempts, active work, and teammate activity.', {
|
|
98
|
+
query: z.string().optional().describe('Optional focus query'),
|
|
99
|
+
token_budget: z.number().optional().describe('Max tokens for context block (default 4000)'),
|
|
100
|
+
}, async ({ query, token_budget }) => {
|
|
101
|
+
const res = await client.assembleContext({
|
|
102
|
+
org_id: config.orgId,
|
|
103
|
+
project_id: config.projectId,
|
|
104
|
+
viewer_user_id: config.userId,
|
|
105
|
+
query,
|
|
106
|
+
token_budget,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: res.context_block || 'No context available for this project yet.',
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
// ── Saved Handoffs ────────────────────────────────────────────────
|
|
116
|
+
server.tool('save_handoff', 'Assemble a prompt-ready context block and save it as a reusable handoff for teammates or other agents.', {
|
|
117
|
+
query: z.string().optional().describe('Optional focus query for the handoff'),
|
|
118
|
+
title: z.string().optional().describe('Optional custom handoff title'),
|
|
119
|
+
token_budget: z.number().optional().describe('Max tokens for the generated handoff block (default 2400)'),
|
|
120
|
+
}, async ({ query, title, token_budget }) => {
|
|
121
|
+
const trimmedQuery = query?.trim() || undefined;
|
|
122
|
+
const assembled = await client.assembleContext({
|
|
123
|
+
org_id: config.orgId,
|
|
124
|
+
project_id: config.projectId,
|
|
125
|
+
viewer_user_id: config.userId,
|
|
126
|
+
query: trimmedQuery,
|
|
127
|
+
token_budget: token_budget ?? 2400,
|
|
128
|
+
});
|
|
129
|
+
const saved = await client.createHandoff({
|
|
130
|
+
org_id: config.orgId,
|
|
131
|
+
project_id: config.projectId,
|
|
132
|
+
viewer_user_id: config.userId,
|
|
133
|
+
title: title?.trim() || undefined,
|
|
134
|
+
query: trimmedQuery,
|
|
135
|
+
context_block: assembled.context_block,
|
|
136
|
+
source_event_ids: assembled.event_ids,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
content: [{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: [
|
|
142
|
+
`Saved handoff ${saved.handoff.id}`,
|
|
143
|
+
`Title: ${saved.handoff.title}`,
|
|
144
|
+
`Summary: ${saved.handoff.summary}`,
|
|
145
|
+
'',
|
|
146
|
+
saved.handoff.context_block,
|
|
147
|
+
].join('\n'),
|
|
148
|
+
}],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
server.tool('list_handoffs', 'List recent saved handoffs for this project so another agent can pick up the right context thread quickly.', {
|
|
152
|
+
limit: z.number().optional().describe('Max handoffs to return (default 10)'),
|
|
153
|
+
}, async ({ limit }) => {
|
|
154
|
+
const res = await client.listHandoffs({
|
|
155
|
+
org_id: config.orgId,
|
|
156
|
+
project_id: config.projectId,
|
|
157
|
+
viewer_user_id: config.userId,
|
|
158
|
+
limit,
|
|
159
|
+
});
|
|
160
|
+
const text = res.handoffs
|
|
161
|
+
.map((handoff) => `[${handoff.id}] ${handoff.title}\n ${handoff.summary}\n created ${handoff.created_at}`)
|
|
162
|
+
.join('\n\n');
|
|
163
|
+
return { content: [{ type: 'text', text: text || 'No saved handoffs yet.' }] };
|
|
164
|
+
});
|
|
165
|
+
server.tool('get_handoff', 'Fetch a saved handoff by ID and return the full prompt-ready context block.', {
|
|
166
|
+
handoff_id: z.string().describe('Saved handoff ID'),
|
|
167
|
+
}, async ({ handoff_id }) => {
|
|
168
|
+
const res = await client.getHandoff({
|
|
169
|
+
org_id: config.orgId,
|
|
170
|
+
project_id: config.projectId,
|
|
171
|
+
viewer_user_id: config.userId,
|
|
172
|
+
handoff_id,
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
content: [{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: [
|
|
178
|
+
`# ${res.handoff.title}`,
|
|
179
|
+
`ID: ${res.handoff.id}`,
|
|
180
|
+
`Summary: ${res.handoff.summary}`,
|
|
181
|
+
'',
|
|
182
|
+
res.handoff.context_block,
|
|
183
|
+
].join('\n'),
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
// ── Auto-subscribe Dependencies ──────────────────────────────────
|
|
188
|
+
server.tool('auto_subscribe_dependencies', 'Parse a package manifest (package.json, requirements.txt, Cargo.toml) and auto-index documentation for all dependencies. Provide the file path and content.', {
|
|
189
|
+
manifest_path: z.string().describe('File path of the manifest (e.g. "package.json")'),
|
|
190
|
+
manifest_content: z.string().describe('Full content of the manifest file'),
|
|
191
|
+
}, async ({ manifest_path, manifest_content }) => {
|
|
192
|
+
const res = await client.autoSubscribeDocs({
|
|
193
|
+
org_id: config.orgId,
|
|
194
|
+
project_id: config.projectId,
|
|
195
|
+
viewer_user_id: config.userId,
|
|
196
|
+
manifest_path,
|
|
197
|
+
manifest_content,
|
|
198
|
+
});
|
|
199
|
+
const parts = [];
|
|
200
|
+
parts.push(`# Auto-Subscribe Results`);
|
|
201
|
+
parts.push(`Dependencies found: ${res.dependencies_found}`);
|
|
202
|
+
parts.push(`Indexed: ${res.dependencies_indexed}`);
|
|
203
|
+
parts.push(`Skipped: ${res.dependencies_skipped}`);
|
|
204
|
+
parts.push(`Failed: ${res.dependencies_failed}`);
|
|
205
|
+
if (res.results.length > 0) {
|
|
206
|
+
parts.push('');
|
|
207
|
+
for (const r of res.results.slice(0, 20)) {
|
|
208
|
+
parts.push(`- ${r.dependency_name}@${r.dependency_version}: ${r.status} (${r.chunks_indexed} chunks)${r.reason ? ` — ${r.reason}` : ''}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
212
|
+
});
|
|
213
|
+
// ── Index Docs ─────────────────────────────────────────────────────
|
|
214
|
+
server.tool('index_docs', 'Index documentation for specific packages by name. Fetches from npm/PyPI/crates registries and indexes for search. Use the language parameter to index language-specific variants (e.g., Python vs JavaScript docs for the same library).', {
|
|
215
|
+
packages: z.array(z.object({
|
|
216
|
+
name: z.string().describe('Package name'),
|
|
217
|
+
version: z.string().optional().describe('Package version (default: latest)'),
|
|
218
|
+
ecosystem: z.enum(['npm', 'pip', 'cargo', 'go', 'internal']).describe('Package ecosystem'),
|
|
219
|
+
language: z.string().optional().describe('Language variant (e.g. "python", "javascript", "typescript")'),
|
|
220
|
+
})).min(1).max(50).describe('Packages to index'),
|
|
221
|
+
}, async ({ packages }) => {
|
|
222
|
+
const dependencies = packages.map((pkg) => ({
|
|
223
|
+
name: pkg.name,
|
|
224
|
+
version: pkg.version ?? 'latest',
|
|
225
|
+
ecosystem: pkg.ecosystem,
|
|
226
|
+
language: pkg.language,
|
|
227
|
+
}));
|
|
228
|
+
const res = await client.indexDocs({
|
|
229
|
+
org_id: config.orgId,
|
|
230
|
+
project_id: config.projectId,
|
|
231
|
+
viewer_user_id: config.userId,
|
|
232
|
+
dependencies,
|
|
233
|
+
});
|
|
234
|
+
const parts = [`# Index Results (${res.results.length} packages)`];
|
|
235
|
+
for (const r of res.results) {
|
|
236
|
+
parts.push(`- ${r.dependency_name}@${r.dependency_version}: ${r.status} (${r.chunks_indexed} chunks)${r.reason ? ` — ${r.reason}` : ''}`);
|
|
237
|
+
}
|
|
238
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
239
|
+
});
|
|
240
|
+
// ── Index URL Docs ─────────────────────────────────────────────────
|
|
241
|
+
server.tool('index_url_docs', 'Index documentation from any URL. Fetches the page content and indexes it for search. Works with any website, documentation page, or llms.txt endpoint.', {
|
|
242
|
+
url: z.string().describe('URL to fetch and index'),
|
|
243
|
+
name: z.string().optional().describe('Name for the documentation source (default: hostname)'),
|
|
244
|
+
}, async ({ url, name }) => {
|
|
245
|
+
const res = await client.indexUrlDocs({
|
|
246
|
+
org_id: config.orgId,
|
|
247
|
+
project_id: config.projectId,
|
|
248
|
+
viewer_user_id: config.userId,
|
|
249
|
+
url,
|
|
250
|
+
name,
|
|
251
|
+
});
|
|
252
|
+
if (res.status === 'indexed') {
|
|
253
|
+
return { content: [{ type: 'text', text: `Indexed "${res.name}" from ${res.url} — ${res.chunks_indexed} chunks stored. Use search_docs to query.` }] };
|
|
254
|
+
}
|
|
255
|
+
return { content: [{ type: 'text', text: `Failed to index ${res.url}: ${res.reason}` }] };
|
|
256
|
+
});
|
|
257
|
+
// ── Scratchpad (short-lived notes) ─────────────────────────────────
|
|
258
|
+
server.tool('write_scratchpad', 'Write a short-lived working note that auto-expires in 24 hours. Visible to all agents. Use for temporary context like "auth uses JWT" or "currently debugging the payment flow".', {
|
|
259
|
+
content: z.string().describe('The scratchpad note content'),
|
|
260
|
+
concepts: z.array(z.string()).optional().describe('Related concepts for search'),
|
|
261
|
+
}, async ({ content, concepts }) => {
|
|
262
|
+
await client.ingestEvent({
|
|
263
|
+
org_id: config.orgId,
|
|
264
|
+
project_id: config.projectId,
|
|
265
|
+
user_id: config.userId,
|
|
266
|
+
session_id: config.sessionId ?? `mcp-${Date.now()}`,
|
|
267
|
+
client: 'cloud',
|
|
268
|
+
event_type: 'observation',
|
|
269
|
+
content: `[scratchpad] ${content}`,
|
|
270
|
+
concepts,
|
|
271
|
+
created_at: new Date().toISOString(),
|
|
272
|
+
});
|
|
273
|
+
return { content: [{ type: 'text', text: `Scratchpad note saved (expires in 24h). Searchable via search_context.` }] };
|
|
274
|
+
});
|
|
275
|
+
server.tool('get_scratchpad', 'Retrieve recent scratchpad notes (short-lived working memory).', {
|
|
276
|
+
limit: z.number().optional().describe('Max notes to retrieve (default 10)'),
|
|
277
|
+
}, async ({ limit }) => {
|
|
278
|
+
const res = await client.searchContext({
|
|
279
|
+
org_id: config.orgId,
|
|
280
|
+
project_id: config.projectId,
|
|
281
|
+
viewer_user_id: config.userId,
|
|
282
|
+
query: '[scratchpad]',
|
|
283
|
+
limit: limit ?? 10,
|
|
284
|
+
});
|
|
285
|
+
const notes = res.results
|
|
286
|
+
.filter((r) => r.content?.includes('[scratchpad]') || r.summary?.includes('[scratchpad]'))
|
|
287
|
+
.map((r) => {
|
|
288
|
+
const text = (r.content ?? r.summary ?? '').replace('[scratchpad] ', '');
|
|
289
|
+
return `- ${text} (by ${r.user_id}, ${r.created_at})`;
|
|
290
|
+
});
|
|
291
|
+
if (notes.length === 0) {
|
|
292
|
+
return { content: [{ type: 'text', text: 'No scratchpad notes found.' }] };
|
|
293
|
+
}
|
|
294
|
+
return { content: [{ type: 'text', text: `# Scratchpad Notes\n${notes.join('\n')}` }] };
|
|
295
|
+
});
|
|
296
|
+
// ── Search Docs ────────────────────────────────────────────────────
|
|
297
|
+
server.tool('search_docs', 'Search indexed dependency and internal package documentation for the current project. Use the language parameter to filter results by language variant.', {
|
|
298
|
+
query: z.string().describe('Documentation search query'),
|
|
299
|
+
dependency_names: z.array(z.string()).optional().describe('Optional dependency filter'),
|
|
300
|
+
language: z.string().optional().describe('Filter by language variant (e.g. "python", "javascript")'),
|
|
301
|
+
limit: z.number().optional().describe('Max results (default 10)'),
|
|
302
|
+
}, async ({ query, dependency_names, language, limit }) => {
|
|
303
|
+
const res = await client.searchDocs({
|
|
304
|
+
org_id: config.orgId,
|
|
305
|
+
project_id: config.projectId,
|
|
306
|
+
viewer_user_id: config.userId,
|
|
307
|
+
query,
|
|
308
|
+
dependency_names,
|
|
309
|
+
language,
|
|
310
|
+
limit,
|
|
311
|
+
});
|
|
312
|
+
const text = res.results
|
|
313
|
+
.map((r) => {
|
|
314
|
+
const langTag = r.language ? ` (${r.language})` : '';
|
|
315
|
+
const notes = Array.isArray(r.attached_annotations) && r.attached_annotations.length > 0
|
|
316
|
+
? `\nNotes:\n${r.attached_annotations
|
|
317
|
+
.map((annotation) => `- ${annotation.annotation_type}${annotation.anchor_text ? ` (${annotation.anchor_text})` : ''}: ${annotation.content}`)
|
|
318
|
+
.join('\n')}`
|
|
319
|
+
: '';
|
|
320
|
+
return `[${r.dependency_name}@${r.dependency_version}${langTag}] ${r.content}${notes}`;
|
|
321
|
+
})
|
|
322
|
+
.join('\n\n');
|
|
323
|
+
return { content: [{ type: 'text', text: text || 'No documentation results found.' }] };
|
|
324
|
+
});
|
|
325
|
+
// ── Build Docs Registry ────────────────────────────────────────────
|
|
326
|
+
server.tool('build_docs_registry', 'Build a registry of all indexed documentation for the project. Returns a structured JSON manifest of all indexed packages, versions, languages, and chunk counts — similar to a package registry index.', {}, async () => {
|
|
327
|
+
const res = await client.docsRegistry({
|
|
328
|
+
org_id: config.orgId,
|
|
329
|
+
project_id: config.projectId,
|
|
330
|
+
viewer_user_id: config.userId,
|
|
331
|
+
});
|
|
332
|
+
const lines = [
|
|
333
|
+
`# Documentation Registry`,
|
|
334
|
+
`Generated: ${res.generated}`,
|
|
335
|
+
`Total entries: ${res.total_entries}`,
|
|
336
|
+
'',
|
|
337
|
+
];
|
|
338
|
+
for (const entry of (res.entries ?? [])) {
|
|
339
|
+
const langTag = entry.language ? ` (${entry.language})` : '';
|
|
340
|
+
lines.push(`- ${entry.name}@${entry.version}${langTag} [${entry.ecosystem}] — ${entry.chunk_count} chunks (${entry.source_type})`);
|
|
341
|
+
}
|
|
342
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
343
|
+
});
|
|
344
|
+
// ── Write Memory Event ─────────────────────────────────────────────
|
|
345
|
+
const writeEventSchema = {
|
|
346
|
+
event_type: z.enum(['decision', 'observation', 'attempt', 'branch_event', 'convention']).describe('Type of event'),
|
|
347
|
+
content: z.string().describe('Event content'),
|
|
348
|
+
files_modified: z.array(z.string()).optional().describe('Files that were modified'),
|
|
349
|
+
concepts: z.array(z.string()).optional().describe('Related concepts'),
|
|
350
|
+
branch_name: z.string().optional().describe('Branch name'),
|
|
351
|
+
};
|
|
352
|
+
async function writeEventTool({ event_type, content, files_modified, concepts, branch_name, }) {
|
|
353
|
+
await client.ingestEvent({
|
|
354
|
+
org_id: config.orgId,
|
|
355
|
+
project_id: config.projectId,
|
|
356
|
+
user_id: config.userId,
|
|
357
|
+
session_id: config.sessionId ?? `mcp-${Date.now()}`,
|
|
358
|
+
client: 'cloud',
|
|
359
|
+
event_type,
|
|
360
|
+
content,
|
|
361
|
+
files_modified,
|
|
362
|
+
concepts,
|
|
363
|
+
branch_name,
|
|
364
|
+
created_at: new Date().toISOString(),
|
|
365
|
+
});
|
|
366
|
+
return { content: [{ type: 'text', text: `✓ ${event_type} recorded.` }] };
|
|
367
|
+
}
|
|
368
|
+
server.tool('write_memory_event', 'Write a memory event — record a decision, observation, or failed attempt for the team.', writeEventSchema, writeEventTool);
|
|
369
|
+
server.tool('write_event', 'Alias for write_memory_event.', writeEventSchema, writeEventTool);
|
|
370
|
+
// ── Oracle Research (NEW) ──────────────────────────────────────────
|
|
371
|
+
server.tool('oracle_research', 'Deep research agent — combines fact store, vector search, full-text search, and multi-hop tracing to answer complex questions about the project. Use for "why did we...", "what happened with...", "how does...work" questions.', {
|
|
372
|
+
question: z.string().describe('Research question about the project'),
|
|
373
|
+
depth: z.enum(['shallow', 'standard', 'deep']).optional().describe('Search depth (default: standard)'),
|
|
374
|
+
}, async ({ question, depth }) => {
|
|
375
|
+
// Oracle is server-side only — call the API endpoint
|
|
376
|
+
try {
|
|
377
|
+
const res = await fetch(`${config.apiUrl}/v1/context/oracle`, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: {
|
|
380
|
+
'Content-Type': 'application/json',
|
|
381
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
org_id: config.orgId,
|
|
385
|
+
project_id: config.projectId,
|
|
386
|
+
viewer_user_id: config.userId,
|
|
387
|
+
question,
|
|
388
|
+
depth,
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok) {
|
|
392
|
+
return { content: [{ type: 'text', text: `Oracle research failed: ${res.status}` }] };
|
|
393
|
+
}
|
|
394
|
+
const data = await res.json();
|
|
395
|
+
const parts = [];
|
|
396
|
+
parts.push(`# Oracle Research: ${question}`);
|
|
397
|
+
parts.push(`Confidence: ${(data.confidence * 100).toFixed(0)}%`);
|
|
398
|
+
parts.push(`Strategies: ${data.search_stats?.strategies_used?.join(', ') ?? 'unknown'}`);
|
|
399
|
+
parts.push(`Events examined: ${data.search_stats?.events_examined ?? 0}`);
|
|
400
|
+
parts.push('');
|
|
401
|
+
parts.push(data.answer);
|
|
402
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
return { content: [{ type: 'text', text: `Oracle research error: ${error}` }] };
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
// ── Tracer Search (NEW) ────────────────────────────────────────────
|
|
409
|
+
server.tool('tracer_search', 'Multi-hop search that follows reference chains through project memory. Useful for tracing the history of a decision, file, or concept across related events.', {
|
|
410
|
+
query: z.string().describe('Starting query to trace'),
|
|
411
|
+
max_hops: z.number().optional().describe('Max trace hops (default: 3)'),
|
|
412
|
+
}, async ({ query, max_hops }) => {
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch(`${config.apiUrl}/v1/context/trace`, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: {
|
|
417
|
+
'Content-Type': 'application/json',
|
|
418
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
419
|
+
},
|
|
420
|
+
body: JSON.stringify({
|
|
421
|
+
org_id: config.orgId,
|
|
422
|
+
project_id: config.projectId,
|
|
423
|
+
viewer_user_id: config.userId,
|
|
424
|
+
query,
|
|
425
|
+
max_hops,
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
if (!res.ok) {
|
|
429
|
+
return { content: [{ type: 'text', text: `Tracer search failed: ${res.status}` }] };
|
|
430
|
+
}
|
|
431
|
+
const data = await res.json();
|
|
432
|
+
const parts = [];
|
|
433
|
+
parts.push(`# Trace: ${query}`);
|
|
434
|
+
parts.push(`Total events examined: ${data.total_events_examined}`);
|
|
435
|
+
parts.push('');
|
|
436
|
+
for (const hop of data.hops ?? []) {
|
|
437
|
+
parts.push(`## Hop ${hop.depth} (pivot: ${hop.pivot_query})`);
|
|
438
|
+
for (const event of hop.events ?? []) {
|
|
439
|
+
parts.push(`- [${event.event_type}] ${event.summary} (relevance: ${event.relevance_score?.toFixed(2)})`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (data.chain_summary) {
|
|
443
|
+
parts.push('');
|
|
444
|
+
parts.push('## Chain Summary');
|
|
445
|
+
parts.push(data.chain_summary);
|
|
446
|
+
}
|
|
447
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
return { content: [{ type: 'text', text: `Tracer search error: ${error}` }] };
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
// ── Web Search (NEW) ───────────────────────────────────────────────
|
|
454
|
+
server.tool('web_search', 'Search the web for real-time information (documentation, Stack Overflow, blog posts). Requires TAVILY_API_KEY or BRAVE_SEARCH_API_KEY to be configured.', {
|
|
455
|
+
query: z.string().describe('Web search query'),
|
|
456
|
+
limit: z.number().optional().describe('Max results (default 5)'),
|
|
457
|
+
}, async ({ query, limit }) => {
|
|
458
|
+
try {
|
|
459
|
+
const res = await fetch(`${config.apiUrl}/v1/context/web-search`, {
|
|
460
|
+
method: 'POST',
|
|
461
|
+
headers: {
|
|
462
|
+
'Content-Type': 'application/json',
|
|
463
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
464
|
+
},
|
|
465
|
+
body: JSON.stringify({ query, limit }),
|
|
466
|
+
});
|
|
467
|
+
if (!res.ok) {
|
|
468
|
+
return { content: [{ type: 'text', text: `Web search failed: ${res.status}. Make sure TAVILY_API_KEY or BRAVE_SEARCH_API_KEY is configured.` }] };
|
|
469
|
+
}
|
|
470
|
+
const data = await res.json();
|
|
471
|
+
const text = (data.results ?? [])
|
|
472
|
+
.map((r) => `[${r.source}] **${r.title}**\n${r.url}\n${r.snippet}`)
|
|
473
|
+
.join('\n\n');
|
|
474
|
+
return { content: [{ type: 'text', text: text || 'No web search results found.' }] };
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
return { content: [{ type: 'text', text: `Web search error: ${error}` }] };
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// ── Fact Store (NEW) ───────────────────────────────────────────────
|
|
481
|
+
server.tool('get_project_knowledge', 'Get structured knowledge (facts) about the project — architecture decisions, conventions, configurations, dependencies. These are extracted from events and maintained with contradiction detection.', {
|
|
482
|
+
category: z.string().optional().describe('Filter by category: architecture, convention, dependency, behavior, config, workflow'),
|
|
483
|
+
}, async ({ category }) => {
|
|
484
|
+
try {
|
|
485
|
+
const url = category
|
|
486
|
+
? `${config.apiUrl}/v1/context/facts/structured?category=${category}`
|
|
487
|
+
: `${config.apiUrl}/v1/context/facts/structured`;
|
|
488
|
+
const res = await fetch(url, {
|
|
489
|
+
method: 'POST',
|
|
490
|
+
headers: {
|
|
491
|
+
'Content-Type': 'application/json',
|
|
492
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
493
|
+
},
|
|
494
|
+
body: JSON.stringify({
|
|
495
|
+
org_id: config.orgId,
|
|
496
|
+
project_id: config.projectId,
|
|
497
|
+
viewer_user_id: config.userId,
|
|
498
|
+
category,
|
|
499
|
+
}),
|
|
500
|
+
});
|
|
501
|
+
if (!res.ok) {
|
|
502
|
+
// Fall back to existing project_facts endpoint
|
|
503
|
+
const fallbackRes = await client.projectFacts({
|
|
504
|
+
org_id: config.orgId,
|
|
505
|
+
project_id: config.projectId,
|
|
506
|
+
viewer_user_id: config.userId,
|
|
507
|
+
});
|
|
508
|
+
const sections = ['# Project Knowledge'];
|
|
509
|
+
appendFactSection(sections, 'Static Facts', fallbackRes.facts.static.map((f) => `${f.text} (${f.category})`));
|
|
510
|
+
appendFactSection(sections, 'Dynamic Facts', fallbackRes.facts.dynamic.map((f) => `${f.text} (${f.category})`));
|
|
511
|
+
return { content: [{ type: 'text', text: sections.join('\n\n') }] };
|
|
512
|
+
}
|
|
513
|
+
const data = await res.json();
|
|
514
|
+
const parts = ['# Project Knowledge'];
|
|
515
|
+
for (const fact of data.facts ?? []) {
|
|
516
|
+
const conf = `${(fact.confidence * 100).toFixed(0)}%`;
|
|
517
|
+
parts.push(`- [${fact.category}] ${fact.subject} ${fact.predicate} ${fact.object} (confidence: ${conf})`);
|
|
518
|
+
}
|
|
519
|
+
return { content: [{ type: 'text', text: parts.join('\n') || 'No structured knowledge available yet.' }] };
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
return { content: [{ type: 'text', text: `Knowledge query error: ${error}` }] };
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
// ── Fetch Dependency Docs (NEW) ─────────────────────────────────────
|
|
526
|
+
server.tool('fetch_dependency_docs', 'Fetch documentation for project dependencies from npm/PyPI/crates registries. Provide a package name and registry.', {
|
|
527
|
+
packages: z.array(z.object({
|
|
528
|
+
name: z.string().describe('Package name'),
|
|
529
|
+
registry: z.enum(['npm', 'pypi', 'crates']).describe('Package registry'),
|
|
530
|
+
})).describe('List of packages to look up'),
|
|
531
|
+
}, async ({ packages }) => {
|
|
532
|
+
const TIMEOUT = 8000;
|
|
533
|
+
const results = [`# Dependency Documentation (${packages.length} packages)`];
|
|
534
|
+
for (const pkg of packages.slice(0, 20)) {
|
|
535
|
+
try {
|
|
536
|
+
let url;
|
|
537
|
+
switch (pkg.registry) {
|
|
538
|
+
case 'npm':
|
|
539
|
+
url = `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`;
|
|
540
|
+
break;
|
|
541
|
+
case 'pypi':
|
|
542
|
+
url = `https://pypi.org/pypi/${encodeURIComponent(pkg.name)}/json`;
|
|
543
|
+
break;
|
|
544
|
+
case 'crates':
|
|
545
|
+
url = `https://crates.io/api/v1/crates/${encodeURIComponent(pkg.name)}`;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT);
|
|
550
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
551
|
+
clearTimeout(timer);
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
results.push(`## ${pkg.name} (${pkg.registry})\nNot found (${res.status})\n`);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const data = await res.json();
|
|
557
|
+
results.push(`## ${pkg.name} (${pkg.registry})`);
|
|
558
|
+
if (pkg.registry === 'npm') {
|
|
559
|
+
if (data.version)
|
|
560
|
+
results.push(`Version: ${data.version}`);
|
|
561
|
+
if (data.description)
|
|
562
|
+
results.push(data.description);
|
|
563
|
+
if (data.readme)
|
|
564
|
+
results.push(String(data.readme).slice(0, 1500));
|
|
565
|
+
}
|
|
566
|
+
else if (pkg.registry === 'pypi') {
|
|
567
|
+
const info = data.info ?? {};
|
|
568
|
+
if (info.version)
|
|
569
|
+
results.push(`Version: ${info.version}`);
|
|
570
|
+
if (info.summary)
|
|
571
|
+
results.push(info.summary);
|
|
572
|
+
if (info.description)
|
|
573
|
+
results.push(String(info.description).slice(0, 1500));
|
|
574
|
+
}
|
|
575
|
+
else if (pkg.registry === 'crates') {
|
|
576
|
+
const crate = data.crate ?? {};
|
|
577
|
+
if (crate.max_version)
|
|
578
|
+
results.push(`Version: ${crate.max_version}`);
|
|
579
|
+
if (crate.description)
|
|
580
|
+
results.push(crate.description);
|
|
581
|
+
}
|
|
582
|
+
results.push('');
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
results.push(`## ${pkg.name} (${pkg.registry})\nFetch failed\n`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
589
|
+
});
|
|
590
|
+
// ── Memory Health (NEW) ────────────────────────────────────────────
|
|
591
|
+
server.tool('memory_health', 'Check the health of project memory — total events, breakdown by type, stale memories, recent activity.', {}, async () => {
|
|
592
|
+
try {
|
|
593
|
+
const [profile, facts] = await Promise.allSettled([
|
|
594
|
+
client.projectProfile({
|
|
595
|
+
org_id: config.orgId,
|
|
596
|
+
project_id: config.projectId,
|
|
597
|
+
viewer_user_id: config.userId,
|
|
598
|
+
}),
|
|
599
|
+
client.projectFacts({
|
|
600
|
+
org_id: config.orgId,
|
|
601
|
+
project_id: config.projectId,
|
|
602
|
+
viewer_user_id: config.userId,
|
|
603
|
+
}),
|
|
604
|
+
]);
|
|
605
|
+
const parts = ['# Memory Health Report'];
|
|
606
|
+
if (profile.status === 'fulfilled') {
|
|
607
|
+
const p = profile.value;
|
|
608
|
+
parts.push(`## Profile`);
|
|
609
|
+
parts.push(`Source events: ${p.source_event_count}`);
|
|
610
|
+
parts.push(`Event types: ${(p.source_event_types ?? []).join(', ')}`);
|
|
611
|
+
}
|
|
612
|
+
if (facts.status === 'fulfilled') {
|
|
613
|
+
const f = facts.value;
|
|
614
|
+
parts.push(`## Knowledge`);
|
|
615
|
+
parts.push(`Static facts: ${f.facts?.static?.length ?? 0}`);
|
|
616
|
+
parts.push(`Dynamic facts: ${f.facts?.dynamic?.length ?? 0}`);
|
|
617
|
+
}
|
|
618
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
return { content: [{ type: 'text', text: `Health check error: ${error}` }] };
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
// ── Code Advisor (NEW) ─────────────────────────────────────────────
|
|
625
|
+
server.tool('memoire_advisor', 'Analyze code against project memory (conventions, past attempts, facts) and get grounded recommendations with citations.', {
|
|
626
|
+
code: z.string().describe('The code snippet to analyze'),
|
|
627
|
+
question: z.string().optional().describe('Specific question about the code'),
|
|
628
|
+
language: z.string().optional().describe('Programming language (auto-detected if omitted)'),
|
|
629
|
+
focus: z.enum(['bugs', 'performance', 'patterns', 'security', 'general']).optional().describe('Analysis focus area'),
|
|
630
|
+
}, async ({ code, question, language, focus }) => {
|
|
631
|
+
try {
|
|
632
|
+
const response = await fetch(`${config.apiUrl}/v1/admin/advisor`, {
|
|
633
|
+
method: 'POST',
|
|
634
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
635
|
+
body: JSON.stringify({
|
|
636
|
+
org_id: config.orgId,
|
|
637
|
+
project_id: config.projectId,
|
|
638
|
+
viewer_user_id: config.userId,
|
|
639
|
+
code,
|
|
640
|
+
question,
|
|
641
|
+
language,
|
|
642
|
+
focus: focus ?? 'general',
|
|
643
|
+
}),
|
|
644
|
+
});
|
|
645
|
+
const data = await response.json();
|
|
646
|
+
const parts = ['# Code Advisor Report'];
|
|
647
|
+
if (data.recommendations) {
|
|
648
|
+
for (const rec of data.recommendations) {
|
|
649
|
+
parts.push(`\n## [${rec.severity.toUpperCase()}] ${rec.title}`);
|
|
650
|
+
parts.push(rec.description);
|
|
651
|
+
if (rec.suggestion)
|
|
652
|
+
parts.push(`**Suggestion:** ${rec.suggestion}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
parts.push(`\n_Consulted ${data.context_used ?? 0} context items, ${data.citations?.length ?? 0} citations._`);
|
|
656
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
return { content: [{ type: 'text', text: `Advisor error: ${error}` }] };
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
// ── Package Source Search (NEW) ─────────────────────────────────────
|
|
663
|
+
server.tool('package_source_search', 'Search the source code of an npm/PyPI package on-the-fly. Downloads, extracts, and searches package tarballs.', {
|
|
664
|
+
name: z.string().describe('Package name (e.g. "express", "fastapi")'),
|
|
665
|
+
version: z.string().describe('Package version (e.g. "4.18.2")'),
|
|
666
|
+
ecosystem: z.enum(['npm', 'pypi', 'crates']).describe('Package ecosystem'),
|
|
667
|
+
pattern: z.string().describe('Regex pattern to search for'),
|
|
668
|
+
max_results: z.number().optional().describe('Max results (default 50)'),
|
|
669
|
+
}, async ({ name, version, ecosystem, pattern, max_results }) => {
|
|
670
|
+
try {
|
|
671
|
+
const response = await fetch(`${config.apiUrl}/v1/admin/package/search`, {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify({ name, version, ecosystem, pattern, max_results: max_results ?? 50 }),
|
|
675
|
+
});
|
|
676
|
+
const data = await response.json();
|
|
677
|
+
const parts = [`# Package Search: ${name}@${version}`];
|
|
678
|
+
parts.push(`Found ${data.results?.length ?? 0} matches in ${data.file_count ?? 0} files\n`);
|
|
679
|
+
for (const result of (data.results ?? []).slice(0, 20)) {
|
|
680
|
+
parts.push(`**${result.file}:${result.line}** — \`${result.match}\``);
|
|
681
|
+
parts.push(` ${result.content}`);
|
|
682
|
+
}
|
|
683
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
return { content: [{ type: 'text', text: `Package search error: ${error}` }] };
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
// ── Package Source Browse (NEW) ─────────────────────────────────────
|
|
690
|
+
server.tool('package_source_browse', 'Browse the file tree of an npm/PyPI package.', {
|
|
691
|
+
name: z.string().describe('Package name'),
|
|
692
|
+
version: z.string().describe('Package version'),
|
|
693
|
+
ecosystem: z.enum(['npm', 'pypi', 'crates']).describe('Package ecosystem'),
|
|
694
|
+
path: z.string().optional().describe('Subdirectory to browse'),
|
|
695
|
+
}, async ({ name, version, ecosystem, path }) => {
|
|
696
|
+
try {
|
|
697
|
+
const response = await fetch(`${config.apiUrl}/v1/admin/package/browse`, {
|
|
698
|
+
method: 'POST',
|
|
699
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
700
|
+
body: JSON.stringify({ name, version, ecosystem, path }),
|
|
701
|
+
});
|
|
702
|
+
const data = await response.json();
|
|
703
|
+
const parts = [`# ${name}@${version} — File Tree`];
|
|
704
|
+
for (const f of (data.files ?? [])) {
|
|
705
|
+
const icon = f.type === 'directory' ? '📁' : '📄';
|
|
706
|
+
parts.push(`${icon} ${f.path}${f.size ? ` (${f.size} bytes)` : ''}`);
|
|
707
|
+
}
|
|
708
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
return { content: [{ type: 'text', text: `Browse error: ${error}` }] };
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
// ── Package Source Read (NEW) ──────────────────────────────────────
|
|
715
|
+
server.tool('package_source_read', 'Read a specific file from an npm/PyPI package source.', {
|
|
716
|
+
name: z.string().describe('Package name'),
|
|
717
|
+
version: z.string().describe('Package version'),
|
|
718
|
+
ecosystem: z.enum(['npm', 'pypi', 'crates']).describe('Package ecosystem'),
|
|
719
|
+
file_path: z.string().describe('File path within the package'),
|
|
720
|
+
}, async ({ name, version, ecosystem, file_path }) => {
|
|
721
|
+
try {
|
|
722
|
+
const response = await fetch(`${config.apiUrl}/v1/admin/package/read`, {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
725
|
+
body: JSON.stringify({ name, version, ecosystem, file_path }),
|
|
726
|
+
});
|
|
727
|
+
const data = await response.json();
|
|
728
|
+
if (data.error) {
|
|
729
|
+
return { content: [{ type: 'text', text: `File not found: ${file_path}` }] };
|
|
730
|
+
}
|
|
731
|
+
return { content: [{ type: 'text', text: `# ${name}@${version} — ${file_path}\n\n\`\`\`\n${data.content}\n\`\`\`` }] };
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
return { content: [{ type: 'text', text: `Read error: ${error}` }] };
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
// ── Export Project (NEW) ──────────────────────────────────────────
|
|
738
|
+
server.tool('export_project', 'Export all project memory (events, facts) as a portable JSON bundle for backup or migration.', {}, async () => {
|
|
739
|
+
try {
|
|
740
|
+
const response = await fetch(`${config.apiUrl}/v1/admin/export`, {
|
|
741
|
+
method: 'POST',
|
|
742
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
743
|
+
body: JSON.stringify({ org_id: config.orgId, project_id: config.projectId }),
|
|
744
|
+
});
|
|
745
|
+
const data = await response.json();
|
|
746
|
+
return { content: [{ type: 'text', text: `# Export Complete\nEvents: ${data.metadata?.event_count ?? 0}\nFacts: ${data.metadata?.fact_count ?? 0}\nDate range: ${data.metadata?.date_range?.earliest ?? 'N/A'} to ${data.metadata?.date_range?.latest ?? 'N/A'}\n\nFull bundle returned as JSON (${JSON.stringify(data).length} bytes).` }] };
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
return { content: [{ type: 'text', text: `Export error: ${error}` }] };
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
// ── Usage Stats (NEW) ──────────────────────────────────────────────
|
|
753
|
+
server.tool('usage_stats', 'View current month API usage statistics for the organization.', {}, async () => {
|
|
754
|
+
try {
|
|
755
|
+
const response = await fetch(`${config.apiUrl}/v1/admin/usage`, {
|
|
756
|
+
method: 'POST',
|
|
757
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
|
|
758
|
+
body: JSON.stringify({ org_id: config.orgId }),
|
|
759
|
+
});
|
|
760
|
+
const data = await response.json();
|
|
761
|
+
const parts = ['# Usage Statistics'];
|
|
762
|
+
parts.push(`Period: ${data.period ?? 'current'}\n`);
|
|
763
|
+
for (const item of (data.usage ?? [])) {
|
|
764
|
+
parts.push(`- **${item.operation}**: ${item.count}`);
|
|
765
|
+
}
|
|
766
|
+
if (!data.usage?.length)
|
|
767
|
+
parts.push('No usage recorded this period.');
|
|
768
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
return { content: [{ type: 'text', text: `Usage error: ${error}` }] };
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
// ── Sync Cursor Rules ───────────────────────────────────────────────
|
|
775
|
+
server.tool('sync_cursor_rules', 'Write project context to .cursor/rules/ for auto-injection into every Cursor conversation. Requires workspace_path.', {
|
|
776
|
+
workspace_path: z.string().describe('Absolute path to the workspace root directory'),
|
|
777
|
+
project_name: z.string().optional().describe('Human-readable project name'),
|
|
778
|
+
}, async ({ workspace_path, project_name }) => {
|
|
779
|
+
try {
|
|
780
|
+
// Fetch profile from API
|
|
781
|
+
const profileResp = await client.projectProfile({
|
|
782
|
+
org_id: config.orgId,
|
|
783
|
+
project_id: config.projectId,
|
|
784
|
+
viewer_user_id: config.userId,
|
|
785
|
+
});
|
|
786
|
+
const pp = profileResp?.project_profile;
|
|
787
|
+
// Build .cursor/rules/memoire-context.mdc content
|
|
788
|
+
const sections = [];
|
|
789
|
+
sections.push(`---\nalwaysApply: true\ndescription: "Memoire shared memory context for ${project_name ?? 'Project'} (auto-updated)"\n---`);
|
|
790
|
+
sections.push(`# Memoire Context: ${project_name ?? 'Project'}`);
|
|
791
|
+
sections.push('> Auto-generated by Memoire. Use MCP tools for deeper queries.\n');
|
|
792
|
+
if (pp) {
|
|
793
|
+
sections.push('## Project Profile');
|
|
794
|
+
if (pp.architecture?.length)
|
|
795
|
+
sections.push(`**Architecture:** ${pp.architecture.join(', ')}`);
|
|
796
|
+
if (pp.conventions?.length) {
|
|
797
|
+
sections.push('**Conventions:**');
|
|
798
|
+
for (const c of pp.conventions.slice(0, 10))
|
|
799
|
+
sections.push(`- ${c}`);
|
|
800
|
+
}
|
|
801
|
+
if (pp.current_focus?.length)
|
|
802
|
+
sections.push(`**Current Focus:** ${pp.current_focus.join(', ')}`);
|
|
803
|
+
if (pp.key_files?.length)
|
|
804
|
+
sections.push(`**Key Files:** ${pp.key_files.slice(0, 5).join(', ')}`);
|
|
805
|
+
if (pp.summary) {
|
|
806
|
+
sections.push('\n## Summary');
|
|
807
|
+
sections.push(pp.summary);
|
|
808
|
+
}
|
|
809
|
+
sections.push('');
|
|
810
|
+
}
|
|
811
|
+
sections.push('---');
|
|
812
|
+
sections.push(`*Use Memoire MCP tools (search_context, project_profile, oracle_research) for detailed queries.*`);
|
|
813
|
+
sections.push(`*API: ${config.apiUrl}*`);
|
|
814
|
+
const content = sections.join('\n');
|
|
815
|
+
// Atomic write to .cursor/rules/
|
|
816
|
+
const fs = await import('node:fs');
|
|
817
|
+
const path = await import('node:path');
|
|
818
|
+
const rulesDir = path.join(workspace_path, '.cursor', 'rules');
|
|
819
|
+
const rulesFile = path.join(rulesDir, 'memoire-context.mdc');
|
|
820
|
+
const tempFile = `${rulesFile}.tmp`;
|
|
821
|
+
if (!fs.existsSync(rulesDir))
|
|
822
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
823
|
+
fs.writeFileSync(tempFile, content, 'utf-8');
|
|
824
|
+
fs.renameSync(tempFile, rulesFile);
|
|
825
|
+
return {
|
|
826
|
+
content: [{
|
|
827
|
+
type: 'text',
|
|
828
|
+
text: `✅ Cursor rules written to ${workspace_path}/.cursor/rules/memoire-context.mdc\n` +
|
|
829
|
+
`Includes project profile with ${pp?.conventions?.length ?? 0} conventions.\n` +
|
|
830
|
+
`Context will be auto-injected into every Cursor conversation.`,
|
|
831
|
+
}],
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
return { content: [{ type: 'text', text: `Failed to sync cursor rules: ${error}` }] };
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
// ─── AST Code Analysis Tools ────────────────────────────────────────────
|
|
839
|
+
// ── Run Tests (Sandbox) ──────────────────────────────────────────────
|
|
840
|
+
server.tool('run_tests', 'Run your project\'s test suite locally. Auto-detects framework (Vitest, Jest, Pytest, Go, Cargo) and returns structured results with failure details.', {
|
|
841
|
+
working_dir: z.string().describe('Absolute path to the project root'),
|
|
842
|
+
test_command: z.string().optional().describe('Override test command (e.g. "npm test", "pytest -x")'),
|
|
843
|
+
scope: z.enum(['all', 'affected']).optional().describe('Run all tests or only affected'),
|
|
844
|
+
timeout_ms: z.number().optional().describe('Timeout in milliseconds (default 300000)'),
|
|
845
|
+
env_vars: z.record(z.string()).optional().describe('Extra environment variables'),
|
|
846
|
+
}, async ({ working_dir, test_command, scope, timeout_ms, env_vars }) => {
|
|
847
|
+
try {
|
|
848
|
+
const res = await client.runTests({
|
|
849
|
+
org_id: config.orgId,
|
|
850
|
+
project_id: config.projectId,
|
|
851
|
+
working_dir,
|
|
852
|
+
test_command,
|
|
853
|
+
scope,
|
|
854
|
+
timeout_ms,
|
|
855
|
+
env_vars,
|
|
856
|
+
});
|
|
857
|
+
const r = res.result;
|
|
858
|
+
const statusIcon = r.status === 'pass' ? '✅' : r.status === 'fail' ? '❌' : r.status === 'timeout' ? '⏱️' : '⚠️';
|
|
859
|
+
const parts = [
|
|
860
|
+
`${statusIcon} **${r.status.toUpperCase()}** — ${r.summary}`,
|
|
861
|
+
`Total: ${r.total} | Passed: ${r.passed} | Failed: ${r.failed} | Skipped: ${r.skipped}`,
|
|
862
|
+
`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`,
|
|
863
|
+
];
|
|
864
|
+
if (r.failures.length > 0) {
|
|
865
|
+
parts.push('\n## Failures\n');
|
|
866
|
+
for (const f of r.failures.slice(0, 10)) {
|
|
867
|
+
parts.push(`### ${f.test_name}${f.file ? ` (${f.file}${f.line ? `:${f.line}` : ''})` : ''}`);
|
|
868
|
+
parts.push('```');
|
|
869
|
+
parts.push(f.error.slice(0, 500));
|
|
870
|
+
parts.push('```\n');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (r.log && r.failures.length === 0 && r.status !== 'pass') {
|
|
874
|
+
parts.push('\n## Log Output\n```\n' + r.log.slice(0, 2000) + '\n```');
|
|
875
|
+
}
|
|
876
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
return { content: [{ type: 'text', text: `Test run failed: ${error}` }] };
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
// ── Record Demo (Sandbox) ──────────────────────────────────────────
|
|
883
|
+
server.tool('record_demo', 'Record a video demo of your web app running locally. Starts your dev server, navigates pages, and captures a video recording. Runs async — returns a job ID to check later.', {
|
|
884
|
+
working_dir: z.string().optional().describe('Project root to run the start command from'),
|
|
885
|
+
start_command: z.string().describe('Command to start the dev server (e.g. "npm run dev")'),
|
|
886
|
+
url: z.string().describe('URL of the running app (e.g. "http://localhost:3000")'),
|
|
887
|
+
flows: z.array(z.object({
|
|
888
|
+
action: z.enum(['navigate', 'click', 'fill', 'wait', 'screenshot', 'scroll']),
|
|
889
|
+
url: z.string().optional(),
|
|
890
|
+
selector: z.string().optional(),
|
|
891
|
+
value: z.string().optional(),
|
|
892
|
+
ms: z.number().optional(),
|
|
893
|
+
name: z.string().optional(),
|
|
894
|
+
direction: z.enum(['up', 'down']).optional(),
|
|
895
|
+
amount: z.number().optional(),
|
|
896
|
+
})).optional().describe('Actions to perform during recording'),
|
|
897
|
+
auto_explore: z.boolean().optional().describe('Auto-discover and visit internal links'),
|
|
898
|
+
duration_ms: z.number().optional().describe('Max recording duration (default 120000)'),
|
|
899
|
+
viewport: z.object({ width: z.number(), height: z.number() }).optional().describe('Browser viewport size'),
|
|
900
|
+
}, async ({ working_dir, start_command, url, flows, auto_explore, duration_ms, viewport }) => {
|
|
901
|
+
try {
|
|
902
|
+
const res = await client.recordDemo({
|
|
903
|
+
org_id: config.orgId,
|
|
904
|
+
project_id: config.projectId,
|
|
905
|
+
working_dir,
|
|
906
|
+
start_command,
|
|
907
|
+
url,
|
|
908
|
+
flows: flows,
|
|
909
|
+
auto_explore,
|
|
910
|
+
duration_ms,
|
|
911
|
+
viewport,
|
|
912
|
+
});
|
|
913
|
+
return {
|
|
914
|
+
content: [{
|
|
915
|
+
type: 'text',
|
|
916
|
+
text: `🎬 Demo recording started!\n\nJob ID: \`${res.job_id}\`\nStatus: ${res.status}\n\nUse \`demo_status\` tool with this job ID to check when the recording is ready.`,
|
|
917
|
+
}],
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
return { content: [{ type: 'text', text: `Demo recording failed to start: ${error}` }] };
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
// ── Demo Status ────────────────────────────────────────────────────
|
|
925
|
+
server.tool('demo_status', 'Check the status of an async demo recording job.', {
|
|
926
|
+
job_id: z.string().describe('Job ID returned by record_demo'),
|
|
927
|
+
}, async ({ job_id }) => {
|
|
928
|
+
try {
|
|
929
|
+
const res = await client.demoStatus(job_id);
|
|
930
|
+
if (res.status === 'complete') {
|
|
931
|
+
const parts = [
|
|
932
|
+
`✅ Demo recording complete!`,
|
|
933
|
+
`Duration: ${((res.duration_ms ?? 0) / 1000).toFixed(1)}s`,
|
|
934
|
+
];
|
|
935
|
+
if (res.recording_path)
|
|
936
|
+
parts.push(`Video: ${res.recording_path}`);
|
|
937
|
+
if (res.pages_visited?.length)
|
|
938
|
+
parts.push(`Pages visited: ${res.pages_visited.join(', ')}`);
|
|
939
|
+
if (res.screenshots?.length)
|
|
940
|
+
parts.push(`Screenshots: ${res.screenshots.length} captured`);
|
|
941
|
+
if (res.expires_at && !res.retained)
|
|
942
|
+
parts.push(`Auto-deletes at: ${res.expires_at}`);
|
|
943
|
+
if (res.retained)
|
|
944
|
+
parts.push('Demo files are marked to keep.');
|
|
945
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
946
|
+
}
|
|
947
|
+
if (res.status === 'failed') {
|
|
948
|
+
return { content: [{ type: 'text', text: `❌ Demo recording failed: ${res.error ?? 'Unknown error'}` }] };
|
|
949
|
+
}
|
|
950
|
+
if (res.status === 'expired') {
|
|
951
|
+
return { content: [{ type: 'text', text: `🧹 Demo expired: ${res.error ?? 'Result has been deleted after 24 hours.'}` }] };
|
|
952
|
+
}
|
|
953
|
+
return { content: [{ type: 'text', text: `⏳ Demo recording still in progress (job: ${job_id})` }] };
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
return { content: [{ type: 'text', text: `Failed to check demo status: ${error}` }] };
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
server.tool('keep_demo', 'Prevent a completed demo recording from auto-deleting after 24 hours.', {
|
|
960
|
+
job_id: z.string().describe('Job ID returned by record_demo'),
|
|
961
|
+
}, async ({ job_id }) => {
|
|
962
|
+
try {
|
|
963
|
+
const res = await client.keepDemo(job_id);
|
|
964
|
+
if (res.status === 'expired') {
|
|
965
|
+
return { content: [{ type: 'text', text: `Demo not found: ${res.error ?? 'It may already be deleted.'}` }] };
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
content: [{
|
|
969
|
+
type: 'text',
|
|
970
|
+
text: `📌 Demo ${job_id} will be kept.${res.recording_path ? `\nVideo: ${res.recording_path}` : ''}`,
|
|
971
|
+
}],
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
return { content: [{ type: 'text', text: `Failed to keep demo: ${error}` }] };
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
// ── GitHub Code Search ───────────────────────────────────────────────
|
|
979
|
+
server.tool('search_github', 'Search GitHub repos for code. No pre-indexing required — give it a question and repo names, it searches live. Great for referencing external codebases, libraries, or open-source projects.', {
|
|
980
|
+
repos: z.array(z.string()).min(1).max(10).describe('GitHub repos to search (e.g. ["vercel/next.js", "facebook/react"])'),
|
|
981
|
+
query: z.string().describe('What to search for — can be a question or code pattern'),
|
|
982
|
+
language: z.string().optional().describe('Filter by language (e.g. "typescript", "python")'),
|
|
983
|
+
path_filter: z.string().optional().describe('Filter by path prefix (e.g. "src/", "lib/")'),
|
|
984
|
+
max_results: z.number().optional().describe('Max results (default 10)'),
|
|
985
|
+
}, async ({ repos, query, language, path_filter, max_results }) => {
|
|
986
|
+
try {
|
|
987
|
+
const res = await client.searchGitHub({
|
|
988
|
+
org_id: config.orgId,
|
|
989
|
+
repos,
|
|
990
|
+
query,
|
|
991
|
+
language,
|
|
992
|
+
path_filter,
|
|
993
|
+
max_results: max_results ?? 10,
|
|
994
|
+
});
|
|
995
|
+
if (res.error) {
|
|
996
|
+
return { content: [{ type: 'text', text: res.error }] };
|
|
997
|
+
}
|
|
998
|
+
if (!res.results || res.results.length === 0) {
|
|
999
|
+
return { content: [{ type: 'text', text: `No results found for "${query}" in ${repos.join(', ')}` }] };
|
|
1000
|
+
}
|
|
1001
|
+
const text = res.results
|
|
1002
|
+
.map((r, i) => `### ${i + 1}. ${r.repo}/${r.path}${r.language ? ` (${r.language})` : ''}\n` +
|
|
1003
|
+
`${r.url}\n\`\`\`\n${r.content.slice(0, 1500)}\n\`\`\``)
|
|
1004
|
+
.join('\n\n');
|
|
1005
|
+
return {
|
|
1006
|
+
content: [{
|
|
1007
|
+
type: 'text',
|
|
1008
|
+
text: `Found ${res.total} results for "${query}" across ${repos.join(', ')}:\n\n${text}`,
|
|
1009
|
+
}],
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
return { content: [{ type: 'text', text: `GitHub search failed: ${error}` }] };
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
server.tool('smart_outline', 'Get structural outline of a file (functions, classes, types). 6-10x cheaper than reading the full file.', { file_path: z.string().describe('Absolute path to the file to outline') }, async ({ file_path }) => {
|
|
1017
|
+
try {
|
|
1018
|
+
const { parseFile, formatOutline } = await import('./ast-parser-inline.js');
|
|
1019
|
+
const fs = await import('node:fs');
|
|
1020
|
+
const content = fs.readFileSync(file_path, 'utf-8');
|
|
1021
|
+
const outline = parseFile(content, file_path);
|
|
1022
|
+
const text = formatOutline(file_path, outline.language, outline.totalLines, outline.imports, outline.symbols);
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{
|
|
1025
|
+
type: 'text',
|
|
1026
|
+
text: `${text}\n\n📊 ${outline.symbols.length} symbols, ~${outline.foldedTokenEstimate} tokens (vs ~${Math.ceil(content.length / 4)} for full file)`,
|
|
1027
|
+
}],
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
return { content: [{ type: 'text', text: `Failed to outline: ${error}` }] };
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
server.tool('smart_search', 'Search for code symbols (functions, classes, types) across a codebase by name. Returns ranked results with context.', {
|
|
1035
|
+
query: z.string().describe('Symbol name or pattern to search for'),
|
|
1036
|
+
root_dir: z.string().describe('Root directory to search in'),
|
|
1037
|
+
max_results: z.number().optional().default(15).describe('Max results to return'),
|
|
1038
|
+
kind: z.string().optional().describe('Filter by kind: function, class, method, interface, type, enum'),
|
|
1039
|
+
}, async ({ query, root_dir, max_results, kind }) => {
|
|
1040
|
+
try {
|
|
1041
|
+
const { smartSearch } = await import('./ast-parser-inline.js');
|
|
1042
|
+
const results = smartSearch({
|
|
1043
|
+
query,
|
|
1044
|
+
rootDir: root_dir,
|
|
1045
|
+
maxResults: max_results,
|
|
1046
|
+
kind: kind,
|
|
1047
|
+
});
|
|
1048
|
+
if (results.length === 0) {
|
|
1049
|
+
return { content: [{ type: 'text', text: `No symbols found matching "${query}"` }] };
|
|
1050
|
+
}
|
|
1051
|
+
const text = results.map((r, i) => `${i + 1}. ${r.symbol.kind} **${r.symbol.name}** (score: ${r.score.toFixed(1)})\n` +
|
|
1052
|
+
` 📁 ${r.filePath} L${r.symbol.lineStart}-${r.symbol.lineEnd}\n` +
|
|
1053
|
+
` ${r.context ?? r.symbol.signature}`).join('\n\n');
|
|
1054
|
+
return {
|
|
1055
|
+
content: [{
|
|
1056
|
+
type: 'text',
|
|
1057
|
+
text: `Found ${results.length} symbols matching "${query}":\n\n${text}`,
|
|
1058
|
+
}],
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
return { content: [{ type: 'text', text: `Search failed: ${error}` }] };
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
server.tool('smart_unfold', 'Expand a specific symbol to see its full source code with context. 4-8x cheaper than reading the full file.', {
|
|
1066
|
+
file_path: z.string().describe('Absolute path to the file'),
|
|
1067
|
+
symbol_name: z.string().describe('Name of the symbol to unfold'),
|
|
1068
|
+
}, async ({ file_path, symbol_name }) => {
|
|
1069
|
+
try {
|
|
1070
|
+
const { smartUnfold } = await import('./ast-parser-inline.js');
|
|
1071
|
+
const result = smartUnfold(file_path, symbol_name);
|
|
1072
|
+
if (!result) {
|
|
1073
|
+
return { content: [{ type: 'text', text: `Symbol "${symbol_name}" not found in ${file_path}` }] };
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
content: [{
|
|
1077
|
+
type: 'text',
|
|
1078
|
+
text: `${result.source}\n\n📊 ${result.kind} "${result.symbolName}" (${result.tokenEstimate} tokens)`,
|
|
1079
|
+
}],
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
return { content: [{ type: 'text', text: `Unfold failed: ${error}` }] };
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
// ── Rate Memory (Feedback) ─────────────────────────────────────────
|
|
1087
|
+
server.tool('rate_memory', 'Rate a retrieved memory as helpful or unhelpful. Feedback improves search ranking and memory lifecycle — downvoted memories decay faster, upvoted memories get boosted. Team-shared: one agent\'s rating benefits all agents.', {
|
|
1088
|
+
event_id: z.string().describe('ID of the memory event to rate'),
|
|
1089
|
+
rating: z.union([z.literal(1), z.literal(-1)]).describe('+1 (helpful) or -1 (unhelpful)'),
|
|
1090
|
+
label: z.enum([
|
|
1091
|
+
'helpful', 'accurate', 'well-structured', 'good-examples',
|
|
1092
|
+
'outdated', 'inaccurate', 'incomplete', 'superseded',
|
|
1093
|
+
'poorly-structured', 'wrong-examples', 'wrong-version',
|
|
1094
|
+
]).optional().describe('Structured feedback label'),
|
|
1095
|
+
note: z.string().optional().describe('Optional annotation explaining the rating'),
|
|
1096
|
+
}, async ({ event_id, rating, label, note }) => {
|
|
1097
|
+
const res = await client.rateFeedback({
|
|
1098
|
+
org_id: config.orgId,
|
|
1099
|
+
project_id: config.projectId,
|
|
1100
|
+
event_id,
|
|
1101
|
+
user_id: config.userId,
|
|
1102
|
+
rating,
|
|
1103
|
+
label,
|
|
1104
|
+
note,
|
|
1105
|
+
});
|
|
1106
|
+
const emoji = rating > 0 ? '+1' : '-1';
|
|
1107
|
+
const labelStr = label ? ` [${label}]` : '';
|
|
1108
|
+
const noteStr = note ? ` — "${note}"` : '';
|
|
1109
|
+
return {
|
|
1110
|
+
content: [{
|
|
1111
|
+
type: 'text',
|
|
1112
|
+
text: `Rated memory ${event_id}: ${emoji}${labelStr}${noteStr}\nAggregate score: ${res.feedback_score}`,
|
|
1113
|
+
}],
|
|
1114
|
+
};
|
|
1115
|
+
});
|
|
1116
|
+
return server;
|
|
1117
|
+
}
|
|
1118
|
+
export async function runMemoireStdioServer(config, deps) {
|
|
1119
|
+
const server = createMemoireMcpServer(config, deps);
|
|
1120
|
+
const transport = new StdioServerTransport();
|
|
1121
|
+
await server.connect(transport);
|
|
1122
|
+
console.error('Memoire MCP server running on stdio');
|
|
1123
|
+
return server;
|
|
1124
|
+
}
|
|
1125
|
+
function appendProfileSection(target, title, items) {
|
|
1126
|
+
if (items.length === 0) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
target.push(`## ${title}\n${items.map((item) => `- ${item}`).join('\n')}`);
|
|
1130
|
+
}
|
|
1131
|
+
function appendFactSection(target, title, items) {
|
|
1132
|
+
if (items.length === 0) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
target.push(`## ${title}\n${items.map((item) => `- ${item}`).join('\n')}`);
|
|
1136
|
+
}
|
|
1137
|
+
//# sourceMappingURL=index.js.map
|