n2n-nexus 0.4.2 → 0.7.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +77 -117
  3. package/assets/n2n-nexus-banner.jpg +0 -0
  4. package/assets/n2n-nexus-icon.png +0 -0
  5. package/assets/n2n-nexus-logo.png +0 -0
  6. package/build/client/nexus-client.js +44 -21
  7. package/build/config/cli.js +0 -3
  8. package/build/config/index.js +0 -10
  9. package/build/constants.js +7 -11
  10. package/build/daemon/index.js +35 -19
  11. package/build/daemon/rate-limit.js +62 -0
  12. package/build/daemon/server.js +422 -545
  13. package/build/daemon/tool-outputs.js +177 -0
  14. package/build/daemon/tools.js +321 -0
  15. package/build/index.js +1 -1
  16. package/build/server/instance-id.js +19 -0
  17. package/build/server/nexus.js +243 -47
  18. package/build/storage/direct-messages.js +89 -0
  19. package/build/storage/docs.js +14 -7
  20. package/build/storage/events.js +31 -0
  21. package/build/storage/fs-utils.js +42 -0
  22. package/build/storage/global-messages.js +44 -0
  23. package/build/storage/index.js +47 -41
  24. package/build/storage/migrations.js +86 -0
  25. package/build/storage/paths.js +0 -3
  26. package/build/storage/projects.js +4 -5
  27. package/build/storage/registry.js +67 -25
  28. package/build/storage/sessions.js +77 -0
  29. package/build/storage/sqlite-meeting.js +35 -23
  30. package/build/storage/sqlite.js +73 -24
  31. package/build/storage/tasks.js +29 -103
  32. package/build/types.js +1 -1
  33. package/docs/ARCHITECTURE.md +36 -25
  34. package/docs/ARCHITECTURE_zh.md +37 -26
  35. package/docs/ASSISTANT_GUIDE.md +77 -65
  36. package/docs/README_zh.md +34 -19
  37. package/llms.txt +12 -2
  38. package/package.json +24 -14
  39. package/build/storage/logs.js +0 -60
  40. package/build/storage/meetings.js +0 -276
  41. package/build/storage/store.js +0 -153
@@ -1,5 +1,4 @@
1
1
  import Database from "better-sqlite3";
2
- import { createRequire } from "module";
3
2
  import { NexusPaths } from "./paths.js";
4
3
  let db = null;
