@datafrog-io/n2n-nexus 0.1.3 → 0.1.5

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,28 @@ 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
+ ## 🏷️ Project ID Conventions (Naming Standard)
49
+
50
+ To ensure clarity and prevent collisions in the flat local namespace, all Project IDs MUST follow the **Prefix Dictionary** format: `[prefix]_[project-name]`.
51
+
52
+ | Prefix | Category | Example |
53
+ | :--- | :--- | :--- |
54
+ | `web_` | Websites, landing pages, domain-based projects | `web_datafrog.io` |
55
+ | `api_` | Backend services, REST/gRPC APIs | `api_user-auth` |
56
+ | `chrome_` | Chrome extensions | `chrome_evisa-helper` |
57
+ | `vscode_` | VSCode extensions | `vscode_super-theme` |
58
+ | `mcp_` | MCP Servers and MCP-related tools | `mcp_github-repo` |
59
+ | `android_` | Native Android projects (Kotlin/Java) | `android_client-app` |
60
+ | `ios_` | Native iOS projects (Swift/ObjC) | `ios_client-app` |
61
+ | `flutter_` | **Mobile Cross-platform Special Case** | `flutter_unified-app` |
62
+ | `desktop_` | General desktop apps (Tauri, Electron, etc.) | `desktop_main-hub` |
63
+ | `lib_` | Shared libraries, SDKs, NPM/Python packages | `lib_crypto-core` |
64
+ | `bot_` | Bots (Discord, Slack, DingTalk, etc.) | `bot_auto-moderator` |
65
+ | `infra_` | Infrastructure as Code, CI/CD, DevOps scripts | `infra_k8s-config` |
66
+ | `doc_` | Pure technical handbooks, strategies, roadmaps | `doc_coding-guide` |
67
+
68
+ ---
69
+
48
70
  ## 🛠️ Toolset
49
71
 
50
72
  ### A. Session & Context
package/build/index.js CHANGED
@@ -1,6 +1,7 @@
1
+ #!/usr/bin/env node
1
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- 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";
4
5
  import { CONFIG } from "./config.js";
5
6
  import { StorageManager } from "./storage/index.js";
6
7
  import { TOOL_DEFINITIONS, handleToolCall } from "./tools/index.js";
@@ -14,7 +15,7 @@ class NexusServer {
14
15
  server;
15
16
  currentProject = null;
16
17
  constructor() {
17
- this.server = new Server({ name: "n2n-nexus", version: "0.1.3" }, { capabilities: { resources: {}, tools: {} } });
18
+ this.server = new Server({ name: "n2n-nexus", version: "0.1.5" }, { capabilities: { resources: {}, tools: {}, prompts: {} } });
18
19
  this.setupHandlers();
19
20
  }
20
21
  /**
@@ -75,7 +76,10 @@ class NexusServer {
75
76
  this.checkModerator(name);
76
77
  const result = await handleToolCall(name, toolArgs, {
77
78
  currentProject: this.currentProject,
78
- setCurrentProject: (id) => { this.currentProject = id; }
79
+ setCurrentProject: (id) => { this.currentProject = id; },
80
+ notifyResourceUpdate: (uri) => {
81
+ this.server.sendResourceUpdated({ uri });
82
+ }
79
83
  });
80
84
  return result;
81
85
  }
@@ -89,6 +93,48 @@ class NexusServer {
89
93
  };
90
94
  }
91
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
+ });
92
138
  }
93
139
  async run() {
94
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." }
@@ -103,11 +103,20 @@ export class StorageManager {
103
103
  return { nodes, edges };
104
104
  }
105
105
  // --- Discussion & Log Management ---
106
- static async addGlobalLog(from, text) {
106
+ static async addGlobalLog(from, text, category) {
107
107
  const logs = await this.loadJsonSafe(this.globalDiscussion, []);
108
- logs.push({ timestamp: new Date().toISOString(), from, text });
108
+ logs.push({
109
+ timestamp: new Date().toISOString(),
110
+ from,
111
+ text,
112
+ category
113
+ });
109
114
  await fs.writeFile(this.globalDiscussion, JSON.stringify(logs, null, 2));
110
115
  }
116
+ static async getRecentLogs(count = 10) {
117
+ const logs = await this.loadJsonSafe(this.globalDiscussion, []);
118
+ return logs.slice(-count);
119
+ }
111
120
  static async getProjectDocs(id) {
112
121
  if (!id)
113
122
  return null;
@@ -249,4 +258,18 @@ export class StorageManager {
249
258
  }
250
259
  return updatedCount;
251
260
  }
261
+ /**
262
+ * Delete a project from the registry and disk.
263
+ */
264
+ static async deleteProject(id) {
265
+ const registry = await this.listRegistry();
266
+ if (registry.projects[id]) {
267
+ delete registry.projects[id];
268
+ await fs.writeFile(this.registryFile, JSON.stringify(registry, null, 2), "utf-8");
269
+ }
270
+ const projectDir = path.join(this.projectsRoot, id);
271
+ if (await this.exists(projectDir)) {
272
+ await fs.rm(projectDir, { recursive: true, force: true });
273
+ }
274
+ }
252
275
  }
@@ -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.3",
3
+ "version": "0.1.5",
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
+ }