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
@@ -0,0 +1,177 @@
1
+ import { z } from "zod";
2
+ import { MESSAGE_CATEGORIES } from "../types.js";
3
+ import { TASK_STATUSES } from "../storage/tasks.js";
4
+ /**
5
+ * Result contracts, one per tool. Every result passes through its contract on
6
+ * the way out of the daemon (`finalizeResult`): undeclared fields are stripped
7
+ * and a result that cannot satisfy the contract becomes an error instead of
8
+ * structuredContent the client would reject. TypeScript only catches drift in
9
+ * object literals, so this runtime pass is the real guarantee. The JSON Schema
10
+ * is published as the tool's `outputSchema` (with additionalProperties:false,
11
+ * which the stripping makes true). Every schema has an object root, as
12
+ * 2025-era clients require.
13
+ */
14
+ const ack = { ok: z.literal(true), message: z.string() };
15
+ const sessionSchema = z.object({
16
+ sessionId: z.string(), instanceId: z.string(), name: z.string(),
17
+ clientName: z.string(), workspace: z.string(), projectId: z.string().nullable(),
18
+ inbound: z.enum(["accept", "refuse"]), reception: z.literal("pull"),
19
+ online: z.boolean(), createdAt: z.string(), lastSeenAt: z.string()
20
+ });
21
+ const deliveryStatus = z.enum(["accepted", "fetched", "acknowledged", "replied", "refused", "expired"]);
22
+ const directMessageSchema = z.object({
23
+ messageId: z.string(), from: z.string(), to: z.string(), text: z.string(),
24
+ category: z.enum(MESSAGE_CATEGORIES).optional(), replyTo: z.string().nullable(),
25
+ status: deliveryStatus, createdAt: z.string(), expiresAt: z.string(),
26
+ fetchedAt: z.string().nullable(), acknowledgedAt: z.string().nullable(),
27
+ repliedAt: z.string().nullable(), reason: z.string().nullable()
28
+ });
29
+ const messageSchema = z.object({
30
+ id: z.number().int().optional(),
31
+ from: z.string(),
32
+ text: z.string(),
33
+ category: z.enum(MESSAGE_CATEGORIES).nullish(),
34
+ timestamp: z.string()
35
+ });
36
+ const taskSchema = z.object({
37
+ id: z.string(),
38
+ status: z.enum(TASK_STATUSES),
39
+ progress: z.number(),
40
+ source_meeting_id: z.string().nullable(),
41
+ metadata: z.record(z.string(), z.unknown()),
42
+ result_uri: z.string().nullable(),
43
+ error_message: z.string().nullable(),
44
+ created_at: z.string(),
45
+ updated_at: z.string(),
46
+ ttl: z.number().nullable()
47
+ });
48
+ const topologyNode = z.object({ id: z.string(), name: z.string(), summary: z.string(), lastActive: z.string() });
49
+ const meetingStatus = z.enum(["active", "closed", "archived"]);
50
+ /**
51
+ * Manifest as read back from storage. Deliberately looser than the input
52
+ * contract: manifests written by older releases were not validated, so
53
+ * ids and enum-like fields are plain strings here.
54
+ */
55
+ const storedManifest = z.object({
56
+ id: z.string(),
57
+ name: z.string(),
58
+ description: z.string(),
59
+ techStack: z.array(z.string()),
60
+ relations: z.array(z.object({ targetId: z.string(), type: z.string() })),
61
+ lastUpdated: z.string(),
62
+ repositoryUrl: z.string(),
63
+ localPath: z.string(),
64
+ endpoints: z.array(z.object({ name: z.string(), url: z.string(), description: z.string() })),
65
+ apiSpec: z.array(z.object({ method: z.string(), path: z.string(), summary: z.string() })),
66
+ billing: z.object({ plain: z.string(), price: z.string() }).optional(),
67
+ apiDependencies: z.record(z.string(), z.string()).optional(),
68
+ gatewayCompatibility: z.string().optional(),
69
+ api_versions: z.record(z.string(), z.string()).optional(),
70
+ feature_tier: z.string().optional()
71
+ });
72
+ export const TOOL_OUTPUT_SCHEMAS = {
73
+ sync_project_assets: z.object({ ...ack, projectId: z.string() }),
74
+ upload_project_asset: z.object({ ...ack, projectId: z.string(), fileName: z.string() }),
75
+ update_project: z.object({ ...ack, projectId: z.string() }),
76
+ rename_project: z.object({ ...ack, oldId: z.string(), newId: z.string() }),
77
+ search_projects: z.object({
78
+ query: z.string(),
79
+ count: z.number().int(),
80
+ results: z.array(z.object({ id: z.string(), name: z.string(), description: z.string().optional() }))
81
+ }),
82
+ get_global_topology: z.object({
83
+ mode: z.enum(["list", "focused"]),
84
+ summary: z.object({ totalProjects: z.number().int(), totalEdges: z.number().int() }).optional()
85
+ .describe("list mode only"),
86
+ projects: z.record(z.string(), topologyNode).optional().describe("list mode only"),
87
+ nodes: z.array(topologyNode).optional().describe("focused mode only"),
88
+ edges: z.array(z.object({
89
+ from: z.string(), to: z.string(),
90
+ type: z.string()
91
+ })).optional().describe("focused mode only")
92
+ }),
93
+ get_project: z.object({
94
+ manifest: storedManifest,
95
+ internalDocs: z.string().optional()
96
+ }),
97
+ list_sessions: z.object({ self: sessionSchema.nullable(), sessions: z.array(sessionSchema), count: z.number().int() }),
98
+ update_session: z.object({ session: sessionSchema }),
99
+ read_inbox: z.object({ sessionId: z.string(), messages: z.array(directMessageSchema), count: z.number().int() }),
100
+ get_message: z.object({ message: directMessageSchema }),
101
+ acknowledge_message: z.object({ message: directMessageSchema }),
102
+ send_message: z.object({
103
+ ...ack,
104
+ sentTo: z.enum(["meeting", "global", "session"]),
105
+ meetingId: z.string().optional(),
106
+ messageId: z.union([z.number().int(), z.string()]).optional(),
107
+ status: deliveryStatus.optional(),
108
+ to: z.string().optional(),
109
+ replyTo: z.string().nullable().optional()
110
+ }),
111
+ read_messages: z.object({
112
+ source: z.enum(["meeting", "global"]),
113
+ meetingId: z.string().optional(),
114
+ messages: z.array(messageSchema),
115
+ count: z.number().int()
116
+ }),
117
+ list_events: z.object({
118
+ count: z.number().int(),
119
+ events: z.array(z.object({
120
+ id: z.number().int(),
121
+ timestamp: z.string(),
122
+ actor: z.string(),
123
+ action: z.string(),
124
+ target: z.string().optional(),
125
+ detail: z.record(z.string(), z.unknown()).optional()
126
+ }))
127
+ }),
128
+ get_global_strategy: z.object({ content: z.string() }),
129
+ update_global_strategy: z.object(ack),
130
+ list_global_docs: z.object({
131
+ count: z.number().int(),
132
+ docs: z.array(z.object({ docId: z.string(), title: z.string(), lastUpdated: z.string(), updatedBy: z.string() }))
133
+ }),
134
+ get_global_doc: z.object({
135
+ docId: z.string(), title: z.string(), lastUpdated: z.string(), updatedBy: z.string(), content: z.string()
136
+ }),
137
+ sync_global_doc: z.object(ack),
138
+ list_meetings: z.object({
139
+ count: z.number().int(),
140
+ activeMeetingId: z.string().nullable(),
141
+ meetings: z.array(z.object({
142
+ id: z.string(),
143
+ topic: z.string(),
144
+ status: meetingStatus,
145
+ initiator: z.string(),
146
+ startTime: z.string(),
147
+ endTime: z.string().optional(),
148
+ participants: z.array(z.string()),
149
+ messageCount: z.number().int(),
150
+ decisionCount: z.number().int(),
151
+ summary: z.string().optional()
152
+ }))
153
+ }),
154
+ start_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), topic: z.string(), status: meetingStatus }),
155
+ end_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), status: z.literal("closed"), summary: z.string().optional() }),
156
+ archive_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), status: z.literal("archived") }),
157
+ reopen_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), status: z.literal("active") }),
158
+ create_task: z.object({ task_id: z.string(), status: z.enum(TASK_STATUSES), message: z.string(), task: taskSchema }),
159
+ get_task: taskSchema,
160
+ list_tasks: z.object({ count: z.number().int(), tasks: z.array(taskSchema) }),
161
+ cancel_task: z.object({ ...ack, taskId: z.string() }),
162
+ host_maintenance: z.object({ ...ack, removed: z.number().int(), remaining: z.number().int() }),
163
+ host_delete_project: z.object({ ...ack, projectId: z.string() })
164
+ };
165
+ export class ResultContractError extends Error {
166
+ constructor(tool, issues) {
167
+ super(`Result of ${tool} does not match its contract:\n${issues}`);
168
+ this.name = "ResultContractError";
169
+ }
170
+ }
171
+ /** Validate a handler result against its contract and strip undeclared fields. */
172
+ export function finalizeResult(name, result) {
173
+ const parsed = TOOL_OUTPUT_SCHEMAS[name].safeParse(result);
174
+ if (!parsed.success)
175
+ throw new ResultContractError(name, z.prettifyError(parsed.error));
176
+ return parsed.data;
177
+ }
@@ -0,0 +1,321 @@
1
+ import { z } from "zod";
2
+ import { MESSAGE_CATEGORIES } from "../types.js";
3
+ import { TASK_STATUSES } from "../storage/tasks.js";
4
+ import { TOOL_OUTPUT_SCHEMAS } from "./tool-outputs.js";
5
+ /**
6
+ * Tool contracts. Each zod schema is the single source of truth: it is
7
+ * exported to MCP clients as JSON Schema (GET /api/tools) and used to
8
+ * validate every call before a handler runs, for both the MCP tool path
9
+ * and the REST endpoints.
10
+ */
11
+ const SAFE_ID = /^[a-zA-Z0-9._-]+$/;
12
+ const safeId = (label) => z.string().min(1).regex(SAFE_ID, `${label} may contain only letters, numbers, dots, underscores and hyphens`);
13
+ export const projectIdSchema = safeId("Project ID").describe("Project ID with prefix, e.g. 'web_example.com' or 'mcp_nexus'");
14
+ const docIdSchema = safeId("Document ID").describe("Stable document ID");
15
+ const positiveInt = (max) => z.number().int().min(1).max(max);
16
+ export const manifestSchema = z.object({
17
+ id: projectIdSchema,
18
+ name: z.string().min(1),
19
+ description: z.string(),
20
+ techStack: z.array(z.string()),
21
+ relations: z.array(z.object({
22
+ targetId: projectIdSchema,
23
+ type: z.enum(["dependency", "parent", "child", "related"])
24
+ })),
25
+ repositoryUrl: z.string(),
26
+ localPath: z.string().describe("Physical disk path of the project"),
27
+ endpoints: z.array(z.object({ name: z.string(), url: z.string(), description: z.string() })),
28
+ apiSpec: z.array(z.object({ method: z.string(), path: z.string(), summary: z.string() })),
29
+ billing: z.object({ plain: z.string(), price: z.string() }).optional(),
30
+ lastUpdated: z.string().optional(),
31
+ apiDependencies: z.record(z.string(), z.string()).optional().describe("projectId → version constraint"),
32
+ gatewayCompatibility: z.string().optional(),
33
+ api_versions: z.record(z.string(), z.string()).optional(),
34
+ feature_tier: z.enum(["standard", "advanced", "team"]).optional()
35
+ }).describe("Project manifest");
36
+ export const manifestPatchSchema = manifestSchema.omit({ id: true, lastUpdated: true }).partial().strict()
37
+ .describe("Manifest fields to update. 'id' cannot be changed here; use rename_project.");
38
+ /** Tools without parameters: the spec recommends a schema that accepts only `{}`. */
39
+ const noParams = z.strictObject({});
40
+ export const TOOL_SCHEMAS = {
41
+ // Projects
42
+ sync_project_assets: z.object({
43
+ manifest: manifestSchema,
44
+ internalDocs: z.string().describe("Markdown implementation guide")
45
+ }),
46
+ upload_project_asset: z.object({
47
+ projectId: projectIdSchema,
48
+ fileName: z.string().min(1).describe("Filename only; unsafe characters are replaced"),
49
+ base64Content: z.string().min(1)
50
+ }),
51
+ update_project: z.object({ projectId: projectIdSchema, patch: manifestPatchSchema }),
52
+ rename_project: z.object({ oldId: projectIdSchema, newId: projectIdSchema }),
53
+ search_projects: z.object({
54
+ query: z.string().min(1),
55
+ limit: positiveInt(200).default(10)
56
+ }),
57
+ get_global_topology: z.object({
58
+ projectId: projectIdSchema.optional().describe("Focus on one project (optional)")
59
+ }),
60
+ get_project: z.object({
61
+ projectId: projectIdSchema,
62
+ includeDocs: z.boolean().default(true).describe("Also return the internal docs (Markdown)")
63
+ }),
64
+ // Messaging
65
+ list_sessions: z.object({
66
+ onlineOnly: z.boolean().default(true),
67
+ projectId: projectIdSchema.optional(),
68
+ limit: positiveInt(500).default(100)
69
+ }),
70
+ update_session: z.object({
71
+ name: z.string().min(1).optional(),
72
+ projectId: projectIdSchema.nullable().optional(),
73
+ inbound: z.enum(["accept", "refuse"]).optional()
74
+ }),
75
+ read_inbox: z.object({ count: positiveInt(500).default(10) }),
76
+ get_message: z.object({ messageId: z.string().min(1) }),
77
+ acknowledge_message: z.object({
78
+ messageId: z.string().min(1),
79
+ outcome: z.enum(["acknowledged", "refused"]).default("acknowledged")
80
+ }),
81
+ send_message: z.object({
82
+ message: z.string().min(1),
83
+ category: z.enum(MESSAGE_CATEGORIES).optional(),
84
+ to: z.string().min(1).optional().describe("Exact sessionId from list_sessions; mutually exclusive with meetingId"),
85
+ replyTo: z.string().min(1).optional().describe("Direct message being answered; requires to = original sender"),
86
+ ttlMs: positiveInt(Number.MAX_SAFE_INTEGER).optional().describe("Direct-message expiry in milliseconds; defaults to 24 hours"),
87
+ meetingId: z.string().min(1).optional().describe("Target meeting. Defaults to the active meeting, or global chat when none is active.")
88
+ }),
89
+ read_messages: z.object({
90
+ count: positiveInt(500).default(10),
91
+ meetingId: z.string().min(1).optional()
92
+ }),
93
+ list_events: z.object({
94
+ limit: positiveInt(500).default(20),
95
+ action: z.string().min(1).optional().describe("Exact action or prefix such as 'project' or 'meeting'")
96
+ }),
97
+ get_global_strategy: noParams,
98
+ update_global_strategy: z.object({ content: z.string().min(1) }),
99
+ list_global_docs: noParams,
100
+ get_global_doc: z.object({ docId: docIdSchema }),
101
+ sync_global_doc: z.object({
102
+ docId: docIdSchema,
103
+ title: z.string().min(1),
104
+ content: z.string().min(1)
105
+ }),
106
+ // Meetings
107
+ list_meetings: z.object({
108
+ status: z.enum(["active", "closed", "archived"]).optional(),
109
+ limit: positiveInt(200).default(20)
110
+ }),
111
+ start_meeting: z.object({ topic: z.string().min(1) }),
112
+ end_meeting: z.object({
113
+ meetingId: z.string().min(1).optional().describe("Defaults to the active meeting"),
114
+ summary: z.string().optional()
115
+ }),
116
+ archive_meeting: z.object({ meetingId: z.string().min(1) }),
117
+ reopen_meeting: z.object({ meetingId: z.string().min(1) }),
118
+ // Tasks
119
+ create_task: z.object({
120
+ source_meeting_id: z.string().min(1).optional().describe("Link to a meeting for traceability"),
121
+ metadata: z.record(z.string(), z.unknown()).optional(),
122
+ ttl: positiveInt(Number.MAX_SAFE_INTEGER).optional().describe("TTL in milliseconds; a pending/running task past its TTL is marked failed")
123
+ }),
124
+ get_task: z.object({ taskId: z.string().min(1) }),
125
+ list_tasks: z.object({
126
+ status: z.enum(TASK_STATUSES).optional(),
127
+ limit: positiveInt(500).default(50)
128
+ }),
129
+ cancel_task: z.object({ taskId: z.string().min(1) }),
130
+ // Maintenance
131
+ host_maintenance: z.object({
132
+ action: z.enum(["prune", "clear"]),
133
+ count: z.number().int().min(0).default(0).describe("Oldest N global messages to remove (prune only)")
134
+ }),
135
+ host_delete_project: z.object({ projectId: projectIdSchema })
136
+ };
137
+ const TOOL_DESCRIPTIONS = {
138
+ sync_project_assets: "Sync full project manifest + internal docs.",
139
+ upload_project_asset: "Upload a binary file (base64) to a project's asset folder.",
140
+ update_project: "Patch project manifest fields (partial update).",
141
+ rename_project: "Rename project ID with cascading relation updates.",
142
+ search_projects: "Search project registry by name or description.",
143
+ get_global_topology: "Default: project list + stats. With projectId: detailed subgraph.",
144
+ get_project: "Read a project's full manifest and, by default, its internal docs.",
145
+ list_sessions: "Discover MCP connections and your own session. Online means a recent connection heartbeat, not model readiness. Delivery is pull-only.",
146
+ update_session: "Set your session name, project association or incoming-message policy. Refusing also rejects pending inbox messages.",
147
+ read_inbox: "Fetch your pending direct messages, oldest first. Unacknowledged messages remain retrievable; fetching does not confirm processing.",
148
+ get_message: "Read a direct message and its receipt status; only the sender and recipient may query it.",
149
+ acknowledge_message: "Explicitly acknowledge or refuse a fetched direct message. Acknowledgement confirms receipt, not completed work.",
150
+ send_message: "Send directly to an exact sessionId using to, optionally replying with replyTo. Otherwise post to meetingId, the active meeting, or global chat. Direct accepted means stored, not read or a woken model.",
151
+ read_messages: "Read messages this instance has not read yet (cursor advances automatically). Global chat by default; pass meetingId for a meeting.",
152
+ list_events: "Activity log: structured records of state changes (project.sync, project.rename, meeting.end, ...), newest first.",
153
+ get_global_strategy: "Read the master strategy document (global/blueprint.md).",
154
+ update_global_strategy: "Overwrite the master strategy document (global/blueprint.md).",
155
+ list_global_docs: "List global shared documents: id, title, last update and author.",
156
+ get_global_doc: "Read a global shared document by docId, with its title and metadata.",
157
+ sync_global_doc: "Create/update a global shared document.",
158
+ list_meetings: "List meetings, newest first: id, topic, status, initiator, participants, message/decision counts, summary. Read messages with read_messages(meetingId).",
159
+ start_meeting: "Start new meeting session. Returns meeting ID.",
160
+ end_meeting: "End active meeting. Locks history.",
161
+ archive_meeting: "Archive closed meeting. Read-only after.",
162
+ reopen_meeting: "Reopen closed/archived meeting.",
163
+ create_task: "Create a task you track yourself. Returns taskId; update it via get_task/cancel_task.",
164
+ get_task: "Get task status and progress by ID.",
165
+ list_tasks: "List tasks with optional status filter.",
166
+ cancel_task: "Cancel pending/running task.",
167
+ host_maintenance: "Manage the global chat stream: 'prune' the oldest N messages or 'clear' all.",
168
+ host_delete_project: "Delete project. Irreversible."
169
+ };
170
+ /** Human-readable names clients show in their UI (MCP `title`). */
171
+ const TOOL_TITLES = {
172
+ sync_project_assets: "Sync Project",
173
+ upload_project_asset: "Upload Project Asset",
174
+ update_project: "Update Project",
175
+ rename_project: "Rename Project",
176
+ search_projects: "Search Projects",
177
+ get_global_topology: "Project Topology",
178
+ get_project: "Read Project",
179
+ send_message: "Send Message",
180
+ list_sessions: "List Sessions",
181
+ update_session: "Update My Session",
182
+ read_inbox: "Read My Inbox",
183
+ get_message: "Read Message Receipt",
184
+ acknowledge_message: "Acknowledge Message",
185
+ read_messages: "Read Messages",
186
+ list_events: "Activity Log",
187
+ get_global_strategy: "Read Strategy",
188
+ update_global_strategy: "Update Strategy",
189
+ list_global_docs: "List Shared Docs",
190
+ get_global_doc: "Read Shared Doc",
191
+ sync_global_doc: "Save Shared Doc",
192
+ list_meetings: "List Meetings",
193
+ start_meeting: "Start Meeting",
194
+ end_meeting: "End Meeting",
195
+ archive_meeting: "Archive Meeting",
196
+ reopen_meeting: "Reopen Meeting",
197
+ create_task: "Create Task",
198
+ get_task: "Read Task",
199
+ list_tasks: "List Tasks",
200
+ cancel_task: "Cancel Task",
201
+ host_maintenance: "Prune Global Chat",
202
+ host_delete_project: "Delete Project"
203
+ };
204
+ const readOnly = { readOnlyHint: true, openWorldHint: false };
205
+ /**
206
+ * Changes state. `destructive` follows the MCP definition: false only when the
207
+ * tool makes purely additive updates; anything that rewrites or removes
208
+ * existing state (status changes included) is destructive. `idempotent` =
209
+ * repeating the same call has no further effect.
210
+ */
211
+ const write = (destructive, idempotent) => ({ readOnlyHint: false, destructiveHint: destructive, idempotentHint: idempotent, openWorldHint: false });
212
+ const TOOL_ANNOTATIONS = {
213
+ sync_project_assets: write(true, true), // replaces manifest and docs
214
+ upload_project_asset: write(true, true), // overwrites a same-named asset
215
+ update_project: write(true, true), // replaces the patched fields
216
+ rename_project: write(true, false), // removes the old id, rewrites relations
217
+ search_projects: readOnly,
218
+ get_global_topology: readOnly,
219
+ get_project: readOnly,
220
+ send_message: write(true, false), // a direct reply also updates the original message's receipt
221
+ list_sessions: readOnly,
222
+ update_session: write(true, true),
223
+ read_inbox: write(false, true),
224
+ get_message: readOnly,
225
+ acknowledge_message: write(true, true),
226
+ read_messages: write(false, false), // only advances this instance's own unread cursor (bookkeeping, not shared data)
227
+ list_events: readOnly,
228
+ get_global_strategy: readOnly,
229
+ update_global_strategy: write(true, true), // overwrites the document
230
+ list_global_docs: readOnly,
231
+ get_global_doc: readOnly,
232
+ sync_global_doc: write(true, true), // overwrites the document
233
+ list_meetings: readOnly,
234
+ start_meeting: write(false, false),
235
+ end_meeting: write(true, false), // closes the meeting, sets summary
236
+ archive_meeting: write(true, false), // changes status
237
+ reopen_meeting: write(true, false), // changes status
238
+ create_task: write(false, false),
239
+ get_task: readOnly,
240
+ list_tasks: readOnly,
241
+ cancel_task: write(true, true), // irreversible status change
242
+ host_maintenance: write(true, false), // deletes chat messages
243
+ host_delete_project: write(true, true) // irreversible
244
+ };
245
+ function toOutputSchema(schema) {
246
+ const json = z.toJSONSchema(schema, { io: "output" });
247
+ delete json.$schema;
248
+ return json;
249
+ }
250
+ function toInputSchema(schema) {
251
+ const json = z.toJSONSchema(schema, { io: "input" });
252
+ delete json.$schema;
253
+ return json;
254
+ }
255
+ // Compile-time check: every tool has a result contract and no contract lacks a tool.
256
+ const _outputsCover = TOOL_OUTPUT_SCHEMAS;
257
+ const _outputsMatch = TOOL_SCHEMAS;
258
+ void _outputsCover;
259
+ void _outputsMatch;
260
+ export const TOOL_NAMES = Object.keys(TOOL_SCHEMAS);
261
+ /**
262
+ * Tools whose outputSchema is published in tools/list. Every tool has a result
263
+ * contract (and every result is sent as structuredContent), but the schemas of
264
+ * write tools only describe a short acknowledgement and nearly doubled the
265
+ * tools/list payload that sits in every model context, so only tools that
266
+ * return data publish theirs.
267
+ */
268
+ const PUBLISHED_OUTPUT_SCHEMAS = new Set([
269
+ "search_projects", "get_global_topology", "get_project",
270
+ "read_messages", "list_events", "list_sessions", "update_session", "read_inbox", "get_message", "acknowledge_message",
271
+ "get_global_strategy", "list_global_docs", "get_global_doc",
272
+ "list_meetings", "get_task", "list_tasks"
273
+ ]);
274
+ export const TOOL_DEFINITIONS = TOOL_NAMES.map((name) => ({
275
+ name,
276
+ title: TOOL_TITLES[name],
277
+ description: TOOL_DESCRIPTIONS[name],
278
+ ...(PUBLISHED_OUTPUT_SCHEMAS.has(name) ? { outputSchema: toOutputSchema(TOOL_OUTPUT_SCHEMAS[name]) } : {}),
279
+ inputSchema: toInputSchema(TOOL_SCHEMAS[name]),
280
+ annotations: TOOL_ANNOTATIONS[name]
281
+ }));
282
+ export function isToolName(name) {
283
+ return Object.prototype.hasOwnProperty.call(TOOL_SCHEMAS, name);
284
+ }
285
+ export class ToolValidationError extends Error {
286
+ constructor(tool, issues) {
287
+ super(`Invalid arguments for ${tool}:\n${issues}`);
288
+ this.name = "ToolValidationError";
289
+ }
290
+ }
291
+ /** Validate raw arguments against a tool's schema; unknown top-level keys are dropped. */
292
+ export function parseToolArgs(name, args) {
293
+ const result = TOOL_SCHEMAS[name].safeParse(args ?? {});
294
+ if (!result.success)
295
+ throw new ToolValidationError(name, z.prettifyError(result.error));
296
+ if (name === "send_message") {
297
+ const message = result.data;
298
+ if (message.to !== undefined && message.meetingId !== undefined) {
299
+ throw new ToolValidationError(name, "to and meetingId are mutually exclusive");
300
+ }
301
+ if (message.to === undefined && (message.replyTo !== undefined || message.ttlMs !== undefined)) {
302
+ throw new ToolValidationError(name, "replyTo and ttlMs require a direct recipient (to)");
303
+ }
304
+ }
305
+ return result.data;
306
+ }
307
+ export const sessionRegistrationSchema = z.object({
308
+ sessionId: z.string().min(1),
309
+ instanceId: z.string().min(1),
310
+ clientName: z.string().optional(),
311
+ workspace: z.string().optional()
312
+ });
313
+ export const sessionDisconnectSchema = sessionRegistrationSchema.pick({ sessionId: true, instanceId: true });
314
+ /** Schema for POST /api/tasks/:id/update (not an MCP tool). */
315
+ export const taskUpdateSchema = z.object({
316
+ status: z.enum(TASK_STATUSES).optional(),
317
+ progress: z.number().min(0).max(1).optional(),
318
+ result_uri: z.string().optional(),
319
+ error_message: z.string().optional(),
320
+ metadata: z.record(z.string(), z.unknown()).optional()
321
+ }).strict();
package/build/index.js CHANGED
@@ -17,7 +17,7 @@ OPTIONS:
17
17
  --root <path> Storage directory for daemon (default: ~/.n2n-nexus)
