@prih/mcp-graph-memory 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +512 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/files.js +64 -0
  7. package/dist/api/rest/graph.js +56 -0
  8. package/dist/api/rest/index.js +117 -0
  9. package/dist/api/rest/knowledge.js +238 -0
  10. package/dist/api/rest/skills.js +284 -0
  11. package/dist/api/rest/tasks.js +272 -0
  12. package/dist/api/rest/tools.js +126 -0
  13. package/dist/api/rest/validation.js +191 -0
  14. package/dist/api/rest/websocket.js +65 -0
  15. package/dist/api/tools/code/get-file-symbols.js +30 -0
  16. package/dist/api/tools/code/get-symbol.js +22 -0
  17. package/dist/api/tools/code/list-files.js +18 -0
  18. package/dist/api/tools/code/search-code.js +27 -0
  19. package/dist/api/tools/code/search-files.js +22 -0
  20. package/dist/api/tools/context/get-context.js +19 -0
  21. package/dist/api/tools/docs/cross-references.js +76 -0
  22. package/dist/api/tools/docs/explain-symbol.js +55 -0
  23. package/dist/api/tools/docs/find-examples.js +52 -0
  24. package/dist/api/tools/docs/get-node.js +24 -0
  25. package/dist/api/tools/docs/get-toc.js +22 -0
  26. package/dist/api/tools/docs/list-snippets.js +46 -0
  27. package/dist/api/tools/docs/list-topics.js +18 -0
  28. package/dist/api/tools/docs/search-files.js +22 -0
  29. package/dist/api/tools/docs/search-snippets.js +43 -0
  30. package/dist/api/tools/docs/search.js +27 -0
  31. package/dist/api/tools/file-index/get-file-info.js +21 -0
  32. package/dist/api/tools/file-index/list-all-files.js +28 -0
  33. package/dist/api/tools/file-index/search-all-files.js +24 -0
  34. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  35. package/dist/api/tools/knowledge/create-note.js +20 -0
  36. package/dist/api/tools/knowledge/create-relation.js +29 -0
  37. package/dist/api/tools/knowledge/delete-note.js +19 -0
  38. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  39. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  40. package/dist/api/tools/knowledge/get-note.js +20 -0
  41. package/dist/api/tools/knowledge/list-notes.js +18 -0
  42. package/dist/api/tools/knowledge/list-relations.js +17 -0
  43. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  44. package/dist/api/tools/knowledge/search-notes.js +25 -0
  45. package/dist/api/tools/knowledge/update-note.js +34 -0
  46. package/dist/api/tools/skills/add-attachment.js +31 -0
  47. package/dist/api/tools/skills/bump-usage.js +19 -0
  48. package/dist/api/tools/skills/create-skill-link.js +25 -0
  49. package/dist/api/tools/skills/create-skill.js +26 -0
  50. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  51. package/dist/api/tools/skills/delete-skill.js +20 -0
  52. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  53. package/dist/api/tools/skills/get-skill.js +21 -0
  54. package/dist/api/tools/skills/link-skill.js +23 -0
  55. package/dist/api/tools/skills/list-skills.js +20 -0
  56. package/dist/api/tools/skills/recall-skills.js +18 -0
  57. package/dist/api/tools/skills/remove-attachment.js +19 -0
  58. package/dist/api/tools/skills/search-skills.js +25 -0
  59. package/dist/api/tools/skills/update-skill.js +58 -0
  60. package/dist/api/tools/tasks/add-attachment.js +31 -0
  61. package/dist/api/tools/tasks/create-task-link.js +25 -0
  62. package/dist/api/tools/tasks/create-task.js +25 -0
  63. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  64. package/dist/api/tools/tasks/delete-task.js +20 -0
  65. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  66. package/dist/api/tools/tasks/get-task.js +20 -0
  67. package/dist/api/tools/tasks/link-task.js +23 -0
  68. package/dist/api/tools/tasks/list-tasks.js +24 -0
  69. package/dist/api/tools/tasks/move-task.js +38 -0
  70. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  71. package/dist/api/tools/tasks/search-tasks.js +25 -0
  72. package/dist/api/tools/tasks/update-task.js +55 -0
  73. package/dist/cli/index.js +451 -0
  74. package/dist/cli/indexer.js +277 -0
  75. package/dist/graphs/attachment-types.js +74 -0
  76. package/dist/graphs/code-types.js +10 -0
  77. package/dist/graphs/code.js +172 -0
  78. package/dist/graphs/docs.js +198 -0
  79. package/dist/graphs/file-index-types.js +10 -0
  80. package/dist/graphs/file-index.js +310 -0
  81. package/dist/graphs/file-lang.js +119 -0
  82. package/dist/graphs/knowledge-types.js +32 -0
  83. package/dist/graphs/knowledge.js +764 -0
  84. package/dist/graphs/manager-types.js +87 -0
  85. package/dist/graphs/skill-types.js +10 -0
  86. package/dist/graphs/skill.js +1013 -0
  87. package/dist/graphs/task-types.js +17 -0
  88. package/dist/graphs/task.js +960 -0
  89. package/dist/lib/embedder.js +101 -0
  90. package/dist/lib/events-log.js +400 -0
  91. package/dist/lib/file-import.js +327 -0
  92. package/dist/lib/file-mirror.js +446 -0
  93. package/dist/lib/frontmatter.js +17 -0
  94. package/dist/lib/mirror-watcher.js +637 -0
  95. package/dist/lib/multi-config.js +254 -0
  96. package/dist/lib/parsers/code.js +246 -0
  97. package/dist/lib/parsers/codeblock.js +66 -0
  98. package/dist/lib/parsers/docs.js +196 -0
  99. package/dist/lib/project-manager.js +418 -0
  100. package/dist/lib/promise-queue.js +22 -0
  101. package/dist/lib/search/bm25.js +167 -0
  102. package/dist/lib/search/code.js +103 -0
  103. package/dist/lib/search/docs.js +108 -0
  104. package/dist/lib/search/file-index.js +31 -0
  105. package/dist/lib/search/files.js +61 -0
  106. package/dist/lib/search/knowledge.js +101 -0
  107. package/dist/lib/search/skills.js +104 -0
  108. package/dist/lib/search/tasks.js +103 -0
  109. package/dist/lib/watcher.js +67 -0
  110. package/package.json +83 -0
  111. package/ui/README.md +54 -0
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createSkillsRouter = createSkillsRouter;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const mime_1 = __importDefault(require("mime"));
9
+ const express_1 = require("express");
10
+ const multer_1 = __importDefault(require("multer"));
11
+ const validation_1 = require("../../api/rest/validation");
12
+ const manager_types_1 = require("../../graphs/manager-types");
13
+ const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
14
+ function createSkillsRouter() {
15
+ const router = (0, express_1.Router)({ mergeParams: true });
16
+ function getProject(req) {
17
+ return req.project;
18
+ }
19
+ // List skills
20
+ router.get('/', (0, validation_1.validateQuery)(validation_1.skillListSchema), (req, res, next) => {
21
+ try {
22
+ const p = getProject(req);
23
+ const q = req.validatedQuery;
24
+ const skills = p.skillManager.listSkills(q);
25
+ res.json({ results: skills });
26
+ }
27
+ catch (err) {
28
+ next(err);
29
+ }
30
+ });
31
+ // Search skills
32
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.skillSearchSchema), async (req, res, next) => {
33
+ try {
34
+ const p = getProject(req);
35
+ const q = req.validatedQuery;
36
+ const results = await p.skillManager.searchSkills(q.q, {
37
+ topK: q.topK,
38
+ minScore: q.minScore,
39
+ searchMode: q.searchMode,
40
+ });
41
+ res.json({ results });
42
+ }
43
+ catch (err) {
44
+ next(err);
45
+ }
46
+ });
47
+ // Recall skills (higher recall search for task contexts)
48
+ router.get('/recall', (0, validation_1.validateQuery)(validation_1.skillSearchSchema), async (req, res, next) => {
49
+ try {
50
+ const p = getProject(req);
51
+ const q = req.validatedQuery;
52
+ const results = await p.skillManager.searchSkills(q.q, {
53
+ topK: q.topK,
54
+ minScore: q.minScore ?? 0.3,
55
+ searchMode: q.searchMode,
56
+ });
57
+ res.json({ results });
58
+ }
59
+ catch (err) {
60
+ next(err);
61
+ }
62
+ });
63
+ // Find skills linked to an external entity
64
+ router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
65
+ try {
66
+ const p = getProject(req);
67
+ const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
68
+ const skills = p.skillManager.findLinkedSkills(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
69
+ res.json({ results: skills });
70
+ }
71
+ catch (err) {
72
+ next(err);
73
+ }
74
+ });
75
+ // Get skill
76
+ router.get('/:skillId', (req, res, next) => {
77
+ try {
78
+ const p = getProject(req);
79
+ const skill = p.skillManager.getSkill(req.params.skillId);
80
+ if (!skill)
81
+ return res.status(404).json({ error: 'Skill not found' });
82
+ const relations = p.skillManager.listRelations(req.params.skillId);
83
+ res.json({ ...skill, relations });
84
+ }
85
+ catch (err) {
86
+ next(err);
87
+ }
88
+ });
89
+ // Create skill
90
+ router.post('/', (0, validation_1.validateBody)(validation_1.createSkillSchema), async (req, res, next) => {
91
+ try {
92
+ const p = getProject(req);
93
+ const { title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence } = req.body;
94
+ const created = await p.mutationQueue.enqueue(async () => {
95
+ const skillId = await p.skillManager.createSkill(title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence);
96
+ return p.skillManager.getSkill(skillId);
97
+ });
98
+ res.status(201).json(created);
99
+ }
100
+ catch (err) {
101
+ next(err);
102
+ }
103
+ });
104
+ // Update skill
105
+ router.put('/:skillId', (0, validation_1.validateBody)(validation_1.updateSkillSchema), async (req, res, next) => {
106
+ try {
107
+ const p = getProject(req);
108
+ const skillId = req.params.skillId;
109
+ const { version, ...patch } = req.body;
110
+ const result = await p.mutationQueue.enqueue(async () => {
111
+ const ok = await p.skillManager.updateSkill(skillId, patch, version);
112
+ if (!ok)
113
+ return null;
114
+ return p.skillManager.getSkill(skillId);
115
+ });
116
+ if (!result)
117
+ return res.status(404).json({ error: 'Skill not found' });
118
+ res.json(result);
119
+ }
120
+ catch (err) {
121
+ if (err instanceof manager_types_1.VersionConflictError) {
122
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
123
+ }
124
+ next(err);
125
+ }
126
+ });
127
+ // Bump usage
128
+ router.post('/:skillId/bump', async (req, res, next) => {
129
+ try {
130
+ const p = getProject(req);
131
+ const skillId = req.params.skillId;
132
+ const result = await p.mutationQueue.enqueue(async () => {
133
+ const ok = p.skillManager.bumpUsage(skillId);
134
+ if (!ok)
135
+ return null;
136
+ return p.skillManager.getSkill(skillId);
137
+ });
138
+ if (!result)
139
+ return res.status(404).json({ error: 'Skill not found' });
140
+ res.json(result);
141
+ }
142
+ catch (err) {
143
+ next(err);
144
+ }
145
+ });
146
+ // Delete skill
147
+ router.delete('/:skillId', async (req, res, next) => {
148
+ try {
149
+ const p = getProject(req);
150
+ const skillId = req.params.skillId;
151
+ const ok = await p.mutationQueue.enqueue(async () => {
152
+ return p.skillManager.deleteSkill(skillId);
153
+ });
154
+ if (!ok)
155
+ return res.status(404).json({ error: 'Skill not found' });
156
+ res.status(204).end();
157
+ }
158
+ catch (err) {
159
+ next(err);
160
+ }
161
+ });
162
+ // Create skill link (skill-to-skill or cross-graph)
163
+ router.post('/links', (0, validation_1.validateBody)(validation_1.createSkillLinkSchema), async (req, res, next) => {
164
+ try {
165
+ const p = getProject(req);
166
+ const { fromId, toId, kind, targetGraph, projectId } = req.body;
167
+ const ok = await p.mutationQueue.enqueue(async () => {
168
+ if (targetGraph) {
169
+ return p.skillManager.createCrossLink(fromId, toId, targetGraph, kind, projectId);
170
+ }
171
+ else {
172
+ return p.skillManager.linkSkills(fromId, toId, kind);
173
+ }
174
+ });
175
+ if (!ok)
176
+ return res.status(400).json({ error: 'Failed to create link' });
177
+ res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
178
+ }
179
+ catch (err) {
180
+ next(err);
181
+ }
182
+ });
183
+ // Delete skill link
184
+ router.delete('/links', (0, validation_1.validateBody)(validation_1.createSkillLinkSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
185
+ try {
186
+ const p = getProject(req);
187
+ const { fromId, toId, targetGraph, projectId } = req.body;
188
+ const ok = await p.mutationQueue.enqueue(async () => {
189
+ if (targetGraph) {
190
+ return p.skillManager.deleteCrossLink(fromId, toId, targetGraph, projectId);
191
+ }
192
+ else {
193
+ return p.skillManager.deleteSkillLink(fromId, toId);
194
+ }
195
+ });
196
+ if (!ok)
197
+ return res.status(404).json({ error: 'Link not found' });
198
+ res.status(204).end();
199
+ }
200
+ catch (err) {
201
+ next(err);
202
+ }
203
+ });
204
+ // List relations for a skill
205
+ router.get('/:skillId/relations', (req, res, next) => {
206
+ try {
207
+ const p = getProject(req);
208
+ const relations = p.skillManager.listRelations(req.params.skillId);
209
+ res.json({ results: relations });
210
+ }
211
+ catch (err) {
212
+ next(err);
213
+ }
214
+ });
215
+ // -- Attachments --
216
+ // Upload attachment
217
+ router.post('/:skillId/attachments', upload.single('file'), async (req, res, next) => {
218
+ try {
219
+ const p = getProject(req);
220
+ const skillId = req.params.skillId;
221
+ const file = req.file;
222
+ if (!file)
223
+ return res.status(400).json({ error: 'No file uploaded' });
224
+ const meta = await p.mutationQueue.enqueue(async () => {
225
+ return p.skillManager.addAttachment(skillId, file.originalname, file.buffer);
226
+ });
227
+ if (!meta)
228
+ return res.status(404).json({ error: 'Skill not found' });
229
+ res.status(201).json(meta);
230
+ }
231
+ catch (err) {
232
+ next(err);
233
+ }
234
+ });
235
+ // List attachments
236
+ router.get('/:skillId/attachments', (req, res, next) => {
237
+ try {
238
+ const p = getProject(req);
239
+ const attachments = p.skillManager.listAttachments(req.params.skillId);
240
+ res.json({ results: attachments });
241
+ }
242
+ catch (err) {
243
+ next(err);
244
+ }
245
+ });
246
+ // Download attachment
247
+ router.get('/:skillId/attachments/:filename', (req, res, next) => {
248
+ try {
249
+ const p = getProject(req);
250
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
251
+ const filePath = p.skillManager.getAttachmentPath(req.params.skillId, filename);
252
+ if (!filePath)
253
+ return res.status(404).json({ error: 'Attachment not found' });
254
+ const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
255
+ res.setHeader('Content-Type', mimeType);
256
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
257
+ res.setHeader('X-Content-Type-Options', 'nosniff');
258
+ const stream = fs_1.default.createReadStream(filePath);
259
+ stream.on('error', (err) => next(err));
260
+ stream.pipe(res);
261
+ }
262
+ catch (err) {
263
+ next(err);
264
+ }
265
+ });
266
+ // Delete attachment
267
+ router.delete('/:skillId/attachments/:filename', async (req, res, next) => {
268
+ try {
269
+ const p = getProject(req);
270
+ const skillId = req.params.skillId;
271
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
272
+ const ok = await p.mutationQueue.enqueue(async () => {
273
+ return p.skillManager.removeAttachment(skillId, filename);
274
+ });
275
+ if (!ok)
276
+ return res.status(404).json({ error: 'Attachment not found' });
277
+ res.status(204).end();
278
+ }
279
+ catch (err) {
280
+ next(err);
281
+ }
282
+ });
283
+ return router;
284
+ }
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createTasksRouter = createTasksRouter;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const mime_1 = __importDefault(require("mime"));
9
+ const express_1 = require("express");
10
+ const multer_1 = __importDefault(require("multer"));
11
+ const validation_1 = require("../../api/rest/validation");
12
+ const manager_types_1 = require("../../graphs/manager-types");
13
+ const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
14
+ function createTasksRouter() {
15
+ const router = (0, express_1.Router)({ mergeParams: true });
16
+ function getProject(req) {
17
+ return req.project;
18
+ }
19
+ // List tasks
20
+ router.get('/', (0, validation_1.validateQuery)(validation_1.taskListSchema), (req, res, next) => {
21
+ try {
22
+ const p = getProject(req);
23
+ const q = req.validatedQuery;
24
+ const tasks = p.taskManager.listTasks(q);
25
+ res.json({ results: tasks });
26
+ }
27
+ catch (err) {
28
+ next(err);
29
+ }
30
+ });
31
+ // Search tasks
32
+ router.get('/search', (0, validation_1.validateQuery)(validation_1.taskSearchSchema), async (req, res, next) => {
33
+ try {
34
+ const p = getProject(req);
35
+ const q = req.validatedQuery;
36
+ const results = await p.taskManager.searchTasks(q.q, {
37
+ topK: q.topK,
38
+ minScore: q.minScore,
39
+ searchMode: q.searchMode,
40
+ });
41
+ res.json({ results });
42
+ }
43
+ catch (err) {
44
+ next(err);
45
+ }
46
+ });
47
+ // Find tasks linked to an external entity
48
+ router.get('/linked', (0, validation_1.validateQuery)(validation_1.linkedQuerySchema), (req, res, next) => {
49
+ try {
50
+ const p = getProject(req);
51
+ const { targetGraph, targetNodeId, kind, projectId } = req.validatedQuery;
52
+ const tasks = p.taskManager.findLinkedTasks(targetGraph, targetNodeId, kind, projectId ?? req.params.projectId);
53
+ res.json({ results: tasks });
54
+ }
55
+ catch (err) {
56
+ next(err);
57
+ }
58
+ });
59
+ // Get task
60
+ router.get('/:taskId', (req, res, next) => {
61
+ try {
62
+ const p = getProject(req);
63
+ const task = p.taskManager.getTask(req.params.taskId);
64
+ if (!task)
65
+ return res.status(404).json({ error: 'Task not found' });
66
+ const relations = p.taskManager.listRelations(req.params.taskId);
67
+ res.json({ ...task, relations });
68
+ }
69
+ catch (err) {
70
+ next(err);
71
+ }
72
+ });
73
+ // Create task
74
+ router.post('/', (0, validation_1.validateBody)(validation_1.createTaskSchema), async (req, res, next) => {
75
+ try {
76
+ const p = getProject(req);
77
+ const { title, description, status, priority, tags, dueDate, estimate } = req.body;
78
+ const created = await p.mutationQueue.enqueue(async () => {
79
+ const taskId = await p.taskManager.createTask(title, description, status, priority, tags, dueDate, estimate);
80
+ return p.taskManager.getTask(taskId);
81
+ });
82
+ res.status(201).json(created);
83
+ }
84
+ catch (err) {
85
+ next(err);
86
+ }
87
+ });
88
+ // Update task
89
+ router.put('/:taskId', (0, validation_1.validateBody)(validation_1.updateTaskSchema), async (req, res, next) => {
90
+ try {
91
+ const p = getProject(req);
92
+ const taskId = req.params.taskId;
93
+ const { version, ...patch } = req.body;
94
+ const result = await p.mutationQueue.enqueue(async () => {
95
+ const ok = await p.taskManager.updateTask(taskId, patch, version);
96
+ if (!ok)
97
+ return null;
98
+ return p.taskManager.getTask(taskId);
99
+ });
100
+ if (!result)
101
+ return res.status(404).json({ error: 'Task not found' });
102
+ res.json(result);
103
+ }
104
+ catch (err) {
105
+ if (err instanceof manager_types_1.VersionConflictError) {
106
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
107
+ }
108
+ next(err);
109
+ }
110
+ });
111
+ // Move task (change status) — action, so POST
112
+ router.post('/:taskId/move', (0, validation_1.validateBody)(validation_1.moveTaskSchema), async (req, res, next) => {
113
+ try {
114
+ const p = getProject(req);
115
+ const taskId = req.params.taskId;
116
+ const { status, version } = req.body;
117
+ const result = await p.mutationQueue.enqueue(async () => {
118
+ const ok = p.taskManager.moveTask(taskId, status, version);
119
+ if (!ok)
120
+ return null;
121
+ return p.taskManager.getTask(taskId);
122
+ });
123
+ if (!result)
124
+ return res.status(404).json({ error: 'Task not found' });
125
+ res.json(result);
126
+ }
127
+ catch (err) {
128
+ if (err instanceof manager_types_1.VersionConflictError) {
129
+ return res.status(409).json({ error: 'version_conflict', current: err.current, expected: err.expected });
130
+ }
131
+ next(err);
132
+ }
133
+ });
134
+ // Delete task
135
+ router.delete('/:taskId', async (req, res, next) => {
136
+ try {
137
+ const p = getProject(req);
138
+ const taskId = req.params.taskId;
139
+ const ok = await p.mutationQueue.enqueue(async () => {
140
+ return p.taskManager.deleteTask(taskId);
141
+ });
142
+ if (!ok)
143
+ return res.status(404).json({ error: 'Task not found' });
144
+ res.status(204).end();
145
+ }
146
+ catch (err) {
147
+ next(err);
148
+ }
149
+ });
150
+ // Create task link (task-to-task or cross-graph)
151
+ router.post('/links', (0, validation_1.validateBody)(validation_1.createTaskLinkSchema), async (req, res, next) => {
152
+ try {
153
+ const p = getProject(req);
154
+ const { fromId, toId, kind, targetGraph, projectId } = req.body;
155
+ const ok = await p.mutationQueue.enqueue(async () => {
156
+ if (targetGraph) {
157
+ return p.taskManager.createCrossLink(fromId, toId, targetGraph, kind, projectId);
158
+ }
159
+ else {
160
+ return p.taskManager.linkTasks(fromId, toId, kind);
161
+ }
162
+ });
163
+ if (!ok)
164
+ return res.status(400).json({ error: 'Failed to create link' });
165
+ res.status(201).json({ fromId, toId, kind, targetGraph: targetGraph || undefined });
166
+ }
167
+ catch (err) {
168
+ next(err);
169
+ }
170
+ });
171
+ // Delete task link
172
+ router.delete('/links', (0, validation_1.validateBody)(validation_1.createTaskLinkSchema.pick({ fromId: true, toId: true, targetGraph: true, projectId: true })), async (req, res, next) => {
173
+ try {
174
+ const p = getProject(req);
175
+ const { fromId, toId, targetGraph, projectId } = req.body;
176
+ const ok = await p.mutationQueue.enqueue(async () => {
177
+ if (targetGraph) {
178
+ return p.taskManager.deleteCrossLink(fromId, toId, targetGraph, projectId);
179
+ }
180
+ else {
181
+ return p.taskManager.deleteTaskLink(fromId, toId);
182
+ }
183
+ });
184
+ if (!ok)
185
+ return res.status(404).json({ error: 'Link not found' });
186
+ res.status(204).end();
187
+ }
188
+ catch (err) {
189
+ next(err);
190
+ }
191
+ });
192
+ // List relations for a task
193
+ router.get('/:taskId/relations', (req, res, next) => {
194
+ try {
195
+ const p = getProject(req);
196
+ const relations = p.taskManager.listRelations(req.params.taskId);
197
+ res.json({ results: relations });
198
+ }
199
+ catch (err) {
200
+ next(err);
201
+ }
202
+ });
203
+ // -- Attachments --
204
+ // Upload attachment
205
+ router.post('/:taskId/attachments', upload.single('file'), async (req, res, next) => {
206
+ try {
207
+ const p = getProject(req);
208
+ const taskId = req.params.taskId;
209
+ const file = req.file;
210
+ if (!file)
211
+ return res.status(400).json({ error: 'No file uploaded' });
212
+ const meta = await p.mutationQueue.enqueue(async () => {
213
+ return p.taskManager.addAttachment(taskId, file.originalname, file.buffer);
214
+ });
215
+ if (!meta)
216
+ return res.status(404).json({ error: 'Task not found' });
217
+ res.status(201).json(meta);
218
+ }
219
+ catch (err) {
220
+ next(err);
221
+ }
222
+ });
223
+ // List attachments
224
+ router.get('/:taskId/attachments', (req, res, next) => {
225
+ try {
226
+ const p = getProject(req);
227
+ const attachments = p.taskManager.listAttachments(req.params.taskId);
228
+ res.json({ results: attachments });
229
+ }
230
+ catch (err) {
231
+ next(err);
232
+ }
233
+ });
234
+ // Download attachment
235
+ router.get('/:taskId/attachments/:filename', (req, res, next) => {
236
+ try {
237
+ const p = getProject(req);
238
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
239
+ const filePath = p.taskManager.getAttachmentPath(req.params.taskId, filename);
240
+ if (!filePath)
241
+ return res.status(404).json({ error: 'Attachment not found' });
242
+ const mimeType = mime_1.default.getType(filePath) ?? 'application/octet-stream';
243
+ res.setHeader('Content-Type', mimeType);
244
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
245
+ res.setHeader('X-Content-Type-Options', 'nosniff');
246
+ const stream = fs_1.default.createReadStream(filePath);
247
+ stream.on('error', (err) => next(err));
248
+ stream.pipe(res);
249
+ }
250
+ catch (err) {
251
+ next(err);
252
+ }
253
+ });
254
+ // Delete attachment
255
+ router.delete('/:taskId/attachments/:filename', async (req, res, next) => {
256
+ try {
257
+ const p = getProject(req);
258
+ const taskId = req.params.taskId;
259
+ const filename = validation_1.attachmentFilenameSchema.parse(req.params.filename);
260
+ const ok = await p.mutationQueue.enqueue(async () => {
261
+ return p.taskManager.removeAttachment(taskId, filename);
262
+ });
263
+ if (!ok)
264
+ return res.status(404).json({ error: 'Attachment not found' });
265
+ res.status(204).end();
266
+ }
267
+ catch (err) {
268
+ next(err);
269
+ }
270
+ });
271
+ return router;
272
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createToolsRouter = createToolsRouter;
4
+ const express_1 = require("express");
5
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
6
+ const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
7
+ const index_1 = require("../../api/index");
8
+ // Tool category detection based on tool name
9
+ const TOOL_CATEGORIES = {
10
+ get_context: 'context',
11
+ list_topics: 'docs', get_toc: 'docs', search: 'docs', get_node: 'docs',
12
+ search_topic_files: 'docs', find_examples: 'docs', search_snippets: 'docs',
13
+ list_snippets: 'docs', explain_symbol: 'docs', cross_references: 'cross-graph',
14
+ list_files: 'code', get_file_symbols: 'code', search_code: 'code',
15
+ get_symbol: 'code', search_files: 'code',
16
+ list_all_files: 'files', search_all_files: 'files', get_file_info: 'files',
17
+ create_note: 'knowledge', update_note: 'knowledge', delete_note: 'knowledge',
18
+ get_note: 'knowledge', list_notes: 'knowledge', search_notes: 'knowledge',
19
+ create_relation: 'knowledge', delete_relation: 'knowledge',
20
+ list_relations: 'knowledge', find_linked_notes: 'knowledge',
21
+ add_note_attachment: 'knowledge', remove_note_attachment: 'knowledge',
22
+ create_task: 'tasks', update_task: 'tasks', delete_task: 'tasks',
23
+ get_task: 'tasks', list_tasks: 'tasks', search_tasks: 'tasks',
24
+ move_task: 'tasks', link_task: 'tasks', create_task_link: 'tasks',
25
+ delete_task_link: 'tasks', find_linked_tasks: 'tasks',
26
+ add_task_attachment: 'tasks', remove_task_attachment: 'tasks',
27
+ create_skill: 'skills', update_skill: 'skills', delete_skill: 'skills',
28
+ get_skill: 'skills', list_skills: 'skills', search_skills: 'skills',
29
+ recall_skills: 'skills', bump_skill_usage: 'skills',
30
+ link_skill: 'skills', create_skill_link: 'skills', delete_skill_link: 'skills',
31
+ find_linked_skills: 'skills',
32
+ add_skill_attachment: 'skills', remove_skill_attachment: 'skills',
33
+ };
34
+ /**
35
+ * Get or create a lazy MCP client for a project instance.
36
+ * The client is cached on the instance for reuse.
37
+ */
38
+ async function getClient(p, pm) {
39
+ if (p.mcpClient)
40
+ return p.mcpClient;
41
+ // Build session context for get_context tool
42
+ const ws = p.workspaceId ? pm.getWorkspace(p.workspaceId) : undefined;
43
+ const sessionCtx = {
44
+ projectId: p.id,
45
+ workspaceId: ws?.id,
46
+ workspaceProjects: ws?.config.projects,
47
+ };
48
+ const [serverTransport, clientTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
49
+ const server = (0, index_1.createMcpServer)(p.docGraph, p.codeGraph, p.knowledgeGraph, p.fileIndexGraph, p.taskGraph, p.embedFns, p.mutationQueue, p.config.projectDir, p.skillGraph, sessionCtx);
50
+ await server.connect(serverTransport);
51
+ const client = new index_js_1.Client({ name: 'tools-explorer', version: '1.0.0' });
52
+ await client.connect(clientTransport);
53
+ p.mcpClient = client;
54
+ p.mcpClientCleanup = async () => {
55
+ await client.close();
56
+ p.mcpClient = undefined;
57
+ p.mcpClientCleanup = undefined;
58
+ };
59
+ return client;
60
+ }
61
+ function createToolsRouter(projectManager) {
62
+ const router = (0, express_1.Router)({ mergeParams: true });
63
+ function getProject(req) {
64
+ return req.project;
65
+ }
66
+ // List all available tools
67
+ router.get('/', async (req, res, next) => {
68
+ try {
69
+ const p = getProject(req);
70
+ const client = await getClient(p, projectManager);
71
+ const { tools } = await client.listTools();
72
+ const results = tools.map(t => ({
73
+ name: t.name,
74
+ description: t.description || '',
75
+ category: TOOL_CATEGORIES[t.name] || 'other',
76
+ inputSchema: t.inputSchema,
77
+ }));
78
+ res.json({ results });
79
+ }
80
+ catch (err) {
81
+ next(err);
82
+ }
83
+ });
84
+ // Get single tool info
85
+ router.get('/:toolName', async (req, res, next) => {
86
+ try {
87
+ const p = getProject(req);
88
+ const client = await getClient(p, projectManager);
89
+ const { tools } = await client.listTools();
90
+ const tool = tools.find(t => t.name === req.params.toolName);
91
+ if (!tool)
92
+ return res.status(404).json({ error: `Tool "${req.params.toolName}" not found` });
93
+ res.json({
94
+ name: tool.name,
95
+ description: tool.description || '',
96
+ category: TOOL_CATEGORIES[tool.name] || 'other',
97
+ inputSchema: tool.inputSchema,
98
+ });
99
+ }
100
+ catch (err) {
101
+ next(err);
102
+ }
103
+ });
104
+ // Call a tool
105
+ router.post('/:toolName/call', async (req, res, next) => {
106
+ try {
107
+ const p = getProject(req);
108
+ const client = await getClient(p, projectManager);
109
+ const start = Date.now();
110
+ const result = await client.callTool({
111
+ name: req.params.toolName,
112
+ arguments: req.body.arguments || {},
113
+ });
114
+ const duration = Date.now() - start;
115
+ res.json({
116
+ result: result.content,
117
+ isError: result.isError || false,
118
+ duration,
119
+ });
120
+ }
121
+ catch (err) {
122
+ next(err);
123
+ }
124
+ });
125
+ return router;
126
+ }