@creator-notes/cnotes 0.16.11
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/.claude-plugin/plugin.json +14 -0
- package/.mcp.json +12 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/cn.d.ts +3 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/cn.js +124 -0
- package/dist/cn.js.map +1 -0
- package/dist/commands/auth.d.ts +10 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +188 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/canvas.d.ts +3 -0
- package/dist/commands/canvas.d.ts.map +1 -0
- package/dist/commands/canvas.js +1383 -0
- package/dist/commands/canvas.js.map +1 -0
- package/dist/commands/claude-hook.d.ts +28 -0
- package/dist/commands/claude-hook.d.ts.map +1 -0
- package/dist/commands/claude-hook.js +59 -0
- package/dist/commands/claude-hook.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +47 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/files.d.ts +3 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +119 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +473 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/mcp.d.ts +15 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +118 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +150 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/notes.d.ts +3 -0
- package/dist/commands/notes.d.ts.map +1 -0
- package/dist/commands/notes.js +706 -0
- package/dist/commands/notes.js.map +1 -0
- package/dist/commands/operations.d.ts +18 -0
- package/dist/commands/operations.d.ts.map +1 -0
- package/dist/commands/operations.js +231 -0
- package/dist/commands/operations.js.map +1 -0
- package/dist/commands/relationships.d.ts +3 -0
- package/dist/commands/relationships.d.ts.map +1 -0
- package/dist/commands/relationships.js +94 -0
- package/dist/commands/relationships.js.map +1 -0
- package/dist/commands/schema.d.ts +12 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +85 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/theme.d.ts +3 -0
- package/dist/commands/theme.d.ts.map +1 -0
- package/dist/commands/theme.js +184 -0
- package/dist/commands/theme.js.map +1 -0
- package/dist/commands/timeline.d.ts +3 -0
- package/dist/commands/timeline.d.ts.map +1 -0
- package/dist/commands/timeline.js +97 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/commands/types.d.ts +3 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +139 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/versions.d.ts +3 -0
- package/dist/commands/versions.d.ts.map +1 -0
- package/dist/commands/versions.js +120 -0
- package/dist/commands/versions.js.map +1 -0
- package/dist/commands/workspace.d.ts +13 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +176 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/lib/api-client.d.ts +45 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +198 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth-store.d.ts +47 -0
- package/dist/lib/auth-store.d.ts.map +1 -0
- package/dist/lib/auth-store.js +116 -0
- package/dist/lib/auth-store.js.map +1 -0
- package/dist/lib/brand.d.ts +32 -0
- package/dist/lib/brand.d.ts.map +1 -0
- package/dist/lib/brand.js +32 -0
- package/dist/lib/brand.js.map +1 -0
- package/dist/lib/build-schema.d.ts +97 -0
- package/dist/lib/build-schema.d.ts.map +1 -0
- package/dist/lib/build-schema.js +139 -0
- package/dist/lib/build-schema.js.map +1 -0
- package/dist/lib/canvas-read.d.ts +54 -0
- package/dist/lib/canvas-read.d.ts.map +1 -0
- package/dist/lib/canvas-read.js +145 -0
- package/dist/lib/canvas-read.js.map +1 -0
- package/dist/lib/claude-session.d.ts +73 -0
- package/dist/lib/claude-session.d.ts.map +1 -0
- package/dist/lib/claude-session.js +104 -0
- package/dist/lib/claude-session.js.map +1 -0
- package/dist/lib/config.d.ts +28 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +66 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/env.d.ts +14 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +16 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/errors.d.ts +47 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +194 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/fs-utils.d.ts +2 -0
- package/dist/lib/fs-utils.d.ts.map +1 -0
- package/dist/lib/fs-utils.js +16 -0
- package/dist/lib/fs-utils.js.map +1 -0
- package/dist/lib/install-hook.d.ts +86 -0
- package/dist/lib/install-hook.d.ts.map +1 -0
- package/dist/lib/install-hook.js +168 -0
- package/dist/lib/install-hook.js.map +1 -0
- package/dist/lib/install-mcp.d.ts +21 -0
- package/dist/lib/install-mcp.d.ts.map +1 -0
- package/dist/lib/install-mcp.js +133 -0
- package/dist/lib/install-mcp.js.map +1 -0
- package/dist/lib/install-skill.d.ts +49 -0
- package/dist/lib/install-skill.d.ts.map +1 -0
- package/dist/lib/install-skill.js +113 -0
- package/dist/lib/install-skill.js.map +1 -0
- package/dist/lib/output.d.ts +29 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +78 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/resolve-note.d.ts +7 -0
- package/dist/lib/resolve-note.d.ts.map +1 -0
- package/dist/lib/resolve-note.js +23 -0
- package/dist/lib/resolve-note.js.map +1 -0
- package/dist/lib/stdin.d.ts +5 -0
- package/dist/lib/stdin.d.ts.map +1 -0
- package/dist/lib/stdin.js +11 -0
- package/dist/lib/stdin.js.map +1 -0
- package/dist/lib/style.d.ts +10 -0
- package/dist/lib/style.d.ts.map +1 -0
- package/dist/lib/style.js +17 -0
- package/dist/lib/style.js.map +1 -0
- package/dist/lib/themes.d.ts +44 -0
- package/dist/lib/themes.d.ts.map +1 -0
- package/dist/lib/themes.js +168 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +782 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +66 -0
- package/skills/cnotes/SKILL.md +680 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// CN MCP Server — exposes CreatorNotes CLI operations as MCP tools.
|
|
4
|
+
//
|
|
5
|
+
// Claude Desktop config (macOS):
|
|
6
|
+
// ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
7
|
+
// {
|
|
8
|
+
// "mcpServers": {
|
|
9
|
+
// "cnotes": {
|
|
10
|
+
// "command": "node",
|
|
11
|
+
// "args": ["/path/to/pm-notes/cli/dist/mcp-server.js"],
|
|
12
|
+
// "env": {
|
|
13
|
+
// "CN_SERVER": "https://your-codespace.app.github.dev",
|
|
14
|
+
// "CN_TOKEN": "your-token",
|
|
15
|
+
// "CN_WORKSPACE": "your-workspace-id"
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
18
|
+
// }
|
|
19
|
+
// }
|
|
20
|
+
//
|
|
21
|
+
import { readFileSync, statSync } from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { ApiClient } from "./lib/api-client.js";
|
|
27
|
+
import { resolveNoteId } from "./lib/resolve-note.js";
|
|
28
|
+
import { readCanvasMarkdown } from "./lib/canvas-read.js";
|
|
29
|
+
import { buildSchema } from "./lib/build-schema.js";
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Environment
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const CN_SERVER = process.env.CN_SERVER;
|
|
34
|
+
const CN_TOKEN = process.env.CN_TOKEN;
|
|
35
|
+
const CN_WORKSPACE = process.env.CN_WORKSPACE;
|
|
36
|
+
if (!CN_SERVER || !CN_TOKEN || !CN_WORKSPACE) {
|
|
37
|
+
const missing = [
|
|
38
|
+
!CN_SERVER && "CN_SERVER",
|
|
39
|
+
!CN_TOKEN && "CN_TOKEN",
|
|
40
|
+
!CN_WORKSPACE && "CN_WORKSPACE",
|
|
41
|
+
].filter(Boolean);
|
|
42
|
+
process.stderr.write(`cnotes-mcp: missing required env vars: ${missing.join(", ")}\n` +
|
|
43
|
+
"Set them in your Claude Desktop config or shell environment.\n");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const api = new ApiClient(CN_SERVER, CN_TOKEN);
|
|
47
|
+
const workspaceId = CN_WORKSPACE;
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Helpers
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
function ok(data) {
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
53
|
+
}
|
|
54
|
+
function arrowToMarkers(arrow) {
|
|
55
|
+
switch (arrow) {
|
|
56
|
+
case "end":
|
|
57
|
+
return { arrowStart: false, arrowEnd: true };
|
|
58
|
+
case "start":
|
|
59
|
+
return { arrowStart: true, arrowEnd: false };
|
|
60
|
+
case "both":
|
|
61
|
+
return { arrowStart: true, arrowEnd: true };
|
|
62
|
+
case "none":
|
|
63
|
+
return { arrowStart: false, arrowEnd: false };
|
|
64
|
+
default:
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function err(message) {
|
|
69
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
70
|
+
}
|
|
71
|
+
async function withErrorHandling(fn) {
|
|
72
|
+
try {
|
|
73
|
+
return ok(await fn());
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
77
|
+
return err(msg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Server
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
const server = new McpServer({
|
|
84
|
+
name: "cnotes-mcp",
|
|
85
|
+
version: "0.3.0",
|
|
86
|
+
});
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Resources
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
server.resource("workspace-current", "cnotes://workspace/current", async () => ({
|
|
91
|
+
contents: [
|
|
92
|
+
{
|
|
93
|
+
uri: "cnotes://workspace/current",
|
|
94
|
+
mimeType: "application/json",
|
|
95
|
+
text: JSON.stringify(await api.get(`/api/workspaces/${workspaceId}`), null, 2),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
}));
|
|
99
|
+
server.resource("types", "cnotes://types", async () => ({
|
|
100
|
+
contents: [
|
|
101
|
+
{
|
|
102
|
+
uri: "cnotes://types",
|
|
103
|
+
mimeType: "application/json",
|
|
104
|
+
text: JSON.stringify(await api.get("/api/supertags", { workspaceId }), null, 2),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}));
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Notes tools
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
server.tool("cn_notes_list", "List notes in the workspace. Returns metadata by default. Set includeContent=true to include full note content (useful to avoid multiple cn_notes_get calls).", {
|
|
112
|
+
search: z.string().optional().describe("Full-text search query"),
|
|
113
|
+
type: z.string().optional().describe("Filter by note type (e.g. Meeting, PRD)"),
|
|
114
|
+
tags: z.string().optional().describe("Filter by tags (comma-separated)"),
|
|
115
|
+
pinned: z.boolean().optional().describe("Only show pinned notes"),
|
|
116
|
+
limit: z.number().optional().default(20).describe("Max results"),
|
|
117
|
+
includeContent: z.boolean().optional().default(false).describe("Include full note content in results"),
|
|
118
|
+
}, async ({ search, type, tags, pinned, limit, includeContent }) => withErrorHandling(() => api.get("/api/notes", {
|
|
119
|
+
workspaceId,
|
|
120
|
+
search,
|
|
121
|
+
types: type,
|
|
122
|
+
tags,
|
|
123
|
+
onlyPinned: pinned ? "true" : undefined,
|
|
124
|
+
excludeDrafts: "true",
|
|
125
|
+
excludeContent: includeContent ? undefined : "true",
|
|
126
|
+
format: includeContent ? "markdown" : undefined,
|
|
127
|
+
limit: String(limit ?? 20),
|
|
128
|
+
})));
|
|
129
|
+
server.tool("cn_notes_get", "Get one or more notes by display ID (e.g. MEETING-12, PRD-3). Always returns an array, in input order, in a single round-trip. Pass one ID for one note, or many IDs to batch-fetch — never call this tool multiple times in parallel for different IDs.", {
|
|
130
|
+
ids: z
|
|
131
|
+
.array(z.string())
|
|
132
|
+
.min(1)
|
|
133
|
+
.describe("Display IDs (e.g. ['MEETING-12','PRD-3','IDEA-7']). Pass an array even for a single ID."),
|
|
134
|
+
}, async ({ ids }) => withErrorHandling(async () => {
|
|
135
|
+
const notesList = await api.get("/api/notes", {
|
|
136
|
+
workspaceId,
|
|
137
|
+
exact_ids: ids.join(","),
|
|
138
|
+
excludeDrafts: "true",
|
|
139
|
+
format: "markdown",
|
|
140
|
+
});
|
|
141
|
+
const found = Array.isArray(notesList) ? notesList : [];
|
|
142
|
+
const byDisplayId = new Map();
|
|
143
|
+
for (const n of found) {
|
|
144
|
+
if (typeof n.displayId === "string")
|
|
145
|
+
byDisplayId.set(n.displayId, n);
|
|
146
|
+
}
|
|
147
|
+
const missing = ids.filter((id) => !byDisplayId.has(id));
|
|
148
|
+
if (missing.length > 0) {
|
|
149
|
+
throw new Error(`Notes not found: ${missing.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
// Preserve input order.
|
|
152
|
+
return ids.map((id) => byDisplayId.get(id));
|
|
153
|
+
}));
|
|
154
|
+
server.tool("cn_notes_delete", "Delete (archive) a note.", {
|
|
155
|
+
id: z.string().describe("Note display ID or Convex ID"),
|
|
156
|
+
}, async ({ id }) => withErrorHandling(async () => {
|
|
157
|
+
const noteId = await resolveNoteId(api, workspaceId, id);
|
|
158
|
+
return api.delete(`/api/notes/${noteId}`);
|
|
159
|
+
}));
|
|
160
|
+
server.tool("cn_notes_bulk_archive", "Archive or unarchive multiple notes in one operation. More efficient than calling cn_notes_delete multiple times.", {
|
|
161
|
+
ids: z.array(z.string()).describe("Array of note display IDs or Convex IDs"),
|
|
162
|
+
unarchive: z.boolean().optional().default(false).describe("Set true to unarchive instead of archive"),
|
|
163
|
+
}, async ({ ids, unarchive }) => withErrorHandling(async () => {
|
|
164
|
+
const noteIds = await Promise.all(ids.map((id) => resolveNoteId(api, workspaceId, id)));
|
|
165
|
+
return api.post("/api/notes/bulk", {
|
|
166
|
+
action: "bulkArchive",
|
|
167
|
+
noteIds,
|
|
168
|
+
isArchived: !unarchive,
|
|
169
|
+
});
|
|
170
|
+
}));
|
|
171
|
+
server.tool("cn_notes_bulk_retype", "Retype multiple notes to a new type in one operation. Each note is atomically retyped (new note created, old archived, cross-linked).", {
|
|
172
|
+
ids: z.array(z.string()).describe("Array of note display IDs or Convex IDs to retype"),
|
|
173
|
+
targetType: z.string().describe("Target type name (must be an existing supertag, e.g. 'Insight')"),
|
|
174
|
+
}, async ({ ids, targetType }) => withErrorHandling(async () => {
|
|
175
|
+
return api.post("/api/notes/bulk", {
|
|
176
|
+
action: "bulkRetype",
|
|
177
|
+
workspaceId,
|
|
178
|
+
noteIds: ids,
|
|
179
|
+
targetType,
|
|
180
|
+
});
|
|
181
|
+
}));
|
|
182
|
+
server.tool("cn_notes_update", "Update note metadata (type, tags, pin/archive status). Does NOT change content — use cn_versions_create for that. Title is read-only (derived from # h1 heading in content).", {
|
|
183
|
+
id: z.string().describe("Note display ID or Convex ID"),
|
|
184
|
+
type: z.string().optional().describe("New note type (must be an existing supertag, e.g. 'Insight')"),
|
|
185
|
+
tags: z.string().optional().describe("Replace tags (comma-separated). Use empty string to clear."),
|
|
186
|
+
pin: z.boolean().optional().describe("Pin the note"),
|
|
187
|
+
unpin: z.boolean().optional().describe("Unpin the note"),
|
|
188
|
+
archive: z.boolean().optional().describe("Archive the note"),
|
|
189
|
+
unarchive: z.boolean().optional().describe("Unarchive the note"),
|
|
190
|
+
}, async ({ id, type, tags, pin, unpin, archive, unarchive }) => withErrorHandling(async () => {
|
|
191
|
+
const body = {};
|
|
192
|
+
if (type !== undefined)
|
|
193
|
+
body.type = type;
|
|
194
|
+
if (tags !== undefined) {
|
|
195
|
+
body.tags = tags === "" ? [] : tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
196
|
+
}
|
|
197
|
+
if (pin)
|
|
198
|
+
body.isPinned = true;
|
|
199
|
+
if (unpin)
|
|
200
|
+
body.isPinned = false;
|
|
201
|
+
if (archive)
|
|
202
|
+
body.isArchived = true;
|
|
203
|
+
if (unarchive)
|
|
204
|
+
body.isArchived = false;
|
|
205
|
+
if (Object.keys(body).length === 0) {
|
|
206
|
+
throw new Error("At least one of type, tags, pin, unpin, archive, or unarchive is required");
|
|
207
|
+
}
|
|
208
|
+
const noteId = await resolveNoteId(api, workspaceId, id);
|
|
209
|
+
return api.patch(`/api/notes/${noteId}`, body);
|
|
210
|
+
}));
|
|
211
|
+
const NoteCreateItem = z.object({
|
|
212
|
+
key: z.string().describe("Unique key for this item, used in @key placeholder cross-references"),
|
|
213
|
+
type: z.string().describe("Note type (must be an existing supertag, e.g. 'Insight', 'Meeting')"),
|
|
214
|
+
markdown: z.string().describe("Note content as markdown (the first # h1 heading becomes the title)"),
|
|
215
|
+
tags: z.array(z.string()).optional().describe("Tags for this note"),
|
|
216
|
+
});
|
|
217
|
+
server.tool("cn_notes_create", "Create one or more interlinked notes in a single atomic transaction. Pass `notes` as a single item object or an array of items. Title is derived from the # h1 heading in each note's markdown. Relationship link syntax inside markdown: use [@key: Title](relationship:references) to cross-reference another note in the same batch (the @key is replaced with the new note's display ID after creation), or [NOTE-12: Title](relationship:references) to reference an existing note. ALWAYS include the `: Title` part — the title-less form `[NOTE-12](relationship:references)` saves a chip with `title: null` that renders as \"Untitled\" in the UI. The relationship name is free-form (e.g. references, supports, triggers); use `references` if unsure. If validation fails, the response includes a structured `details.items` array pinpointing which item (by index + key) is malformed and why.", {
|
|
218
|
+
notes: z
|
|
219
|
+
.union([NoteCreateItem, z.array(NoteCreateItem).min(1)])
|
|
220
|
+
.describe("Either a single item object or a non-empty array of items."),
|
|
221
|
+
}, async ({ notes }) => withErrorHandling(() => api.post("/api/notes/bulk", {
|
|
222
|
+
action: "bulkCreate",
|
|
223
|
+
workspaceId,
|
|
224
|
+
notes: Array.isArray(notes) ? notes : [notes],
|
|
225
|
+
})));
|
|
226
|
+
server.tool("cn_notes_retype", "Change a single note's type. Atomic operation: creates a new note with the target type, archives the old one, and cross-links both.", {
|
|
227
|
+
noteId: z.string().describe("Display ID of the note to retype (e.g. NOTE-1)"),
|
|
228
|
+
targetType: z.string().describe("Target type name (must be an existing supertag)"),
|
|
229
|
+
}, async ({ noteId, targetType }) => withErrorHandling(() => api.post("/api/notes/retype", {
|
|
230
|
+
workspaceId,
|
|
231
|
+
noteId,
|
|
232
|
+
targetType,
|
|
233
|
+
})));
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Search tools
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
server.tool("cn_search_semantic", "Semantic (vector) search across notes. Better for meaning-based queries.", {
|
|
238
|
+
query: z.string().describe("Search query"),
|
|
239
|
+
limit: z.number().optional().default(10).describe("Max results"),
|
|
240
|
+
canvasId: z.string().optional().describe("Scope search to notes on a canvas"),
|
|
241
|
+
}, async ({ query, limit, canvasId }) => withErrorHandling(() => api.post("/api/semantic-search", {
|
|
242
|
+
query,
|
|
243
|
+
workspaceId,
|
|
244
|
+
limit: limit ?? 10,
|
|
245
|
+
...(canvasId ? { canvasId } : {}),
|
|
246
|
+
})));
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Canvas tools
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
server.tool("cn_canvas_list", "List canvases in the workspace. Archived canvases are excluded by default.", {
|
|
251
|
+
includeArchived: z.boolean().optional().default(false).describe("Include archived canvases"),
|
|
252
|
+
}, async ({ includeArchived }) => withErrorHandling(() => api.get("/api/canvas", {
|
|
253
|
+
workspaceId,
|
|
254
|
+
...(includeArchived ? { includeArchived: "true" } : {}),
|
|
255
|
+
})));
|
|
256
|
+
server.tool("cn_canvas_get", "Get canvas details including nodes, edges, and canvas links. Richtext node content is returned as markdown.", {
|
|
257
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
258
|
+
}, async ({ canvasId }) => withErrorHandling(() => api.get(`/api/canvas/${encodeURIComponent(canvasId)}`, { workspaceId, format: "markdown" })));
|
|
259
|
+
server.tool("cn_canvas_read", "Read every note on a canvas as one concatenated markdown document, in display order (top-to-bottom, then left-to-right). Use this to think across a canvas in a single round-trip — preferred over canvas_get + N notes_get calls.", {
|
|
260
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
261
|
+
}, async ({ canvasId }) => withErrorHandling(async () => readCanvasMarkdown(api, workspaceId, canvasId)));
|
|
262
|
+
server.tool("cn_canvas_create", "Create a new canvas. Goal and target audience are required — every canvas must declare its purpose and who it serves.", {
|
|
263
|
+
name: z.string().describe("Canvas name"),
|
|
264
|
+
goal: z.string().describe("Canvas goal — the purpose, desired outcome, and what it should communicate (markdown). Required."),
|
|
265
|
+
targetAudience: z.string().describe("Target audience — who this canvas is meant to serve or inform. Required."),
|
|
266
|
+
}, async ({ name, goal, targetAudience }) => {
|
|
267
|
+
return withErrorHandling(() => api.post("/api/canvas", { workspaceId, title: name, goal, targetAudience }));
|
|
268
|
+
});
|
|
269
|
+
server.tool("cn_canvas_update", "Update canvas metadata: title, goal (purpose/outcome), and/or target audience. Use this to set or change the intent metadata that guides agents working on this canvas.", {
|
|
270
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
271
|
+
title: z.string().optional().describe("New canvas title"),
|
|
272
|
+
goal: z.string().optional().describe("Canvas goal — the purpose, desired outcome, and what it should communicate (markdown)"),
|
|
273
|
+
targetAudience: z.string().optional().describe("Target audience — who this canvas is meant to serve or inform"),
|
|
274
|
+
}, async ({ canvasId, title, goal, targetAudience }) => {
|
|
275
|
+
if (!title && !goal && !targetAudience) {
|
|
276
|
+
return { content: [{ type: "text", text: "Error: at least one of title, goal, or targetAudience is required" }] };
|
|
277
|
+
}
|
|
278
|
+
const body = {};
|
|
279
|
+
if (title !== undefined)
|
|
280
|
+
body.title = title;
|
|
281
|
+
if (goal !== undefined)
|
|
282
|
+
body.goal = goal;
|
|
283
|
+
if (targetAudience !== undefined)
|
|
284
|
+
body.targetAudience = targetAudience;
|
|
285
|
+
return withErrorHandling(() => api.patch(`/api/canvas/${encodeURIComponent(canvasId)}`, body, { workspaceId }));
|
|
286
|
+
});
|
|
287
|
+
server.tool("cn_canvas_delete", "Delete a canvas.", {
|
|
288
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
289
|
+
}, async ({ canvasId }) => withErrorHandling(() => api.delete(`/api/canvas/${canvasId}`)));
|
|
290
|
+
server.tool("cn_canvas_archive", "Archive or unarchive a canvas. Archiving is a soft delete (sets archivedAt timestamp). Use unarchive=true to restore.", {
|
|
291
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
292
|
+
unarchive: z.boolean().optional().default(false).describe("Set true to unarchive/restore instead of archive"),
|
|
293
|
+
}, async ({ canvasId, unarchive }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
294
|
+
action: unarchive ? "unarchive" : "archive",
|
|
295
|
+
})));
|
|
296
|
+
server.tool("cn_canvas_set_home", "Set a canvas as the workspace home/default canvas. Clears any previous home canvas.", {
|
|
297
|
+
canvasId: z.string().describe("Canvas ID to set as home"),
|
|
298
|
+
}, async ({ canvasId }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, { action: "setAsHome" })));
|
|
299
|
+
// --- Deprecated single-item wrappers (delegate to bulk_add) ---
|
|
300
|
+
// Kept for backward compatibility with existing MCP clients.
|
|
301
|
+
server.tool("cn_canvas_add_node", "[Deprecated: use cn_canvas_bulk_add] Add a note to a canvas as a node.", {
|
|
302
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
303
|
+
noteId: z.string().describe("Note display ID or Convex ID"),
|
|
304
|
+
x: z.number().optional().default(100).describe("X position"),
|
|
305
|
+
y: z.number().optional().default(100).describe("Y position"),
|
|
306
|
+
}, async ({ canvasId, noteId, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
307
|
+
action: "bulkAddNodes",
|
|
308
|
+
items: [{ type: "note", noteId, positionX: x, positionY: y }],
|
|
309
|
+
})));
|
|
310
|
+
server.tool("cn_canvas_add_text", "[Deprecated: use cn_canvas_bulk_add] Add a text annotation node to a canvas.", {
|
|
311
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
312
|
+
text: z.string().describe("Text content"),
|
|
313
|
+
size: z.enum(["heading", "paragraph"]).optional().default("heading").describe("Text size"),
|
|
314
|
+
color: z.enum(["normal", "muted", "highlighted"]).optional().describe("Color variant"),
|
|
315
|
+
x: z.number().optional().default(100).describe("X position"),
|
|
316
|
+
y: z.number().optional().default(100).describe("Y position"),
|
|
317
|
+
}, async ({ canvasId, text, size, color, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
318
|
+
action: "bulkAddNodes",
|
|
319
|
+
items: [{ type: "text", content: text, fontSize: size === "paragraph" ? 18 : 32, colorVariant: color, positionX: x, positionY: y }],
|
|
320
|
+
})));
|
|
321
|
+
server.tool("cn_canvas_add_richtext", "[Deprecated: use cn_canvas_bulk_add] Add a richtext annotation node to a canvas.", {
|
|
322
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
323
|
+
content: z.string().describe("Richtext content as markdown. To link to notes, use [NOTE-123: Title](relationship:references)."),
|
|
324
|
+
size: z.enum(["small", "medium", "large"]).optional().default("small").describe("Display size"),
|
|
325
|
+
color: z.string().optional().describe("Background color hex"),
|
|
326
|
+
x: z.number().optional().default(100).describe("X position"),
|
|
327
|
+
y: z.number().optional().default(100).describe("Y position"),
|
|
328
|
+
}, async ({ canvasId, content, size, color, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
329
|
+
action: "bulkAddNodes",
|
|
330
|
+
items: [{ type: "richtext", content, size, colorHex: color, positionX: x, positionY: y }],
|
|
331
|
+
})));
|
|
332
|
+
server.tool("cn_canvas_add_list", "[Deprecated: use cn_canvas_bulk_add] Add a list/grid node grouping notes on a canvas. The description is the only label; its first paragraph derives the searchable title.", {
|
|
333
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
334
|
+
description: z.string().min(1).describe("Rich-text description (markdown). Required. First paragraph derives the searchable title."),
|
|
335
|
+
noteIds: z.string().optional().describe("Comma-separated note IDs"),
|
|
336
|
+
viewMode: z.enum(["list", "grid"]).optional().default("list").describe("View mode"),
|
|
337
|
+
x: z.number().optional().default(100).describe("X position"),
|
|
338
|
+
y: z.number().optional().default(100).describe("Y position"),
|
|
339
|
+
}, async ({ canvasId, description, noteIds, viewMode, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
340
|
+
action: "bulkAddNodes",
|
|
341
|
+
items: [{
|
|
342
|
+
type: "list",
|
|
343
|
+
description,
|
|
344
|
+
viewMode,
|
|
345
|
+
noteIds: noteIds ? noteIds.split(",").map((id) => id.trim()) : undefined,
|
|
346
|
+
positionX: x,
|
|
347
|
+
positionY: y,
|
|
348
|
+
}],
|
|
349
|
+
})));
|
|
350
|
+
server.tool("cn_canvas_add_link", "[Deprecated: use cn_canvas_bulk_add] Add a link to another canvas.", {
|
|
351
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
352
|
+
targetCanvasId: z.string().describe("Target canvas ID to link to"),
|
|
353
|
+
x: z.number().optional().default(100).describe("X position"),
|
|
354
|
+
y: z.number().optional().default(100).describe("Y position"),
|
|
355
|
+
}, async ({ canvasId, targetCanvasId, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
356
|
+
action: "bulkAddNodes",
|
|
357
|
+
items: [{ type: "canvas", linkedCanvasId: targetCanvasId, positionX: x, positionY: y }],
|
|
358
|
+
})));
|
|
359
|
+
// --- Primary bulk tool ---
|
|
360
|
+
server.tool("cn_canvas_bulk_add", "Add one or more items to a canvas. Use this for ALL canvas additions — single or multiple. Supports all node types: note, text, richtext, list, canvas (link to another canvas), and edge. ALWAYS prefer this over repeated single-item calls. Items are processed in order so edges can reference nodes created earlier in the same batch via sourceItemIndex/targetItemIndex.", {
|
|
361
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
362
|
+
notes: z.array(z.object({
|
|
363
|
+
noteId: z.string().describe("Note display ID or Convex ID"),
|
|
364
|
+
x: z.number().describe("X position"),
|
|
365
|
+
y: z.number().describe("Y position"),
|
|
366
|
+
})).optional().describe("Legacy: array of note nodes (backward compat). Use 'items' for mixed types."),
|
|
367
|
+
items: z.array(z.discriminatedUnion("type", [
|
|
368
|
+
z.object({
|
|
369
|
+
type: z.literal("note"),
|
|
370
|
+
noteId: z.string().describe("Note display ID or Convex ID"),
|
|
371
|
+
x: z.number().describe("X position"),
|
|
372
|
+
y: z.number().describe("Y position"),
|
|
373
|
+
}),
|
|
374
|
+
z.object({
|
|
375
|
+
type: z.literal("text"),
|
|
376
|
+
content: z.string().describe("Text content"),
|
|
377
|
+
x: z.number().describe("X position"),
|
|
378
|
+
y: z.number().describe("Y position"),
|
|
379
|
+
fontSize: z.number().optional().describe("Font size (default 18, h1=32, h2=24)"),
|
|
380
|
+
colorVariant: z.enum(["normal", "muted", "highlighted"]).optional(),
|
|
381
|
+
}),
|
|
382
|
+
z.object({
|
|
383
|
+
type: z.literal("richtext"),
|
|
384
|
+
content: z.string().describe("Markdown content. To link to notes, use [NOTE-123: Title](relationship:references)."),
|
|
385
|
+
x: z.number().describe("X position"),
|
|
386
|
+
y: z.number().describe("Y position"),
|
|
387
|
+
size: z.enum(["small", "medium", "large"]).optional().describe("Node size"),
|
|
388
|
+
colorHex: z.string().optional().describe("Background color hex"),
|
|
389
|
+
}),
|
|
390
|
+
z.object({
|
|
391
|
+
type: z.literal("list"),
|
|
392
|
+
description: z.string().min(1).describe("Rich-text description (markdown). Required. First paragraph derives the searchable title — this is the only label for list nodes."),
|
|
393
|
+
x: z.number().describe("X position"),
|
|
394
|
+
y: z.number().describe("Y position"),
|
|
395
|
+
viewMode: z.enum(["list", "grid"]).optional(),
|
|
396
|
+
noteIds: z.array(z.string()).optional().describe("Note IDs to add as list items"),
|
|
397
|
+
}),
|
|
398
|
+
z.object({
|
|
399
|
+
type: z.literal("canvas"),
|
|
400
|
+
linkedCanvasId: z.string().describe("Canvas ID to link to"),
|
|
401
|
+
x: z.number().describe("X position"),
|
|
402
|
+
y: z.number().describe("Y position"),
|
|
403
|
+
}),
|
|
404
|
+
z.object({
|
|
405
|
+
type: z.literal("edge"),
|
|
406
|
+
sourceNodeId: z.string().optional().describe("Existing source node ID"),
|
|
407
|
+
targetNodeId: z.string().optional().describe("Existing target node ID"),
|
|
408
|
+
sourceItemIndex: z.number().optional().describe("Index of source item in this batch (0-based)"),
|
|
409
|
+
targetItemIndex: z.number().optional().describe("Index of target item in this batch (0-based)"),
|
|
410
|
+
label: z.string().optional().describe("Edge label"),
|
|
411
|
+
}),
|
|
412
|
+
])).optional().describe("Array of items to add (supports all node types and edges). Item indices for edges are relative to this array, not the merged result."),
|
|
413
|
+
}, async ({ canvasId, notes, items }) => withErrorHandling(() => {
|
|
414
|
+
// Normalize: convert legacy notes to items format, merge with items
|
|
415
|
+
const noteItems = (notes ?? []).map((n) => ({
|
|
416
|
+
type: "note",
|
|
417
|
+
noteId: n.noteId,
|
|
418
|
+
positionX: n.x,
|
|
419
|
+
positionY: n.y,
|
|
420
|
+
}));
|
|
421
|
+
const noteOffset = noteItems.length;
|
|
422
|
+
const allItems = [
|
|
423
|
+
...noteItems,
|
|
424
|
+
...(items ?? []).map((item) => {
|
|
425
|
+
if (item.type === 'edge') {
|
|
426
|
+
// Shift sourceItemIndex/targetItemIndex to account for prepended legacy notes
|
|
427
|
+
return {
|
|
428
|
+
...item,
|
|
429
|
+
sourceItemIndex: item.sourceItemIndex != null ? item.sourceItemIndex + noteOffset : undefined,
|
|
430
|
+
targetItemIndex: item.targetItemIndex != null ? item.targetItemIndex + noteOffset : undefined,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const { x, y, ...rest } = item;
|
|
434
|
+
return { ...rest, positionX: x, positionY: y };
|
|
435
|
+
}),
|
|
436
|
+
];
|
|
437
|
+
return api.post(`/api/canvas/${canvasId}`, {
|
|
438
|
+
action: "bulkAddNodes",
|
|
439
|
+
items: allItems,
|
|
440
|
+
});
|
|
441
|
+
}));
|
|
442
|
+
server.tool("cn_canvas_place", "Place items on a canvas using a DECLARATIVE LAYOUT (no x/y math). The server measures every item, packs them with no overlaps, and inserts them in a single batch. PREFER THIS over cn_canvas_bulk_add whenever you can express the layout as stacks/grids/anchors — which is almost always. The agent should describe STRUCTURE (rows, columns, what's near what), not pixel coordinates.\n\nThe call is best-effort, NOT atomic: per-item failures are reported in the response (HTTP 207) but the items that succeeded stay on the canvas. Always inspect `items[].error` and `edges[].error` in the response.\n\nThe `doc` argument is a layout document:\n { root: <LayoutNode>, edges?: [{from, to, label?}], origin?: {x, y} }\n\n<LayoutNode> is one of:\n • { kind: 'stack', axis: 'vertical'|'horizontal', gap?: 'tight'|'medium'|'spacious'|<px>, align?: 'start'|'center'|'end', items: [<LayoutNode>...] }\n • { kind: 'grid', columns: <n>, gap?: ..., items: [<LayoutNode>...] }\n • { kind: 'anchor', to: '<displayId or canvas node id>', direction: 'right'|'left'|'above'|'below', gap?: ..., child: <LayoutNode> }\n • { kind: 'item', type: 'note'|'text'|'richtext'|'list'|'canvas', key?: '<name>', ...item-specific fields }\n\nLeaf item shapes (no x/y!):\n note: { kind:'item', type:'note', noteId:'NOTE-3', key?:'a' }\n text: { kind:'item', type:'text', content:'Hello', fontSize?:32, colorVariant?:'normal'|'muted'|'highlighted' }\n richtext: { kind:'item', type:'richtext', content:'# Markdown OK', size?:'small'|'medium'|'large', colorHex?:'#3B82F6' }\n list: { kind:'item', type:'list', name:'My list', noteIds?:['NOTE-1','NOTE-2'], viewMode?:'list'|'grid' }\n canvas: { kind:'item', type:'canvas', linkedCanvasId:'<id>' }\n\nEdges reference endpoints by, in priority order: `@<key>` (any item with a `key` placed in the same call), the original noteId/displayId of a note placed in this call (e.g. 'NOTE-7'), or the displayId / convex id of a node already on the canvas.\n\nExample (a SWOT-like quadrant):\n {\n root: {\n kind:'stack', axis:'vertical', gap:'spacious',\n items:[\n { kind:'stack', axis:'horizontal', gap:'spacious', items:[\n { kind:'grid', columns:2, items:[\n { kind:'item', type:'note', noteId:'STR-1' },\n { kind:'item', type:'note', noteId:'STR-2' },\n ]},\n { kind:'grid', columns:2, items:[\n { kind:'item', type:'note', noteId:'WEAK-1' },\n ]},\n ]},\n { kind:'stack', axis:'horizontal', gap:'spacious', items:[ /* opportunities + threats */ ]},\n ],\n },\n }", {
|
|
443
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
444
|
+
doc: z.any().describe("Layout document. See tool description for shape."),
|
|
445
|
+
}, async ({ canvasId, doc }) => withErrorHandling(() => api.post(`/api/canvas/${encodeURIComponent(canvasId)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
|
|
446
|
+
action: "placeLayout",
|
|
447
|
+
doc,
|
|
448
|
+
})));
|
|
449
|
+
server.tool("cn_canvas_bulk_move", "Move multiple nodes to new positions in one operation. More efficient than calling cn_canvas_move_node multiple times.", {
|
|
450
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
451
|
+
moves: z.array(z.object({
|
|
452
|
+
nodeId: z.string().describe("Canvas node ID"),
|
|
453
|
+
nodeType: z.enum(["note", "text", "list", "canvas", "richtext"]).optional().default("note").describe("Node type"),
|
|
454
|
+
x: z.number().describe("New X position"),
|
|
455
|
+
y: z.number().describe("New Y position"),
|
|
456
|
+
})).describe("Array of node moves"),
|
|
457
|
+
}, async ({ canvasId, moves }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
458
|
+
action: "bulkMoveNodes",
|
|
459
|
+
moves: moves.map((m) => ({
|
|
460
|
+
nodeId: m.nodeId,
|
|
461
|
+
nodeType: m.nodeType || "note",
|
|
462
|
+
positionX: m.x,
|
|
463
|
+
positionY: m.y,
|
|
464
|
+
})),
|
|
465
|
+
})));
|
|
466
|
+
server.tool("cn_canvas_bulk_remove", "Remove multiple nodes from a canvas in one operation. More efficient than calling cn_canvas_remove_node multiple times.", {
|
|
467
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
468
|
+
nodeIds: z.array(z.string()).describe("Array of node IDs to remove"),
|
|
469
|
+
}, async ({ canvasId, nodeIds }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
470
|
+
action: "bulkRemoveNodes",
|
|
471
|
+
nodeIds,
|
|
472
|
+
})));
|
|
473
|
+
server.tool("cn_canvas_add_edge", "Connect two nodes on a canvas with an edge. Defaults to an arrow at the target end and a solid line.", {
|
|
474
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
475
|
+
sourceNodeId: z.string().describe("Source node ID"),
|
|
476
|
+
targetNodeId: z.string().describe("Target node ID"),
|
|
477
|
+
label: z.string().optional().describe("Edge label"),
|
|
478
|
+
arrow: z
|
|
479
|
+
.enum(["end", "start", "both", "none"])
|
|
480
|
+
.optional()
|
|
481
|
+
.describe("Arrow direction (default: end)"),
|
|
482
|
+
lineStyle: z
|
|
483
|
+
.enum(["solid", "dashed"])
|
|
484
|
+
.optional()
|
|
485
|
+
.describe("Line style (default: solid)"),
|
|
486
|
+
}, async ({ canvasId, sourceNodeId, targetNodeId, label, arrow, lineStyle }) => withErrorHandling(() => {
|
|
487
|
+
const markers = arrowToMarkers(arrow);
|
|
488
|
+
return api.post(`/api/canvas/${canvasId}`, {
|
|
489
|
+
action: "addEdge",
|
|
490
|
+
sourceNodeId,
|
|
491
|
+
targetNodeId,
|
|
492
|
+
label,
|
|
493
|
+
...markers,
|
|
494
|
+
lineStyle,
|
|
495
|
+
});
|
|
496
|
+
}));
|
|
497
|
+
server.tool("cn_canvas_update_edge", "Update an edge: change its label, toggle arrowheads, switch line style, or reverse direction. Only the fields you pass are changed.", {
|
|
498
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
499
|
+
edgeId: z.string().describe("Canvas edge ID"),
|
|
500
|
+
label: z
|
|
501
|
+
.string()
|
|
502
|
+
.nullable()
|
|
503
|
+
.optional()
|
|
504
|
+
.describe("Set label text, or pass null to clear it"),
|
|
505
|
+
arrow: z
|
|
506
|
+
.enum(["end", "start", "both", "none"])
|
|
507
|
+
.optional()
|
|
508
|
+
.describe("Arrow direction"),
|
|
509
|
+
lineStyle: z.enum(["solid", "dashed"]).optional().describe("Line style"),
|
|
510
|
+
reverse: z.boolean().optional().describe("Swap source and target nodes"),
|
|
511
|
+
}, async ({ canvasId, edgeId, label, arrow, lineStyle, reverse }) => withErrorHandling(() => {
|
|
512
|
+
const body = {
|
|
513
|
+
action: "updateEdge",
|
|
514
|
+
edgeId,
|
|
515
|
+
};
|
|
516
|
+
if (label !== undefined)
|
|
517
|
+
body.label = label;
|
|
518
|
+
if (arrow !== undefined)
|
|
519
|
+
Object.assign(body, arrowToMarkers(arrow));
|
|
520
|
+
if (lineStyle !== undefined)
|
|
521
|
+
body.lineStyle = lineStyle;
|
|
522
|
+
if (reverse !== undefined)
|
|
523
|
+
body.reverse = reverse;
|
|
524
|
+
return api.post(`/api/canvas/${canvasId}`, body);
|
|
525
|
+
}));
|
|
526
|
+
server.tool("cn_canvas_move_node", "Move a node to a new position on a canvas.", {
|
|
527
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
528
|
+
nodeId: z.string().describe("Canvas node ID"),
|
|
529
|
+
nodeType: z.enum(["note", "text", "list", "canvas", "richtext"]).optional().default("note").describe("Node type"),
|
|
530
|
+
x: z.number().describe("New X position"),
|
|
531
|
+
y: z.number().describe("New Y position"),
|
|
532
|
+
}, async ({ canvasId, nodeId, nodeType, x, y }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
533
|
+
action: "moveNode",
|
|
534
|
+
nodeId,
|
|
535
|
+
nodeType,
|
|
536
|
+
positionX: x,
|
|
537
|
+
positionY: y,
|
|
538
|
+
})));
|
|
539
|
+
server.tool("cn_canvas_remove_node", "Remove a node from a canvas.", {
|
|
540
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
541
|
+
nodeId: z.string().describe("Canvas node ID"),
|
|
542
|
+
}, async ({ canvasId, nodeId }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
543
|
+
action: "removeNode",
|
|
544
|
+
nodeId,
|
|
545
|
+
})));
|
|
546
|
+
server.tool("cn_canvas_remove_edge", "Remove an edge from a canvas.", {
|
|
547
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
548
|
+
edgeId: z.string().describe("Canvas edge ID"),
|
|
549
|
+
}, async ({ canvasId, edgeId }) => withErrorHandling(() => api.post(`/api/canvas/${canvasId}`, {
|
|
550
|
+
action: "removeEdge",
|
|
551
|
+
edgeId,
|
|
552
|
+
})));
|
|
553
|
+
server.tool("cn_canvas_digest", "Get the AI-generated digest of a canvas: narrative summary, key themes, and a per-note summary list covering every note on the canvas. Run this first when starting work on any canvas — it gives you full orientation in a single call, so you don't need to read each note individually. Each note entry has shape { displayId, title, type, summary }; when summary is null the AI summary hasn't generated yet. The notes list is always populated, even when digest status is 'none' or 'stale'.", {
|
|
554
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
555
|
+
}, async ({ canvasId }) => withErrorHandling(() => api.get(`/api/canvas/${encodeURIComponent(canvasId)}/digest`, { workspaceId })));
|
|
556
|
+
server.tool("cn_canvas_digest_generate", "Request digest (re)generation for a canvas. Use when digest status is 'none' or you want a fresh summary. The digest is generated async — poll cn_canvas_digest after ~60s.", {
|
|
557
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
558
|
+
}, async ({ canvasId }) => withErrorHandling(() => api.post(`/api/canvas/${encodeURIComponent(canvasId)}/digest?workspaceId=${encodeURIComponent(workspaceId)}`, {})));
|
|
559
|
+
server.tool("cn_canvas_activity", "Show recent activity on a canvas — edits, agent runs, gestures, restores — newest first. Use this to find out what changed and who changed it before making further edits, or to confirm a previous agent run actually applied. Returns { canvasName, operations: Array<{ _id, operationType, actorName, actorSource ('human'|'agent'|'import'|'api'), agentRunId, userPrompt, status ('in_progress'|'applied'|'reverted'|'failed'), eventCount, affectedEntityIds, summary, structuredSummary: { headline, summary, byCategory[], themes[] }, startedAt, completedAt }>}.", {
|
|
560
|
+
canvasId: z.string().describe("Canvas ID"),
|
|
561
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max entries (default 50, max 200)"),
|
|
562
|
+
}, async ({ canvasId, limit }) => {
|
|
563
|
+
const params = { workspaceId };
|
|
564
|
+
if (limit !== undefined)
|
|
565
|
+
params.limit = String(limit);
|
|
566
|
+
return withErrorHandling(() => api.get(`/api/canvas/${encodeURIComponent(canvasId)}/activity`, params));
|
|
567
|
+
});
|
|
568
|
+
server.tool("cn_canvas_templates", "List available canvas templates.", {}, async () => withErrorHandling(() => api.get("/api/canvas/template")));
|
|
569
|
+
server.tool("cn_canvas_from_template", "Create a canvas from a predefined template.", {
|
|
570
|
+
templateId: z.string().describe("Template ID"),
|
|
571
|
+
title: z.string().optional().describe("Custom canvas title"),
|
|
572
|
+
}, async ({ templateId, title }) => withErrorHandling(() => api.post("/api/canvas/template", {
|
|
573
|
+
workspaceId,
|
|
574
|
+
templateId,
|
|
575
|
+
title,
|
|
576
|
+
})));
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// Timeline
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
server.tool("cn_timeline", "Show recent changes across notes. Supports relative time (7d, 2w) or ISO dates.", {
|
|
581
|
+
since: z.string().optional().default("7d").describe("Start of range (e.g. 7d, 2w, 2026-03-21)"),
|
|
582
|
+
until: z.string().optional().describe("End of range (default: now)"),
|
|
583
|
+
type: z.string().optional().describe("Filter by note type (comma-separated)"),
|
|
584
|
+
noteFilter: z.string().optional().describe("Filter by note ID or title substring"),
|
|
585
|
+
limit: z.number().optional().default(50).describe("Max results (1-100)"),
|
|
586
|
+
}, async ({ since, until, type, noteFilter, limit }) => withErrorHandling(() => {
|
|
587
|
+
const sinceMs = parseDuration(since ?? "7d");
|
|
588
|
+
const untilMs = until ? parseDuration(until) : undefined;
|
|
589
|
+
return api.get("/api/timeline", {
|
|
590
|
+
workspaceId,
|
|
591
|
+
afterTimestamp: String(sinceMs),
|
|
592
|
+
beforeTimestamp: untilMs ? String(untilMs) : undefined,
|
|
593
|
+
limit: String(Math.max(1, Math.min(100, limit ?? 50))),
|
|
594
|
+
noteTypes: type,
|
|
595
|
+
noteFilter,
|
|
596
|
+
});
|
|
597
|
+
}));
|
|
598
|
+
const DURATION_UNITS = {
|
|
599
|
+
m: 60_000,
|
|
600
|
+
h: 3_600_000,
|
|
601
|
+
d: 86_400_000,
|
|
602
|
+
w: 604_800_000,
|
|
603
|
+
};
|
|
604
|
+
function parseDuration(input) {
|
|
605
|
+
const rel = input.match(/^(\d+)([mhdw])$/);
|
|
606
|
+
if (rel) {
|
|
607
|
+
return Date.now() - parseInt(rel[1], 10) * DURATION_UNITS[rel[2]];
|
|
608
|
+
}
|
|
609
|
+
if (/^\d{10,13}$/.test(input)) {
|
|
610
|
+
const n = parseInt(input, 10);
|
|
611
|
+
return input.length <= 10 ? n * 1000 : n;
|
|
612
|
+
}
|
|
613
|
+
const ms = new Date(input).getTime();
|
|
614
|
+
if (isNaN(ms))
|
|
615
|
+
throw new Error(`Invalid date: "${input}"`);
|
|
616
|
+
return ms;
|
|
617
|
+
}
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Types (supertags)
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
server.tool("cn_types_list", "List note types (supertags) in the workspace.", {}, async () => withErrorHandling(() => api.get("/api/supertags", { workspaceId })));
|
|
622
|
+
server.tool("cn_types_create", "Create a new note type (supertag).", {
|
|
623
|
+
name: z.string().describe("Type name, PascalCase, no spaces (e.g. PainPoint, UserStory)"),
|
|
624
|
+
displayName: z.string().optional().describe("Display name (can have spaces)"),
|
|
625
|
+
prefix: z.string().optional().describe("ID prefix, uppercase (e.g. PAINPOINT, USERSTORY)"),
|
|
626
|
+
description: z.string().optional().describe("Description"),
|
|
627
|
+
color: z.string().optional().describe("Color hex code (e.g. #FF5733)"),
|
|
628
|
+
}, async ({ name, displayName, prefix, description, color }) => withErrorHandling(() => api.post("/api/supertags", {
|
|
629
|
+
workspaceId,
|
|
630
|
+
name,
|
|
631
|
+
displayName,
|
|
632
|
+
prefix,
|
|
633
|
+
description,
|
|
634
|
+
color,
|
|
635
|
+
})));
|
|
636
|
+
server.tool("cn_types_update", "Update a note type (supertag).", {
|
|
637
|
+
name: z.string().describe("Type name to update"),
|
|
638
|
+
displayName: z.string().optional().describe("New display name"),
|
|
639
|
+
prefix: z.string().optional().describe("New ID prefix (uppercase)"),
|
|
640
|
+
description: z.string().optional().describe("New description"),
|
|
641
|
+
color: z.string().optional().describe("Color hex code"),
|
|
642
|
+
}, async ({ name, displayName, prefix, description, color }) => withErrorHandling(() => {
|
|
643
|
+
const body = { workspaceId, name };
|
|
644
|
+
if (displayName !== undefined)
|
|
645
|
+
body.displayName = displayName;
|
|
646
|
+
if (prefix !== undefined)
|
|
647
|
+
body.prefix = prefix;
|
|
648
|
+
if (description !== undefined)
|
|
649
|
+
body.description = description;
|
|
650
|
+
if (color !== undefined)
|
|
651
|
+
body.color = color;
|
|
652
|
+
return api.patch("/api/supertags", body);
|
|
653
|
+
}));
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
// Relationships
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
server.tool("cn_relationships_list", "List note relationships in the workspace.", {
|
|
658
|
+
noteDisplayId: z.string().optional().describe("Filter by note display ID"),
|
|
659
|
+
}, async ({ noteDisplayId }) => withErrorHandling(() => api.get("/api/relationships", {
|
|
660
|
+
workspaceId,
|
|
661
|
+
displayId: noteDisplayId,
|
|
662
|
+
})));
|
|
663
|
+
// ---------------------------------------------------------------------------
|
|
664
|
+
// Memory (Zep)
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
server.tool("cn_memory_query", "Search facts and entities in the knowledge graph by relevance.", {
|
|
667
|
+
query: z.string().describe("Search query"),
|
|
668
|
+
limit: z.number().optional().default(10).describe("Max results per category"),
|
|
669
|
+
}, async ({ query, limit }) => withErrorHandling(() => api.post("/api/zep/query", {
|
|
670
|
+
workspaceId,
|
|
671
|
+
query,
|
|
672
|
+
limit: limit ?? 10,
|
|
673
|
+
})));
|
|
674
|
+
server.tool("cn_memory_facts", "List all workspace facts from the knowledge graph.", {}, async () => withErrorHandling(async () => {
|
|
675
|
+
const data = await api.post("/api/zep/query", {
|
|
676
|
+
workspaceId,
|
|
677
|
+
query: "all facts",
|
|
678
|
+
limit: 1,
|
|
679
|
+
});
|
|
680
|
+
return data.allFacts;
|
|
681
|
+
}));
|
|
682
|
+
server.tool("cn_memory_entities", "List all workspace entities (people, topics, orgs) from the knowledge graph.", {}, async () => withErrorHandling(async () => {
|
|
683
|
+
const data = await api.post("/api/zep/query", {
|
|
684
|
+
workspaceId,
|
|
685
|
+
query: "all entities",
|
|
686
|
+
limit: 1,
|
|
687
|
+
});
|
|
688
|
+
return data.allEntities;
|
|
689
|
+
}));
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// Versions
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
server.tool("cn_versions_list", "List versions of a note.", {
|
|
694
|
+
noteId: z.string().describe("Note display ID or Convex ID"),
|
|
695
|
+
}, async ({ noteId }) => withErrorHandling(async () => {
|
|
696
|
+
const notesList = await api.get("/api/notes", {
|
|
697
|
+
workspaceId,
|
|
698
|
+
exact_id: noteId,
|
|
699
|
+
includeVersions: "true",
|
|
700
|
+
});
|
|
701
|
+
const note = Array.isArray(notesList) ? notesList[0] : null;
|
|
702
|
+
if (!note)
|
|
703
|
+
throw new Error(`Note not found: ${noteId}`);
|
|
704
|
+
return note.versions || [];
|
|
705
|
+
}));
|
|
706
|
+
server.tool("cn_versions_create", "Create a new version of a note. Content is markdown. To link to other notes, use [NOTE-123: Title](relationship:references) inside the markdown — always include the title (the part after the colon) or the chip renders as 'Untitled'. The relationship name is free-form (e.g. references, triggers, caused-by); use 'references' if unsure.", {
|
|
707
|
+
noteId: z.string().describe("Note display ID or Convex ID"),
|
|
708
|
+
markdown: z.string().describe("New content as markdown"),
|
|
709
|
+
description: z.string().describe("Change description (required)"),
|
|
710
|
+
}, async ({ noteId, markdown, description }) => withErrorHandling(async () => {
|
|
711
|
+
const resolvedId = await resolveNoteId(api, workspaceId, noteId);
|
|
712
|
+
return api.post(`/api/notes/${resolvedId}/versions`, {
|
|
713
|
+
content: markdown,
|
|
714
|
+
description,
|
|
715
|
+
});
|
|
716
|
+
}));
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// Workspace
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
server.tool("cn_workspace_list", "List all workspaces the authenticated user belongs to.", {}, async () => withErrorHandling(() => api.get("/api/workspaces")));
|
|
721
|
+
server.tool("cn_workspace_current", "Get details about the current workspace.", {}, async () => withErrorHandling(() => api.get(`/api/workspaces/${workspaceId}`)));
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Files
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
const FILE_MIME_MAP = {
|
|
726
|
+
".jpg": "image/jpeg",
|
|
727
|
+
".jpeg": "image/jpeg",
|
|
728
|
+
".png": "image/png",
|
|
729
|
+
".gif": "image/gif",
|
|
730
|
+
".webp": "image/webp",
|
|
731
|
+
};
|
|
732
|
+
server.tool("cn_files_upload", "Upload an image file to the workspace. Returns the public URL. Supports .jpg, .jpeg, .png, .gif, .webp (max 5MB).", {
|
|
733
|
+
filePath: z.string().describe("Absolute path to the image file on disk"),
|
|
734
|
+
}, async ({ filePath }) => withErrorHandling(async () => {
|
|
735
|
+
const absPath = path.resolve(filePath);
|
|
736
|
+
let stat;
|
|
737
|
+
try {
|
|
738
|
+
stat = statSync(absPath);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
throw new Error(`File not found: ${absPath}`);
|
|
742
|
+
}
|
|
743
|
+
if (!stat.isFile())
|
|
744
|
+
throw new Error(`Not a file: ${absPath}`);
|
|
745
|
+
if (stat.size > 5 * 1024 * 1024)
|
|
746
|
+
throw new Error(`File too large (${stat.size} bytes). Maximum is 5MB.`);
|
|
747
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
748
|
+
const mimeType = FILE_MIME_MAP[ext];
|
|
749
|
+
if (!mimeType)
|
|
750
|
+
throw new Error(`Unsupported file type: ${ext}. Allowed: .jpg, .jpeg, .png, .gif, .webp`);
|
|
751
|
+
const buffer = readFileSync(absPath);
|
|
752
|
+
const filename = path.basename(absPath);
|
|
753
|
+
const buildFormData = () => {
|
|
754
|
+
const fd = new FormData();
|
|
755
|
+
fd.append("image", new Blob([buffer], { type: mimeType }), filename);
|
|
756
|
+
fd.append("workspaceId", workspaceId);
|
|
757
|
+
return fd;
|
|
758
|
+
};
|
|
759
|
+
const result = await api.postFormData("/api/upload-image", buildFormData);
|
|
760
|
+
return {
|
|
761
|
+
...result,
|
|
762
|
+
fullUrl: `${CN_SERVER}${result.url}`,
|
|
763
|
+
markdown: ``,
|
|
764
|
+
};
|
|
765
|
+
}));
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
// Schema
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
server.tool("cn_schema", "Describe this workspace in one call: valid note types (with prefixes), common relationship slugs, resource field shapes, and the canvas node kinds. Call this once at the start of a session instead of guessing type names or reading docs.", {}, async () => withErrorHandling(() => buildSchema(api, workspaceId)));
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
// Start
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
async function main() {
|
|
774
|
+
const transport = new StdioServerTransport();
|
|
775
|
+
await server.connect(transport);
|
|
776
|
+
process.stderr.write("cnotes-mcp server running on stdio\n");
|
|
777
|
+
}
|
|
778
|
+
main().catch((e) => {
|
|
779
|
+
process.stderr.write(`cnotes-mcp fatal: ${e}\n`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
});
|
|
782
|
+
//# sourceMappingURL=mcp-server.js.map
|