5
4
  /**
@@ -50,6 +49,46 @@ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
50
49
  CREATE INDEX IF NOT EXISTS idx_decisions_meeting ON decisions(meeting_id);
51
50
  CREATE INDEX IF NOT EXISTS idx_meetings_status ON meetings(status);
52
51
 
52
+ -- Global (non-meeting) chat stream
53
+ CREATE TABLE IF NOT EXISTS global_messages (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ sender TEXT NOT NULL,
56
+ text TEXT NOT NULL,
57
+ category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
58
+ timestamp TEXT NOT NULL
59
+ );
60
+
61
+ -- Audit trail of state changes made through the daemon
62
+ CREATE TABLE IF NOT EXISTS events (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ timestamp TEXT NOT NULL,
65
+ actor TEXT NOT NULL,
66
+ action TEXT NOT NULL,
67
+ target TEXT,
68
+ detail TEXT
69
+ );
70
+ CREATE INDEX IF NOT EXISTS idx_events_action ON events(action);
71
+
72
+ -- Tasks managed by assistants
73
+ CREATE TABLE IF NOT EXISTS tasks (
74
+ id TEXT PRIMARY KEY,
75
+ status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
76
+ progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
77
+ source_meeting_id TEXT,
78
+ metadata TEXT,
79
+ result_uri TEXT,
80
+ error_message TEXT,
81
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
82
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
83
+ ttl INTEGER,
84
+ FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
85
+ );
86
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
87
+ CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
88
+ -- updateTask sets updated_at itself in ISO 8601; the old trigger overwrote it
89
+ -- with datetime('now') in a different format, breaking timestamp comparisons.
90
+ DROP TRIGGER IF EXISTS tasks_updated_at;
91
+
53
92
  -- Read cursors table (tracks each IDE's last read message per context)
54
93
  CREATE TABLE IF NOT EXISTS read_cursors (
55
94
  instance_id TEXT NOT NULL,
@@ -59,6 +98,38 @@ CREATE TABLE IF NOT EXISTS read_cursors (
59
98
  updated_at TEXT,
60
99
  PRIMARY KEY (instance_id, context_type, context_id)
61
100
  );
101
+
102
+ -- Live MCP connections have separate identities even within one IDE/project.
103
+ CREATE TABLE IF NOT EXISTS sessions (
104
+ session_id TEXT PRIMARY KEY,
105
+ instance_id TEXT NOT NULL,
106
+ name TEXT NOT NULL,
107
+ client_name TEXT NOT NULL,
108
+ workspace TEXT NOT NULL,
109
+ project_id TEXT,
110
+ inbound TEXT NOT NULL CHECK(inbound IN ('accept', 'refuse')) DEFAULT 'accept',
111
+ connected INTEGER NOT NULL DEFAULT 1,
112
+ created_at TEXT NOT NULL,
113
+ last_seen_at TEXT NOT NULL
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
116
+
117
+ CREATE TABLE IF NOT EXISTS direct_messages (
118
+ message_id TEXT PRIMARY KEY,
119
+ from_session_id TEXT NOT NULL REFERENCES sessions(session_id),
120
+ to_session_id TEXT NOT NULL REFERENCES sessions(session_id),
121
+ text TEXT NOT NULL,
122
+ category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
123
+ reply_to TEXT REFERENCES direct_messages(message_id),
124
+ status TEXT NOT NULL CHECK(status IN ('accepted', 'fetched', 'acknowledged', 'replied', 'refused', 'expired')),
125
+ created_at TEXT NOT NULL,
126
+ expires_at TEXT NOT NULL,
127
+ fetched_at TEXT,
128
+ acknowledged_at TEXT,
129
+ replied_at TEXT,
130
+ reason TEXT
131
+ );
132
+ CREATE INDEX IF NOT EXISTS idx_direct_messages_inbox ON direct_messages(to_session_id, status);
62
133
  `;
63
134
  /**
64
135
  * Get the database file path
@@ -76,6 +147,7 @@ export function initDatabase() {
76
147
  db = new Database(dbPath);
77
148
  // Enable WAL mode for better concurrent access
78
149
  db.pragma("journal_mode = WAL");
150
+ db.pragma("foreign_keys = ON");
79
151
  // Initialize schema
80
152
  db.exec(SCHEMA);
81
153
  // Migration: Add initiator column if it doesn't exist (Upgrade from v0.1.7)
@@ -116,26 +188,3 @@ export function closeDatabase() {
116
188
  console.error("[Nexus] SQLite database closed");
117
189
  }
118
190
  }
119
- export function isSqliteAvailable() {
120
- const require = createRequire(import.meta.url);
121
- try {
122
- // Try to load better-sqlite3
123
- require("better-sqlite3");
124
- return true;
125
- }
126
- catch {
127
- return false;
128
- }
129
- }
130
- // Cleanup on process exit
131
- process.on("exit", () => {
132
- closeDatabase();
133
- });
134
- process.on("SIGINT", () => {
135
- closeDatabase();
136
- process.exit(0);
137
- });
138
- process.on("SIGTERM", () => {
139
- closeDatabase();
140
- process.exit(0);
141
- });
@@ -6,59 +6,13 @@
6
6
  */
7
7
  import { getDatabase } from "./sqlite.js";
8
8
  import { TASK_CLEANUP_MAX_AGE_MS } from "../constants.js";
9
+ export const TASK_STATUSES = ["pending", "running", "completed", "failed", "cancelled"];
9
10
  // Generate unique task ID
10
11
  function generateTaskId() {
11
12
  const timestamp = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
12
13
  const random = Math.random().toString(36).substring(2, 6);
13
14
  return `task_${timestamp}_${random}`;
14
15
  }
