nexus-mcp-agent 0.1.2

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 (72) hide show
  1. package/.env.example +56 -0
  2. package/README.md +74 -0
  3. package/dist/core/config.d.ts +46 -0
  4. package/dist/core/config.d.ts.map +1 -0
  5. package/dist/core/config.js +68 -0
  6. package/dist/core/config.js.map +1 -0
  7. package/dist/core/logger.d.ts +7 -0
  8. package/dist/core/logger.d.ts.map +1 -0
  9. package/dist/core/logger.js +54 -0
  10. package/dist/core/logger.js.map +1 -0
  11. package/dist/core/registry.d.ts +21 -0
  12. package/dist/core/registry.d.ts.map +1 -0
  13. package/dist/core/registry.js +52 -0
  14. package/dist/core/registry.js.map +1 -0
  15. package/dist/core/server.d.ts +12 -0
  16. package/dist/core/server.d.ts.map +1 -0
  17. package/dist/core/server.js +120 -0
  18. package/dist/core/server.js.map +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +38 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/tools/agents/index.d.ts +18 -0
  24. package/dist/tools/agents/index.d.ts.map +1 -0
  25. package/dist/tools/agents/index.js +134 -0
  26. package/dist/tools/agents/index.js.map +1 -0
  27. package/dist/tools/automation/index.d.ts +4 -0
  28. package/dist/tools/automation/index.d.ts.map +1 -0
  29. package/dist/tools/automation/index.js +95 -0
  30. package/dist/tools/automation/index.js.map +1 -0
  31. package/dist/tools/filesystem/index.d.ts +4 -0
  32. package/dist/tools/filesystem/index.d.ts.map +1 -0
  33. package/dist/tools/filesystem/index.js +261 -0
  34. package/dist/tools/filesystem/index.js.map +1 -0
  35. package/dist/tools/git/index.d.ts +4 -0
  36. package/dist/tools/git/index.d.ts.map +1 -0
  37. package/dist/tools/git/index.js +177 -0
  38. package/dist/tools/git/index.js.map +1 -0
  39. package/dist/tools/memory/index.d.ts +4 -0
  40. package/dist/tools/memory/index.d.ts.map +1 -0
  41. package/dist/tools/memory/index.js +178 -0
  42. package/dist/tools/memory/index.js.map +1 -0
  43. package/dist/tools/system/index.d.ts +4 -0
  44. package/dist/tools/system/index.d.ts.map +1 -0
  45. package/dist/tools/system/index.js +121 -0
  46. package/dist/tools/system/index.js.map +1 -0
  47. package/dist/tools/terminal/index.d.ts +4 -0
  48. package/dist/tools/terminal/index.d.ts.map +1 -0
  49. package/dist/tools/terminal/index.js +86 -0
  50. package/dist/tools/terminal/index.js.map +1 -0
  51. package/dist/tools/web/index.d.ts +4 -0
  52. package/dist/tools/web/index.d.ts.map +1 -0
  53. package/dist/tools/web/index.js +193 -0
  54. package/dist/tools/web/index.js.map +1 -0
  55. package/docs/SECURITY.md +31 -0
  56. package/mcp-config.json +9 -0
  57. package/package.json +65 -0
  58. package/scripts/setup.js +102 -0
  59. package/src/core/config.ts +138 -0
  60. package/src/core/logger.ts +61 -0
  61. package/src/core/registry.ts +72 -0
  62. package/src/core/server.ts +144 -0
  63. package/src/index.ts +46 -0
  64. package/src/tools/agents/index.ts +172 -0
  65. package/src/tools/automation/index.ts +127 -0
  66. package/src/tools/filesystem/index.ts +303 -0
  67. package/src/tools/git/index.ts +201 -0
  68. package/src/tools/memory/index.ts +204 -0
  69. package/src/tools/system/index.ts +140 -0
  70. package/src/tools/terminal/index.ts +105 -0
  71. package/src/tools/web/index.ts +224 -0
  72. package/tsconfig.json +30 -0
