@datafrog-io/n2n-nexus 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -45,6 +45,30 @@ Nexus_Storage/
45
45
 
46
46
  **Self-healing**: Core data files (e.g., `registry.json`, `discussion.json`) include automatic detection and repair mechanisms. If files are corrupted or missing, the system automatically rebuilds the initial state to ensure uninterrupted service.
47
47
 
48
+ **Concurrency Safety (v0.1.6+)**: All write operations to shared files (`discussion.json`, `registry.json`) are protected by an `AsyncMutex` lock, preventing race conditions when multiple AI agents communicate simultaneously.
49
+
50
+ ## 🏷️ Project ID Conventions (Naming Standard)
51
+
52
+ To ensure clarity and prevent collisions in the flat local namespace, all Project IDs MUST follow the **Prefix Dictionary** format: `[prefix]_[project-name]`.
53
+
54
+ | Prefix | Category | Example |
55
+ | :--- | :--- | :--- |
56
+ | `web_` | Websites, landing pages, domain-based projects | `web_datafrog.io` |
57
+ | `api_` | Backend services, REST/gRPC APIs | `api_user-auth` |
58
+ | `chrome_` | Chrome extensions | `chrome_evisa-helper` |
59
+ | `vscode_` | VSCode extensions | `vscode_super-theme` |
60
+ | `mcp_` | MCP Servers and MCP-related tools | `mcp_github-repo` |
61
+ | `android_` | Native Android projects (Kotlin/Java) | `android_client-app` |
62
+ | `ios_` | Native iOS projects (Swift/ObjC) | `ios_client-app` |
63
+ | `flutter_` | **Mobile Cross-platform Special Case** | `flutter_unified-app` |
64
+ | `desktop_` | General desktop apps (Tauri, Electron, etc.) | `desktop_main-hub` |
65
+ | `lib_` | Shared libraries, SDKs, NPM/Python packages | `lib_crypto-core` |
66
+ | `bot_` | Bots (Discord, Slack, DingTalk, etc.) | `bot_auto-moderator` |
67
+ | `infra_` | Infrastructure as Code, CI/CD, DevOps scripts | `infra_k8s-config` |
68
+ | `doc_` | Pure technical handbooks, strategies, roadmaps | `doc_coding-guide` |
69
+
70
+ ---
71
+
48
72
  ## 🛠️ Toolset
49
73
 
50
74
  ### A. Session & Context
@@ -54,6 +78,7 @@ Nexus_Storage/
54
78
  ### B. Project Asset Management
55
79
  - `sync_project_assets`: **[Core]** Submit full Project Manifest and Internal Docs.
56
80
  - **Manifest**: Includes ID, Tech Stack, **Relations**, Repo URL, Local Path, API Spec, etc.
81
+ - **Schema v2.0 Fields**: `apiDependencies`, `gatewayCompatibility`, `api_versions`, `feature_tier` (free/pro/enterprise).
57
82
  - `update_project`: Partially update Manifest fields (e.g., endpoints or description only).
58
83
  - `rename_project`: Rename Project ID with automatic cascading updates to all dependency references.
59
84
  - `upload_project_asset`: Upload binary/text files (Base64) to the project vault.
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { CONFIG } from "./config.js";
6
6
  import { StorageManager } from "./storage/index.js";
7
7
  import { TOOL_DEFINITIONS, handleToolCall } from "./tools/index.js";