15
- let initialized = false;
16
- /**
17
- * Initialize the tasks table (run migrations)
18
- */
19
- export function initTasksTable() {
20
- if (initialized)
21
- return;
22
- const db = getDatabase();
23
- const TASKS_SCHEMA = `
24
- CREATE TABLE IF NOT EXISTS tasks (
25
- id TEXT PRIMARY KEY,
26
- status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
27
- progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
28
- source_meeting_id TEXT,
29
- metadata TEXT,
30
- result_uri TEXT,
31
- error_message TEXT,
32
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
33
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
34
- ttl INTEGER,
35
- FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
36
- );
37
-
38
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
39
- CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
40
- `;
41
- db.exec(TASKS_SCHEMA);
42
- // Add trigger for auto-updating updated_at (separate exec to handle IF NOT EXISTS)
43
- try {
44
- db.exec(`
45
- CREATE TRIGGER IF NOT EXISTS tasks_updated_at
46
- AFTER UPDATE ON tasks
47
- FOR EACH ROW
48
- BEGIN
49
- UPDATE tasks SET updated_at = datetime('now') WHERE id = OLD.id;
50
- END;
51
- `);
52
- }
53
- catch {
54
- // Trigger may already exist in older SQLite versions without IF NOT EXISTS support
55
- }
56
- console.error("[Nexus] Tasks table initialized");
57
- initialized = true;
58
- }
59
- export function resetTasksInit() {
60
- initialized = false;
61
- }
62
16
  /**
63
17
  * Create a new task
64
18
  */
@@ -73,14 +27,7 @@ export function createTask(input = {}) {
73
27
  stmt.run(id, input.source_meeting_id || null, input.metadata ? JSON.stringify(input.metadata) : null, now, now, input.ttl || null);
74
28
  return getTask(id);
75
29
  }