18
18
  --port <port> HTTP port for daemon (default: 5688)
19
19
  --host <host> Host address for daemon (default: 127.0.0.1)
20
- --id <id> Instance ID for MCP proxy
20
+ --id <id> Instance ID for MCP proxy (default: mcp-<hostname>-<cwd hash>-<IDE name>)
21
21
  --version, -v Show version
22
22
  --help, -h Show this help
23
23
 
@@ -0,0 +1,19 @@
1
+ import { createHash } from "node:crypto";
2
+ import os from "node:os";
3
+ /**
4
+ * Identity of this MCP adapter as seen by the daemon. Read cursors and
5
+ * meeting initiator permissions are keyed on it, so it has to
6
+ * survive an IDE restart: derive it from the machine, the workspace the IDE
7
+ * launched the adapter in and the IDE itself (two IDEs open on the same
8
+ * workspace must not share cursors), unless the user pins one explicitly.
9
+ */
10
+ export function resolveInstanceId(env = process.env, argv = process.argv, cwd = process.cwd(), clientName) {
11
+ if (env.NEXUS_INSTANCE_ID)
12
+ return env.NEXUS_INSTANCE_ID;
13
+ const idx = argv.indexOf("--id");
14
+ if (idx !== -1 && argv[idx + 1])
15
+ return argv[idx + 1];
16
+ const digest = createHash("sha1").update(cwd).digest("hex").slice(0, 8);
17
+ const client = clientName ? `-${clientName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24)}` : "";
18
+ return `mcp-${os.hostname()}-${digest}${client}`;
19
+ }