@getlore/cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/handlers/research-agent.d.ts +2 -1
- package/dist/mcp/handlers/research-agent.js +37 -7
- package/dist/mcp/handlers/research.d.ts +19 -0
- package/dist/mcp/handlers/research.js +144 -3
- package/dist/mcp/handlers/sync.d.ts +1 -0
- package/dist/mcp/handlers/sync.js +5 -0
- package/dist/mcp/server.js +28 -5
- package/dist/mcp/tools.js +16 -2
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 4. Synthesizes findings into a comprehensive research package
|
|
9
9
|
*/
|
|
10
10
|
import type { ResearchPackage } from '../../core/types.js';
|
|
11
|
+
import type { ProgressCallback } from './research.js';
|
|
11
12
|
interface ResearchAgentArgs {
|
|
12
13
|
task: string;
|
|
13
14
|
project?: string;
|
|
@@ -17,5 +18,5 @@ interface ResearchAgentArgs {
|
|
|
17
18
|
/**
|
|
18
19
|
* Run the agentic research
|
|
19
20
|
*/
|
|
20
|
-
export declare function runResearchAgent(dbPath: string, dataDir: string, args: ResearchAgentArgs): Promise<ResearchPackage>;
|
|
21
|
+
export declare function runResearchAgent(dbPath: string, dataDir: string, args: ResearchAgentArgs, onProgress?: ProgressCallback): Promise<ResearchPackage>;
|
|
21
22
|
export {};
|
|
@@ -232,7 +232,7 @@ Now begin your research. Use the tools iteratively until you have comprehensive
|
|
|
232
232
|
/**
|
|
233
233
|
* Run the agentic research
|
|
234
234
|
*/
|
|
235
|
-
export async function runResearchAgent(dbPath, dataDir, args) {
|
|
235
|
+
export async function runResearchAgent(dbPath, dataDir, args, onProgress) {
|
|
236
236
|
const { task, project, include_sources = true } = args;
|
|
237
237
|
// Load archived projects to filter (extract just the project names)
|
|
238
238
|
const archivedProjectsData = await loadArchivedProjects(dataDir);
|
|
@@ -245,6 +245,8 @@ export async function runResearchAgent(dbPath, dataDir, args) {
|
|
|
245
245
|
let lastAssistantMessage = '';
|
|
246
246
|
try {
|
|
247
247
|
// Run the agent
|
|
248
|
+
let turnCount = 0;
|
|
249
|
+
await onProgress?.(5, undefined, 'Starting research agent...');
|
|
248
250
|
for await (const message of query({
|
|
249
251
|
prompt: `Research task: ${task}${project ? ` (project: ${project})` : ''}`,
|
|
250
252
|
options: {
|
|
@@ -261,8 +263,9 @@ export async function runResearchAgent(dbPath, dataDir, args) {
|
|
|
261
263
|
permissionMode: 'acceptEdits', // Auto-approve tool calls
|
|
262
264
|
},
|
|
263
265
|
})) {
|
|
264
|
-
// Capture assistant messages
|
|
266
|
+
// Capture assistant messages and extract tool call details
|
|
265
267
|
if (message.type === 'assistant') {
|
|
268
|
+
turnCount++;
|
|
266
269
|
const msg = message;
|
|
267
270
|
if (msg.message?.content) {
|
|
268
271
|
const content = msg.message.content;
|
|
@@ -270,9 +273,30 @@ export async function runResearchAgent(dbPath, dataDir, args) {
|
|
|
270
273
|
lastAssistantMessage = content;
|
|
271
274
|
}
|
|
272
275
|
else if (Array.isArray(content)) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
// Extract tool_use blocks to report what the agent is doing
|
|
277
|
+
for (const block of content) {
|
|
278
|
+
if (block.type === 'tool_use') {
|
|
279
|
+
const input = block.input;
|
|
280
|
+
const toolShort = block.name.replace('mcp__lore-tools__', '');
|
|
281
|
+
if (toolShort === 'search' && input.query) {
|
|
282
|
+
await onProgress?.(0, undefined, `Searching: "${input.query}"`);
|
|
283
|
+
}
|
|
284
|
+
else if (toolShort === 'get_source' && input.source_id) {
|
|
285
|
+
await onProgress?.(0, undefined, `Reading source: ${input.source_id}`);
|
|
286
|
+
}
|
|
287
|
+
else if (toolShort === 'list_sources') {
|
|
288
|
+
const filter = input.project ? ` (project: ${input.project})` : '';
|
|
289
|
+
await onProgress?.(0, undefined, `Listing sources${filter}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (block.type === 'text' && block.text) {
|
|
293
|
+
lastAssistantMessage = block.text;
|
|
294
|
+
// Send a brief snippet of agent reasoning
|
|
295
|
+
const snippet = block.text.substring(0, 120).replace(/\n/g, ' ');
|
|
296
|
+
if (snippet.length > 10) {
|
|
297
|
+
await onProgress?.(0, undefined, `Agent thinking: ${snippet}...`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
276
300
|
}
|
|
277
301
|
}
|
|
278
302
|
}
|
|
@@ -282,16 +306,22 @@ export async function runResearchAgent(dbPath, dataDir, args) {
|
|
|
282
306
|
const msg = message;
|
|
283
307
|
if (msg.subtype === 'success' && msg.result) {
|
|
284
308
|
lastAssistantMessage = msg.result;
|
|
309
|
+
await onProgress?.(0, undefined, `Research complete (${msg.num_turns} turns)`);
|
|
285
310
|
console.error(`[research-agent] Completed in ${msg.num_turns} turns`);
|
|
286
311
|
}
|
|
287
312
|
else if (msg.subtype?.startsWith('error')) {
|
|
288
313
|
console.error(`[research-agent] Error: ${msg.subtype}`, msg.errors);
|
|
289
314
|
}
|
|
290
315
|
}
|
|
291
|
-
// Log tool
|
|
316
|
+
// Log tool results via the summary message
|
|
292
317
|
if (message.type === 'tool_use_summary') {
|
|
293
318
|
const msg = message;
|
|
294
|
-
|
|
319
|
+
if (msg.summary) {
|
|
320
|
+
// The summary often contains "Found X results" or similar
|
|
321
|
+
const summarySnippet = msg.summary.substring(0, 150).replace(/\n/g, ' ');
|
|
322
|
+
await onProgress?.(0, undefined, `Result: ${summarySnippet}`);
|
|
323
|
+
}
|
|
324
|
+
console.error(`[research-agent] Tool complete (turn ${turnCount})`);
|
|
295
325
|
}
|
|
296
326
|
}
|
|
297
327
|
// Parse the final result from the agent's output
|
|
@@ -6,17 +6,36 @@
|
|
|
6
6
|
* 2. SIMPLE (fallback): Single-pass search + GPT-4o-mini synthesis
|
|
7
7
|
*
|
|
8
8
|
* Set LORE_RESEARCH_MODE=simple to use the fallback mode.
|
|
9
|
+
*
|
|
10
|
+
* MCP integration: Research runs asynchronously. The `research` tool returns
|
|
11
|
+
* immediately with a job_id. Use `research_status` to poll for results.
|
|
9
12
|
*/
|
|
10
13
|
import type { ResearchPackage } from '../../core/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Start research asynchronously and return a job ID immediately.
|
|
16
|
+
*/
|
|
17
|
+
export declare function startResearchJob(dbPath: string, dataDir: string, args: ResearchArgs, options?: {
|
|
18
|
+
hookContext?: {
|
|
19
|
+
mode: 'mcp' | 'cli';
|
|
20
|
+
};
|
|
21
|
+
onProgress?: ProgressCallback;
|
|
22
|
+
}): {
|
|
23
|
+
job_id: string;
|
|
24
|
+
status: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function getResearchJobStatus(jobId: string): Promise<Record<string, unknown>>;
|
|
11
28
|
interface ResearchArgs {
|
|
12
29
|
task: string;
|
|
13
30
|
project?: string;
|
|
14
31
|
content_type?: string;
|
|
15
32
|
include_sources?: boolean;
|
|
16
33
|
}
|
|
34
|
+
export type ProgressCallback = (progress: number, total?: number, message?: string) => Promise<void>;
|
|
17
35
|
export declare function handleResearch(dbPath: string, dataDir: string, args: ResearchArgs, options?: {
|
|
18
36
|
hookContext?: {
|
|
19
37
|
mode: 'mcp' | 'cli';
|
|
20
38
|
};
|
|
39
|
+
onProgress?: ProgressCallback;
|
|
21
40
|
}): Promise<ResearchPackage>;
|
|
22
41
|
export {};
|
|
@@ -6,13 +6,145 @@
|
|
|
6
6
|
* 2. SIMPLE (fallback): Single-pass search + GPT-4o-mini synthesis
|
|
7
7
|
*
|
|
8
8
|
* Set LORE_RESEARCH_MODE=simple to use the fallback mode.
|
|
9
|
+
*
|
|
10
|
+
* MCP integration: Research runs asynchronously. The `research` tool returns
|
|
11
|
+
* immediately with a job_id. Use `research_status` to poll for results.
|
|
9
12
|
*/
|
|
10
13
|
import OpenAI from 'openai';
|
|
14
|
+
import { randomUUID } from 'crypto';
|
|
11
15
|
import { searchSources } from '../../core/vector-store.js';
|
|
12
16
|
import { generateEmbedding } from '../../core/embedder.js';
|
|
13
17
|
import { loadArchivedProjects } from './archive-project.js';
|
|
14
18
|
import { runResearchAgent } from './research-agent.js';
|
|
15
19
|
import { getExtensionRegistry } from '../../extensions/registry.js';
|
|
20
|
+
const jobStore = new Map();
|
|
21
|
+
// Clean up old jobs after 10 minutes
|
|
22
|
+
const JOB_TTL_MS = 10 * 60 * 1000;
|
|
23
|
+
function cleanOldJobs() {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [id, job] of jobStore) {
|
|
26
|
+
const startTime = new Date(job.startedAt).getTime();
|
|
27
|
+
if (now - startTime > JOB_TTL_MS) {
|
|
28
|
+
jobStore.delete(id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Start research asynchronously and return a job ID immediately.
|
|
34
|
+
*/
|
|
35
|
+
export function startResearchJob(dbPath, dataDir, args, options = {}) {
|
|
36
|
+
cleanOldJobs();
|
|
37
|
+
const jobId = randomUUID();
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const job = {
|
|
40
|
+
id: jobId,
|
|
41
|
+
task: args.task,
|
|
42
|
+
project: args.project,
|
|
43
|
+
status: 'running',
|
|
44
|
+
startedAt: now,
|
|
45
|
+
lastActivityAt: now,
|
|
46
|
+
activity: ['Starting research...'],
|
|
47
|
+
};
|
|
48
|
+
jobStore.set(jobId, job);
|
|
49
|
+
// Fire and forget — runs in the background
|
|
50
|
+
handleResearch(dbPath, dataDir, args, {
|
|
51
|
+
...options,
|
|
52
|
+
onProgress: async (_p, _t, message) => {
|
|
53
|
+
const j = jobStore.get(jobId);
|
|
54
|
+
if (j && message) {
|
|
55
|
+
j.activity.push(message);
|
|
56
|
+
j.lastActivityAt = new Date().toISOString();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
.then((result) => {
|
|
61
|
+
const j = jobStore.get(jobId);
|
|
62
|
+
if (j) {
|
|
63
|
+
j.status = 'complete';
|
|
64
|
+
j.completedAt = new Date().toISOString();
|
|
65
|
+
j.result = result;
|
|
66
|
+
j.activity.push('Research complete');
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.catch((err) => {
|
|
70
|
+
const j = jobStore.get(jobId);
|
|
71
|
+
if (j) {
|
|
72
|
+
j.status = 'error';
|
|
73
|
+
j.completedAt = new Date().toISOString();
|
|
74
|
+
j.error = err instanceof Error ? err.message : String(err);
|
|
75
|
+
j.activity.push(`Failed: ${j.error}`);
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.catch((err) => {
|
|
79
|
+
// Final safety net for errors in the handlers above
|
|
80
|
+
console.error(`[research] Critical error in job ${jobId}:`, err);
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
job_id: jobId,
|
|
84
|
+
status: 'running',
|
|
85
|
+
message: `Research started for: "${args.task}". Poll research_status with job_id "${jobId}" every 15-20 seconds. This typically takes 2-8 minutes — do not abandon early.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check status of a research job.
|
|
90
|
+
* Long-polls for up to POLL_WAIT_MS, returning early if the job completes.
|
|
91
|
+
*/
|
|
92
|
+
const POLL_WAIT_MS = 20_000;
|
|
93
|
+
const POLL_INTERVAL_MS = 1_000;
|
|
94
|
+
export async function getResearchJobStatus(jobId) {
|
|
95
|
+
let job = jobStore.get(jobId);
|
|
96
|
+
if (!job) {
|
|
97
|
+
return { status: 'not_found', job_id: jobId };
|
|
98
|
+
}
|
|
99
|
+
// If already done, return immediately
|
|
100
|
+
if (job.status !== 'running') {
|
|
101
|
+
return formatJobResponse(job);
|
|
102
|
+
}
|
|
103
|
+
// Long-poll: wait up to POLL_WAIT_MS for completion, checking every second
|
|
104
|
+
const deadline = Date.now() + POLL_WAIT_MS;
|
|
105
|
+
while (Date.now() < deadline) {
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
107
|
+
// Re-fetch to avoid stale reference if job was cleaned up
|
|
108
|
+
job = jobStore.get(jobId);
|
|
109
|
+
if (!job) {
|
|
110
|
+
return { status: 'not_found', job_id: jobId };
|
|
111
|
+
}
|
|
112
|
+
if (job.status !== 'running') {
|
|
113
|
+
return formatJobResponse(job);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return formatJobResponse(job);
|
|
117
|
+
}
|
|
118
|
+
function formatJobResponse(job) {
|
|
119
|
+
const elapsed = Math.round((Date.now() - new Date(job.startedAt).getTime()) / 1000);
|
|
120
|
+
if (job.status === 'complete') {
|
|
121
|
+
return {
|
|
122
|
+
status: 'complete',
|
|
123
|
+
job_id: job.id,
|
|
124
|
+
task: job.task,
|
|
125
|
+
elapsed_seconds: elapsed,
|
|
126
|
+
result: job.result,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (job.status === 'error') {
|
|
130
|
+
return {
|
|
131
|
+
status: 'error',
|
|
132
|
+
job_id: job.id,
|
|
133
|
+
task: job.task,
|
|
134
|
+
elapsed_seconds: elapsed,
|
|
135
|
+
error: job.error,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
status: 'running',
|
|
140
|
+
job_id: job.id,
|
|
141
|
+
task: job.task,
|
|
142
|
+
elapsed_seconds: elapsed,
|
|
143
|
+
total_steps: job.activity.length,
|
|
144
|
+
activity: job.activity,
|
|
145
|
+
message: `Research is still running (${elapsed}s elapsed, ${job.activity.length} steps completed). This is normal — deep research takes 2-8 minutes. Keep polling.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
16
148
|
// Lazy initialization for OpenAI (only used in simple mode)
|
|
17
149
|
let openaiClient = null;
|
|
18
150
|
function getOpenAI() {
|
|
@@ -109,12 +241,15 @@ Respond with only the JSON object.`;
|
|
|
109
241
|
}
|
|
110
242
|
export async function handleResearch(dbPath, dataDir, args, options = {}) {
|
|
111
243
|
const { task, project, include_sources = true } = args;
|
|
244
|
+
const { onProgress } = options;
|
|
112
245
|
// Check if we should use agentic mode (default) or simple mode (fallback)
|
|
113
246
|
const useAgenticMode = process.env.LORE_RESEARCH_MODE !== 'simple';
|
|
114
247
|
if (useAgenticMode) {
|
|
115
248
|
console.error('[research] Using agentic mode (Claude Agent SDK)');
|
|
249
|
+
await onProgress?.(0, undefined, 'Starting agentic research...');
|
|
116
250
|
try {
|
|
117
|
-
const result = await runResearchAgent(dbPath, dataDir, args);
|
|
251
|
+
const result = await runResearchAgent(dbPath, dataDir, args, onProgress);
|
|
252
|
+
await onProgress?.(100, 100, 'Research complete');
|
|
118
253
|
await runResearchCompletedHook(result, {
|
|
119
254
|
mode: options.hookContext?.mode || 'mcp',
|
|
120
255
|
dataDir,
|
|
@@ -124,11 +259,14 @@ export async function handleResearch(dbPath, dataDir, args, options = {}) {
|
|
|
124
259
|
}
|
|
125
260
|
catch (error) {
|
|
126
261
|
console.error('[research] Agentic mode failed, falling back to simple mode:', error);
|
|
262
|
+
await onProgress?.(0, undefined, 'Agentic mode failed, falling back to simple mode...');
|
|
127
263
|
// Fall through to simple mode
|
|
128
264
|
}
|
|
129
265
|
}
|
|
130
266
|
console.error('[research] Using simple mode (single-pass synthesis)');
|
|
131
|
-
|
|
267
|
+
await onProgress?.(0, undefined, 'Starting simple research...');
|
|
268
|
+
const result = await handleResearchSimple(dbPath, dataDir, args, onProgress);
|
|
269
|
+
await onProgress?.(100, 100, 'Research complete');
|
|
132
270
|
await runResearchCompletedHook(result, {
|
|
133
271
|
mode: options.hookContext?.mode || 'mcp',
|
|
134
272
|
dataDir,
|
|
@@ -140,7 +278,7 @@ export async function handleResearch(dbPath, dataDir, args, options = {}) {
|
|
|
140
278
|
* Simple research mode - single pass search + synthesis
|
|
141
279
|
* This is the fallback when agentic mode fails or is disabled
|
|
142
280
|
*/
|
|
143
|
-
async function handleResearchSimple(dbPath, dataDir, args) {
|
|
281
|
+
async function handleResearchSimple(dbPath, dataDir, args, onProgress) {
|
|
144
282
|
const { task, project, include_sources = true } = args;
|
|
145
283
|
// Use sensible defaults for simple mode
|
|
146
284
|
const sourceLimit = 10;
|
|
@@ -149,7 +287,9 @@ async function handleResearchSimple(dbPath, dataDir, args) {
|
|
|
149
287
|
const archivedProjects = await loadArchivedProjects(dataDir);
|
|
150
288
|
const archivedNames = new Set(archivedProjects.map((p) => p.project.toLowerCase()));
|
|
151
289
|
// Step 1: Search for relevant sources (fetch extra to account for archived filtering)
|
|
290
|
+
await onProgress?.(10, 100, 'Generating embeddings...');
|
|
152
291
|
const queryVector = await generateEmbedding(task);
|
|
292
|
+
await onProgress?.(30, 100, 'Searching sources...');
|
|
153
293
|
const rawSources = await searchSources(dbPath, queryVector, {
|
|
154
294
|
limit: sourceLimit * 2,
|
|
155
295
|
project,
|
|
@@ -172,6 +312,7 @@ async function handleResearchSimple(dbPath, dataDir, args) {
|
|
|
172
312
|
}
|
|
173
313
|
}
|
|
174
314
|
// Step 3: Synthesize findings with LLM (conflict-aware)
|
|
315
|
+
await onProgress?.(60, 100, 'Synthesizing findings...');
|
|
175
316
|
// Note: Decisions are now extracted at query time by the agentic research mode
|
|
176
317
|
const synthesis = await synthesizeFindings(task, sources.map((s) => ({
|
|
177
318
|
id: s.id,
|
|
@@ -233,8 +233,10 @@ export async function handleSync(dbPath, dataDir, args, options = {}) {
|
|
|
233
233
|
already_indexed: 0,
|
|
234
234
|
reconciled: 0,
|
|
235
235
|
};
|
|
236
|
+
const { onProgress } = options;
|
|
236
237
|
// 1. Git pull
|
|
237
238
|
if (doPull) {
|
|
239
|
+
await onProgress?.(5, undefined, 'Pulling from git...');
|
|
238
240
|
const pullResult = await gitPull(dataDir);
|
|
239
241
|
result.git_pulled = pullResult.success && (pullResult.message?.includes('Pulled') || false);
|
|
240
242
|
if (pullResult.error) {
|
|
@@ -248,17 +250,20 @@ export async function handleSync(dbPath, dataDir, args, options = {}) {
|
|
|
248
250
|
const hasUniversalSources = getEnabledSources(config).length > 0;
|
|
249
251
|
if (hasUniversalSources && !useLegacy) {
|
|
250
252
|
// Use new universal sync
|
|
253
|
+
await onProgress?.(20, undefined, 'Discovering new files...');
|
|
251
254
|
const { discovery, processing } = await universalSync(dataDir, dryRun, options.hookContext);
|
|
252
255
|
result.discovery = discovery;
|
|
253
256
|
result.processing = processing;
|
|
254
257
|
}
|
|
255
258
|
// Always run legacy disk sync for backward compatibility
|
|
256
259
|
// (picks up sources added via old `lore ingest` command)
|
|
260
|
+
await onProgress?.(60, undefined, 'Running legacy sync...');
|
|
257
261
|
const legacyResult = await legacyDiskSync(dbPath, dataDir);
|
|
258
262
|
result.sources_found = legacyResult.sources_found;
|
|
259
263
|
result.sources_indexed = legacyResult.sources_indexed;
|
|
260
264
|
result.already_indexed = legacyResult.already_indexed;
|
|
261
265
|
// Reconcile: ensure every Supabase source has local content.md
|
|
266
|
+
await onProgress?.(80, undefined, 'Reconciling local content...');
|
|
262
267
|
result.reconciled = await reconcileLocalContent(dataDir);
|
|
263
268
|
}
|
|
264
269
|
// 3. Git push
|
package/dist/mcp/server.js
CHANGED
|
@@ -21,7 +21,7 @@ import { handleGetSource } from './handlers/get-source.js';
|
|
|
21
21
|
import { handleListSources } from './handlers/list-sources.js';
|
|
22
22
|
import { handleRetain } from './handlers/retain.js';
|
|
23
23
|
import { handleIngest } from './handlers/ingest.js';
|
|
24
|
-
import {
|
|
24
|
+
import { startResearchJob, getResearchJobStatus } from './handlers/research.js';
|
|
25
25
|
import { handleListProjects } from './handlers/list-projects.js';
|
|
26
26
|
import { handleSync } from './handlers/sync.js';
|
|
27
27
|
import { handleArchiveProject } from './handlers/archive-project.js';
|
|
@@ -136,7 +136,7 @@ async function main() {
|
|
|
136
136
|
}
|
|
137
137
|
const server = new Server({
|
|
138
138
|
name: 'lore',
|
|
139
|
-
version: '0.
|
|
139
|
+
version: '0.7.0',
|
|
140
140
|
}, {
|
|
141
141
|
capabilities: {
|
|
142
142
|
tools: {},
|
|
@@ -184,8 +184,25 @@ async function main() {
|
|
|
184
184
|
return { tools: toolDefinitions };
|
|
185
185
|
});
|
|
186
186
|
// Handle tool calls (core tools only)
|
|
187
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
187
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
188
188
|
const { name, arguments: args } = request.params;
|
|
189
|
+
// Build a progress callback for long-running tools.
|
|
190
|
+
// If the client sent a progressToken, we send notifications/progress back;
|
|
191
|
+
// otherwise, onProgress is a no-op.
|
|
192
|
+
const progressToken = request.params._meta?.progressToken;
|
|
193
|
+
const onProgress = progressToken
|
|
194
|
+
? async (progress, total, message) => {
|
|
195
|
+
try {
|
|
196
|
+
await extra.sendNotification({
|
|
197
|
+
method: 'notifications/progress',
|
|
198
|
+
params: { progressToken, progress, ...(total != null ? { total } : {}), ...(message ? { message } : {}) },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Progress notifications are best-effort
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
: undefined;
|
|
189
206
|
try {
|
|
190
207
|
let result;
|
|
191
208
|
switch (name) {
|
|
@@ -215,16 +232,22 @@ async function main() {
|
|
|
215
232
|
hookContext: { mode: 'mcp' },
|
|
216
233
|
});
|
|
217
234
|
break;
|
|
218
|
-
// Agentic research tool
|
|
235
|
+
// Agentic research tool — runs async, returns job_id immediately
|
|
219
236
|
case 'research':
|
|
220
|
-
result =
|
|
237
|
+
result = startResearchJob(DB_PATH, LORE_DATA_DIR, args, {
|
|
221
238
|
hookContext: { mode: 'mcp' },
|
|
239
|
+
onProgress,
|
|
222
240
|
});
|
|
223
241
|
break;
|
|
242
|
+
// Poll for research results (long-polls up to 20s)
|
|
243
|
+
case 'research_status':
|
|
244
|
+
result = await getResearchJobStatus(args?.job_id);
|
|
245
|
+
break;
|
|
224
246
|
// Sync tool
|
|
225
247
|
case 'sync':
|
|
226
248
|
result = await handleSync(DB_PATH, LORE_DATA_DIR, args, {
|
|
227
249
|
hookContext: { mode: 'mcp' },
|
|
250
|
+
onProgress,
|
|
228
251
|
});
|
|
229
252
|
break;
|
|
230
253
|
// Project management
|
package/dist/mcp/tools.js
CHANGED
|
@@ -241,7 +241,7 @@ USE 'ingest' INSTEAD for full documents, meeting notes, transcripts, or any cont
|
|
|
241
241
|
name: 'research',
|
|
242
242
|
description: `Run a comprehensive research query across the knowledge base. An internal agent iteratively searches, reads sources, cross-references findings, and synthesizes a research package with full citations.
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
ASYNC: This tool returns immediately with a job_id. You MUST then poll 'research_status' with that job_id to get results. Research typically takes 2-8 minutes depending on the amount of data. Poll every 15-20 seconds. Do NOT assume it is stuck — check the 'activity' array in the status response to see what the agent is doing.
|
|
245
245
|
|
|
246
246
|
WHEN TO USE:
|
|
247
247
|
- Questions that span multiple sources ("What do we know about authentication?")
|
|
@@ -249,9 +249,23 @@ WHEN TO USE:
|
|
|
249
249
|
- Building a cited research package for decision-making
|
|
250
250
|
- Open-ended exploration of a topic
|
|
251
251
|
|
|
252
|
-
COST: This tool makes multiple LLM calls internally (typically
|
|
252
|
+
COST: This tool makes multiple LLM calls internally (typically 10-30 search + read cycles). For simple lookups, use 'search' instead — it's 10x cheaper and faster.`,
|
|
253
253
|
inputSchema: zodToJsonSchema(ResearchSchema),
|
|
254
254
|
},
|
|
255
|
+
// Research status (polling for async results)
|
|
256
|
+
{
|
|
257
|
+
name: 'research_status',
|
|
258
|
+
description: `Check the status of a running research job. Returns the full research package when complete.
|
|
259
|
+
|
|
260
|
+
Call this after 'research' returns a job_id. Research typically takes 2-8 minutes. Poll every 15-20 seconds. The response includes an 'activity' array showing exactly what the research agent is doing (searches, sources being read, reasoning). As long as 'total_steps' is increasing or 'elapsed_seconds' is under 8 minutes, the research is progressing normally — do NOT abandon it.`,
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
job_id: { type: 'string', description: 'The job_id returned by the research tool' },
|
|
265
|
+
},
|
|
266
|
+
required: ['job_id'],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
255
269
|
// Ingest tool
|
|
256
270
|
{
|
|
257
271
|
name: 'ingest',
|