@@ -15,7 +15,7 @@ class NexusServer {
15
15
  server;
16
16
  currentProject = null;
17
17
  constructor() {
18
- this.server = new Server({ name: "n2n-nexus", version: "0.1.4" }, { capabilities: { resources: {}, tools: {} } });
18
+ this.server = new Server({ name: "n2n-nexus", version: "0.1.5" }, { capabilities: { resources: {}, tools: {}, prompts: {} } });
19
19
  this.setupHandlers();
20
20
  }
21
21
  /**
@@ -76,7 +76,10 @@ class NexusServer {
76
76
  this.checkModerator(name);
77
77
  const result = await handleToolCall(name, toolArgs, {
78
78
  currentProject: this.currentProject,
79
- setCurrentProject: (id) => { this.currentProject = id; }
79
+ setCurrentProject: (id) => { this.currentProject = id; },
80
+ notifyResourceUpdate: (uri) => {
81
+ this.server.sendResourceUpdated({ uri });
82
+ }
80
83
  });
81
84
  return result;
82
85
  }
@@ -90,6 +93,48 @@ class NexusServer {
90
93
  };
91
94
  }
92
95
  });
96
+ // --- Prompt Listing ---
97
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
98
+ prompts: [
99
+ {
100
+ name: "init_project_nexus",
101
+ description: "Step-by-step guide for registering a new project with proper ID naming conventions.",
102
+ arguments: [
103
+ { name: "projectType", description: "Type: web, api, chrome, vscode, mcp, android, ios, flutter, desktop, lib, bot, infra, doc", required: true },
104
+ { name: "technicalName", description: "Domain (e.g., example.com) or repo slug (e.g., my-library)", required: true }
105
+ ]
106
+ }
107
+ ]
108
+ }));
109
+ // --- Prompt Retrieval ---
110
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
111
+ const { name, arguments: args } = request.params;
112
+ if (name === "init_project_nexus") {
113
+ const projectType = args?.projectType || "[TYPE]";
114
+ const technicalName = args?.technicalName || "[NAME]";
115
+ const projectId = `${projectType}_${technicalName}`;
116
+ return {
117
+ description: "Initialize a new Nexus project",
118
+ messages: [
119
+ {
120
+ role: "user",
121
+ content: {
122
+ type: "text",
123
+ text: `I want to register a new project in Nexus.\n\n**Project Type:** ${projectType}\n**Technical Name:** ${technicalName}`
124
+ }
125
+ },
126
+ {
127
+ role: "assistant",
128
+ content: {
129
+ type: "text",
130
+ text: `## Project ID Convention\n\nBased on your input, the correct Project ID is:\n\n\`\`\`\n${projectId}\n\`\`\`\n\n### Prefix Dictionary\n| Prefix | Use Case |\n|--------|----------|\n| web_ | Websites/Domains |\n| api_ | Backend Services |\n| chrome_ | Chrome Extensions |\n| vscode_ | VSCode Extensions |\n| mcp_ | MCP Servers |\n| android_ | Native Android |\n| ios_ | Native iOS |\n| flutter_ | Cross-platform Mobile |\n| desktop_ | Desktop Apps |\n| lib_ | Libraries/SDKs |\n| bot_ | Bots |\n| infra_ | Infrastructure as Code |\n| doc_ | Technical Docs |\n\n### Next Steps\n1. Call \`register_session_context\` with projectId: \`${projectId}\`\n2. Call \`sync_project_assets\` with your manifest and internal docs.`
131
+ }
132
+ }
133
+ ]
134
+ };
135
+ }
136
+ throw new McpError(ErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
137
+ });
93
138
  }
94
139
  async run() {
95
140
  const transport = new StdioServerTransport();
@@ -57,11 +57,20 @@ export async function listResources() {
57
57
  { uri: "mcp://hub/registry", name: "Global Project Registry", description: "Consolidated index of all local projects." },
58
58
  { uri: "mcp://docs/global-strategy", name: "Master Strategy Blueprint", description: "Top-level cross-project coordination." },
59
59
  { uri: "mcp://nexus/session", name: "Current Session Info", description: "Your identity and role in this Nexus instance." },
60
- ...projectIds.map(id => ({
61
- uri: `mcp://hub/projects/${id}/manifest`,
62
- name: `Manifest: ${id}`,
63
- description: `Structured metadata (Tech stack, relations) for ${id}`
64
- }))
60
+ ...projectIds.map(id => {
61
+ const prefix = id.split("_")[0];
62
+ const typeLabel = {
63
+ web: "🌐 Website", api: "⚙️ API", chrome: "🧩 Chrome Ext",
64
+ vscode: "💻 VSCode Ext", mcp: "🔌 MCP Server", android: "📱 Android",
65
+ ios: "🍎 iOS", flutter: "📲 Flutter", desktop: "🖥️ Desktop",
66
+ lib: "📦 Library", bot: "🤖 Bot", infra: "☁️ Infra", doc: "📄 Docs"
67
+ }[prefix] || "📁 Project";
68
+ return {
69
+ uri: `mcp://hub/projects/${id}/manifest`,
70
+ name: `${typeLabel}: ${id}`,
71
+ description: `Structured metadata (Tech stack, relations) for ${id}`
72
+ };
73
+ })
65
74
  ],
66
75
  resourceTemplates: [
67
76
  { uriTemplate: "mcp://hub/projects/{projectId}/internal-docs", name: "Internal Project Docs", description: "Markdown-based detailed implementation plans." }
@@ -1,7 +1,45 @@
1
1
  import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { CONFIG } from "../config.js";
4
+ /**
5
+ * Simple mutex lock for preventing concurrent file writes.
6
+ * Ensures that only one write operation can happen at a time.
7
+ */
8
+ class AsyncMutex {
9
+ locked = false;
10
+ queue = [];
11
+ async acquire() {
12
+ if (!this.locked) {
13
+ this.locked = true;
14
+ return;
15
+ }
16
+ return new Promise((resolve) => {
17
+ this.queue.push(resolve);
18
+ });
19
+ }
20
+ release() {
21
+ if (this.queue.length > 0) {
22
+ const next = this.queue.shift();
23
+ next?.();
24
+ }
25
+ else {
26
+ this.locked = false;
27
+ }
28
+ }
29
+ async withLock(fn) {
30
+ await this.acquire();
31
+ try {
32
+ return await fn();
33
+ }
34
+ finally {
35
+ this.release();
36
+ }
37
+ }
38
+ }
4
39
  export class StorageManager {
40
+ // --- Concurrency Control ---
41
+ static discussionLock = new AsyncMutex();
42
+ static registryLock = new AsyncMutex();
5
43
  // --- Path Definitions ---
6
44
  static get globalDir() { return path.join(CONFIG.rootStorage, "global"); }
7
45
  static get globalBlueprint() { return path.join(this.globalDir, "blueprint.md"); }
@@ -62,6 +100,10 @@ export class StorageManager {
62
100
  return null;
63
101
  return JSON.parse(await fs.readFile(p, "utf-8"));
64
102
  }
103
+ /**
104
+ * Save a project manifest and update the global registry.
105
+ * Uses mutex lock to prevent concurrent registry write conflicts.
106
+ */
65
107
  static async saveProjectManifest(manifest) {
66
108
  const id = manifest.id;
67
109
  if (!id)
@@ -69,14 +111,16 @@ export class StorageManager {
69
111
  const projectDir = path.join(this.projectsRoot, id);
70
112
  await fs.mkdir(projectDir, { recursive: true });
71
113
  await fs.writeFile(path.join(projectDir, "manifest.json"), JSON.stringify(manifest, null, 2));
72
- // Update global registry
73
- const registry = await this.listRegistry();
74
- registry.projects[id] = {
75
- name: manifest.name,
76
- summary: manifest.description,
77
- lastActive: new Date().toISOString()
78
- };
79
- await fs.writeFile(this.registryFile, JSON.stringify(registry, null, 2));
114
+ // Update global registry with lock
115
+ await this.registryLock.withLock(async () => {
116
+ const registry = await this.listRegistry();
117
+ registry.projects[id] = {
118
+ name: manifest.name,
119
+ summary: manifest.description,
120
+ lastActive: new Date().toISOString()
121
+ };
122
+ await fs.writeFile(this.registryFile, JSON.stringify(registry, null, 2));
123
+ });
80
124
  }
81
125
  static async saveAsset(id, fileName, content) {
82
126
  if (!id || !fileName)
@@ -103,10 +147,25 @@ export class StorageManager {
103
147
  return { nodes, edges };
104
148
  }
105
149
  // --- Discussion & Log Management ---
106
- static async addGlobalLog(from, text) {
150
+ /**
151
+ * Add a message to the global discussion log.
152
+ * Uses mutex lock to prevent concurrent write conflicts.
153
+ */
154
+ static async addGlobalLog(from, text, category) {
155
+ await this.discussionLock.withLock(async () => {
156
+ const logs = await this.loadJsonSafe(this.globalDiscussion, []);
157
+ logs.push({
158
+ timestamp: new Date().toISOString(),
159
+ from,
160
+ text,
161
+ category
162
+ });
163
+ await fs.writeFile(this.globalDiscussion, JSON.stringify(logs, null, 2));
164
+ });
165
+ }
166
+ static async getRecentLogs(count = 10) {
107
167
  const logs = await this.loadJsonSafe(this.globalDiscussion, []);
108
- logs.push({ timestamp: new Date().toISOString(), from, text });
109
- await fs.writeFile(this.globalDiscussion, JSON.stringify(logs, null, 2));
168
+ return logs.slice(-count);
110
169
  }
111
170
  static async getProjectDocs(id) {
112
171
  if (!id)
@@ -124,12 +183,24 @@ export class StorageManager {
124
183
  static async listRegistry() {
125
184
  return this.loadJsonSafe(this.registryFile, { projects: {} });
126
185
  }
186
+ /**
187
+ * Prune global logs, keeping only messages after the specified count.
188
+ * Uses mutex lock to prevent concurrent write conflicts.
189
+ */
127
190
  static async pruneGlobalLogs(count) {
128
- const logs = await this.loadJsonSafe(this.globalDiscussion, []);
129
- await fs.writeFile(this.globalDiscussion, JSON.stringify(logs.slice(count), null, 2));
191
+ await this.discussionLock.withLock(async () => {
192
+ const logs = await this.loadJsonSafe(this.globalDiscussion, []);
193
+ await fs.writeFile(this.globalDiscussion, JSON.stringify(logs.slice(count), null, 2));
194
+ });
130
195
  }
196
+ /**
197
+ * Clear all global logs.
198
+ * Uses mutex lock to prevent concurrent write conflicts.
199
+ */
131
200
  static async clearGlobalLogs() {
132
- await fs.writeFile(this.globalDiscussion, "[]");
201
+ await this.discussionLock.withLock(async () => {
202
+ await fs.writeFile(this.globalDiscussion, "[]");
203
+ });
133
204
  }
134
205
  // --- Global Document Management ---
135
206
  static get globalDocsDir() { return path.join(this.globalDir, "docs"); }
@@ -219,34 +290,56 @@ export class StorageManager {
219
290
  manifest.id = newId;
220
291
  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
221
292
  }
222
- // 3. Update registry
223
- const registry = await this.listRegistry();
224
- if (registry.projects[oldId]) {
225
- registry.projects[newId] = registry.projects[oldId];
226
- delete registry.projects[oldId];
227
- await fs.writeFile(this.registryFile, JSON.stringify(registry, null, 2), "utf-8");
228
- }
229
- // 4. Cascade: Update relations in ALL other projects
293
+ // 3. Update registry with lock and cascade updates
230
294
  let updatedCount = 0;
231
- const projectIds = Object.keys(registry.projects);
232
- for (const id of projectIds) {
233
- if (id === newId)
234
- continue; // Skip the renamed project itself
235
- const otherManifest = await this.getProjectManifest(id);
236
- if (!otherManifest || !otherManifest.relations)
237
- continue;
238
- let changed = false;
239
- for (const rel of otherManifest.relations) {
240
- if (rel.targetId === oldId) {
241
- rel.targetId = newId;
242
- changed = true;
295
+ await this.registryLock.withLock(async () => {
296
+ const registry = await this.listRegistry();
297
+ if (registry.projects[oldId]) {
298
+ registry.projects[newId] = registry.projects[oldId];
299
+ delete registry.projects[oldId];
300
+ await fs.writeFile(this.registryFile, JSON.stringify(registry, null, 2), "utf-8");
301
+ }
302
+ // 4. Cascade: Update relations in ALL other projects
303
+ const projectIds = Object.keys(registry.projects);
304
+ for (const id of projectIds) {
305
+ if (id === newId)
306
+ continue; // Skip the renamed project itself
307
+ const otherManifest = await this.getProjectManifest(id);
308
+ if (!otherManifest || !otherManifest.relations)
309
+ continue;
310
+ let changed = false;
311
+ for (const rel of otherManifest.relations) {
312
+ if (rel.targetId === oldId) {
313
+ rel.targetId = newId;
314
+ changed = true;
315
+ }
316
+ }
317
+ if (changed) {
318
+ // Note: saveProjectManifest will try to acquire registryLock again,
319
+ // but since we're already holding it, we write directly here
320
+ const projectDir = path.join(this.projectsRoot, id);
321
+ await fs.writeFile(path.join(projectDir, "manifest.json"), JSON.stringify(otherManifest, null, 2));
322
+ updatedCount++;
243
323
  }
244
324
  }
245
- if (changed) {
246
- await this.saveProjectManifest(otherManifest);
247
- updatedCount++;
325
+ });
326
+ return updatedCount;
327
+ }
328
+ /**
329
+ * Delete a project from the registry and disk.
330
+ * Uses mutex lock to prevent concurrent registry write conflicts.
331
+ */
332
+ static async deleteProject(id) {
333
+ await this.registryLock.withLock(async () => {
334
+ const registry = await this.listRegistry();
335
+ if (registry.projects[id]) {
336
+ delete registry.projects[id];
337
+ await fs.writeFile(this.registryFile, JSON.stringify(registry, null, 2), "utf-8");
248
338
  }
339
+ });
340
+ const projectDir = path.join(this.projectsRoot, id);
341
+ if (await this.exists(projectDir)) {
342
+ await fs.rm(projectDir, { recursive: true, force: true });
249
343
  }
250
- return updatedCount;
251
344
  }
252
345
  }
@@ -4,12 +4,21 @@
4
4
  export const TOOL_DEFINITIONS = [
5
5
  {
6
6
  name: "register_session_context",
7
- description: "Declare the project you are currently working on in this IDE session.",
8
- inputSchema: { type: "object", properties: { projectId: { type: "string" } }, required: ["projectId"] }
7
+ description: "[IDENTITY] Declare the PROJECT identity. Format: [prefix]_[technical-identifier]. (e.g., 'web_datafrog.io', 'mcp_nexus-core').",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ projectId: {
12
+ type: "string",
13
+ description: "Strict flat identifier. MUST start with a type-prefix (web_, api_, chrome_, vscode_, mcp_, android_, ios_, flutter_, desktop_, lib_, bot_, infra_, doc_) followed by an underscore and a technical name (Domain for websites, Repo name/Slug for code). Use kebab-case. No hierarchy dots except in domains."
14
+ }
15
+ },
16
+ required: ["projectId"]
17
+ }
9
18
  },
10
19
  {
11
20
  name: "sync_project_assets",
12
- description: "CRITICAL: Sync full project state. Both manifest and documentation are MANDATORY.",
21
+ description: "CRITICAL: [PREREQUISITE: register_session_context] Sync full project state. Both manifest and documentation are MANDATORY.",
13
22
  inputSchema: {
14
23
  type: "object",
15
24
  properties: {
@@ -17,7 +26,7 @@ export const TOOL_DEFINITIONS = [
17
26
  type: "object",
18
27
  description: "Full ProjectManifest metadata.",
19
28
  properties: {
20
- id: { type: "string" },
29
+ id: { type: "string", description: "Project ID. MUST follow '[prefix]_[technical-name]' format and match active session." },
21
30
  name: { type: "string" },
22
31
  description: { type: "string" },
23
32
  techStack: { type: "array", items: { type: "string" } },
@@ -26,13 +35,13 @@ export const TOOL_DEFINITIONS = [
26
35
  items: {
27
36
  type: "object",
28
37
  properties: {
29
- targetId: { type: "string" },
38
+ targetId: { type: "string", description: "ID of the target project (e.g., 'acme.auth-service')." },
30
39
  type: { type: "string", enum: ["dependency", "parent", "child", "related"] }
31
40
  },
32
41
  required: ["targetId", "type"]
33
42
  }
34
43
  },
35
- lastUpdated: { type: "string", description: "ISO timestamp." },
44
+ lastUpdated: { type: "string", description: "ISO timestamp (e.g., 2025-12-29T...)." },
36
45
  repositoryUrl: { type: "string", description: "GitHub repository URL." },
37
46
  endpoints: {
38
47
  type: "array",
@@ -81,7 +90,12 @@ export const TOOL_DEFINITIONS = [
81
90
  },
82
91
  {
83
92
  name: "get_global_topology",
84
- description: "Retrieve complete project relationship graph.",
93
+ description: "Retrieve complete project relationship graph. Use this to understand current IDs and their connections.",
94
+ inputSchema: { type: "object", properties: {} }
95
+ },
96
+ {
97
+ name: "list_projects",
98
+ description: "List all existing projects registered in the Nexus Hub. Use this to find correct IDs before performing project-specific operations.",
85
99
  inputSchema: { type: "object", properties: {} }
86
100
  },
87
101
  {
@@ -90,7 +104,7 @@ export const TOOL_DEFINITIONS = [
90
104
  inputSchema: {
91
105
  type: "object",
92
106
  properties: {
93
- projectId: { type: "string", description: "Project identifier (e.g., 'n2ns.com.backend')" },
107
+ projectId: { type: "string", description: "Project ID (e.g., 'web_datafrog.io', 'mcp_nexus-hub')." },
94
108
  include: {
95
109
  type: "string",
96
110
  enum: ["manifest", "docs", "repo", "endpoints", "api", "relations", "summary", "all"],
@@ -102,8 +116,29 @@ export const TOOL_DEFINITIONS = [
102
116
  },
103
117
  {
104
118
  name: "post_global_discussion",
105
- description: "Broadcast a message. Content is MANDATORY.",
106
- inputSchema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] }
119
+ description: "Join the 'Nexus Meeting Room' to collaborate with other AI agents. Use this for initiating meetings, making cross-project proposals, or announcing key decisions. Every message is shared across all assistants in real-time.",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ message: { type: "string", description: "The core content of your speech, proposal, or announcement." },
124
+ category: {
125
+ type: "string",
126
+ enum: ["MEETING_START", "PROPOSAL", "DECISION", "UPDATE", "CHAT"],
127
+ description: "The nature of this message. Use MEETING_START to call for a synchronous discussion."
128
+ }
129
+ },
130
+ required: ["message"]
131
+ }
132
+ },
133
+ {
134
+ name: "read_recent_discussion",
135
+ description: "Quickly 'listen' to the last few messages in the Nexus Room to catch up on the context of the current meeting or collaboration.",
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ count: { type: "number", description: "Number of recent messages to retrieve (defaults to 10).", default: 10 }
140
+ }
141
+ }
107
142
  },
108
143
  {
109
144
  name: "update_global_strategy",
@@ -145,7 +180,7 @@ export const TOOL_DEFINITIONS = [
145
180
  inputSchema: {
146
181
  type: "object",
147
182
  properties: {
148
- projectId: { type: "string", description: "Project ID to update" },
183
+ projectId: { type: "string", description: "Project ID to update (e.g., 'web_datafrog.io')." },
149
184
  patch: {
150
185
  type: "object",
151
186
  description: "Fields to update (e.g., description, techStack, endpoints, apiSpec, relations)",
@@ -161,8 +196,8 @@ export const TOOL_DEFINITIONS = [
161
196
  inputSchema: {
162
197
  type: "object",
163
198
  properties: {
164
- oldId: { type: "string", description: "Current project ID" },
165
- newId: { type: "string", description: "New project ID" }
199
+ oldId: { type: "string", description: "Current project ID (e.g., 'web_oldname.com')." },
200
+ newId: { type: "string", description: "New project ID following the '[prefix]_[name]' standard." }
166
201
  },
167
202
  required: ["oldId", "newId"]
168
203
  }
@@ -178,5 +213,16 @@ export const TOOL_DEFINITIONS = [
178
213
  },
179
214
  required: ["action", "count"]
180
215
  }
216
+ },
217
+ {
218
+ name: "delete_project",
219
+ description: "[ADMIN ONLY] Completely remove a project, its manifest, and all its assets from Nexus.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ projectId: { type: "string", description: "The ID of the project to destroy." }
224
+ },
225
+ required: ["projectId"]
226
+ }
181
227
  }
182
228
  ];
@@ -2,6 +2,18 @@ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { promises as fs } from "fs";
3
3
  import { CONFIG } from "../config.js";
4
4
  import { StorageManager } from "../storage/index.js";
5
+ /**
6
+ * Validation helper for Project IDs.
7
+ */
8
+ function validateProjectId(id) {
9
+ if (!id)
10
+ throw new McpError(ErrorCode.InvalidParams, "Project ID cannot be empty.");
11
+ const validPrefixes = ["web_", "api_", "chrome_", "vscode_", "mcp_", "android_", "ios_", "flutter_", "desktop_", "lib_", "bot_", "infra_", "doc_"];
12
+ const hasPrefix = validPrefixes.some(p => id.startsWith(p));
13
+ if (!hasPrefix || id.includes("..") || id.startsWith("/") || id.endsWith("/")) {
14
+ throw new McpError(ErrorCode.InvalidParams, "Project ID must follow the standard '[prefix]_[technical-name]' format and cannot contain '..' or slashes.");
15
+ }
16
+ }
5
17
  /**
6
18
  * Handles all tool executions
7
19
  */
@@ -20,8 +32,10 @@ export async function handleToolCall(name, toolArgs, ctx) {
20
32
  return handleReadProject(toolArgs);
21
33
  case "post_global_discussion":
22
34
  return handlePostDiscussion(toolArgs, ctx);
35
+ case "read_recent_discussion":
36
+ return handleReadRecentDiscussion(toolArgs);
23
37
  case "update_global_strategy":
24
- return handleUpdateStrategy(toolArgs);
38
+ return handleUpdateStrategy(toolArgs, ctx);
25
39
  case "sync_global_doc":
26
40
  return handleSyncGlobalDoc(toolArgs);
27
41
  case "list_global_docs":
@@ -31,9 +45,13 @@ export async function handleToolCall(name, toolArgs, ctx) {
31
45
  case "update_project":
32
46
  return handleUpdateProject(toolArgs);
33
47
  case "rename_project":
34
- return handleRenameProject(toolArgs);
48
+ return handleRenameProject(toolArgs, ctx);
49
+ case "list_projects":
50
+ return handleListProjects();
51
+ case "delete_project":
52
+ return handleRemoveProject(toolArgs, ctx);
35
53
  case "moderator_maintenance":
36
- return handleModeratorMaintenance(toolArgs);
54
+ return handleModeratorMaintenance(toolArgs, ctx);
37
55
  default:
38
56
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
39
57
  }
@@ -42,6 +60,7 @@ export async function handleToolCall(name, toolArgs, ctx) {
42
60
  function handleRegisterSession(args, ctx) {
43
61
  if (!args?.projectId)
44
62
  throw new McpError(ErrorCode.InvalidParams, "Missing required parameter: projectId");
63
+ validateProjectId(args.projectId);
45
64
  ctx.setCurrentProject(args.projectId);
46
65
  return { content: [{ type: "text", text: `Active Nexus Context: ${args.projectId}` }] };
47
66
  }
@@ -53,18 +72,24 @@ async function handleSyncProjectAssets(args, ctx) {
53
72
  throw new McpError(ErrorCode.InvalidParams, "Both 'manifest' and 'internalDocs' are mandatory.");
54
73
  }
55
74
  const m = args.manifest;
56
- if (!m.id || !m.name || !m.description || !m.techStack || !m.relations || !m.lastUpdated || !m.repositoryUrl || !m.localPath || !m.endpoints || !m.apiSpec) {
57
- throw new McpError(ErrorCode.InvalidParams, "Project manifest incomplete. Required: id, name, description, techStack, relations, lastUpdated, repositoryUrl, localPath, endpoints, apiSpec.");
58
- }
59
- if (m.id.includes("..") || m.id.startsWith("/") || m.id.endsWith("/")) {
60
- throw new McpError(ErrorCode.InvalidParams, "Project ID cannot contain '..' or start/end with '/'. Use '/' for namespacing (e.g., 'parent/child').");
75
+ const requiredFields = ["id", "name", "description", "techStack", "relations", "lastUpdated", "repositoryUrl", "localPath", "endpoints", "apiSpec"];
76
+ for (const field of requiredFields) {
77
+ if (m[field] === undefined || m[field] === null) {
78
+ throw new McpError(ErrorCode.InvalidParams, `Project manifest incomplete. Missing field: ${field}`);
79
+ }
61
80
  }
81
+ validateProjectId(m.id);
62
82
  if (!await StorageManager.exists(m.localPath)) {
63
83
  throw new McpError(ErrorCode.InvalidParams, `localPath does not exist: '${m.localPath}'. Please provide a valid directory path.`);
64
84
  }
65
85
  await StorageManager.saveProjectManifest(m);
66
86
  await StorageManager.saveProjectDocs(ctx.currentProject, args.internalDocs);
67
87
  await StorageManager.addGlobalLog("SYSTEM", `[${CONFIG.instanceId}@${ctx.currentProject}] Asset Sync: Full sync of manifest and docs.`);
88
+ // Notify updates
89
+ ctx.notifyResourceUpdate(`mcp://hub/projects/${m.id}/manifest`);
90
+ ctx.notifyResourceUpdate(`mcp://hub/projects/${m.id}/internal-docs`);
91
+ ctx.notifyResourceUpdate("mcp://hub/registry");
92
+ ctx.notifyResourceUpdate("mcp://chat/global");
68
93
  return { content: [{ type: "text", text: "Project assets synchronized (Manifest + Docs)." }] };
69
94
  }
70
95
  async function handleUploadAsset(args, ctx) {
@@ -140,14 +165,16 @@ async function handleUpdateProject(args) {
140
165
  const changedFields = Object.keys(args.patch).join(", ");
141
166
  return { content: [{ type: "text", text: `Project '${args.projectId}' updated. Changed fields: ${changedFields}.` }] };
142
167
  }
143
- async function handleRenameProject(args) {
168
+ async function handleRenameProject(args, ctx) {
144
169
  if (!args?.oldId || !args?.newId) {
145
170
  throw new McpError(ErrorCode.InvalidParams, "Both 'oldId' and 'newId' are required.");
146
171
  }
147
- if (args.newId.includes("..") || args.newId.startsWith("/") || args.newId.endsWith("/")) {
148
- throw new McpError(ErrorCode.InvalidParams, "New ID cannot contain '..' or start/end with '/'.");
149
- }
172
+ validateProjectId(args.newId);
150
173
  const updatedCount = await StorageManager.renameProject(args.oldId, args.newId);
174
+ // Notify all affected project resources and registry
175
+ ctx.notifyResourceUpdate("mcp://hub/registry");
176
+ ctx.notifyResourceUpdate(`mcp://hub/projects/${args.newId}/manifest`);
177
+ ctx.notifyResourceUpdate("mcp://get_global_topology"); // Topology changed
151
178
  return { content: [{ type: "text", text: `Project renamed: '${args.oldId}' → '${args.newId}'. Cascading updates: ${updatedCount} project(s).` }] };
152
179
  }
153
180
  // --- Global Handlers ---
@@ -158,16 +185,32 @@ async function handleGetTopology() {
158
185
  async function handlePostDiscussion(args, ctx) {
159
186
  if (!args?.message)
160
187
  throw new McpError(ErrorCode.InvalidParams, "Message content cannot be empty.");
161
- await StorageManager.addGlobalLog(`${CONFIG.instanceId}@${ctx.currentProject || "Global"}`, args.message);
162
- return { content: [{ type: "text", text: "Message broadcasted." }] };
188
+ await StorageManager.addGlobalLog(`${CONFIG.instanceId}@${ctx.currentProject || "Global"}`, args.message, args.category);
189
+ // Notify chat resource update
190
+ ctx.notifyResourceUpdate("mcp://chat/global");
191
+ return { content: [{ type: "text", text: `Message broadcasted to Nexus Room${args.category ? ` [${args.category}]` : ""}.` }] };
192
+ }
193
+ async function handleReadRecentDiscussion(args) {
194
+ const count = args?.count || 10;
195
+ const logs = await StorageManager.getRecentLogs(count);
196
+ return { content: [{ type: "text", text: JSON.stringify(logs, null, 2) }] };
163
197
  }
164
- async function handleUpdateStrategy(args) {
198
+ async function handleUpdateStrategy(args, ctx) {
165
199
  if (!args?.content)
166
200
  throw new McpError(ErrorCode.InvalidParams, "Strategy content cannot be empty.");
167
201
  await fs.writeFile(StorageManager.globalBlueprint, args.content);
168
202
  await StorageManager.addGlobalLog("SYSTEM", `[${CONFIG.instanceId}] Updated Coordination Strategy.`);
203
+ // Notify strategy update
169
204
  return { content: [{ type: "text", text: "Strategy updated." }] };
170
205
  }
206
+ async function handleRemoveProject(args, ctx) {
207
+ if (!args?.projectId)
208
+ throw new McpError(ErrorCode.InvalidParams, "projectId is required.");
209
+ await StorageManager.deleteProject(args.projectId);
210
+ ctx.notifyResourceUpdate("mcp://hub/registry");
211
+ ctx.notifyResourceUpdate("mcp://get_global_topology");
212
+ return { content: [{ type: "text", text: `Project '${args.projectId}' removed from Nexus.` }] };
213
+ }
171
214
  async function handleSyncGlobalDoc(args) {
172
215
  if (!args?.docId || !args?.title || !args?.content) {
173
216
  throw new McpError(ErrorCode.InvalidParams, "All fields required: docId, title, content.");
@@ -189,17 +232,30 @@ async function handleReadGlobalDoc(args) {
189
232
  return { content: [{ type: "text", text: content }] };
190
233
  }
191
234
  // --- Admin Handlers ---
192
- async function handleModeratorMaintenance(args) {
235
+ async function handleListProjects() {
236
+ const registry = await StorageManager.listRegistry();
237
+ const projects = Object.entries(registry.projects).map(([id, p]) => ({
238
+ id,
239
+ name: p.name,
240
+ summary: p.summary,
241
+ lastActive: p.lastActive
242
+ }));
243
+ return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
244
+ }
245
+ // --- Admin Handlers ---
246
+ async function handleModeratorMaintenance(args, ctx) {
193
247
  if (!args.action || args.count === undefined) {
194
248
  throw new McpError(ErrorCode.InvalidParams, "Both 'action' and 'count' are mandatory for maintenance.");
195
249
  }
196
250
  if (args.action === "clear") {
197
251
  await StorageManager.clearGlobalLogs();
252
+ ctx.notifyResourceUpdate("mcp://chat/global");
198
253
  return { content: [{ type: "text", text: "History wiped." }] };
199
254
  }
200
255
  else {
201
256
  try {
202
257
  await StorageManager.pruneGlobalLogs(args.count);
258
+ ctx.notifyResourceUpdate("mcp://chat/global");
203
259
  return { content: [{ type: "text", text: `Pruned ${args.count} logs.` }] };
204
260
  }
205
261
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datafrog-io/n2n-nexus",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Unified Project Asset & Collaboration Hub (MCP Server) designed for AI agent coordination, featuring structured metadata, real-time messaging, and dependency topology.",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -53,4 +53,4 @@
53
53
  "typescript-eslint": "^8.18.0",
54
54
  "vitest": "^2.1.8"
55
55
  }
56
- }
56
+ }