@@ -0,0 +1,303 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod/v3';
3
+ import { readFile, writeFile, readdir, stat, unlink, mkdir, copyFile, rename } from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { ToolRegistry } from '../../core/registry.js';
7
+ import { createContextLogger, logAudit } from '../../core/logger.js';
8
+ import { nexusConfig } from '../../core/config.js';
9
+
10
+ const log = createContextLogger('tools/filesystem');
11
+
12
+ function isPathAllowed(path: string): boolean {
13
+ const absPath = resolve(path);
14
+ const allowed = nexusConfig.security.allowedDirs.map(d => resolve(d));
15
+ if (allowed.length === 0) return true;
16
+ return allowed.some(dir => absPath.startsWith(dir));
17
+ }
18
+
19
+ function ensureAllowed(path: string): void {
20
+ if (!isPathAllowed(path)) {
21
+ throw new Error(`Access denied: ${path} is outside allowed directories`);
22
+ }
23
+ }
24
+
25
+ const FsReadSchema = z.object({
26
+ path: z.string().describe('Path to read'),
27
+ encoding: z.string().optional().default('utf-8'),
28
+ });
29
+
30
+ const FsWriteSchema = z.object({
31
+ path: z.string().describe('File path'),
32
+ content: z.string().describe('Content to write'),
33
+ mode: z.enum(['overwrite', 'append']).optional().default('overwrite'),
34
+ });
35
+
36
+ const FsListSchema = z.object({
37
+ path: z.string().describe('Directory path'),
38
+ recursive: z.boolean().optional().default(false),
39
+ });
40
+
41
+ const FsDeleteSchema = z.object({
42
+ path: z.string().describe('File path'),
43
+ });
44
+
45
+ const FsMoveSchema = z.object({
46
+ source: z.string(),
47
+ destination: z.string(),
48
+ });
49
+
50
+ const FsCopySchema = z.object({
51
+ source: z.string(),
52
+ destination: z.string(),
53
+ });
54
+
55
+ const FsSearchSchema = z.object({
56
+ pattern: z.string().describe('Regex pattern'),
57
+ path: z.string().optional().default('.'),
58
+ maxResults: z.number().optional().default(50),
59
+ });
60
+
61
+ type FsReadParams = z.infer<typeof FsReadSchema>;
62
+ type FsWriteParams = z.infer<typeof FsWriteSchema>;
63
+ type FsListParams = z.infer<typeof FsListSchema>;
64
+ type FsDeleteParams = z.infer<typeof FsDeleteSchema>;
65
+ type FsMoveParams = z.infer<typeof FsMoveSchema>;
66
+ type FsCopyParams = z.infer<typeof FsCopySchema>;
67
+ type FsSearchParams = z.infer<typeof FsSearchSchema>;
68
+
69
+ async function fsRead(params: FsReadParams): Promise<any> {
70
+ const { path, encoding = 'utf-8' } = params;
71
+ ensureAllowed(path);
72
+ log.info(`Reading: ${path}`);
73
+
74
+ try {
75
+ const stats = await stat(path);
76
+
77
+ if (stats.isDirectory()) {
78
+ const files = await readdir(path);
79
+ return { type: 'directory', path, files };
80
+ } else {
81
+ const content = await readFile(path, encoding as any);
82
+ return { type: 'file', path, size: stats.size, content };
83
+ }
84
+ } catch (error: any) {
85
+ return { error: error.message };
86
+ }
87
+ }
88
+
89
+ async function fsWrite(params: FsWriteParams): Promise<any> {
90
+ const { path, content, mode = 'overwrite' } = params;
91
+ ensureAllowed(path);
92
+ logAudit('fs_write', { path, mode, size: content.length });
93
+
94
+ try {
95
+ const dir = resolve(path, '..');
96
+ if (!existsSync(dir)) {
97
+ await mkdir(dir, { recursive: true });
98
+ }
99
+
100
+ if (mode === 'append') {
101
+ const existing = existsSync(path) ? await readFile(path, 'utf-8') : '';
102
+ await writeFile(path, existing + content, 'utf-8');
103
+ } else {
104
+ await writeFile(path, content, 'utf-8');
105
+ }
106
+
107
+ return { success: true, path, size: content.length };
108
+ } catch (error: any) {
109
+ return { success: false, error: error.message };
110
+ }
111
+ }
112
+
113
+ async function fsList(params: FsListParams): Promise<any> {
114
+ const { path, recursive = false } = params;
115
+ ensureAllowed(path);
116
+
117
+ try {
118
+ const listDir = async (dir: string, depth = 0): Promise<any[]> => {
119
+ const entries = await readdir(dir, { withFileTypes: true });
120
+ const results: any[] = [];
121
+
122
+ for (const entry of entries) {
123
+ const fullPath = resolve(dir, entry.name);
124
+ const relPath = path;
125
+
126
+ if (entry.isDirectory()) {
127
+ results.push({ type: 'directory', name: entry.name, path: relPath });
128
+ if (recursive && depth < 5) {
129
+ const children = await listDir(fullPath, depth + 1);
130
+ results.push(...children);
131
+ }
132
+ } else {
133
+ const stats = await stat(fullPath);
134
+ results.push({ type: 'file', name: entry.name, path: relPath, size: stats.size });
135
+ }
136
+ }
137
+
138
+ return results;
139
+ };
140
+
141
+ const items = await listDir(path);
142
+ return { success: true, path, count: items.length, items };
143
+ } catch (error: any) {
144
+ return { success: false, error: error.message };
145
+ }
146
+ }
147
+
148
+ async function fsDelete(params: FsDeleteParams): Promise<any> {
149
+ const { path } = params;
150
+ ensureAllowed(path);
151
+ logAudit('fs_delete', { path });
152
+
153
+ try {
154
+ if (!existsSync(path)) {
155
+ return { success: false, error: 'File does not exist' };
156
+ }
157
+
158
+ const stats = await stat(path);
159
+ if (stats.isDirectory()) {
160
+ return { success: false, error: 'Cannot delete directories with fs_delete (safety)' };
161
+ }
162
+
163
+ await unlink(path);
164
+ return { success: true, path };
165
+ } catch (error: any) {
166
+ return { success: false, error: error.message };
167
+ }
168
+ }
169
+
170
+ async function fsMove(params: FsMoveParams): Promise<any> {
171
+ const { source, destination } = params;
172
+ ensureAllowed(source);
173
+ ensureAllowed(destination);
174
+ logAudit('fs_move', { source, destination });
175
+
176
+ try {
177
+ await rename(source, destination);
178
+ return { success: true, source, destination };
179
+ } catch (error: any) {
180
+ return { success: false, error: error.message };
181
+ }
182
+ }
183
+
184
+ async function fsCopy(params: FsCopyParams): Promise<any> {
185
+ const { source, destination } = params;
186
+ ensureAllowed(source);
187
+ ensureAllowed(destination);
188
+
189
+ try {
190
+ await copyFile(source, destination);
191
+ return { success: true, source, destination };
192
+ } catch (error: any) {
193
+ return { success: false, error: error.message };
194
+ }
195
+ }
196
+
197
+ async function fsSearch(params: FsSearchParams): Promise<any> {
198
+ const { pattern, path = '.', maxResults = 50 } = params;
199
+ ensureAllowed(path);
200
+ log.info(`Searching files for: "${pattern}"`);
201
+
202
+ try {
203
+ const results: any[] = [];
204
+ const regex = new RegExp(pattern, 'gi');
205
+
206
+ const searchFile = async (filePath: string) => {
207
+ try {
208
+ const content = await readFile(filePath, 'utf-8');
209
+ const lines = content.split('\n');
210
+
211
+ lines.forEach((line, idx) => {
212
+ if (regex.test(line)) {
213
+ results.push({ file: filePath, line: idx + 1, content: line.trim() });
214
+ }
215
+ regex.lastIndex = 0;
216
+ });
217
+ } catch {
218
+ }
219
+ };
220
+
221
+ const searchDir = async (dir: string) => {
222
+ const entries = await readdir(dir, { withFileTypes: true });
223
+ for (const entry of entries) {
224
+ if (results.length >= maxResults) return;
225
+ const fullPath = resolve(dir, entry.name);
226
+
227
+ if (entry.isDirectory()) {
228
+ if (!['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
229
+ await searchDir(fullPath);
230
+ }
231
+ } else {
232
+ await searchFile(fullPath);
233
+ }
234
+ }
235
+ };
236
+
237
+ await searchDir(path);
238
+ return { success: true, pattern, count: results.length, results };
239
+ } catch (error: any) {
240
+ return { success: false, error: error.message };
241
+ }
242
+ }
243
+
244
+ export function registerFilesystemTools(server: McpServer, registry: ToolRegistry): void {
245
+ server.registerTool('fs_read', {
246
+ description: 'Read file or directory',
247
+ inputSchema: FsReadSchema,
248
+ }, async (params) => ({
249
+ content: [{ type: 'text', text: JSON.stringify(await fsRead(params), null, 2) }],
250
+ }));
251
+
252
+ server.registerTool('fs_write', {
253
+ description: 'Write content to file',
254
+ inputSchema: FsWriteSchema,
255
+ }, async (params) => ({
256
+ content: [{ type: 'text', text: JSON.stringify(await fsWrite(params), null, 2) }],
257
+ }));
258
+
259
+ server.registerTool('fs_list', {
260
+ description: 'List directory contents',
261
+ inputSchema: FsListSchema,
262
+ }, async (params) => ({
263
+ content: [{ type: 'text', text: JSON.stringify(await fsList(params), null, 2) }],
264
+ }));
265
+
266
+ server.registerTool('fs_delete', {
267
+ description: 'Delete a file',
268
+ inputSchema: FsDeleteSchema,
269
+ }, async (params) => ({
270
+ content: [{ type: 'text', text: JSON.stringify(await fsDelete(params), null, 2) }],
271
+ }));
272
+
273
+ server.registerTool('fs_move', {
274
+ description: 'Move/rename file',
275
+ inputSchema: FsMoveSchema,
276
+ }, async (params) => ({
277
+ content: [{ type: 'text', text: JSON.stringify(await fsMove(params), null, 2) }],
278
+ }));
279
+
280
+ server.registerTool('fs_copy', {
281
+ description: 'Copy file',
282
+ inputSchema: FsCopySchema,
283
+ }, async (params) => ({
284
+ content: [{ type: 'text', text: JSON.stringify(await fsCopy(params), null, 2) }],
285
+ }));
286
+
287
+ server.registerTool('fs_search', {
288
+ description: 'Search file contents',
289
+ inputSchema: FsSearchSchema,
290
+ }, async (params) => ({
291
+ content: [{ type: 'text', text: JSON.stringify(await fsSearch(params), null, 2) }],
292
+ }));
293
+
294
+ registry.register({ name: 'fs_read', category: 'filesystem', description: 'Read file/dir', handler: fsRead });
295
+ registry.register({ name: 'fs_write', category: 'filesystem', description: 'Write file', handler: fsWrite });
296
+ registry.register({ name: 'fs_list', category: 'filesystem', description: 'List directory', handler: fsList });
297
+ registry.register({ name: 'fs_delete', category: 'filesystem', description: 'Delete file', handler: fsDelete });
298
+ registry.register({ name: 'fs_move', category: 'filesystem', description: 'Move file', handler: fsMove });
299
+ registry.register({ name: 'fs_copy', category: 'filesystem', description: 'Copy file', handler: fsCopy });
300
+ registry.register({ name: 'fs_search', category: 'filesystem', description: 'Search content', handler: fsSearch });
301
+
302
+ log.info('Registered 7 filesystem tools');
303
+ }
@@ -0,0 +1,201 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod/v3';
3
+ import simpleGit, { SimpleGit } from 'simple-git';
4
+ import { ToolRegistry } from '../../core/registry.js';
5
+ import { createContextLogger, logAudit } from '../../core/logger.js';
6
+
7
+ const log = createContextLogger('tools/git');
8
+
9
+ function getGit(cwd?: string): SimpleGit {
10
+ return simpleGit(cwd || process.cwd());
11
+ }
12
+
13
+ const GitCwdSchema = z.object({
14
+ cwd: z.string().optional(),
15
+ });
16
+
17
+ const GitDiffSchema = z.object({
18
+ cwd: z.string().optional(),
19
+ staged: z.boolean().optional().default(false),
20
+ });
21
+
22
+ const GitLogSchema = z.object({
23
+ cwd: z.string().optional(),
24
+ maxCount: z.number().optional().default(20),
25
+ });
26
+
27
+ const GitCommitSchema = z.object({
28
+ message: z.string(),
29
+ cwd: z.string().optional(),
30
+ all: z.boolean().optional().default(false),
31
+ });
32
+
33
+ const GitPushSchema = z.object({
34
+ cwd: z.string().optional(),
35
+ remote: z.string().optional(),
36
+ branch: z.string().optional(),
37
+ });
38
+
39
+ const GitPullSchema = z.object({
40
+ cwd: z.string().optional(),
41
+ remote: z.string().optional(),
42
+ branch: z.string().optional(),
43
+ });
44
+
45
+ const GitBranchSchema = z.object({
46
+ cwd: z.string().optional(),
47
+ name: z.string().optional(),
48
+ checkout: z.boolean().optional().default(false),
49
+ });
50
+
51
+ type GitCwdParams = z.infer<typeof GitCwdSchema>;
52
+ type GitDiffParams = z.infer<typeof GitDiffSchema>;
53
+ type GitLogParams = z.infer<typeof GitLogSchema>;
54
+ type GitCommitParams = z.infer<typeof GitCommitSchema>;
55
+ type GitPushParams = z.infer<typeof GitPushSchema>;
56
+ type GitPullParams = z.infer<typeof GitPullSchema>;
57
+ type GitBranchParams = z.infer<typeof GitBranchSchema>;
58
+
59
+ async function gitStatus(params: GitCwdParams): Promise<any> {
60
+ try {
61
+ const git = getGit(params.cwd);
62
+ const status = await git.status();
63
+ return { success: true, ...status };
64
+ } catch (error: any) {
65
+ return { success: false, error: error.message };
66
+ }
67
+ }
68
+
69
+ async function gitDiff(params: GitDiffParams): Promise<any> {
70
+ try {
71
+ const git = getGit(params.cwd);
72
+ const diff = params.staged ? await git.diff(['--staged']) : await git.diff();
73
+ return { success: true, diff };
74
+ } catch (error: any) {
75
+ return { success: false, error: error.message };
76
+ }
77
+ }
78
+
79
+ async function gitLog(params: GitLogParams): Promise<any> {
80
+ try {
81
+ const git = getGit(params.cwd);
82
+ const log_ = await git.log({ maxCount: params.maxCount || 20 });
83
+ return { success: true, commits: log_.all };
84
+ } catch (error: any) {
85
+ return { success: false, error: error.message };
86
+ }
87
+ }
88
+
89
+ async function gitCommit(params: GitCommitParams): Promise<any> {
90
+ logAudit('git_commit', { message: params.message, cwd: params.cwd });
91
+ try {
92
+ const git = getGit(params.cwd);
93
+ if (params.all) await git.add('.');
94
+ const result = await git.commit(params.message);
95
+ return { success: true, ...result };
96
+ } catch (error: any) {
97
+ return { success: false, error: error.message };
98
+ }
99
+ }
100
+
101
+ async function gitPush(params: GitPushParams): Promise<any> {
102
+ logAudit('git_push', { cwd: params.cwd });
103
+ try {
104
+ const git = getGit(params.cwd);
105
+ const result = await git.push(params.remote || 'origin', params.branch);
106
+ return { success: true, ...result };
107
+ } catch (error: any) {
108
+ return { success: false, error: error.message };
109
+ }
110
+ }
111
+
112
+ async function gitPull(params: GitPullParams): Promise<any> {
113
+ try {
114
+ const git = getGit(params.cwd);
115
+ const result = await git.pull(params.remote || 'origin', params.branch);
116
+ return { success: true, ...result };
117
+ } catch (error: any) {
118
+ return { success: false, error: error.message };
119
+ }
120
+ }
121
+
122
+ async function gitBranch(params: GitBranchParams): Promise<any> {
123
+ try {
124
+ const git = getGit(params.cwd);
125
+ if (params.name) {
126
+ if (params.checkout) {
127
+ await git.checkoutLocalBranch(params.name);
128
+ return { success: true, message: `Created and checked out ${params.name}` };
129
+ } else {
130
+ await git.branch([params.name]);
131
+ return { success: true, message: `Created branch ${params.name}` };
132
+ }
133
+ } else {
134
+ const branches = await git.branch();
135
+ return { success: true, ...branches };
136
+ }
137
+ } catch (error: any) {
138
+ return { success: false, error: error.message };
139
+ }
140
+ }
141
+
142
+ export function registerGitTools(server: McpServer, registry: ToolRegistry): void {
143
+ server.registerTool('git_status', {
144
+ description: 'Get git status',
145
+ inputSchema: GitCwdSchema,
146
+ }, async (params) => ({
147
+ content: [{ type: 'text', text: JSON.stringify(await gitStatus(params), null, 2) }],
148
+ }));
149
+
150
+ server.registerTool('git_diff', {
151
+ description: 'Get git diff',
152
+ inputSchema: GitDiffSchema,
153
+ }, async (params) => ({
154
+ content: [{ type: 'text', text: JSON.stringify(await gitDiff(params), null, 2) }],
155
+ }));
156
+
157
+ server.registerTool('git_log', {
158
+ description: 'Get git log',
159
+ inputSchema: GitLogSchema,
160
+ }, async (params) => ({
161
+ content: [{ type: 'text', text: JSON.stringify(await gitLog(params), null, 2) }],
162
+ }));
163
+
164
+ server.registerTool('git_commit', {
165
+ description: 'Create git commit',
166
+ inputSchema: GitCommitSchema,
167
+ }, async (params) => ({
168
+ content: [{ type: 'text', text: JSON.stringify(await gitCommit(params), null, 2) }],
169
+ }));
170
+
171
+ server.registerTool('git_push', {
172
+ description: 'Push to remote',
173
+ inputSchema: GitPushSchema,
174
+ }, async (params) => ({
175
+ content: [{ type: 'text', text: JSON.stringify(await gitPush(params), null, 2) }],
176
+ }));
177
+
178
+ server.registerTool('git_pull', {
179
+ description: 'Pull from remote',
180
+ inputSchema: GitPullSchema,
181
+ }, async (params) => ({
182
+ content: [{ type: 'text', text: JSON.stringify(await gitPull(params), null, 2) }],
183
+ }));
184
+
185
+ server.registerTool('git_branch', {
186
+ description: 'Create/list branches',
187
+ inputSchema: GitBranchSchema,
188
+ }, async (params) => ({
189
+ content: [{ type: 'text', text: JSON.stringify(await gitBranch(params), null, 2) }],
190
+ }));
191
+
192
+ registry.register({ name: 'git_status', category: 'git', description: 'Git status', handler: gitStatus });
193
+ registry.register({ name: 'git_diff', category: 'git', description: 'Git diff', handler: gitDiff });
194
+ registry.register({ name: 'git_log', category: 'git', description: 'Git log', handler: gitLog });
195
+ registry.register({ name: 'git_commit', category: 'git', description: 'Git commit', handler: gitCommit });
196
+ registry.register({ name: 'git_push', category: 'git', description: 'Git push', handler: gitPush });
197
+ registry.register({ name: 'git_pull', category: 'git', description: 'Git pull', handler: gitPull });
198
+ registry.register({ name: 'git_branch', category: 'git', description: 'Git branch', handler: gitBranch });
199
+
200
+ log.info('Registered 7 git tools');
201
+ }
@@ -0,0 +1,204 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod/v3';
3
+ import { ChromaClient, Collection } from 'chromadb';
4
+ import { ToolRegistry } from '../../core/registry.js';
5
+ import { createContextLogger } from '../../core/logger.js';
6
+ import { nexusConfig } from '../../core/config.js';
7
+ import { v4 as uuid } from 'uuid';
8
+
9
+ const log = createContextLogger('tools/memory');
10
+
11
+ let chromaClient: ChromaClient | null = null;
12
+ let collection: Collection | null = null;
13
+
14
+ async function getCollection(): Promise<Collection> {
15
+ if (collection) return collection;
16
+
17
+ try {
18
+ chromaClient = new ChromaClient({ path: nexusConfig.vector.chromaUrl });
19
+ collection = await chromaClient.getOrCreateCollection({
20
+ name: nexusConfig.vector.collection,
21
+ metadata: { description: 'NEXUS long-term memory' },
22
+ });
23
+ return collection;
24
+ } catch (error: any) {
25
+ log.warn(`ChromaDB not available: ${error.message}. Using in-memory fallback.`);
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ const memoryFallback: Map<string, { content: string; metadata: any; timestamp: number }> = new Map();
31
+
32
+ const MemoryStoreSchema = z.object({
33
+ content: z.string().describe('Content to remember'),
34
+ metadata: z.record(z.any()).optional().describe('Additional metadata'),
35
+ tags: z.array(z.string()).optional().describe('Tags for categorization'),
36
+ });
37
+
38
+ const MemorySearchSchema = z.object({
39
+ query: z.string().describe('Search query'),
40
+ limit: z.number().optional().default(10),
41
+ tags: z.array(z.string()).optional(),
42
+ });
43
+
44
+ const MemoryGetSchema = z.object({
45
+ id: z.string(),
46
+ });
47
+
48
+ const MemoryForgetSchema = z.object({
49
+ id: z.string(),
50
+ });
51
+
52
+ const MemoryListSchema = z.object({
53
+ limit: z.number().optional().default(50),
54
+ });
55
+
56
+ type MemoryStoreParams = z.infer<typeof MemoryStoreSchema>;
57
+ type MemorySearchParams = z.infer<typeof MemorySearchSchema>;
58
+ type MemoryGetParams = z.infer<typeof MemoryGetSchema>;
59
+ type MemoryForgetParams = z.infer<typeof MemoryForgetSchema>;
60
+ type MemoryListParams = z.infer<typeof MemoryListSchema>;
61
+
62
+ async function memoryStore(params: MemoryStoreParams): Promise<any> {
63
+ const { content, metadata = {}, tags = [] } = params;
64
+ const id = uuid();
65
+ const timestamp = Date.now();
66
+
67
+ log.info(`Storing memory: ${id} (${content.length} chars)`);
68
+
69
+ try {
70
+ const col = await getCollection();
71
+ await col.add({
72
+ ids: [id],
73
+ documents: [content],
74
+ metadatas: [{ ...metadata, tags: tags.join(','), timestamp }],
75
+ });
76
+ return { success: true, id, stored: 'chromadb' };
77
+ } catch (error) {
78
+ memoryFallback.set(id, { content, metadata: { ...metadata, tags }, timestamp });
79
+ log.warn(`Stored in memory fallback: ${id}`);
80
+ return { success: true, id, stored: 'memory-fallback', warning: 'ChromaDB unavailable' };
81
+ }
82
+ }
83
+
84
+ async function memorySearch(params: MemorySearchParams): Promise<any> {
85
+ const { query, limit = 10, tags = [] } = params;
86
+
87
+ log.info(`Searching memory: "${query}"`);
88
+
89
+ try {
90
+ const col = await getCollection();
91
+ const results = await col.query({
92
+ queryTexts: [query],
93
+ nResults: limit,
94
+ where: tags.length > 0 ? { tags: { $contains: tags.join(',') } as any } : undefined,
95
+ });
96
+
97
+ const memories = (results.documents[0] || []).map((doc, i) => ({
98
+ id: results.ids[0][i],
99
+ content: doc,
100
+ metadata: results.metadatas?.[0]?.[i],
101
+ distance: results.distances?.[0]?.[i],
102
+ }));
103
+
104
+ return { success: true, count: memories.length, results: memories };
105
+ } catch (error) {
106
+ const results = Array.from(memoryFallback.entries())
107
+ .filter(([_, m]) => m.content.toLowerCase().includes(query.toLowerCase()))
108
+ .slice(0, limit)
109
+ .map(([id, m]) => ({ id, ...m }));
110
+ return { success: true, count: results.length, results, source: 'memory-fallback' };
111
+ }
112
+ }
113
+
114
+ async function memoryGet(params: MemoryGetParams): Promise<any> {
115
+ try {
116
+ const col = await getCollection();
117
+ const result = await col.get({ ids: [params.id] });
118
+ if (result.documents.length === 0) return { success: false, error: 'Not found' };
119
+ return { success: true, id: params.id, content: result.documents[0], metadata: result.metadatas?.[0] };
120
+ } catch (error) {
121
+ const mem = memoryFallback.get(params.id);
122
+ if (!mem) return { success: false, error: 'Not found' };
123
+ return { success: true, id: params.id, ...mem, source: 'memory-fallback' };
124
+ }
125
+ }
126
+
127
+ async function memoryForget(params: MemoryForgetParams): Promise<any> {
128
+ log.info(`Forgetting memory: ${params.id}`);
129
+ try {
130
+ const col = await getCollection();
131
+ await col.delete({ ids: [params.id] });
132
+ return { success: true };
133
+ } catch (error) {
134
+ memoryFallback.delete(params.id);
135
+ return { success: true, source: 'memory-fallback' };
136
+ }
137
+ }
138
+
139
+ async function memoryList(params: MemoryListParams): Promise<any> {
140
+ const limit = params.limit || 50;
141
+ try {
142
+ const col = await getCollection();
143
+ const result = await col.get({ limit });
144
+ return {
145
+ success: true,
146
+ count: result.ids.length,
147
+ memories: result.ids.map((id, i) => ({
148
+ id,
149
+ preview: (result.documents[i] || '').slice(0, 200),
150
+ metadata: result.metadatas?.[i],
151
+ })),
152
+ };
153
+ } catch (error) {
154
+ const memories = Array.from(memoryFallback.entries()).slice(0, limit).map(([id, m]) => ({
155
+ id, preview: m.content.slice(0, 200), metadata: m.metadata,
156
+ }));
157
+ return { success: true, count: memories.length, memories, source: 'memory-fallback' };
158
+ }
159
+ }
160
+
161
+ export function registerMemoryTools(server: McpServer, registry: ToolRegistry): void {
162
+ server.registerTool('memory_store', {
163
+ description: 'Store information in long-term memory',
164
+ inputSchema: MemoryStoreSchema,
165
+ }, async (params) => ({
166
+ content: [{ type: 'text', text: JSON.stringify(await memoryStore(params), null, 2) }],
167
+ }));
168
+
169
+ server.registerTool('memory_search', {
170
+ description: 'Search long-term memory',
171
+ inputSchema: MemorySearchSchema,
172
+ }, async (params) => ({
173
+ content: [{ type: 'text', text: JSON.stringify(await memorySearch(params), null, 2) }],
174
+ }));
175
+
176
+ server.registerTool('memory_get', {
177
+ description: 'Get specific memory by ID',
178
+ inputSchema: MemoryGetSchema,
179
+ }, async (params) => ({
180
+ content: [{ type: 'text', text: JSON.stringify(await memoryGet(params), null, 2) }],
181
+ }));
182
+
183
+ server.registerTool('memory_forget', {
184
+ description: 'Delete a memory',
185
+ inputSchema: MemoryForgetSchema,
186
+ }, async (params) => ({
187
+ content: [{ type: 'text', text: JSON.stringify(await memoryForget(params), null, 2) }],
188
+ }));
189
+
190
+ server.registerTool('memory_list', {
191
+ description: 'List recent memories',
192
+ inputSchema: MemoryListSchema,
193
+ }, async (params) => ({
194
+ content: [{ type: 'text', text: JSON.stringify(await memoryList(params), null, 2) }],
195
+ }));
196
+
197
+ registry.register({ name: 'memory_store', category: 'memory', description: 'Store memory', handler: memoryStore });
198
+ registry.register({ name: 'memory_search', category: 'memory', description: 'Search memory', handler: memorySearch });
199
+ registry.register({ name: 'memory_get', category: 'memory', description: 'Get memory', handler: memoryGet });
200
+ registry.register({ name: 'memory_forget', category: 'memory', description: 'Forget memory', handler: memoryForget });
201
+ registry.register({ name: 'memory_list', category: 'memory', description: 'List memories', handler: memoryList });
202
+
203
+ log.info('Registered 5 memory tools');
204
+ }