@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 +25 -0
- package/build/index.js +48 -3
- package/build/resources/index.js +14 -5
- package/build/storage/index.js +131 -38
- package/build/tools/definitions.js +59 -13
- package/build/tools/handlers.js +72 -16
- package/package.json +2 -2
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.
|
|
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();
|
package/build/resources/index.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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." }
|
package/build/storage/index.js
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
8
|
-
inputSchema: {
|
|
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
|
|
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: "
|
|
106
|
-
inputSchema: {
|
|
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
|
];
|
package/build/tools/handlers.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
"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
|
+
}
|