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
|
@@ -0,0 +1,155 @@
|
|
|
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 messageSchema = z.object({
|
|
16
|
+
id: z.number().int().optional(),
|
|
17
|
+
from: z.string(),
|
|
18
|
+
text: z.string(),
|
|
19
|
+
category: z.enum(MESSAGE_CATEGORIES).nullish(),
|
|
20
|
+
timestamp: z.string()
|
|
21
|
+
});
|
|
22
|
+
const taskSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
status: z.enum(TASK_STATUSES),
|
|
25
|
+
progress: z.number(),
|
|
26
|
+
source_meeting_id: z.string().nullable(),
|
|
27
|
+
metadata: z.record(z.string(), z.unknown()),
|
|
28
|
+
result_uri: z.string().nullable(),
|
|
29
|
+
error_message: z.string().nullable(),
|
|
30
|
+
created_at: z.string(),
|
|
31
|
+
updated_at: z.string(),
|
|
32
|
+
ttl: z.number().nullable()
|
|
33
|
+
});
|
|
34
|
+
const topologyNode = z.object({ id: z.string(), name: z.string(), summary: z.string(), lastActive: z.string() });
|
|
35
|
+
const meetingStatus = z.enum(["active", "closed", "archived"]);
|
|
36
|
+
/**
|
|
37
|
+
* Manifest as read back from storage. Deliberately looser than the input
|
|
38
|
+
* contract: manifests written by older releases were not validated, so
|
|
39
|
+
* ids and enum-like fields are plain strings here.
|
|
40
|
+
*/
|
|
41
|
+
const storedManifest = z.object({
|
|
42
|
+
id: z.string(),
|
|
43
|
+
name: z.string(),
|
|
44
|
+
description: z.string(),
|
|
45
|
+
techStack: z.array(z.string()),
|
|
46
|
+
relations: z.array(z.object({ targetId: z.string(), type: z.string() })),
|
|
47
|
+
lastUpdated: z.string(),
|
|
48
|
+
repositoryUrl: z.string(),
|
|
49
|
+
localPath: z.string(),
|
|
50
|
+
endpoints: z.array(z.object({ name: z.string(), url: z.string(), description: z.string() })),
|
|
51
|
+
apiSpec: z.array(z.object({ method: z.string(), path: z.string(), summary: z.string() })),
|
|
52
|
+
billing: z.object({ plain: z.string(), price: z.string() }).optional(),
|
|
53
|
+
apiDependencies: z.record(z.string(), z.string()).optional(),
|
|
54
|
+
gatewayCompatibility: z.string().optional(),
|
|
55
|
+
api_versions: z.record(z.string(), z.string()).optional(),
|
|
56
|
+
feature_tier: z.string().optional()
|
|
57
|
+
});
|
|
58
|
+
export const TOOL_OUTPUT_SCHEMAS = {
|
|
59
|
+
sync_project_assets: z.object({ ...ack, projectId: z.string() }),
|
|
60
|
+
upload_project_asset: z.object({ ...ack, projectId: z.string(), fileName: z.string() }),
|
|
61
|
+
update_project: z.object({ ...ack, projectId: z.string() }),
|
|
62
|
+
rename_project: z.object({ ...ack, oldId: z.string(), newId: z.string() }),
|
|
63
|
+
search_projects: z.object({
|
|
64
|
+
query: z.string(),
|
|
65
|
+
count: z.number().int(),
|
|
66
|
+
results: z.array(z.object({ id: z.string(), name: z.string(), description: z.string().optional() }))
|
|
67
|
+
}),
|
|
68
|
+
get_global_topology: z.object({
|
|
69
|
+
mode: z.enum(["list", "focused"]),
|
|
70
|
+
summary: z.object({ totalProjects: z.number().int(), totalEdges: z.number().int() }).optional()
|
|
71
|
+
.describe("list mode only"),
|
|
72
|
+
projects: z.record(z.string(), topologyNode).optional().describe("list mode only"),
|
|
73
|
+
nodes: z.array(topologyNode).optional().describe("focused mode only"),
|
|
74
|
+
edges: z.array(z.object({
|
|
75
|
+
from: z.string(), to: z.string(),
|
|
76
|
+
type: z.string()
|
|
77
|
+
})).optional().describe("focused mode only")
|
|
78
|
+
}),
|
|
79
|
+
get_project: z.object({
|
|
80
|
+
manifest: storedManifest,
|
|
81
|
+
internalDocs: z.string().optional()
|
|
82
|
+
}),
|
|
83
|
+
send_message: z.object({
|
|
84
|
+
...ack,
|
|
85
|
+
sentTo: z.enum(["meeting", "global"]),
|
|
86
|
+
meetingId: z.string().optional(),
|
|
87
|
+
messageId: z.number().int().optional()
|
|
88
|
+
}),
|
|
89
|
+
read_messages: z.object({
|
|
90
|
+
source: z.enum(["meeting", "global"]),
|
|
91
|
+
meetingId: z.string().optional(),
|
|
92
|
+
messages: z.array(messageSchema),
|
|
93
|
+
count: z.number().int()
|
|
94
|
+
}),
|
|
95
|
+
list_events: z.object({
|
|
96
|
+
count: z.number().int(),
|
|
97
|
+
events: z.array(z.object({
|
|
98
|
+
id: z.number().int(),
|
|
99
|
+
timestamp: z.string(),
|
|
100
|
+
actor: z.string(),
|
|
101
|
+
action: z.string(),
|
|
102
|
+
target: z.string().optional(),
|
|
103
|
+
detail: z.record(z.string(), z.unknown()).optional()
|
|
104
|
+
}))
|
|
105
|
+
}),
|
|
106
|
+
get_global_strategy: z.object({ content: z.string() }),
|
|
107
|
+
update_global_strategy: z.object(ack),
|
|
108
|
+
list_global_docs: z.object({
|
|
109
|
+
count: z.number().int(),
|
|
110
|
+
docs: z.array(z.object({ docId: z.string(), title: z.string(), lastUpdated: z.string(), updatedBy: z.string() }))
|
|
111
|
+
}),
|
|
112
|
+
get_global_doc: z.object({
|
|
113
|
+
docId: z.string(), title: z.string(), lastUpdated: z.string(), updatedBy: z.string(), content: z.string()
|
|
114
|
+
}),
|
|
115
|
+
sync_global_doc: z.object(ack),
|
|
116
|
+
list_meetings: z.object({
|
|
117
|
+
count: z.number().int(),
|
|
118
|
+
activeMeetingId: z.string().nullable(),
|
|
119
|
+
meetings: z.array(z.object({
|
|
120
|
+
id: z.string(),
|
|
121
|
+
topic: z.string(),
|
|
122
|
+
status: meetingStatus,
|
|
123
|
+
initiator: z.string(),
|
|
124
|
+
startTime: z.string(),
|
|
125
|
+
endTime: z.string().optional(),
|
|
126
|
+
participants: z.array(z.string()),
|
|
127
|
+
messageCount: z.number().int(),
|
|
128
|
+
decisionCount: z.number().int(),
|
|
129
|
+
summary: z.string().optional()
|
|
130
|
+
}))
|
|
131
|
+
}),
|
|
132
|
+
start_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), topic: z.string(), status: meetingStatus }),
|
|
133
|
+
end_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), status: z.literal("closed"), summary: z.string().optional() }),
|
|
134
|
+
archive_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), status: z.literal("archived") }),
|
|
135
|
+
reopen_meeting: z.object({ ok: z.literal(true), meetingId: z.string(), status: z.literal("active") }),
|
|
136
|
+
create_task: z.object({ task_id: z.string(), status: z.enum(TASK_STATUSES), message: z.string(), task: taskSchema }),
|
|
137
|
+
get_task: taskSchema,
|
|
138
|
+
list_tasks: z.object({ count: z.number().int(), tasks: z.array(taskSchema) }),
|
|
139
|
+
cancel_task: z.object({ ...ack, taskId: z.string() }),
|
|
140
|
+
host_maintenance: z.object({ ...ack, removed: z.number().int(), remaining: z.number().int() }),
|
|
141
|
+
host_delete_project: z.object({ ...ack, projectId: z.string() })
|
|
142
|
+
};
|
|
143
|
+
export class ResultContractError extends Error {
|
|
144
|
+
constructor(tool, issues) {
|
|
145
|
+
super(`Result of ${tool} does not match its contract:\n${issues}`);
|
|
146
|
+
this.name = "ResultContractError";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Validate a handler result against its contract and strip undeclared fields. */
|
|
150
|
+
export function finalizeResult(name, result) {
|
|
151
|
+
const parsed = TOOL_OUTPUT_SCHEMAS[name].safeParse(result);
|
|
152
|
+
if (!parsed.success)
|
|
153
|
+
throw new ResultContractError(name, z.prettifyError(parsed.error));
|
|
154
|
+
return parsed.data;
|
|
155
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
send_message: z.object({
|
|
66
|
+
message: z.string().min(1),
|
|
67
|
+
category: z.enum(MESSAGE_CATEGORIES).optional(),
|
|
68
|
+
meetingId: z.string().min(1).optional().describe("Target meeting. Defaults to the active meeting, or global chat when none is active.")
|
|
69
|
+
}),
|
|
70
|
+
read_messages: z.object({
|
|
71
|
+
count: positiveInt(500).default(10),
|
|
72
|
+
meetingId: z.string().min(1).optional()
|
|
73
|
+
}),
|
|
74
|
+
list_events: z.object({
|
|
75
|
+
limit: positiveInt(500).default(20),
|
|
76
|
+
action: z.string().min(1).optional().describe("Exact action or prefix such as 'project' or 'meeting'")
|
|
77
|
+
}),
|
|
78
|
+
get_global_strategy: noParams,
|
|
79
|
+
update_global_strategy: z.object({ content: z.string().min(1) }),
|
|
80
|
+
list_global_docs: noParams,
|
|
81
|
+
get_global_doc: z.object({ docId: docIdSchema }),
|
|
82
|
+
sync_global_doc: z.object({
|
|
83
|
+
docId: docIdSchema,
|
|
84
|
+
title: z.string().min(1),
|
|
85
|
+
content: z.string().min(1)
|
|
86
|
+
}),
|
|
87
|
+
// Meetings
|
|
88
|
+
list_meetings: z.object({
|
|
89
|
+
status: z.enum(["active", "closed", "archived"]).optional(),
|
|
90
|
+
limit: positiveInt(200).default(20)
|
|
91
|
+
}),
|
|
92
|
+
start_meeting: z.object({ topic: z.string().min(1) }),
|
|
93
|
+
end_meeting: z.object({
|
|
94
|
+
meetingId: z.string().min(1).optional().describe("Defaults to the active meeting"),
|
|
95
|
+
summary: z.string().optional()
|
|
96
|
+
}),
|
|
97
|
+
archive_meeting: z.object({ meetingId: z.string().min(1) }),
|
|
98
|
+
reopen_meeting: z.object({ meetingId: z.string().min(1) }),
|
|
99
|
+
// Tasks
|
|
100
|
+
create_task: z.object({
|
|
101
|
+
source_meeting_id: z.string().min(1).optional().describe("Link to a meeting for traceability"),
|
|
102
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
103
|
+
ttl: positiveInt(Number.MAX_SAFE_INTEGER).optional().describe("TTL in milliseconds; a pending/running task past its TTL is marked failed")
|
|
104
|
+
}),
|
|
105
|
+
get_task: z.object({ taskId: z.string().min(1) }),
|
|
106
|
+
list_tasks: z.object({
|
|
107
|
+
status: z.enum(TASK_STATUSES).optional(),
|
|
108
|
+
limit: positiveInt(500).default(50)
|
|
109
|
+
}),
|
|
110
|
+
cancel_task: z.object({ taskId: z.string().min(1) }),
|
|
111
|
+
// Maintenance
|
|
112
|
+
host_maintenance: z.object({
|
|
113
|
+
action: z.enum(["prune", "clear"]),
|
|
114
|
+
count: z.number().int().min(0).default(0).describe("Oldest N global messages to remove (prune only)")
|
|
115
|
+
}),
|
|
116
|
+
host_delete_project: z.object({ projectId: projectIdSchema })
|
|
117
|
+
};
|
|
118
|
+
const TOOL_DESCRIPTIONS = {
|
|
119
|
+
sync_project_assets: "Sync full project manifest + internal docs.",
|
|
120
|
+
upload_project_asset: "Upload a binary file (base64) to a project's asset folder.",
|
|
121
|
+
update_project: "Patch project manifest fields (partial update).",
|
|
122
|
+
rename_project: "Rename project ID with cascading relation updates.",
|
|
123
|
+
search_projects: "Search project registry by name or description.",
|
|
124
|
+
get_global_topology: "Default: project list + stats. With projectId: detailed subgraph.",
|
|
125
|
+
get_project: "Read a project's full manifest and, by default, its internal docs.",
|
|
126
|
+
send_message: "Post a message. Goes to meetingId if given, else to the active meeting, else to global chat.",
|
|
127
|
+
read_messages: "Read messages this instance has not read yet (cursor advances automatically). Global chat by default; pass meetingId for a meeting.",
|
|
128
|
+
list_events: "Activity log: structured records of state changes (project.sync, project.rename, meeting.end, ...), newest first.",
|
|
129
|
+
get_global_strategy: "Read the master strategy document (global/blueprint.md).",
|
|
130
|
+
update_global_strategy: "Overwrite the master strategy document (global/blueprint.md).",
|
|
131
|
+
list_global_docs: "List global shared documents: id, title, last update and author.",
|
|
132
|
+
get_global_doc: "Read a global shared document by docId, with its title and metadata.",
|
|
133
|
+
sync_global_doc: "Create/update a global shared document.",
|
|
134
|
+
list_meetings: "List meetings, newest first: id, topic, status, initiator, participants, message/decision counts, summary. Read messages with read_messages(meetingId).",
|
|
135
|
+
start_meeting: "Start new meeting session. Returns meeting ID.",
|
|
136
|
+
end_meeting: "End active meeting. Locks history.",
|
|
137
|
+
archive_meeting: "Archive closed meeting. Read-only after.",
|
|
138
|
+
reopen_meeting: "Reopen closed/archived meeting.",
|
|
139
|
+
create_task: "Create a task you track yourself. Returns taskId; update it via get_task/cancel_task.",
|
|
140
|
+
get_task: "Get task status and progress by ID.",
|
|
141
|
+
list_tasks: "List tasks with optional status filter.",
|
|
142
|
+
cancel_task: "Cancel pending/running task.",
|
|
143
|
+
host_maintenance: "Manage the global chat stream: 'prune' the oldest N messages or 'clear' all.",
|
|
144
|
+
host_delete_project: "Delete project. Irreversible."
|
|
145
|
+
};
|
|
146
|
+
/** Human-readable names clients show in their UI (MCP `title`). */
|
|
147
|
+
const TOOL_TITLES = {
|
|
148
|
+
sync_project_assets: "Sync Project",
|
|
149
|
+
upload_project_asset: "Upload Project Asset",
|
|
150
|
+
update_project: "Update Project",
|
|
151
|
+
rename_project: "Rename Project",
|
|
152
|
+
search_projects: "Search Projects",
|
|
153
|
+
get_global_topology: "Project Topology",
|
|
154
|
+
get_project: "Read Project",
|
|
155
|
+
send_message: "Send Message",
|
|
156
|
+
read_messages: "Read Messages",
|
|
157
|
+
list_events: "Activity Log",
|
|
158
|
+
get_global_strategy: "Read Strategy",
|
|
159
|
+
update_global_strategy: "Update Strategy",
|
|
160
|
+
list_global_docs: "List Shared Docs",
|
|
161
|
+
get_global_doc: "Read Shared Doc",
|
|
162
|
+
sync_global_doc: "Save Shared Doc",
|
|
163
|
+
list_meetings: "List Meetings",
|
|
164
|
+
start_meeting: "Start Meeting",
|
|
165
|
+
end_meeting: "End Meeting",
|
|
166
|
+
archive_meeting: "Archive Meeting",
|
|
167
|
+
reopen_meeting: "Reopen Meeting",
|
|
168
|
+
create_task: "Create Task",
|
|
169
|
+
get_task: "Read Task",
|
|
170
|
+
list_tasks: "List Tasks",
|
|
171
|
+
cancel_task: "Cancel Task",
|
|
172
|
+
host_maintenance: "Prune Global Chat",
|
|
173
|
+
host_delete_project: "Delete Project"
|
|
174
|
+
};
|
|
175
|
+
const readOnly = { readOnlyHint: true, openWorldHint: false };
|
|
176
|
+
/**
|
|
177
|
+
* Changes state. `destructive` follows the MCP definition: false only when the
|
|
178
|
+
* tool makes purely additive updates; anything that rewrites or removes
|
|
179
|
+
* existing state (status changes included) is destructive. `idempotent` =
|
|
180
|
+
* repeating the same call has no further effect.
|
|
181
|
+
*/
|
|
182
|
+
const write = (destructive, idempotent) => ({ readOnlyHint: false, destructiveHint: destructive, idempotentHint: idempotent, openWorldHint: false });
|
|
183
|
+
const TOOL_ANNOTATIONS = {
|
|
184
|
+
sync_project_assets: write(true, true), // replaces manifest and docs
|
|
185
|
+
upload_project_asset: write(true, true), // overwrites a same-named asset
|
|
186
|
+
update_project: write(true, true), // replaces the patched fields
|
|
187
|
+
rename_project: write(true, false), // removes the old id, rewrites relations
|
|
188
|
+
search_projects: readOnly,
|
|
189
|
+
get_global_topology: readOnly,
|
|
190
|
+
get_project: readOnly,
|
|
191
|
+
send_message: write(false, false),
|
|
192
|
+
read_messages: write(false, false), // only advances this instance's own unread cursor (bookkeeping, not shared data)
|
|
193
|
+
list_events: readOnly,
|
|
194
|
+
get_global_strategy: readOnly,
|
|
195
|
+
update_global_strategy: write(true, true), // overwrites the document
|
|
196
|
+
list_global_docs: readOnly,
|
|
197
|
+
get_global_doc: readOnly,
|
|
198
|
+
sync_global_doc: write(true, true), // overwrites the document
|
|
199
|
+
list_meetings: readOnly,
|
|
200
|
+
start_meeting: write(false, false),
|
|
201
|
+
end_meeting: write(true, false), // closes the meeting, sets summary
|
|
202
|
+
archive_meeting: write(true, false), // changes status
|
|
203
|
+
reopen_meeting: write(true, false), // changes status
|
|
204
|
+
create_task: write(false, false),
|
|
205
|
+
get_task: readOnly,
|
|
206
|
+
list_tasks: readOnly,
|
|
207
|
+
cancel_task: write(true, true), // irreversible status change
|
|
208
|
+
host_maintenance: write(true, false), // deletes chat messages
|
|
209
|
+
host_delete_project: write(true, true) // irreversible
|
|
210
|
+
};
|
|
211
|
+
function toOutputSchema(schema) {
|
|
212
|
+
const json = z.toJSONSchema(schema, { io: "output" });
|
|
213
|
+
delete json.$schema;
|
|
214
|
+
return json;
|
|
215
|
+
}
|
|
216
|
+
function toInputSchema(schema) {
|
|
217
|
+
const json = z.toJSONSchema(schema, { io: "input" });
|
|
218
|
+
delete json.$schema;
|
|
219
|
+
return json;
|
|
220
|
+
}
|
|
221
|
+
// Compile-time check: every tool has a result contract and no contract lacks a tool.
|
|
222
|
+
const _outputsCover = TOOL_OUTPUT_SCHEMAS;
|
|
223
|
+
const _outputsMatch = TOOL_SCHEMAS;
|
|
224
|
+
void _outputsCover;
|
|
225
|
+
void _outputsMatch;
|
|
226
|
+
export const TOOL_NAMES = Object.keys(TOOL_SCHEMAS);
|
|
227
|
+
/**
|
|
228
|
+
* Tools whose outputSchema is published in tools/list. Every tool has a result
|
|
229
|
+
* contract (and every result is sent as structuredContent), but the schemas of
|
|
230
|
+
* write tools only describe a short acknowledgement and nearly doubled the
|
|
231
|
+
* tools/list payload that sits in every model context, so only tools that
|
|
232
|
+
* return data publish theirs.
|
|
233
|
+
*/
|
|
234
|
+
const PUBLISHED_OUTPUT_SCHEMAS = new Set([
|
|
235
|
+
"search_projects", "get_global_topology", "get_project",
|
|
236
|
+
"read_messages", "list_events",
|
|
237
|
+
"get_global_strategy", "list_global_docs", "get_global_doc",
|
|
238
|
+
"list_meetings", "get_task", "list_tasks"
|
|
239
|
+
]);
|
|
240
|
+
export const TOOL_DEFINITIONS = TOOL_NAMES.map((name) => ({
|
|
241
|
+
name,
|
|
242
|
+
title: TOOL_TITLES[name],
|
|
243
|
+
description: TOOL_DESCRIPTIONS[name],
|
|
244
|
+
...(PUBLISHED_OUTPUT_SCHEMAS.has(name) ? { outputSchema: toOutputSchema(TOOL_OUTPUT_SCHEMAS[name]) } : {}),
|
|
245
|
+
inputSchema: toInputSchema(TOOL_SCHEMAS[name]),
|
|
246
|
+
annotations: TOOL_ANNOTATIONS[name]
|
|
247
|
+
}));
|
|
248
|
+
export function isToolName(name) {
|
|
249
|
+
return Object.prototype.hasOwnProperty.call(TOOL_SCHEMAS, name);
|
|
250
|
+
}
|
|
251
|
+
export class ToolValidationError extends Error {
|
|
252
|
+
constructor(tool, issues) {
|
|
253
|
+
super(`Invalid arguments for ${tool}:\n${issues}`);
|
|
254
|
+
this.name = "ToolValidationError";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Validate raw arguments against a tool's schema; unknown top-level keys are dropped. */
|
|
258
|
+
export function parseToolArgs(name, args) {
|
|
259
|
+
const result = TOOL_SCHEMAS[name].safeParse(args ?? {});
|
|
260
|
+
if (!result.success)
|
|
261
|
+
throw new ToolValidationError(name, z.prettifyError(result.error));
|
|
262
|
+
return result.data;
|
|
263
|
+
}
|
|
264
|
+
/** Schema for POST /api/tasks/:id/update (not an MCP tool). */
|
|
265
|
+
export const taskUpdateSchema = z.object({
|
|
266
|
+
status: z.enum(TASK_STATUSES).optional(),
|
|
267
|
+
progress: z.number().min(0).max(1).optional(),
|
|
268
|
+
result_uri: z.string().optional(),
|
|
269
|
+
error_message: z.string().optional(),
|
|
270
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
271
|
+
}).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
|
+
}
|