76
- /**
77
- * Get a task by ID
78
- */
79
- export function getTask(id) {
80
- const db = getDatabase();
81
- const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
82
- if (!row)
83
- return null;
30
+ function rowToTask(row) {
84
31
  return {
85
32
  id: row.id,
86
33
  status: row.status,
@@ -94,6 +41,13 @@ export function getTask(id) {
94
41
  ttl: row.ttl || null
95
42
  };
96
43
  }
44
+ /**
45
+ * Get a task by ID
46
+ */
47
+ export function getTask(id) {
48
+ const row = getDatabase().prepare("SELECT * FROM tasks WHERE id = ?").get(id);
49
+ return row ? rowToTask(row) : null;
50
+ }
97
51
  /**
98
52
  * Update task status and progress
99
53
  */
@@ -141,39 +95,7 @@ export function listTasks(status, limit = 50) {
141
95
  sql += " ORDER BY created_at DESC LIMIT ?";
142
96
  params.push(limit);
143
97
  const rows = db.prepare(sql).all(...params);
144
- return rows.map(row => ({
145
- id: row.id,
146
- status: row.status,
147
- progress: row.progress,
148
- source_meeting_id: row.source_meeting_id || null,
149
- metadata: row.metadata ? JSON.parse(row.metadata) : {},
150
- result_uri: row.result_uri || null,
151
- error_message: row.error_message || null,
152
- created_at: row.created_at,
153
- updated_at: row.updated_at,
154
- ttl: row.ttl || null
155
- }));
156
- }
157
- /**
158
- * Get tasks by meeting ID
159
- */
160
- export function getTasksByMeeting(meetingId) {
161
- const db = getDatabase();
162
- const rows = db.prepare(`
163
- SELECT * FROM tasks WHERE source_meeting_id = ? ORDER BY created_at DESC
164
- `).all(meetingId);
165
- return rows.map(row => ({
166
- id: row.id,
167
- status: row.status,
168
- progress: row.progress,
169
- source_meeting_id: row.source_meeting_id || null,
170
- metadata: row.metadata ? JSON.parse(row.metadata) : {},
171
- result_uri: row.result_uri || null,
172
- error_message: row.error_message || null,
173
- created_at: row.created_at,
174
- updated_at: row.updated_at,
175
- ttl: row.ttl || null
176
- }));
98
+ return rows.map(rowToTask);
177
99
  }
178
100
  /**
179
101
  * Cancel a pending or running task
@@ -187,26 +109,30 @@ export function cancelTask(id) {
187
109
  updateTask(id, { status: "cancelled" });
188
110
  return true;
189
111
  }
112
+ /**
113
+ * Mark pending/running tasks whose TTL has elapsed as failed.
114
+ */
115
+ export function expireTasks(now = Date.now()) {
116
+ return getDatabase().prepare(`
117
+ UPDATE tasks
118
+ SET status = 'failed', error_message = 'TTL expired', updated_at = ?
119
+ WHERE status IN ('pending', 'running')
120
+ AND ttl IS NOT NULL
121
+ AND ROUND(unixepoch(created_at, 'subsec') * 1000) + ttl < ?
122
+ `).run(new Date(now).toISOString(), now).changes;
123
+ }
190
124
  /**
191
125
  * Delete completed/failed/cancelled tasks older than specified age
192
126
  */
193
- export function cleanupTasks(maxAgeMs = TASK_CLEANUP_MAX_AGE_MS) {
194
- const db = getDatabase();
195
- const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
196
- const result = db.prepare(`
127
+ export function cleanupTasks(maxAgeMs = TASK_CLEANUP_MAX_AGE_MS, now = Date.now()) {
128
+ const cutoff = new Date(now - maxAgeMs).toISOString();
129
+ return getDatabase().prepare(`
197
130
  DELETE FROM tasks
198
131
  WHERE status IN ('completed', 'failed', 'cancelled')
199
132
  AND updated_at < ?
200
- `).run(cutoff);
201
- return result.changes;
133
+ `).run(cutoff).changes;
202
134
  }
203
- /**
204
- * Get active (pending/running) task count
205
- */
206
- export function getActiveTaskCount() {
207
- const db = getDatabase();
208
- const row = db.prepare(`
209
- SELECT COUNT(*) as count FROM tasks WHERE status IN ('pending', 'running')
210
- `).get();
211
- return row.count;
135
+ /** Expire overdue tasks, then purge finished ones past the retention window. */
136
+ export function runTaskMaintenance(now = Date.now()) {
137
+ return { expired: expireTasks(now), removed: cleanupTasks(TASK_CLEANUP_MAX_AGE_MS, now) };
212
138
  }
package/build/types.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export const MESSAGE_CATEGORIES = ["MEETING_START", "PROPOSAL", "DECISION", "UPDATE", "CHAT"];
@@ -20,36 +20,39 @@
20
20
  ▼ ▼ ▼
21
21
  MCP-A MCP-B MCP-C
22
22
  (Win) (WSL) (VM)
23
- Stateless protocol adapter, one per IDE
23
+ Lightweight protocol adapter, one per MCP connection
24
24
  ```
25
25
 
26
26
  ### Core Principles
27
27
 
28
28
  1. **Daemon is the single source of truth** — all reads/writes, business logic, and tool definitions live in the daemon.
29
- 2. **MCP is a stateless protocol adapter** — no tool definitions, no hardcoded names, no local data.
30
- 3. **Daemon decides tool capability** — MCP fetches `GET /api/tools` at startup; any daemon upgrade is immediately visible to all connected MCPs.
31
- 4. **No local fallback** — if the daemon is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation.
32
- 5. **Plain HTTP between MCP and daemon** — no SSE; AI is request-driven and doesn't need push.
29
+ 2. **MCP holds connection identity only** — no hardcoded tool names or local persistent data; sessions and inboxes live in the daemon.
30
+ 3. **Daemon decides tool capability** — MCP fetches `GET /api/tools` at startup and reconnect. Session inboxes require both the session-aware adapter and daemon; legacy clients can continue using the existing shared-message APIs.
31
+ 4. **No local fallback** — if the daemon is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation. A tool call the daemon rejects (validation, not found, permission) is returned to the IDE as an error result (`isError: true`) and does not affect the connection; an unknown tool name is a JSON-RPC `-32602` error.
32
+ 5. **Plain HTTP between MCP and daemon** — inboxes are pulled explicitly; heartbeats maintain presence but do not wake the model.
33
33
 
34
34
  ### MCP Startup Flow
35
35
 
36
36
  ```
37
37
  Read NEXUS_ENDPOINT (default: http://127.0.0.1:5688)
38
38
  │
39
- ├─ Connect stdio transport → IDE sees MCP as ready
39
+ ├─ Serve stdio (MCP TypeScript SDK v2, serveStdio) and GET /api/tools once;
40
+ │ the first tools/list waits up to 1 s for that fetch
41
+ │ ├─ 2026-07-28 clients: server/discover, per-request _meta envelope,
42
+ │ │ list_changed delivered on subscriptions/listen
43
+ │ └─ 2025-era clients: initialize handshake, unsolicited list_changed
40
44
  │
41
- └─ Background retry loop (every 3 s)
42
- │
43
- ├─ GET /api/tools fails
44
- │ → log "[n2n-nexus] Waiting for daemon..."
45
- │ → keep retrying, do not exit
46
- │
47
- └─ GET /api/tools succeeds
48
- → cache tool list
49
- → send notifications/tools/list_changed
50
- → IDE re-fetches: tools appear
45
+ ├─ If the daemon was down: retry loop (every 3 s)
46
+ │ └─ GET /api/tools succeeds → cache tool list
47
+ │ → notifications/tools/list_changed → IDE re-fetches
48
+ │
49
+ └─ stdin closes → stop retrying, close, exit
51
50
  ```
52
51
 
52
+ The adapter's instance ID (read cursors, meeting permissions) uses the first client name seen on the connection, from the request's `io.modelcontextprotocol/clientInfo` (2026-07-28) or from `initialize` (2025 era), unless `NEXUS_INSTANCE_ID` / `--id` pins it.
53
+
54
+ Each MCP connection also gets a random session ID, registered on its first tool request and refreshed every 20 seconds with a 60-second presence lease. Disconnect marks it offline; an adapter restart creates a new session, so pending messages for the old session are not transferred. Multiple host chats sharing a connection share its inbox. Project association is directory metadata, not an implicit target for project tools. Registration and disconnect use `POST /api/sessions/register` and `POST /api/sessions/disconnect`; direct tools receive session identity outside their model-supplied arguments. This remains a trusted local daemon, not an authentication boundary. See [tool reference](TOOLS_REFERENCE.md#session-directory-and-direct-inboxes).
55
+
53
56
  ---
54
57
 
55
58
  ## 💾 Data Persistence
@@ -60,7 +63,6 @@ All data lives under the daemon's storage root (default `~/.n2n-nexus`):
60
63
  ~/.n2n-nexus/
61
64
  ├── global/
62
65
  │ ├── blueprint.md # Master strategy document
63
- │ ├── discussion.json # Global chat (JSON fallback)
64
66
  │ ├── docs_index.json # Global docs index
65
67
  │ └── docs/ # Shared markdown documents
66
68
  ├── projects/
@@ -68,14 +70,17 @@ All data lives under the daemon's storage root (default `~/.n2n-nexus`):
68
70
  │ ├── manifest.json # Project metadata
69
71
  │ ├── internal_blueprint.md # Internal technical docs
70
72
  │ └── assets/ # Binary assets (images, PDFs)
71
- ├── meetings/ # Meeting files (JSON fallback mode)
72
73
  ├── registry.json # Global project index
73
- └── nexus.db # SQLite database (meetings, tasks, cursors)
74
+ └── nexus.db # SQLite database (meetings, global chat, events, tasks, cursors)
74
75
  ```
75
76
 
76
- **SQLite WAL mode**: Only the daemon process writes to SQLite directly. MCP processes never touch the DB — they go through HTTP, which serializes all writes naturally.
77
+ **SQLite WAL mode**: Only the daemon process opens the database. MCP adapters never touch it — they go through HTTP.
78
+
79
+ **File-store writes**: HTTP requests are handled concurrently, so every mutation of `registry.json`, manifests and the docs index is queued behind one daemon-wide lock and written atomically (temp file + rename).
77
80
 
78
- **Self-healing**: Core JSON files (`registry.json`, `discussion.json`) are auto-repaired if corrupted or missing.
81
+ **Activity log**: state changes made through the daemon (project sync/rename/delete, meeting start/end, doc sync, ...) are recorded as structured events in SQLite and read with `list_events` / `GET /api/events`; they are not mixed into chat streams.
82
+
83
+ **Self-healing**: `registry.json` is auto-repaired if corrupted or missing.
79
84
 
80
85
  ---
81
86
 
@@ -142,38 +147,40 @@ npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
142
147
  ### Tool capability (MCP-facing)
143
148
  ```
144
149
  GET /api/tools Return full tool definition list (JSON Schema)
145
- POST /api/tools/call Execute tool call { tool, args, instanceId }
150
+ POST /api/tools/call Execute tool call { tool, args, instanceId, sessionId? }
146
151
  ```
147
152
 
148
153
  ### System
149
154
  ```
150
155
  GET /health Health status + version
151
- GET /api/storage/info Storage mode and stats
152
156
  ```
153
157
 
154
- ### Session & Projects
158
+ ### Projects
155
159
  ```
156
- POST /api/session/register
157
160
  POST /api/projects/sync
158
161
  POST /api/projects/update
159
162
  POST /api/projects/rename
160
163
  POST /api/projects/delete
161
164
  GET /api/projects/search
162
165
  GET /api/projects/topology
166
+ GET /api/projects/:projectId
163
167
  ```
164
168
 
165
169
  ### Messages & Global
166
170
  ```
167
171
  POST /api/messages/send
168
172
  GET /api/messages/unread
173
+ GET /api/events
169
174
  GET /api/global/docs
170
175
  GET /api/global/docs/:docId
171
176
  POST /api/global/docs/:docId
177
+ GET /api/global/strategy
172
178
  POST /api/global/strategy
173
179
  ```
174
180
 
175
181
  ### Meetings
176
182
  ```
183
+ GET /api/meetings
177
184
  POST /api/meetings/start
178
185
  POST /api/meetings/end
179
186
  POST /api/meetings/archive
@@ -197,6 +204,10 @@ POST /api/maintenance/logs
197
204
 
198
205
  ---
199
206
 
207
+ ### Request guards
208
+
209
+ The daemon rejects browser-originated requests (`Origin` header or cross-site `Sec-Fetch-Site`: 403), `Host` names that are neither IP literals nor local/allow-listed names (403, prevents DNS rebinding), non-JSON POST bodies (415) and bodies above 16 MB (413). Tool calls are rate limited per instance and per client address (429 with `Retry-After`). See `SECURITY.md`.
210
+
200
211
  ## Scope Boundaries
201
212
 
202
213
  - No cloud bridge by default.
@@ -20,36 +20,39 @@
20
20
  ▼ ▼ ▼
21
21
  MCP-A MCP-B MCP-C
22
22
  (Win) (WSL) (VM)
23
- 无状态协议适配器,每个 IDE 一个
23
+ 轻量协议适配器,每条 MCP 连接一个
24
24
  ```
25
25
 
26
26
  ### 核心原则
27
27
 
28
28
  1. **Daemon 是唯一的真相来源** — 所有读写、业务逻辑和工具定义都在 daemon 侧。
29
- 2. **MCP 是无状态协议适配器** — 不含工具定义、不含 hardcode 工具名、不存本地数据。
30
- 3. **Daemon 决定工具能力** — MCP 启动时拉取 `GET /api/tools`;daemon 升级新工具后,所有已连接 MCP 立即获得新能力,无需更新。
31
- 4. **无本地降级** — daemon 不可达时,MCP 每 3 秒重试并报告错误,不做本地 fallback(避免数据分裂)。
32
- 5. **MCP ↔ Daemon 全部走普通 HTTP** — 不使用 SSE;AI 是请求驱动模型,不需要推送。
29
+ 2. **MCP 只维护连接身份** — 不含 hardcode 工具名、不存本地持久化数据;会话目录和收件箱在 daemon。
30
+ 3. **Daemon 决定工具能力** — MCP 启动和重连时拉取 `GET /api/tools`。个人收件箱需要适配器和 daemon 都支持会话身份;旧客户端仍可使用原有共享消息接口。
31
+ 4. **无本地降级** — daemon 不可达时,MCP 每 3 秒重试并报告错误,不做本地 fallback(避免数据分裂)。daemon 拒绝的工具调用(校验失败、不存在、无权限)作为错误结果(`isError: true`)返回给 IDE,不影响连接;未知工具名返回 JSON-RPC `-32602` 错误。
32
+ 5. **MCP ↔ Daemon 全部走普通 HTTP** — 收件箱由助手主动拉取;心跳只维护在线状态,不唤醒模型。
33
33
 
34
34
  ### MCP 启动流程
35
35
 
36
36
  ```
37
37
  读取 NEXUS_ENDPOINT(默认 http://127.0.0.1:5688)
38
38
  │
39
- ├─ 连接 stdio transport → IDE 认为 MCP 已就绪
39
+ ├─ 开始 stdio 服务(MCP TypeScript SDK v2,serveStdio),同时拉一次 GET /api/tools;
40
+ │ 第一次 tools/list 最多等这次拉取 1 秒
41
+ │ ├─ 2026-07-28 客户端:server/discover、每请求 _meta 信封,
42
+ │ │ list_changed 经 subscriptions/listen 下发
43
+ │ └─ 2025 代客户端:initialize 握手,主动下发 list_changed
40
44
  │
41
- └─ 后台重试循环(每 3 秒)
42
- │
43
- ├─ GET /api/tools 失败
44
- │ → 打印 "[n2n-nexus] Waiting for daemon..."
45
- │ → 继续等待,不退出
46
- │
47
- └─ GET /api/tools 成功
48
- → 缓存工具列表
49
- → 发送 notifications/tools/list_changed
50
- → IDE 重新拉取,工具出现
45
+ ├─ 若 daemon 不在线:重试循环(每 3 秒)
46
+ │ └─ GET /api/tools 成功 → 缓存工具列表
47
+ │ → notifications/tools/list_changed → IDE 重新拉取
48
+ │
49
+ └─ stdin 关闭 → 停止重试、关闭、退出
51
50
  ```
52
51
 
52
+ 适配器的实例 ID(未读游标、会议权限以它为键)取自该连接上第一次见到的客户端名,来源是请求 `_meta` 中的 `io.modelcontextprotocol/clientInfo`(2026-07-28)或 `initialize`(2025 代),`NEXUS_INSTANCE_ID` / `--id` 可固定。
53
+
54
+ 每条 MCP 连接还会生成随机会话 ID,在首次工具请求时注册,每 20 秒刷新一次心跳,在线租约为 60 秒。断连后标记离线;适配器重启会创建新会话,旧会话的待收消息不会转移。宿主多个聊天共用连接时也共用收件箱。项目关联仅用于目录筛选,不作为项目工具的隐式操作目标。注册和断连分别使用 `POST /api/sessions/register` 与 `POST /api/sessions/disconnect`,工具请求的会话身份放在模型参数之外。系统仍采用可信本地 daemon 边界,这不是身份认证机制。精确契约见[工具参考](TOOLS_REFERENCE.md#session-directory-and-direct-inboxes)。
55
+
53
56
  ---
54
57
 
55
58
  ## 💾 数据持久化
@@ -60,7 +63,6 @@ MCP-A MCP-B MCP-C
60
63
  ~/.n2n-nexus/
61
64
  ├── global/
62
65
  │ ├── blueprint.md # 主战略文档
63
- │ ├── discussion.json # 全局聊天(JSON 回退)
64
66
  │ ├── docs_index.json # 全局文档索引
65
67
  │ └── docs/ # 共享 Markdown 文档
66
68
  ├── projects/
@@ -68,14 +70,17 @@ MCP-A MCP-B MCP-C
68
70
  │ ├── manifest.json # 项目元数据
69
71
  │ ├── internal_blueprint.md # 内部技术文档
70
72
  │ └── assets/ # 二进制资产(图片、PDF)
71
- ├── meetings/ # 会议文件(JSON 回退模式)
72
73
  ├── registry.json # 全局项目索引
73
- └── nexus.db # SQLite 数据库(会议、任务、游标)
74
+ └── nexus.db # SQLite 数据库(会议、全局聊天、事件、任务、游标)
74
75
  ```
75
76
 
76
- **SQLite WAL 模式**:只有 daemon 进程直接写入 SQLite。MCP 进程通过 HTTP 间接访问,天然串行,无并发冲突。
77
+ **SQLite WAL 模式**:只有 daemon 进程打开数据库。MCP 适配器不直接访问,全部经由 HTTP。
78
+
79
+ **文件存储写入**:HTTP 请求是并发处理的,因此对 `registry.json`、manifest 和文档索引的每次修改都排在一把 daemon 级写锁后面,并以原子方式写入(临时文件 + rename)。
77
80
 
78
- **自我修复**:核心 JSON 文件(`registry.json`、`discussion.json`)在损坏或丢失时自动重建初始状态。
81
+ **活动日志**:经由 daemon 发生的状态变更(项目同步/重命名/删除、会议开始/结束、文档同步等)以结构化事件记录在 SQLite 中,通过 `list_events` / `GET /api/events` 读取,不混入聊天流。
82
+
83
+ **自我修复**:`registry.json` 在损坏或丢失时自动重建初始状态。
79
84
 
80
85
  ---
81
86
 
@@ -110,7 +115,7 @@ n2n-nexus
110
115
  │
111
116
  ├─ n2n-nexus daemon 用户手动启动一次,持续运行,持有所有数据。
112
117
  │
113
- └─ n2n-nexus mcp IDE 通过 npx 自动启动,无状态代理。
118
+ └─ n2n-nexus mcp IDE 通过 npx 自动启动,维护连接身份。
114
119
  ```
115
120
 
116
121
  **Daemon**(用户手动启动):
@@ -142,38 +147,40 @@ npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
142
147
  ### 工具能力(MCP 对接)
143
148
  ```
144
149
  GET /api/tools 返回完整工具定义列表(JSON Schema)
145
- POST /api/tools/call 执行工具调用 { tool, args, instanceId }
150
+ POST /api/tools/call 执行工具调用 { tool, args, instanceId, sessionId? }
146
151
  ```
147
152
 
148
153
  ### 系统
149
154
  ```
150
155
  GET /health 健康状态 + 版本信息
151
- GET /api/storage/info 存储模式和统计
152
156
  ```
153
157
 
154
- ### 会话与项目
158
+ ### 项目
155
159
  ```
156
- POST /api/session/register
157
160
  POST /api/projects/sync
158
161
  POST /api/projects/update
159
162
  POST /api/projects/rename
160
163
  POST /api/projects/delete
161
164
  GET /api/projects/search
162
165
  GET /api/projects/topology
166
+ GET /api/projects/:projectId
163
167
  ```
164
168
 
165
169
  ### 消息与全局协作
166
170
  ```
167
171
  POST /api/messages/send
168
172
  GET /api/messages/unread
173
+ GET /api/events
169
174
  GET /api/global/docs
170
175
  GET /api/global/docs/:docId
171
176
  POST /api/global/docs/:docId
177
+ GET /api/global/strategy
172
178
  POST /api/global/strategy
173
179
  ```
174
180
 
175
181
  ### 会议
176
182
  ```
183
+ GET /api/meetings
177
184
  POST /api/meetings/start
178
185
  POST /api/meetings/end
179
186
  POST /api/meetings/archive
@@ -197,6 +204,10 @@ POST /api/maintenance/logs
197
204
 
198
205
  ---
199
206
 
207
+ ### 请求防护
208
+
209
+ daemon 拒绝来自浏览器的请求(带 `Origin` 头或跨站 `Sec-Fetch-Site`:403)、既非 IP 也不在本地/允许列表中的 `Host`(403,防 DNS rebinding)、非 JSON 的 POST 请求体(415)以及超过 16 MB 的请求体(413)。工具调用按实例和客户端地址限流(429,带 `Retry-After`)。详见 `SECURITY.md`。
210
+
200
211
  ## 边界说明
201
212
 
202
213
  - 不默认提供云端桥接。