n2n-nexus 0.4.2 → 0.6.0
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/CHANGELOG.md +87 -0
- package/README.md +74 -117
- package/assets/n2n-nexus-banner.jpg +0 -0
- package/assets/n2n-nexus-icon.png +0 -0
- package/assets/n2n-nexus-logo.png +0 -0
- package/build/client/nexus-client.js +30 -14
- package/build/config/cli.js +0 -3
- package/build/config/index.js +0 -10
- package/build/constants.js +7 -11
- package/build/daemon/index.js +35 -19
- package/build/daemon/rate-limit.js +62 -0
- package/build/daemon/server.js +349 -542
- package/build/daemon/tool-outputs.js +155 -0
- package/build/daemon/tools.js +271 -0
- package/build/index.js +1 -1
- package/build/server/instance-id.js +19 -0
- package/build/server/nexus.js +152 -43
- package/build/storage/docs.js +14 -7
- package/build/storage/events.js +31 -0
- package/build/storage/fs-utils.js +42 -0
- package/build/storage/global-messages.js +44 -0
- package/build/storage/index.js +44 -41
- package/build/storage/migrations.js +79 -0
- package/build/storage/paths.js +0 -3
- package/build/storage/projects.js +4 -5
- package/build/storage/registry.js +67 -25
- package/build/storage/sqlite-meeting.js +35 -23
- package/build/storage/sqlite.js +41 -24
- package/build/storage/tasks.js +29 -103
- package/build/types.js +1 -1
- package/docs/ARCHITECTURE.md +29 -20
- package/docs/ARCHITECTURE_zh.md +29 -20
- package/docs/ASSISTANT_GUIDE.md +70 -67
- package/docs/README_zh.md +25 -15
- package/llms.txt +11 -1
- package/package.json +24 -14
- package/build/storage/logs.js +0 -60
- package/build/storage/meetings.js +0 -276
- package/build/storage/store.js +0 -153
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-10-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Rate limiting, which the MCP tools specification requires of servers: a token bucket per instance ID (default 20 calls/s, burst 60; `NEXUS_RATE_LIMIT`, `NEXUS_RATE_BURST`, `0` disables) plus one per client address at five times that, since instance IDs are self-declared. Requests without an instance ID (most REST reads) are limited per address only. Over the limit the daemon answers `429` with `Retry-After`; the MCP adapter reports it to the model as a tool error and keeps the connection. `/health` and `GET /api/tools` are exempt.
|
|
10
|
+
|
|
11
|
+
- Structured tool results: every successful call returns `structuredContent` alongside the JSON text, and the eleven data-returning tools publish an `outputSchema`. Result shapes are zod contracts in `src/daemon/tool-outputs.ts` that type the handlers; a test calls all tools through the adapter for both protocol eras and checks each result against its contract. Publishing schemas for write tools as well would have grown `tools/list` from about 14 KB to 27 KB for a one-line acknowledgement, so they are left out (22 KB with the read schemas).
|
|
12
|
+
|
|
13
|
+
- A display `title` on every tool (for example "Delete Project"), which MCP clients show instead of the raw tool name.
|
|
14
|
+
|
|
15
|
+
- MCP tool annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) on every tool, so clients can tell read-only tools from ones that overwrite or delete data and ask the user before the latter.
|
|
16
|
+
|
|
17
|
+
- `list_meetings` tool and `GET /api/meetings`: list meetings with status, initiator, participants, message and decision counts and summary. Assistants could only reach a meeting whose id they already knew.
|
|
18
|
+
|
|
19
|
+
- `get_project` tool and `GET /api/projects/:projectId`: read a project's full manifest and internal docs. Assistants could sync them but had no tool to read them back.
|
|
20
|
+
|
|
21
|
+
- `get_global_strategy`, `list_global_docs` and `get_global_doc` tools. Assistants could write the strategy and shared docs but had no tool to read them back; only the REST API could.
|
|
22
|
+
|
|
23
|
+
- `list_events` tool and `GET /api/events`: a structured activity log (actor, action, target, detail) of every state change made through the daemon.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- `register_session_context` and `POST /api/session/register`. The daemon kept an in-memory "current project" per instance that only `upload_project_asset` used; MCP 2026-07-28 makes protocol sessions go away and recommends passing cross-call state as explicit tool arguments. `upload_project_asset` now takes `projectId` and checks that the project exists.
|
|
28
|
+
|
|
29
|
+
- Leftovers from the pre-0.5 election architecture and other dead code: unused constants (`PORT_RANGE_*`, `HANDSHAKE_TIMEOUT`, `HEARTBEAT_INTERVAL`), `hasFlag`, `HubConfig`/`ToolContext`/`MeetingState` types, `isSqliteAvailable`, `getTasksByMeeting`, `getActiveTaskCount`, `NexusClient.health`, the never-used `archives/` directory and `tasks.json` path, `suggestedSyncTargets` (parsed a `name@project` instance-id format that no longer exists), the undocumented `NEXUS_PORT` alias of `NEXUS_DAEMON_PORT`, and `migrations/003_add_tasks_table.sql` (the schema lives in `sqlite.ts`). The two `getRootPath` implementations are merged into `config/paths.ts`, so `--root ./relative` and `~` are resolved consistently. `billing` is now preserved on read and patch instead of being dropped.
|
|
30
|
+
|
|
31
|
+
- JSON meeting-store fallback (`store.ts`, `meetings.ts`) and the `/api/storage/info` endpoint. SQLite is required; the daemon no longer reports a "degraded" storage mode.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `NEXUS_RATE_BURST` between 0 and 1 made every call fail with 429 forever, and `NEXUS_RATE_BURST=0` silently disabled rate limiting. The burst is now at least 1, and only `NEXUS_RATE_LIMIT=0` disables limiting.
|
|
36
|
+
|
|
37
|
+
- Manifests written by earlier releases (whose `update_project` stored patches unvalidated) could hold nested relations, endpoints or API specs with missing or extra fields, which made `get_project` and `get_global_topology` fail outputSchema validation on 2026-07-28 clients. Reads now normalize stored manifests: invalid entries are dropped, missing strings become empty, an unknown relation type reads as `related`. The files are not rewritten.
|
|
38
|
+
|
|
39
|
+
- Result contracts were only enforced by TypeScript, which ignores extra fields on returned variables, so a new field in stored data could reach clients as structuredContent that violates the published `outputSchema`. Every result now passes through its zod contract on the way out: undeclared fields are stripped, and a result that cannot meet the contract becomes a server error.
|
|
40
|
+
|
|
41
|
+
- If a tool call lost the daemon while a reconnect attempt that had just succeeded was still notifying the client, the attempt then stopped the retry loop although the adapter was disconnected again; every later call answered "Daemon is not ready yet" until the IDE restarted. The loop now stops only if the connection is still up.
|
|
42
|
+
|
|
43
|
+
- The adapter's reconnect loop used a 3-second `setInterval` while each attempt could take up to the 5-second request timeout, so a hung daemon accumulated overlapping requests. Each attempt is now scheduled only after the previous one finishes.
|
|
44
|
+
|
|
45
|
+
- `rename_project`, `cancel_task`, `end_meeting`, `archive_meeting` and `reopen_meeting` were annotated `destructiveHint: false`, which MCP reserves for purely additive tools; clients could run them without asking the user. They are now marked destructive.
|
|
46
|
+
|
|
47
|
+
- When the daemon reported a tool as unknown (for example after an upgrade the adapter did not notice), the adapter returned `-32602` but kept offering the tool in `tools/list`. It now re-fetches the tool list and sends `list_changed` if it differs.
|
|
48
|
+
|
|
49
|
+
- The adapter fetched the daemon tool list before it began reading stdin. A daemon that accepted connections but never answered held `server/discover` and `initialize` for the 5-second client timeout, long enough for probing clients to misclassify the server as legacy, and delayed exit after stdin closed. The adapter now serves immediately and fetches in the background.
|
|
50
|
+
|
|
51
|
+
- On 2026-07-28 connections, a request without `io.modelcontextprotocol/clientInfo` (only a SHOULD in the spec) got an instance ID without the IDE name, so one client could appear as two instances with split unread cursors and meeting permissions. The adapter now keeps the first client name seen on the connection.
|
|
52
|
+
|
|
53
|
+
- Calling a tool the adapter does not know returned a tool result with `isError: true`. The MCP specification classifies an unknown tool as a protocol error; the adapter now answers with JSON-RPC error `-32602` (Invalid Params), also when the daemon reports the tool as unknown.
|
|
54
|
+
|
|
55
|
+
- The MCP adapter did not exit when the IDE closed its stdin: while the daemon was unreachable the 3-second reconnect timer kept the process alive indefinitely, leaving orphaned adapters. The adapter now shuts down and exits when stdin ends, as the stdio transport specification requires.
|
|
56
|
+
|
|
57
|
+
- Project and document ids that match `Object.prototype` keys were mishandled: `register_session_context({ projectId: "constructor" })` reported success for a project that did not exist, and a project or doc named `__proto__` was silently lost because assigning it rewrote the map's prototype. The registry and docs index are now prototype-free maps.
|
|
58
|
+
|
|
59
|
+
- `sync_project_assets` wrote the manifest and the internal docs as two separately locked steps, so a concurrent `host_delete_project` could run between them and leave a project directory on disk that the registry no longer knew about. Both writes now happen in one critical section.
|
|
60
|
+
|
|
61
|
+
- `npm run build` now clears `build/` first. `tsc` alone left compiled files of deleted sources behind (for example `build/storage/store.js`), and `npm publish` would have shipped them.
|
|
62
|
+
|
|
63
|
+
- `ttl` on tasks was stored but never enforced and `cleanupTasks` was never scheduled, so the tasks table grew forever. The daemon now runs task maintenance at startup and hourly: pending/running tasks past their TTL are marked `failed` (`TTL expired`) and finished tasks older than 7 days are purged. The `tasks_updated_at` trigger is dropped: it overwrote the ISO 8601 `updated_at` written by `updateTask` with `datetime('now')` in a different format, so age comparisons mixed two timestamp formats.
|
|
64
|
+
|
|
65
|
+
- `update_global_strategy` wrote `global/docs/strategy.md` while the documentation described `global/blueprint.md` as the master strategy document, and two initializers created `blueprint.md` with different placeholder text. The tool now writes `blueprint.md`; `GET /api/global/strategy` reads it. On first start an existing `strategy` global doc is moved into `blueprint.md` (unless a non-placeholder blueprint is already there) and removed from the docs index.
|
|
66
|
+
|
|
67
|
+
- `sqlite.ts` registered SIGINT/SIGTERM handlers at import time that called `process.exit(0)` immediately, so the daemon's graceful `server.close` never ran. Signal handling now lives in the daemon entry point: stop accepting connections, finish in-flight requests, close the database, exit.
|
|
68
|
+
|
|
69
|
+
- Tool arguments were never validated: the daemon cast whatever arrived and let SQLite or the filesystem fail later (for example `category: "SYSTEM"` surfaced as a `CHECK constraint failed` 500). Every tool now has a zod contract in `src/daemon/tools.ts`; the JSON Schema served to MCP clients is generated from it and the same contract validates tool calls and REST endpoints, returning 400 with the failing field. `patch` for `update_project` rejects unknown fields, `create_task` checks that `source_meeting_id` exists, and SQLite foreign keys are enforced.
|
|
70
|
+
|
|
71
|
+
- The MCP adapter generated a random instance ID on every start, so an IDE restart reset unread cursors and left meetings that no one could end or archive (only the initiator may). The default is now `mcp-<hostname>-<hash of working directory>-<IDE name>` (IDE name from the MCP handshake), stable per machine, workspace and IDE. `reopen_meeting` applies the same initiator check as `end_meeting` and `archive_meeting`.
|
|
72
|
+
|
|
73
|
+
- The daemon accepted any HTTP request, so a web page open in the user's browser could call `host_delete_project` on the local daemon with a cross-site `text/plain` POST. Requests carrying a browser `Origin` or cross-site `Sec-Fetch-Site` header are refused (403), POST requires `Content-Type: application/json` (415), and request bodies are capped at 16 MB (413) instead of being buffered without limit. The `Host` header must be an IP literal, `localhost`, `host.docker.internal` or a name in `NEXUS_ALLOWED_HOSTS`, which blocks DNS-rebinding pages that would otherwise reach the daemon as same-origin GETs.
|
|
74
|
+
|
|
75
|
+
- The MCP adapter treated every failed tool call (unknown task, closed meeting, permission denied, ...) as the daemon going away: it cleared the tool list, notified the IDE and entered the reconnect loop. Transport failures and tool failures are now distinct (`NexusTransportError` vs `NexusToolError`); only the former triggers reconnection, and tool failures are returned as `isError` results. `POST /api/tools/call` answers 404 for an unknown tool and 400 when the tool rejects the call.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- The MCP adapter moved from `@modelcontextprotocol/sdk` 1.x (protocol 2025-11-25) to the v2 SDK (`@modelcontextprotocol/server` 2.3) and serves both the stateless 2026-07-28 revision and 2025-era clients over stdio via `serveStdio`. 2026-07-28 clients get `server/discover`, the per-request `_meta` envelope and list-changed notifications on `subscriptions/listen`; the instance ID reads the client name from the request envelope. The first `tools/list` waits up to one second for the startup daemon fetch, so it is complete whenever a local daemon is up, while `server/discover` and `initialize` are answered immediately even if the daemon hangs.
|
|
80
|
+
|
|
81
|
+
- Tools without parameters (`get_global_strategy`, `list_global_docs`) publish `{ "type": "object", "properties": {}, "additionalProperties": false }`, the form the MCP specification recommends, and reject unexpected arguments.
|
|
82
|
+
|
|
83
|
+
- `send_message` routing no longer depends on `register_session_context`: a message goes to `meetingId` when given, else to the active meeting, else to global chat. `POST /api/messages/send` uses the same rule (its `source` parameter is gone) instead of a separate code path with different defaults.
|
|
84
|
+
|
|
85
|
+
- Global chat moved from `global/discussion.json` into SQLite. `read_messages` without `meetingId` is now truly incremental per instance (the tool description had promised this), appends no longer rewrite the whole file, and `host_maintenance` reports how many messages were removed. On first start the daemon imports an existing `discussion.json` (categories outside the five known values become unset) and renames it to `discussion.json.migrated`.
|
|
86
|
+
- Daemon state changes (session register, project sync/rename/delete, meeting end, ...) are recorded as events instead of `SYSTEM` lines in the global chat.
|
|
87
|
+
|
|
88
|
+
- `sync_project_assets`, `update_project`, `rename_project`, and `host_delete_project` now run synchronously and return their result directly instead of a `task_id` to poll. The operations take milliseconds; the background-task wrapper only added a polling round-trip and let the work race with other requests.
|
|
89
|
+
|
|
90
|
+
- Require Node.js 22.14+ on the 22.x release line, or Node.js 23.6+ and update MCP SDK, SQLite, and test dependencies to their latest compatible stable releases.
|
|
91
|
+
|
|
5
92
|
## [0.5.0] - 2026-06-14
|
|
6
93
|
|
|
7
94
|
### 🏗️ Daemon + MCP Architecture (Breaking Refactor)
|
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/n2n-nexus-logo.png" width="128" alt="n2n-nexus logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# n2n-nexus
|
|
2
6
|
|
|
3
7
|
Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaboration across IDEs, machines, and projects.
|
|
@@ -7,7 +11,6 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
|
|
|
7
11
|
[](https://github.com/n2ns/n2n-nexus/blob/main/LICENSE)
|
|
8
12
|
[](https://modelcontextprotocol.io)
|
|
9
13
|
[](https://nodejs.org)
|
|
10
|
-
[](https://datafrog.io)
|
|
11
14
|
|
|
12
15
|
[中文版](./docs/README_zh.md)
|
|
13
16
|
|
|
@@ -15,45 +18,33 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
|
|
|
15
18
|
|
|
16
19
|
> **One daemon. Many assistants. Shared project state.**
|
|
17
20
|
|
|
18
|
-
n2n-nexus is an open-source Model Context Protocol (MCP) coordination server for
|
|
19
|
-
|
|
20
|
-
Use it when Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, JetBrains, or other MCP-enabled clients need a shared local coordination layer instead of isolated per-chat context.
|
|
21
|
-
|
|
22
|
-
## Search-friendly positioning
|
|
23
|
-
|
|
24
|
-
If you are searching for:
|
|
25
|
-
|
|
26
|
-
- multi-agent MCP coordination
|
|
27
|
-
- multi-AI assistant collaboration
|
|
28
|
-
- local MCP coordination hub
|
|
29
|
-
- shared project context for AI coding agents
|
|
30
|
-
- cross-IDE MCP collaboration server
|
|
31
|
-
- local-first project registry for AI assistants
|
|
32
|
-
- meeting and task coordination for coding agents
|
|
21
|
+
n2n-nexus is an open-source Model Context Protocol (MCP) coordination server for developers and teams who use multiple AI coding assistants across IDEs, machines, and projects. Use it when Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, JetBrains, or other MCP-enabled clients need a shared local coordination layer instead of isolated per-chat context.
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
## 📚 Contents
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
- [What is n2n-nexus?](#-what-is-n2n-nexus)
|
|
26
|
+
- [Architecture](#️-architecture)
|
|
27
|
+
- [Quick start](#-quick-start)
|
|
28
|
+
- [Toolset](#️-toolset)
|
|
29
|
+
- [Data storage](#-data-storage)
|
|
30
|
+
- [Project ID conventions](#️-project-id-conventions)
|
|
31
|
+
- [CLI reference](#-cli-reference)
|
|
32
|
+
- [Security and governance notes](#-security-and-governance-notes)
|
|
33
|
+
- [Real-world example](#-real-world-example)
|
|
34
|
+
- [Local development](#-local-development)
|
|
35
|
+
- [Related docs](#-related-docs)
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
## 💡 What is n2n-nexus?
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **Install**: `npx n2n-nexus daemon --port 5688`
|
|
43
|
-
- **Protocol**: Model Context Protocol (MCP), adapter-to-daemon HTTP bridge
|
|
44
|
-
- **Storage**: local filesystem plus SQLite under `~/.n2n-nexus` by default
|
|
45
|
-
- **Best for**: multi-assistant coding sessions, project handoffs, shared decisions, meeting notes, async tasks, project manifests
|
|
46
|
-
- **Not for**: cloud team chat, public project management SaaS, source code indexing, vector search, or remote database hosting
|
|
47
|
-
|
|
48
|
-
## Why Use It?
|
|
39
|
+
n2n-nexus gives AI assistants a shared coordination workspace. A long-running local daemon owns project state, meetings, messages, tasks, shared docs, and project assets. Lightweight MCP adapters connect each IDE or assistant to the same daemon.
|
|
49
40
|
|
|
50
41
|
- Coordinate multiple AI assistants working on the same project.
|
|
51
|
-
- Share project manifests,
|
|
52
|
-
- Keep decisions, proposals, and updates in a local meeting
|
|
53
|
-
- Run the daemon once
|
|
54
|
-
- Avoid giving each assistant broad filesystem or backend access
|
|
42
|
+
- Share project manifests, decisions, and assets across IDEs and machines.
|
|
43
|
+
- Keep decisions, proposals, and updates in a local meeting and message log.
|
|
44
|
+
- Run the daemon once; connect adapters from Windows, WSL, SSH hosts, or multiple editors.
|
|
45
|
+
- Avoid giving each assistant broad filesystem or backend access.
|
|
55
46
|
|
|
56
|
-
## Architecture
|
|
47
|
+
## 🏗️ Architecture
|
|
57
48
|
|
|
58
49
|
```text
|
|
59
50
|
┌──────────────────────────────────────┐
|
|
@@ -73,7 +64,9 @@ n2n-nexus gives AI assistants a shared coordination workspace. Instead of each a
|
|
|
73
64
|
- **MCP adapters are stateless**: each IDE starts an adapter through `npx`, then forwards tool calls to the daemon.
|
|
74
65
|
- **Cross-environment by design**: point `NEXUS_ENDPOINT` at the same daemon from different machines or shells. The daemon binds to localhost by default; use `--host 0.0.0.0` only on a trusted network.
|
|
75
66
|
|
|
76
|
-
## Quick
|
|
67
|
+
## 🚀 Quick start
|
|
68
|
+
|
|
69
|
+
Requires Node.js 22.14+ on the 22.x release line, or Node.js 23.6+.
|
|
77
70
|
|
|
78
71
|
### 1. Start the daemon
|
|
79
72
|
|
|
@@ -108,48 +101,23 @@ The adapter can start before the daemon. It loads the daemon tool list once the
|
|
|
108
101
|
| Windows IDE to WSL daemon | `http://<WSL-IP>:5688` |
|
|
109
102
|
| Remote machine | `http://<Server-IP>:5688` |
|
|
110
103
|
|
|
111
|
-
## Toolset
|
|
112
|
-
|
|
113
|
-
### Session and context
|
|
114
|
-
|
|
115
|
-
- `register_session_context`: declare the active project ID.
|
|
116
|
-
|
|
117
|
-
### Project asset management
|
|
118
|
-
|
|
119
|
-
- `sync_project_assets`: submit a project manifest and internal docs.
|
|
120
|
-
- `update_project`: patch a project manifest.
|
|
121
|
-
- `rename_project`: rename a project ID and update relations.
|
|
122
|
-
- `upload_project_asset`: upload binary or text assets.
|
|
123
|
-
- `search_projects`: search the project registry.
|
|
124
|
-
- `get_global_topology`: inspect project topology and dependencies.
|
|
125
|
-
|
|
126
|
-
### Messaging and collaboration
|
|
127
|
-
|
|
128
|
-
- `send_message`: post meeting or global messages.
|
|
129
|
-
- `read_messages`: read unread messages incrementally.
|
|
130
|
-
- `update_global_strategy`: update the master strategy document.
|
|
131
|
-
- `sync_global_doc`: create or update shared docs.
|
|
132
|
-
|
|
133
|
-
### Meeting management
|
|
134
|
-
|
|
135
|
-
- `start_meeting`: open a meeting session.
|
|
136
|
-
- `end_meeting`: close and lock a meeting.
|
|
137
|
-
- `archive_meeting`: move a closed meeting to archive.
|
|
138
|
-
- `reopen_meeting`: reactivate a closed or archived meeting.
|
|
139
|
-
|
|
140
|
-
### Async task management
|
|
104
|
+
## 🛠️ Toolset
|
|
141
105
|
|
|
142
|
-
|
|
143
|
-
- `get_task`: poll task status and result.
|
|
144
|
-
- `list_tasks`: list tasks by status.
|
|
145
|
-
- `cancel_task`: cancel a pending or running task.
|
|
106
|
+
The daemon exposes 26 tools, grouped by purpose:
|
|
146
107
|
|
|
147
|
-
|
|
108
|
+
| Group | Tools | Purpose |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| **Topology** | `get_global_topology` | Read the project list or a detailed per-project subgraph. |
|
|
111
|
+
| **Projects & assets** | `search_projects`, `get_project`, `update_project`, `rename_project`, `sync_project_assets`, `upload_project_asset` | Find, read, update, and rename projects; sync manifests and internal docs; upload binary assets. |
|
|
112
|
+
| **Global docs** | `list_global_docs`, `get_global_doc`, `sync_global_doc`, `get_global_strategy`, `update_global_strategy` | List, read, create and update shared global documents and the master strategy document. |
|
|
113
|
+
| **Messaging** | `send_message`, `read_messages`, `list_events` | Post and read meeting and global messages; read the structured activity log. |
|
|
114
|
+
| **Meetings** | `list_meetings`, `start_meeting`, `end_meeting`, `archive_meeting`, `reopen_meeting` | List, open, close, archive, and reopen meeting sessions. |
|
|
115
|
+
| **Tasks** | `create_task`, `get_task`, `list_tasks`, `cancel_task` | Create and track tasks that assistants manage themselves. |
|
|
116
|
+
| **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; delete a project. |
|
|
148
117
|
|
|
149
|
-
|
|
150
|
-
- `host_delete_project`: delete a project and assets.
|
|
118
|
+
See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call examples.
|
|
151
119
|
|
|
152
|
-
## Data
|
|
120
|
+
## 💾 Data storage
|
|
153
121
|
|
|
154
122
|
Default storage root:
|
|
155
123
|
|
|
@@ -173,10 +141,10 @@ Override with `--root <path>` or `NEXUS_ROOT`.
|
|
|
173
141
|
│ ├── internal_blueprint.md
|
|
174
142
|
│ └── assets/
|
|
175
143
|
├── registry.json
|
|
176
|
-
└── nexus.db
|
|
144
|
+
└── nexus.db # meetings, global chat, events, tasks, read cursors
|
|
177
145
|
```
|
|
178
146
|
|
|
179
|
-
## Project ID
|
|
147
|
+
## 🏷️ Project ID conventions
|
|
180
148
|
|
|
181
149
|
Project IDs follow `[prefix]_[name]`.
|
|
182
150
|
|
|
@@ -192,7 +160,7 @@ Project IDs follow `[prefix]_[name]`.
|
|
|
192
160
|
| `infra_` | Infrastructure / DevOps | `infra_k8s-config` |
|
|
193
161
|
| `doc_` | Documentation | `doc_coding-guide` |
|
|
194
162
|
|
|
195
|
-
## CLI
|
|
163
|
+
## 💻 CLI reference
|
|
196
164
|
|
|
197
165
|
```bash
|
|
198
166
|
# Start daemon
|
|
@@ -209,9 +177,13 @@ NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp
|
|
|
209
177
|
| `NEXUS_ENDPOINT` | Daemon URL for MCP adapter | `http://127.0.0.1:5688` |
|
|
210
178
|
| `NEXUS_ROOT` | Storage root for daemon | `~/.n2n-nexus` |
|
|
211
179
|
| `NEXUS_HOST` | Daemon bind host | `127.0.0.1` |
|
|
212
|
-
| `
|
|
180
|
+
| `NEXUS_DAEMON_PORT` | Daemon port (overridden by `--port`) | `5688` |
|
|
181
|
+
| `NEXUS_RATE_LIMIT` | Tool calls per second per instance; each client address gets 5x. `0` disables rate limiting | `20` |
|
|
182
|
+
| `NEXUS_RATE_BURST` | Calls an instance may make at once before the per-second limit applies (minimum 1) | `60` |
|
|
183
|
+
| `NEXUS_ALLOWED_HOSTS` | Extra DNS names accepted in the `Host` header (IP literals, `localhost`, `host.docker.internal` are always accepted) | empty |
|
|
184
|
+
| `NEXUS_INSTANCE_ID` | Override MCP instance ID (read cursors and meeting permissions are keyed on it) | `mcp-<hostname>-<hash of working directory>-<IDE name>` |
|
|
213
185
|
|
|
214
|
-
## Security and governance notes
|
|
186
|
+
## 🔐 Security and governance notes
|
|
215
187
|
|
|
216
188
|
- n2n-nexus stores coordination data locally by default.
|
|
217
189
|
- Do not put secrets, credentials, customer data, or private tokens in meeting messages or project assets unless your local policy allows it.
|
|
@@ -219,16 +191,26 @@ NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp
|
|
|
219
191
|
- Exposing the daemon beyond localhost is an operational choice; restrict network access when using remote endpoints.
|
|
220
192
|
- Treat project manifests and internal docs as potentially sensitive implementation data.
|
|
221
193
|
|
|
222
|
-
## Real-world example
|
|
194
|
+
## 🌐 Real-world example
|
|
195
|
+
|
|
196
|
+
The docs include a recorded multi-agent session that shows n2n-nexus in action. Four AI assistants — running in different IDEs and assigned to different projects — used a shared n2n-nexus daemon to coordinate in real time.
|
|
197
|
+
|
|
198
|
+
What to look for in the session:
|
|
223
199
|
|
|
224
|
-
|
|
200
|
+
- **Shared coordination space**: each assistant registered its active project (with the `register_session_context` tool of that release; current versions pass `projectId` explicitly instead), then read and wrote to the same daemon throughout the session.
|
|
201
|
+
- **Structured message categories**: `PROPOSAL`, `DECISION`, `UPDATE`, and `MEETING_START` tags let assistants signal intent, not just post text.
|
|
202
|
+
- **SYSTEM entries**: lines like `[Augment] Synced global doc: edge-sync-protocol-v1` record actual tool calls. That version wrote them into the chat stream; current versions keep them out of the conversation and expose them as structured events through `list_events`.
|
|
203
|
+
- **Async debugging across instances**: when one assistant hit an auth error, others responded with fixes in real time, each from their own IDE context.
|
|
204
|
+
- **Two output formats**: the discussion log is the raw chronological stream; the meeting minutes are a structured summary derived from it, showing how a long session distills into decisions and action items.
|
|
225
205
|
|
|
226
|
-
| File |
|
|
206
|
+
| File | What it shows |
|
|
227
207
|
| --- | --- |
|
|
228
|
-
| [
|
|
229
|
-
| [
|
|
208
|
+
| [Discussion Log](docs/discussion_2025-12-29.md) | The full chronological stream: proposals, debug exchanges, decisions, and SYSTEM tool entries |
|
|
209
|
+
| [Meeting Minutes](docs/MEETING_MINUTES_2025-12-29.md) | The structured output: participants, key decisions, test results, and action items |
|
|
230
210
|
|
|
231
|
-
## Local
|
|
211
|
+
## 🔧 Local development
|
|
212
|
+
|
|
213
|
+
Use Node.js 24 for development, matching CI and the lint toolchain.
|
|
232
214
|
|
|
233
215
|
```bash
|
|
234
216
|
git clone https://github.com/n2ns/n2n-nexus.git
|
|
@@ -243,44 +225,19 @@ node build/index.js daemon --root /tmp/nexus-test --port 5688
|
|
|
243
225
|
NEXUS_ENDPOINT=http://127.0.0.1:5688 node build/index.js mcp
|
|
244
226
|
```
|
|
245
227
|
|
|
246
|
-
##
|
|
247
|
-
|
|
248
|
-
### Is n2n-nexus a project management SaaS?
|
|
249
|
-
|
|
250
|
-
No. It is a local MCP coordination server for AI assistants. It stores state locally and exposes MCP tools to connected clients.
|
|
251
|
-
|
|
252
|
-
### Does it replace n2n-memory?
|
|
253
|
-
|
|
254
|
-
No. `n2n-memory` is repository-local memory for one project. `n2n-nexus` is a shared coordination hub for multiple assistants, project manifests, messages, meetings, and tasks.
|
|
255
|
-
|
|
256
|
-
### Does it work with Claude Desktop, Cursor, VS Code, and other IDEs?
|
|
257
|
-
|
|
258
|
-
Yes, when the client supports local MCP command servers. The adapter is started by the IDE and connects to the local daemon through `NEXUS_ENDPOINT`.
|
|
259
|
-
|
|
260
|
-
### Can it coordinate assistants across Windows and WSL?
|
|
261
|
-
|
|
262
|
-
Yes. Run the daemon in one environment and point each adapter at it using `NEXUS_ENDPOINT`.
|
|
263
|
-
|
|
264
|
-
### Does it send data to the cloud?
|
|
265
|
-
|
|
266
|
-
No cloud service is required. Data is stored under the configured local root. If you expose the daemon to a remote machine, that is your own network configuration.
|
|
267
|
-
|
|
268
|
-
### Is this a vector database or code indexer?
|
|
269
|
-
|
|
270
|
-
No. n2n-nexus coordinates project metadata, messages, meetings, docs, tasks, and assets. It is not a semantic code search engine.
|
|
271
|
-
|
|
272
|
-
## Related docs
|
|
228
|
+
## 📖 Related docs
|
|
273
229
|
|
|
274
|
-
- [Architecture](./docs/ARCHITECTURE.md)
|
|
275
|
-
- [
|
|
276
|
-
- [
|
|
277
|
-
- [Changelog](./CHANGELOG.md)
|
|
278
|
-
- [
|
|
230
|
+
- **[Architecture](./docs/ARCHITECTURE.md)**: System design and daemon-adapter separation explained.
|
|
231
|
+
- **[Tools reference](./docs/TOOLS_REFERENCE.md)**: Full tool list and parameters.
|
|
232
|
+
- **[AI Assistant Guide](./docs/ASSISTANT_GUIDE.md)**: How AI assistants should call tools and stay in project context.
|
|
233
|
+
- **[Changelog](./CHANGELOG.md)**: Version history and release notes.
|
|
234
|
+
- **[Contributing](./CONTRIBUTING.md)**: How to report issues and contribute.
|
|
235
|
+
- **[Security](./SECURITY.md)**: How to report vulnerabilities.
|
|
279
236
|
|
|
280
|
-
## License
|
|
237
|
+
## 📄 License
|
|
281
238
|
|
|
282
239
|
This project is licensed under the [Apache-2.0 License](./LICENSE).
|
|
283
240
|
|
|
284
241
|
---
|
|
285
242
|
|
|
286
|
-
Built by N2NS Lab,
|
|
243
|
+
Built by [N2NS Lab](https://n2ns.com), the open-source lab of [datafrog.io](https://datafrog.io) for practical AI developer tools.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
|
+
/** The daemon could not be reached or answered with something that is not JSON. */
|
|
5
|
+
export class NexusTransportError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "NexusTransportError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** The daemon answered, but the tool call itself failed (unknown tool, validation, not found, ...). */
|
|
12
|
+
export class NexusToolError extends Error {
|
|
13
|
+
status;
|
|
14
|
+
constructor(message, status) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.name = "NexusToolError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
4
20
|
function normalizeEndpoint(endpoint) {
|
|
5
21
|
const raw = endpoint || process.env.NEXUS_ENDPOINT || "http://127.0.0.1:5688";
|
|
6
22
|
if (/^https?:\/\//i.test(raw))
|
|
@@ -34,30 +50,30 @@ export class NexusClient {
|
|
|
34
50
|
let data = "";
|
|
35
51
|
res.on("data", chunk => { data += chunk; });
|
|
36
52
|
res.on("end", () => {
|
|
53
|
+
let parsed;
|
|
37
54
|
try {
|
|
38
|
-
|
|
39
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
40
|
-
reject(new Error(parsed.error || `HTTP ${res.statusCode}`));
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
resolve(parsed);
|
|
44
|
-
}
|
|
55
|
+
parsed = data ? JSON.parse(data) : {};
|
|
45
56
|
}
|
|
46
57
|
catch {
|
|
47
|
-
reject(new
|
|
58
|
+
reject(new NexusTransportError(`Invalid JSON response from daemon: ${data.slice(0, 200)}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const status = res.statusCode ?? 0;
|
|
62
|
+
if (status >= 400) {
|
|
63
|
+
reject(new NexusToolError(parsed.error || `HTTP ${status}`, status));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
resolve(parsed);
|
|
48
67
|
}
|
|
49
68
|
});
|
|
50
69
|
});
|
|
51
|
-
req.on("error", reject);
|
|
52
|
-
req.on("timeout", () => req.destroy(new
|
|
70
|
+
req.on("error", (e) => reject(e instanceof NexusTransportError ? e : new NexusTransportError(e.message)));
|
|
71
|
+
req.on("timeout", () => req.destroy(new NexusTransportError(`Request to ${this.endpoint} timed out after ${this.timeoutMs} ms`)));
|
|
53
72
|
if (payload)
|
|
54
73
|
req.write(payload);
|
|
55
74
|
req.end();
|
|
56
75
|
});
|
|
57
76
|
}
|
|
58
|
-
async health() {
|
|
59
|
-
return this.request("GET", "/health");
|
|
60
|
-
}
|
|
61
77
|
async fetchTools() {
|
|
62
78
|
const res = await this.request("GET", "/api/tools");
|
|
63
79
|
return res.tools;
|
|
@@ -65,7 +81,7 @@ export class NexusClient {
|
|
|
65
81
|
async callTool(name, args, instanceId) {
|
|
66
82
|
const res = await this.request("POST", "/api/tools/call", { tool: name, args: args || {}, instanceId: instanceId || "unknown" });
|
|
67
83
|
if (!res.ok)
|
|
68
|
-
throw new
|
|
84
|
+
throw new NexusToolError(res.error || "Tool call failed", 200);
|
|
69
85
|
return res.result;
|
|
70
86
|
}
|
|
71
87
|
}
|
package/build/config/cli.js
CHANGED
package/build/config/index.js
CHANGED
|
@@ -4,13 +4,3 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
5
|
const pkgPath = path.resolve(__dirname, "../../package.json");
|
|
6
6
|
export const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
7
|
-
export function getRootPath() {
|
|
8
|
-
// Priority: --root CLI arg → NEXUS_ROOT env → ~/.n2n-nexus
|
|
9
|
-
const argIndex = process.argv.indexOf("--root");
|
|
10
|
-
if (argIndex !== -1 && process.argv[argIndex + 1]) {
|
|
11
|
-
return process.argv[argIndex + 1];
|
|
12
|
-
}
|
|
13
|
-
if (process.env.NEXUS_ROOT)
|
|
14
|
-
return process.env.NEXUS_ROOT;
|
|
15
|
-
return path.join(process.env.HOME || process.env.USERPROFILE || ".", ".n2n-nexus");
|
|
16
|
-
}
|
package/build/constants.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Nexus
|
|
3
|
-
*
|
|
4
|
-
* Centralized configuration for network-related settings.
|
|
2
|
+
* Nexus constants
|
|
5
3
|
*/
|
|
6
4
|
// Service identification
|
|
7
5
|
export const SERVICE_NAME = "n2n-nexus";
|
|
@@ -9,14 +7,12 @@ export const SERVICE_NAME = "n2n-nexus";
|
|
|
9
7
|
// Default to localhost because the daemon exposes unauthenticated local admin APIs.
|
|
10
8
|
// Use "0.0.0.0" only when you intentionally expose it to another trusted environment.
|
|
11
9
|
export const NEXUS_HOST = "127.0.0.1";
|
|
12
|
-
//
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const HEARTBEAT_INTERVAL = 30000;
|
|
18
|
-
// Task cleanup
|
|
10
|
+
// Default daemon port
|
|
11
|
+
export const NEXUS_PORT = 5688;
|
|
12
|
+
// HTTP request body limit for the daemon (base64 asset uploads are the largest payload)
|
|
13
|
+
export const MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
// Task lifecycle: finished tasks are purged after this age; maintenance runs at this interval
|
|
19
15
|
export const TASK_CLEANUP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
16
|
+
export const TASK_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
20
17
|
// File I/O
|
|
21
18
|
export const FILE_ENCODING = "utf-8";
|
|
22
|
-
export const PACKAGE_JSON = "package.json";
|
package/build/daemon/index.js
CHANGED
|
@@ -1,41 +1,57 @@
|
|
|
1
1
|
import { createDaemonServer } from "./server.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { closeDatabase } from "../storage/sqlite.js";
|
|
3
|
+
import { runTaskMaintenance } from "../storage/tasks.js";
|
|
4
|
+
import { TASK_MAINTENANCE_INTERVAL_MS } from "../constants.js";
|
|
5
|
+
import { pkg } from "../config/index.js";
|
|
6
|
+
import { getArg } from "../config/cli.js";
|
|
7
|
+
import { getRootPath } from "../config/paths.js";
|
|
8
|
+
import { NEXUS_HOST, NEXUS_PORT } from "../constants.js";
|
|
4
9
|
function parsePort() {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const parsed = parseInt(process.argv[argIndex + 1] || "", 10);
|
|
10
|
+
for (const candidate of [getArg("--port"), process.env.NEXUS_DAEMON_PORT]) {
|
|
11
|
+
const parsed = parseInt(candidate || "", 10);
|
|
8
12
|
if (!isNaN(parsed) && parsed > 0)
|
|
9
13
|
return parsed;
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
if (!isNaN(envPort) && envPort > 0)
|
|
13
|
-
return envPort;
|
|
14
|
-
return 5688;
|
|
15
|
+
return NEXUS_PORT;
|
|
15
16
|
}
|
|
16
17
|
function parseHost() {
|
|
17
|
-
|
|
18
|
-
if (argIndex !== -1 && process.argv[argIndex + 1]) {
|
|
19
|
-
return process.argv[argIndex + 1];
|
|
20
|
-
}
|
|
21
|
-
return process.env.NEXUS_HOST || NEXUS_HOST;
|
|
18
|
+
return getArg("--host") || process.env.NEXUS_HOST || NEXUS_HOST;
|
|
22
19
|
}
|
|
23
20
|
export async function runDaemon() {
|
|
24
21
|
const port = parsePort();
|
|
25
22
|
const host = parseHost();
|
|
26
23
|
const root = getRootPath();
|
|
27
|
-
//
|
|
24
|
+
// Publish the resolved root so every storage module (which reads NEXUS_ROOT) agrees with the log line below
|
|
28
25
|
process.env.NEXUS_ROOT = root;
|
|
29
|
-
const { server
|
|
26
|
+
const { server } = await createDaemonServer({ port, host, version: pkg.version });
|
|
30
27
|
await new Promise((resolve, reject) => {
|
|
31
28
|
server.once("error", reject);
|
|
32
29
|
server.listen(port, host, () => {
|
|
33
30
|
console.error(`[n2n-nexus] Daemon v${pkg.version} listening on http://${host}:${port}`);
|
|
34
|
-
console.error(`[n2n-nexus] Storage: ${root}
|
|
31
|
+
console.error(`[n2n-nexus] Storage: ${root}`);
|
|
35
32
|
resolve();
|
|
36
33
|
});
|
|
37
34
|
});
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
// Expire overdue tasks and purge old finished ones at startup and then periodically
|
|
36
|
+
const maintain = () => {
|
|
37
|
+
const { expired, removed } = runTaskMaintenance();
|
|
38
|
+
if (expired || removed)
|
|
39
|
+
console.error(`[n2n-nexus] Task maintenance: ${expired} expired, ${removed} removed.`);
|
|
40
|
+
};
|
|
41
|
+
maintain();
|
|
42
|
+
setInterval(maintain, TASK_MAINTENANCE_INTERVAL_MS).unref();
|
|
43
|
+
// Graceful shutdown: stop accepting connections, let in-flight requests
|
|
44
|
+
// finish, then close SQLite. A second signal exits immediately.
|
|
45
|
+
const shutdown = (signal) => {
|
|
46
|
+
console.error(`[n2n-nexus] ${signal} received, shutting down.`);
|
|
47
|
+
server.close(() => {
|
|
48
|
+
closeDatabase();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
process.once(signal, () => process.exit(1));
|
|
52
|
+
};
|
|
53
|
+
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
54
|
+
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
55
|
+
process.on("exit", () => closeDatabase());
|
|
40
56
|
console.error("[n2n-nexus] Ready. Press Ctrl+C to stop.");
|
|
41
57
|
}
|