Skip to content

Commit b528a72

Browse files
committed
feat(phase2): switch MCP tools and REST routes to StoreManager
- Rewrite 51 MCP tool files (knowledge, tasks, skills, epics) to use StoreManager - Rewrite 4 REST route files to use StoreManager - Wire createMcpServer to accept and use StoreManager - Add storeManager field to ProjectInstance - Add StoreManager methods: getAttachmentPath, listEpicTasks, getEpicBySlug - Add EpicsStore.listTasks for epic→task query - IDs change from string to number (breaking MCP API change) - Author resolution deferred to Phase 4 - Search params simplified: drop BFS-specific params Note: MCP/REST integration tests need rewriting (next commit)
1 parent fc22c73 commit b528a72

62 files changed

Lines changed: 940 additions & 926 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/api/index.ts

Lines changed: 69 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@ import { createRestApp } from '@/api/rest/index';
1111
import { attachWebSocket } from '@/api/rest/websocket';
1212
import { resolveUserFromBearer, resolveAccess, canWrite, canRead } from '@/lib/access';
1313
import { MAX_BODY_SIZE, SESSION_SWEEP_INTERVAL_MS } from '@/lib/defaults';
14-
import { GRAPH_NAMES, type GraphName, type AccessLevel, resolveRequestAuthor, type UserConfig } from '@/lib/multi-config';
14+
import { GRAPH_NAMES, type GraphName, type AccessLevel, type UserConfig } from '@/lib/multi-config';
1515
import type { DocGraph } from '@/graphs/docs';
1616
import { DocGraphManager } from '@/graphs/docs';
1717
import type { CodeGraph } from '@/graphs/code-types';
1818
import { CodeGraphManager } from '@/graphs/code';
1919
import type { KnowledgeGraph } from '@/graphs/knowledge-types';
20-
import { KnowledgeGraphManager } from '@/graphs/knowledge';
20+
// KnowledgeGraphManager removed — using StoreManager
2121
import type { FileIndexGraph } from '@/graphs/file-index-types';
2222
import { FileIndexGraphManager } from '@/graphs/file-index';
2323
import type { TaskGraph } from '@/graphs/task-types';
24-
import { TaskGraphManager } from '@/graphs/task';
24+
// TaskGraphManager removed — using StoreManager
2525
import type { SkillGraph } from '@/graphs/skill-types';
26-
import { SkillGraphManager } from '@/graphs/skill';
27-
import { noopContext, type ExternalGraphs } from '@/graphs/manager-types';
26+
// SkillGraphManager removed — using StoreManager
27+
import { type ExternalGraphs } from '@/graphs/manager-types';
28+
import type { StoreManager } from '@/lib/store-manager';
2829
import * as listTopics from '@/api/tools/docs/list-topics';
2930
import * as getToc from '@/api/tools/docs/get-toc';
3031
import * as search from '@/api/tools/docs/search';
@@ -184,13 +185,14 @@ export function createMcpServer(
184185
taskGraph?: TaskGraph,
185186
embedFn?: EmbedFn | Partial<EmbedFnMap>,
186187
mutationQueue?: PromiseQueue,
187-
projectDir?: string,
188+
_projectDir?: string,
188189
skillGraph?: SkillGraph,
189190
sessionContext?: McpSessionContext,
190191
readonlyGraphs?: Set<string>,
191192
userAccess?: Map<string, AccessLevel>,
192193
getSessionId?: () => string | undefined,
193-
users?: Record<string, UserConfig>,
194+
_users?: Record<string, UserConfig>,
195+
storeManager?: StoreManager,
194196
): McpServer {
195197
// Backward-compat: single EmbedFn → use for both document and query
196198
const defaultPair: EmbedFns = { document: (q) => embed(q, ''), query: (q) => embed(q, '') };
@@ -222,7 +224,7 @@ export function createMcpServer(
222224
);
223225
// Mutation tools are registered through mutServer to serialize concurrent writes
224226
const mutServer = mutationQueue ? createMutationServer(server, mutationQueue, getSessionId) : server;
225-
const resolveAuthor = () => resolveRequestAuthor(sessionContext?.userId, users);
227+
void _users; // reserved for future author resolution wiring
226228

227229
// Check if mutation tools should be registered for a graph:
228230
// - graph must not be readonly (global setting — tools hidden for all)
@@ -288,88 +290,76 @@ export function createMcpServer(
288290
getFileInfo.register(server, fileIndexMgr);
289291
}
290292

291-
// Knowledge tools — read tools gated by canAccess, mutation tools gated by canMutate
292-
if (knowledgeGraph && canAccess('knowledge')) {
293-
const ctx = projectDir ? { ...noopContext(), projectDir } : noopContext();
294-
const knowledgeMgr = new KnowledgeGraphManager(knowledgeGraph, fns.knowledge, ctx, {
295-
docGraph, codeGraph, fileIndexGraph, taskGraph, skillGraph,
296-
});
297-
getNote.register(server, knowledgeMgr);
298-
listNotes.register(server, knowledgeMgr);
299-
searchNotes.register(server, knowledgeMgr);
300-
listRelations.register(server, knowledgeMgr);
301-
findLinkedNotes.register(server, knowledgeMgr);
293+
// Knowledge tools — uses StoreManager (read gated by canAccess, mutation by canMutate)
294+
if (storeManager && canAccess('knowledge')) {
295+
getNote.register(server, storeManager);
296+
listNotes.register(server, storeManager);
297+
searchNotes.register(server, storeManager);
298+
listRelations.register(server, storeManager);
299+
findLinkedNotes.register(server, storeManager);
302300
if (canMutate('knowledge')) {
303-
createNote.register(mutServer, knowledgeMgr, resolveAuthor);
304-
updateNote.register(mutServer, knowledgeMgr, resolveAuthor);
305-
deleteNote.register(mutServer, knowledgeMgr, resolveAuthor);
306-
createRelation.register(mutServer, knowledgeMgr, resolveAuthor);
307-
deleteRelation.register(mutServer, knowledgeMgr, resolveAuthor);
308-
addNoteAttachment.register(mutServer, knowledgeMgr, resolveAuthor);
309-
removeNoteAttachment.register(mutServer, knowledgeMgr, resolveAuthor);
301+
createNote.register(mutServer, storeManager);
302+
updateNote.register(mutServer, storeManager);
303+
deleteNote.register(mutServer, storeManager);
304+
createRelation.register(mutServer, storeManager);
305+
deleteRelation.register(mutServer, storeManager);
306+
addNoteAttachment.register(mutServer, storeManager);
307+
removeNoteAttachment.register(mutServer, storeManager);
310308
}
311309
}
312310

313-
// Task tools — read tools gated by canAccess, mutation tools gated by canMutate
314-
if (taskGraph && canAccess('tasks')) {
315-
const taskCtx = projectDir ? { ...noopContext(), projectDir } : noopContext();
316-
const taskMgr = new TaskGraphManager(taskGraph, fns.tasks, taskCtx, {
317-
docGraph, codeGraph, knowledgeGraph, fileIndexGraph, skillGraph,
318-
});
319-
getTask.register(server, taskMgr);
320-
listTasksTool.register(server, taskMgr);
321-
searchTasksTool.register(server, taskMgr);
322-
findLinkedTasks.register(server, taskMgr);
311+
// Task tools — uses StoreManager (read gated by canAccess, mutation by canMutate)
312+
if (storeManager && canAccess('tasks')) {
313+
getTask.register(server, storeManager);
314+
listTasksTool.register(server, storeManager);
315+
searchTasksTool.register(server, storeManager);
316+
findLinkedTasks.register(server, storeManager);
323317
if (canMutate('tasks')) {
324-
createTask.register(mutServer, taskMgr, resolveAuthor);
325-
updateTask.register(mutServer, taskMgr, resolveAuthor);
326-
deleteTask.register(mutServer, taskMgr, resolveAuthor);
327-
moveTask.register(mutServer, taskMgr, resolveAuthor);
328-
reorderTaskTool.register(mutServer, taskMgr, resolveAuthor);
329-
linkTask.register(mutServer, taskMgr, resolveAuthor);
330-
createTaskLink.register(mutServer, taskMgr, resolveAuthor);
331-
deleteTaskLink.register(mutServer, taskMgr, resolveAuthor);
332-
bulkMove.register(mutServer, taskMgr, resolveAuthor);
333-
bulkPriority.register(mutServer, taskMgr, resolveAuthor);
334-
bulkDelete.register(mutServer, taskMgr, resolveAuthor);
335-
addTaskAttachment.register(mutServer, taskMgr, resolveAuthor);
336-
removeTaskAttachment.register(mutServer, taskMgr, resolveAuthor);
318+
createTask.register(mutServer, storeManager);
319+
updateTask.register(mutServer, storeManager);
320+
deleteTask.register(mutServer, storeManager);
321+
moveTask.register(mutServer, storeManager);
322+
reorderTaskTool.register(mutServer, storeManager);
323+
linkTask.register(mutServer, storeManager);
324+
createTaskLink.register(mutServer, storeManager);
325+
deleteTaskLink.register(mutServer, storeManager);
326+
bulkMove.register(mutServer, storeManager);
327+
bulkPriority.register(mutServer, storeManager);
328+
bulkDelete.register(mutServer, storeManager);
329+
addTaskAttachment.register(mutServer, storeManager);
330+
removeTaskAttachment.register(mutServer, storeManager);
337331
}
338332

339-
// Epic tools (same graph, same access)
340-
getEpicTool.register(server, taskMgr);
341-
listEpicsTool.register(server, taskMgr);
342-
searchEpicsTool.register(server, taskMgr);
333+
// Epic tools (same access as tasks)
334+
getEpicTool.register(server, storeManager);
335+
listEpicsTool.register(server, storeManager);
336+
searchEpicsTool.register(server, storeManager);
343337
if (canMutate('tasks')) {
344-
createEpicTool.register(mutServer, taskMgr, resolveAuthor);
345-
updateEpicTool.register(mutServer, taskMgr, resolveAuthor);
346-
deleteEpicTool.register(mutServer, taskMgr, resolveAuthor);
347-
linkEpicTaskTool.register(mutServer, taskMgr, resolveAuthor);
348-
unlinkEpicTaskTool.register(mutServer, taskMgr, resolveAuthor);
338+
createEpicTool.register(mutServer, storeManager);
339+
updateEpicTool.register(mutServer, storeManager);
340+
deleteEpicTool.register(mutServer, storeManager);
341+
linkEpicTaskTool.register(mutServer, storeManager);
342+
unlinkEpicTaskTool.register(mutServer, storeManager);
349343
}
350344
}
351345

352-
// Skill tools — read tools gated by canAccess, mutation tools gated by canMutate
353-
if (skillGraph && canAccess('skills')) {
354-
const skillCtx = projectDir ? { ...noopContext(), projectDir } : noopContext();
355-
const skillMgr = new SkillGraphManager(skillGraph, fns.skills, skillCtx, {
356-
docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph,
357-
});
358-
getSkillTool.register(server, skillMgr);
359-
listSkillsTool.register(server, skillMgr);
360-
searchSkillsTool.register(server, skillMgr);
361-
findLinkedSkills.register(server, skillMgr);
362-
recallSkills.register(server, skillMgr);
346+
// Skill tools — uses StoreManager (read gated by canAccess, mutation by canMutate)
347+
if (storeManager && canAccess('skills')) {
348+
getSkillTool.register(server, storeManager);
349+
listSkillsTool.register(server, storeManager);
350+
searchSkillsTool.register(server, storeManager);
351+
findLinkedSkills.register(server, storeManager);
352+
recallSkills.register(server, storeManager);
363353
if (canMutate('skills')) {
364-
createSkillTool.register(mutServer, skillMgr, resolveAuthor);
365-
updateSkillTool.register(mutServer, skillMgr, resolveAuthor);
366-
deleteSkillTool.register(mutServer, skillMgr, resolveAuthor);
367-
linkSkill.register(mutServer, skillMgr, resolveAuthor);
368-
createSkillLink.register(mutServer, skillMgr, resolveAuthor);
369-
deleteSkillLink.register(mutServer, skillMgr, resolveAuthor);
370-
addSkillAttachment.register(mutServer, skillMgr, resolveAuthor);
371-
removeSkillAttachment.register(mutServer, skillMgr, resolveAuthor);
372-
bumpSkillUsage.register(mutServer, skillMgr, resolveAuthor);
354+
createSkillTool.register(mutServer, storeManager);
355+
updateSkillTool.register(mutServer, storeManager);
356+
deleteSkillTool.register(mutServer, storeManager);
357+
linkSkill.register(mutServer, storeManager);
358+
createSkillLink.register(mutServer, storeManager);
359+
deleteSkillLink.register(mutServer, storeManager);
360+
addSkillAttachment.register(mutServer, storeManager);
361+
removeSkillAttachment.register(mutServer, storeManager);
362+
bumpSkillUsage.register(mutServer, storeManager);
373363
}
374364
}
375365

@@ -687,6 +677,7 @@ export async function startMultiProjectHttpServer(
687677
mcpUserAccess,
688678
() => transport.sessionId,
689679
users,
680+
project.storeManager,
690681
);
691682
await mcpServer.connect(transport);
692683
await transport.handleRequest(req, res, body);

src/api/rest/epics.ts

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@ import { Router } from 'express';
22
import type { ProjectInstance } from '@/lib/project-manager';
33
import { validateBody, validateQuery, createEpicSchema, updateEpicSchema, epicSearchSchema, epicListSchema, epicLinkSchema } from '@/api/rest/validation';
44
import { requireWriteAccess } from '@/api/rest/index';
5-
import { VersionConflictError } from '@/graphs/manager-types';
6-
import type { EpicStatus } from '@/graphs/task-types';
7-
import { resolveRequestAuthor, type UserConfig } from '@/lib/multi-config';
5+
import { VersionConflictError } from '@/store/types';
6+
import type { UserConfig } from '@/lib/multi-config';
87

9-
export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
8+
export function createEpicsRouter(_users?: Record<string, UserConfig>): Router {
109
const router = Router({ mergeParams: true });
1110

1211
function getProject(req: any) {
13-
return req.project as ProjectInstance & { taskManager: NonNullable<ProjectInstance['taskManager']> };
12+
return req.project as ProjectInstance & { storeManager: NonNullable<ProjectInstance['storeManager']> };
1413
}
1514

1615
// List epics
1716
router.get('/', validateQuery(epicListSchema), (req, res, next) => {
1817
try {
1918
const p = getProject(req);
2019
const q = req.validatedQuery;
21-
const { results: epics, total } = p.taskManager.listEpics(q);
22-
res.json({ results: epics, total });
20+
const { results, total } = p.storeManager.listEpics(q);
21+
res.json({ results, total });
2322
} catch (err) { next(err); }
2423
});
2524

@@ -28,8 +27,11 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
2827
try {
2928
const p = getProject(req);
3029
const q = req.validatedQuery;
31-
const results = await p.taskManager.searchEpics(q.q, {
32-
topK: q.topK, minScore: q.minScore, searchMode: q.searchMode,
30+
const results = await p.storeManager.searchEpics({
31+
text: q.q,
32+
searchMode: q.searchMode,
33+
maxResults: q.maxResults,
34+
minScore: q.minScore,
3335
});
3436
res.json({ results });
3537
} catch (err) { next(err); }
@@ -39,7 +41,8 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
3941
router.get('/:epicId', (req, res, next) => {
4042
try {
4143
const p = getProject(req);
42-
const epic = p.taskManager.getEpic(req.params.epicId as string);
44+
const epicId = Number(req.params.epicId);
45+
const epic = p.storeManager.getEpic(epicId);
4346
if (!epic) return res.status(404).json({ error: 'Epic not found' });
4447
res.json(epic);
4548
} catch (err) { next(err); }
@@ -49,9 +52,10 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
4952
router.get('/:epicId/tasks', (req, res, next) => {
5053
try {
5154
const p = getProject(req);
52-
const epic = p.taskManager.getEpic(req.params.epicId as string);
55+
const epicId = Number(req.params.epicId);
56+
const epic = p.storeManager.getEpic(epicId);
5357
if (!epic) return res.status(404).json({ error: 'Epic not found' });
54-
const tasks = p.taskManager.listEpicTasks(req.params.epicId as string);
58+
const tasks = p.storeManager.listEpicTasks(epicId);
5559
res.json({ results: tasks, progress: epic.progress });
5660
} catch (err) { next(err); }
5761
});
@@ -60,11 +64,12 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
6064
router.post('/', requireWriteAccess, validateBody(createEpicSchema), async (req, res, next) => {
6165
try {
6266
const p = getProject(req);
63-
const author = resolveRequestAuthor(req.userId, users);
6467
const { title, description, status, priority, tags } = req.body;
6568
const created = await p.mutationQueue.enqueue(async () => {
66-
const epicId = await p.taskManager.createEpic(title, description, status, priority, tags, author);
67-
return p.taskManager.getEpic(epicId);
69+
const epic = await p.storeManager.createEpic({
70+
title, description: description ?? '', status, priority, tags,
71+
});
72+
return p.storeManager.getEpic(epic.id);
6873
});
6974
res.status(201).json(created);
7075
} catch (err) { next(err); }
@@ -74,15 +79,12 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
7479
router.put('/:epicId', requireWriteAccess, validateBody(updateEpicSchema), async (req, res, next) => {
7580
try {
7681
const p = getProject(req);
77-
const author = resolveRequestAuthor(req.userId, users);
78-
const epicId = req.params.epicId as string;
79-
const { version, status, ...patch } = req.body;
82+
const epicId = Number(req.params.epicId);
83+
const { version, ...patch } = req.body;
8084
const result = await p.mutationQueue.enqueue(async () => {
81-
const ok = await p.taskManager.updateEpic(epicId, patch, status as EpicStatus | undefined, version, author);
82-
if (!ok) return null;
83-
return p.taskManager.getEpic(epicId);
85+
const updated = await p.storeManager.updateEpic(epicId, patch, undefined, version);
86+
return p.storeManager.getEpic(updated.id);
8487
});
85-
if (!result) return res.status(404).json({ error: 'Epic not found' });
8688
res.json(result);
8789
} catch (err) {
8890
if (err instanceof VersionConflictError) {
@@ -96,12 +98,12 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
9698
router.delete('/:epicId', requireWriteAccess, async (req, res, next) => {
9799
try {
98100
const p = getProject(req);
99-
const author = resolveRequestAuthor(req.userId, users);
100-
const epicId = req.params.epicId as string;
101-
const ok = await p.mutationQueue.enqueue(async () => {
102-
return p.taskManager.deleteEpic(epicId, author);
101+
const epicId = Number(req.params.epicId);
102+
const existing = p.storeManager.getEpic(epicId);
103+
if (!existing) return res.status(404).json({ error: 'Epic not found' });
104+
await p.mutationQueue.enqueue(async () => {
105+
p.storeManager.deleteEpic(epicId);
103106
});
104-
if (!ok) return res.status(404).json({ error: 'Epic not found' });
105107
res.status(204).end();
106108
} catch (err) { next(err); }
107109
});
@@ -110,13 +112,11 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
110112
router.post('/:epicId/link', requireWriteAccess, validateBody(epicLinkSchema), async (req, res, next) => {
111113
try {
112114
const p = getProject(req);
113-
const author = resolveRequestAuthor(req.userId, users);
114-
const epicId = req.params.epicId as string;
115-
const { taskId } = req.body;
116-
const ok = await p.mutationQueue.enqueue(async () => {
117-
return p.taskManager.linkTaskToEpic(taskId, epicId, author);
115+
const epicId = Number(req.params.epicId);
116+
const taskId = Number(req.body.taskId);
117+
await p.mutationQueue.enqueue(async () => {
118+
p.storeManager.linkTaskToEpic(epicId, taskId);
118119
});
119-
if (!ok) return res.status(400).json({ error: 'Failed to link task to epic' });
120120
res.status(201).json({ taskId, epicId, linked: true });
121121
} catch (err) { next(err); }
122122
});
@@ -125,13 +125,11 @@ export function createEpicsRouter(users?: Record<string, UserConfig>): Router {
125125
router.delete('/:epicId/link', requireWriteAccess, validateBody(epicLinkSchema), async (req, res, next) => {
126126
try {
127127
const p = getProject(req);
128-
const author = resolveRequestAuthor(req.userId, users);
129-
const epicId = req.params.epicId as string;
130-
const { taskId } = req.body;
131-
const ok = await p.mutationQueue.enqueue(async () => {
132-
return p.taskManager.unlinkTaskFromEpic(taskId, epicId, author);
128+
const epicId = Number(req.params.epicId);
129+
const taskId = Number(req.body.taskId);
130+
await p.mutationQueue.enqueue(async () => {
131+
p.storeManager.unlinkTaskFromEpic(epicId, taskId);
133132
});
134-
if (!ok) return res.status(404).json({ error: 'Link not found' });
135133
res.status(204).end();
136134
} catch (err) { next(err); }
137135
});

0 commit comments

Comments
 (0)