@mseep/affine-mcp-server 2.3.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 (43) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +270 -0
  3. package/bin/affine-mcp +5 -0
  4. package/dist/auth.js +61 -0
  5. package/dist/cli.js +726 -0
  6. package/dist/config.js +178 -0
  7. package/dist/edgeless/layout.js +222 -0
  8. package/dist/graphqlClient.js +116 -0
  9. package/dist/httpAuth.js +147 -0
  10. package/dist/httpDiagnostics.js +38 -0
  11. package/dist/index.js +209 -0
  12. package/dist/markdown/parse.js +559 -0
  13. package/dist/markdown/render.js +227 -0
  14. package/dist/markdown/types.js +1 -0
  15. package/dist/oauth.js +154 -0
  16. package/dist/sse.js +261 -0
  17. package/dist/toolSurface.js +349 -0
  18. package/dist/tools/accessTokens.js +45 -0
  19. package/dist/tools/auth.js +18 -0
  20. package/dist/tools/blobStorage.js +136 -0
  21. package/dist/tools/comments.js +104 -0
  22. package/dist/tools/docs.js +7478 -0
  23. package/dist/tools/history.js +22 -0
  24. package/dist/tools/icons.js +125 -0
  25. package/dist/tools/notifications.js +79 -0
  26. package/dist/tools/organize.js +1145 -0
  27. package/dist/tools/properties.js +426 -0
  28. package/dist/tools/user.js +13 -0
  29. package/dist/tools/userCRUD.js +77 -0
  30. package/dist/tools/workspaces.js +322 -0
  31. package/dist/util/explorerIcon.js +95 -0
  32. package/dist/util/mcp.js +28 -0
  33. package/dist/ws.js +113 -0
  34. package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
  35. package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
  36. package/docs/client-setup.md +174 -0
  37. package/docs/configuration-and-deployment.md +265 -0
  38. package/docs/edgeless-canvas-cookbook.md +226 -0
  39. package/docs/getting-started.md +229 -0
  40. package/docs/tool-reference.md +200 -0
  41. package/docs/workflow-recipes.md +147 -0
  42. package/package.json +118 -0
  43. package/tool-manifest.json +99 -0
@@ -0,0 +1,426 @@
1
+ import { z } from "zod";
2
+ import { generateKeyBetween } from "fractional-indexing";
3
+ import * as Y from "yjs";
4
+ import { text } from "../util/mcp.js";
5
+ import { wsUrlFromGraphQLEndpoint, connectWorkspaceSocket, joinWorkspace, loadDoc, pushDocUpdate, } from "../ws.js";
6
+ /**
7
+ * Doc custom properties live in dedicated Yjs sub-docs synced by guid, NOT in
8
+ * the page doc or the workspace root meta. AFFiNE's WorkspaceDB (an ORM on top
9
+ * of Yjs) maps one table to one sub-doc whose guid is `db$<tableName>`:
10
+ *
11
+ * - `db$docCustomPropertyInfo`: the workspace-wide property *definitions*
12
+ * (schema). Top-level YMap keyed by propertyId -> { id, name, type, index,
13
+ * icon, show, isDeleted }.
14
+ * - `db$docProperties`: the per-doc property *values*. Top-level YMap keyed by
15
+ * docId -> { id, ...builtins, "custom:<propertyId>": <value> }.
16
+ *
17
+ * A custom property must have a definition in `db$docCustomPropertyInfo` to be
18
+ * rendered/editable in the AFFiNE UI. Writing a value without a matching
19
+ * definition stores orphan data that the UI ignores.
20
+ *
21
+ * Values are stored as strings, encoded per type:
22
+ * - text: raw string
23
+ * - number: stringified number
24
+ * - checkbox: "true" | "false"
25
+ * - date: "YYYY-MM-DD"
26
+ *
27
+ * References (AFFiNE repo):
28
+ * - modules/db/services/db.ts -> guid `db$${tableName}`
29
+ * - orm/core/adapters/yjs/table.ts -> record = top-level YMap keyed by primary key
30
+ * - modules/doc/entities/record.ts -> value key is `custom:<propertyId>`
31
+ */
32
+ const DOC_PROPERTIES_GUID = "db$docProperties";
33
+ const CUSTOM_PROPERTY_INFO_GUID = "db$docCustomPropertyInfo";
34
+ const DELETED_FLAG = "$$DELETED";
35
+ const CUSTOM_PREFIX = "custom:";
36
+ const SUPPORTED_TYPES = ["text", "number", "checkbox", "date"];
37
+ const WorkspaceId = z.string().min(1, "workspaceId required");
38
+ const DocId = z.string().min(1, "docId required");
39
+ const NANOID_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
40
+ /** Generate a 21-char nanoid-style id, matching AFFiNE's property id format. */
41
+ function generatePropertyId() {
42
+ let id = "";
43
+ for (let i = 0; i < 21; i++) {
44
+ id += NANOID_ALPHABET.charAt(Math.floor(Math.random() * NANOID_ALPHABET.length));
45
+ }
46
+ return id;
47
+ }
48
+ /**
49
+ * Read all live custom-property definitions from the `db$docCustomPropertyInfo`
50
+ * sub-doc, skipping soft-deleted and empty records.
51
+ */
52
+ function readPropertyDefinitions(doc) {
53
+ const defs = [];
54
+ // Records are top-level (root) Yjs types keyed by id. After applyUpdate, root
55
+ // types are generic AbstractType until doc.getMap(key) casts them, so an
56
+ // `instanceof Y.Map` check would skip every record. Mirror AFFiNE's ORM
57
+ // adapter and materialize each record via getMap.
58
+ for (const key of doc.share.keys()) {
59
+ const data = doc.getMap(key).toJSON();
60
+ if (data[DELETED_FLAG] === true || data.isDeleted === true)
61
+ continue;
62
+ if (Object.keys(data).length === 0)
63
+ continue;
64
+ const type = typeof data.type === "string" ? data.type : "unknown";
65
+ defs.push({
66
+ id: typeof data.id === "string" ? data.id : key,
67
+ name: typeof data.name === "string" ? data.name : null,
68
+ type,
69
+ index: typeof data.index === "string" ? data.index : null,
70
+ icon: typeof data.icon === "string" ? data.icon : null,
71
+ show: typeof data.show === "string" ? data.show : null,
72
+ });
73
+ }
74
+ defs.sort((a, b) => (a.index || "").localeCompare(b.index || ""));
75
+ return defs;
76
+ }
77
+ /**
78
+ * Resolve a definition by exact id, then by unique case-insensitive name.
79
+ * Throws if a name matches more than one definition.
80
+ */
81
+ function resolveDefinition(defs, property) {
82
+ const byId = defs.find((d) => d.id === property);
83
+ if (byId)
84
+ return byId;
85
+ const lowered = property.trim().toLowerCase();
86
+ const byName = defs.filter((d) => (d.name || "").trim().toLowerCase() === lowered);
87
+ if (byName.length === 1)
88
+ return byName[0];
89
+ if (byName.length > 1) {
90
+ throw new Error(`Property name "${property}" is ambiguous (${byName.length} matches). Use the property id instead.`);
91
+ }
92
+ return null;
93
+ }
94
+ /** Compute the next fractional index, appending after the current last definition. */
95
+ function nextIndex(defs) {
96
+ const indexes = defs
97
+ .map((d) => d.index)
98
+ .filter((i) => typeof i === "string" && i.length > 0)
99
+ .sort();
100
+ const last = indexes.length ? indexes[indexes.length - 1] : null;
101
+ return generateKeyBetween(last, null);
102
+ }
103
+ /** Encode a JS value into AFFiNE's per-type string representation; throws on invalid input. */
104
+ function encodeValue(type, value) {
105
+ switch (type) {
106
+ case "checkbox": {
107
+ const truthy = value === true ||
108
+ value === 1 ||
109
+ (typeof value === "string" && ["true", "1", "yes"].includes(value.trim().toLowerCase()));
110
+ return truthy ? "true" : "false";
111
+ }
112
+ case "number": {
113
+ const n = typeof value === "number" ? value : Number(String(value).trim());
114
+ if (!Number.isFinite(n)) {
115
+ throw new Error(`number property requires a numeric value, got ${JSON.stringify(value)}`);
116
+ }
117
+ return String(n);
118
+ }
119
+ case "date": {
120
+ const s = String(value).trim();
121
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
122
+ throw new Error(`date property requires "YYYY-MM-DD", got ${JSON.stringify(value)}`);
123
+ }
124
+ const parsed = new Date(`${s}T00:00:00.000Z`);
125
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== s) {
126
+ throw new Error(`date property requires a valid "YYYY-MM-DD" date, got ${JSON.stringify(value)}`);
127
+ }
128
+ return s;
129
+ }
130
+ case "text":
131
+ default:
132
+ return String(value);
133
+ }
134
+ }
135
+ /** Decode a stored string back into a typed JS value for output. */
136
+ function decodeValue(type, raw) {
137
+ if (raw === undefined || raw === null)
138
+ return null;
139
+ switch (type) {
140
+ case "checkbox":
141
+ return raw === "true" || raw === true;
142
+ case "number": {
143
+ const n = Number(raw);
144
+ return Number.isFinite(n) ? n : raw;
145
+ }
146
+ default:
147
+ return raw;
148
+ }
149
+ }
150
+ /** Register the five document custom-property tools on the MCP server. */
151
+ export function registerPropertyTools(server, gql, defaults) {
152
+ /** Snapshot the current GraphQL endpoint and auth material for WebSocket use. */
153
+ function getCookieAndEndpoint() {
154
+ return { endpoint: gql.endpoint, cookie: gql.cookie, bearer: gql.bearer };
155
+ }
156
+ /** Resolve the workspace id from the argument or the configured default; throws if absent. */
157
+ function requireWorkspaceId(workspaceId) {
158
+ const id = workspaceId || defaults.workspaceId;
159
+ if (!id) {
160
+ throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
161
+ }
162
+ return id;
163
+ }
164
+ /** Load a WorkspaceDB sub-doc by guid and return it with its pre-mutation state vector. */
165
+ async function loadSubdoc(socket, workspaceId, guid) {
166
+ const snapshot = await loadDoc(socket, workspaceId, guid);
167
+ const doc = new Y.Doc();
168
+ let existed = false;
169
+ if (snapshot.missing) {
170
+ Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
171
+ existed = true;
172
+ }
173
+ return { doc, prevSV: Y.encodeStateVector(doc), existed };
174
+ }
175
+ /** Push only the delta accumulated since `prevSV` back to the sync gateway. */
176
+ async function pushSubdoc(socket, workspaceId, guid, doc, prevSV) {
177
+ const delta = Y.encodeStateAsUpdate(doc, prevSV);
178
+ await pushDocUpdate(socket, workspaceId, guid, Buffer.from(delta).toString("base64"));
179
+ }
180
+ /** Throw if the workspace root or the docId is not found in workspace metadata. */
181
+ async function assertDocExists(socket, workspaceId, docId) {
182
+ const snapshot = await loadDoc(socket, workspaceId, workspaceId);
183
+ if (!snapshot.missing) {
184
+ throw new Error(`Workspace root document not found for workspace ${workspaceId}`);
185
+ }
186
+ const wsDoc = new Y.Doc();
187
+ Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
188
+ const pages = wsDoc.getMap("meta").get("pages");
189
+ const exists = pages instanceof Y.Array &&
190
+ pages.toArray().some((p) => p instanceof Y.Map && p.get("id") === docId);
191
+ if (!exists) {
192
+ throw new Error(`docId ${docId} is not present in workspace ${workspaceId}`);
193
+ }
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // list_doc_properties
197
+ // ---------------------------------------------------------------------------
198
+ /** Handle `list_doc_properties`: definitions, decoded per-doc values, and orphan values. */
199
+ const listDocPropertiesHandler = async (parsed) => {
200
+ const workspaceId = requireWorkspaceId(parsed.workspaceId);
201
+ const { endpoint, cookie, bearer } = getCookieAndEndpoint();
202
+ const socket = await connectWorkspaceSocket(wsUrlFromGraphQLEndpoint(endpoint), cookie, bearer);
203
+ try {
204
+ await joinWorkspace(socket, workspaceId);
205
+ const { doc: infoDoc } = await loadSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID);
206
+ const defs = readPropertyDefinitions(infoDoc);
207
+ const { doc: propsDoc } = await loadSubdoc(socket, workspaceId, DOC_PROPERTIES_GUID);
208
+ const record = propsDoc.share.has(parsed.docId)
209
+ ? propsDoc.getMap(parsed.docId).toJSON()
210
+ : {};
211
+ const byId = new Map(defs.map((d) => [d.id, d]));
212
+ const properties = defs.map((def) => {
213
+ const raw = record[CUSTOM_PREFIX + def.id];
214
+ return {
215
+ propertyId: def.id,
216
+ name: def.name,
217
+ type: def.type,
218
+ value: decodeValue(def.type, raw),
219
+ set: raw !== undefined && raw !== null,
220
+ };
221
+ });
222
+ // Surface custom values that have no matching (live) definition.
223
+ const orphans = Object.keys(record)
224
+ .filter((k) => k.startsWith(CUSTOM_PREFIX))
225
+ .map((k) => k.slice(CUSTOM_PREFIX.length))
226
+ .filter((id) => !byId.has(id))
227
+ .map((id) => ({ propertyId: id, value: record[CUSTOM_PREFIX + id] }));
228
+ return text({
229
+ workspaceId,
230
+ docId: parsed.docId,
231
+ definitions: defs,
232
+ properties,
233
+ orphanValues: orphans,
234
+ });
235
+ }
236
+ finally {
237
+ socket.disconnect();
238
+ }
239
+ };
240
+ server.registerTool("list_doc_properties", {
241
+ title: "List Document Properties",
242
+ description: "List the workspace custom-property definitions and a document's current values for them.",
243
+ inputSchema: {
244
+ workspaceId: WorkspaceId.optional(),
245
+ docId: DocId,
246
+ },
247
+ }, listDocPropertiesHandler);
248
+ // ---------------------------------------------------------------------------
249
+ // create_custom_property
250
+ // ---------------------------------------------------------------------------
251
+ /** Handle `create_custom_property`: append a new workspace-wide definition. */
252
+ const createCustomPropertyHandler = async (parsed) => {
253
+ const workspaceId = requireWorkspaceId(parsed.workspaceId);
254
+ const name = parsed.name.trim();
255
+ if (!name)
256
+ throw new Error("name is required");
257
+ const { endpoint, cookie, bearer } = getCookieAndEndpoint();
258
+ const socket = await connectWorkspaceSocket(wsUrlFromGraphQLEndpoint(endpoint), cookie, bearer);
259
+ try {
260
+ await joinWorkspace(socket, workspaceId);
261
+ const { doc, prevSV } = await loadSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID);
262
+ const defs = readPropertyDefinitions(doc);
263
+ const id = generatePropertyId();
264
+ const index = nextIndex(defs);
265
+ const record = doc.getMap(id);
266
+ record.set("id", id);
267
+ record.set("name", name);
268
+ record.set("type", parsed.type);
269
+ record.set("index", index);
270
+ if (parsed.icon)
271
+ record.set("icon", parsed.icon);
272
+ await pushSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID, doc, prevSV);
273
+ return text({
274
+ workspaceId,
275
+ propertyId: id,
276
+ name,
277
+ type: parsed.type,
278
+ index,
279
+ created: true,
280
+ });
281
+ }
282
+ finally {
283
+ socket.disconnect();
284
+ }
285
+ };
286
+ server.registerTool("create_custom_property", {
287
+ title: "Create Custom Property",
288
+ description: "Create a workspace-wide custom property definition (text, number, checkbox, or date). Returns its propertyId.",
289
+ inputSchema: {
290
+ workspaceId: WorkspaceId.optional(),
291
+ name: z.string().min(1).describe("Display name of the property"),
292
+ type: z.enum(SUPPORTED_TYPES).describe("Property value type"),
293
+ icon: z.string().optional().describe("Optional icon name"),
294
+ },
295
+ }, createCustomPropertyHandler);
296
+ // ---------------------------------------------------------------------------
297
+ // delete_custom_property
298
+ // ---------------------------------------------------------------------------
299
+ /** Handle `delete_custom_property`: soft-delete a definition by id or name. */
300
+ const deleteCustomPropertyHandler = async (parsed) => {
301
+ const workspaceId = requireWorkspaceId(parsed.workspaceId);
302
+ const { endpoint, cookie, bearer } = getCookieAndEndpoint();
303
+ const socket = await connectWorkspaceSocket(wsUrlFromGraphQLEndpoint(endpoint), cookie, bearer);
304
+ try {
305
+ await joinWorkspace(socket, workspaceId);
306
+ const { doc, prevSV } = await loadSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID);
307
+ const defs = readPropertyDefinitions(doc);
308
+ const def = resolveDefinition(defs, parsed.property);
309
+ if (!def) {
310
+ throw new Error(`No custom property matches "${parsed.property}" in workspace ${workspaceId}`);
311
+ }
312
+ // Mirror AFFiNE: keep the record for legacy override, flag it deleted.
313
+ const record = doc.getMap(def.id);
314
+ record.set("isDeleted", true);
315
+ await pushSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID, doc, prevSV);
316
+ return text({ workspaceId, propertyId: def.id, name: def.name, deleted: true });
317
+ }
318
+ finally {
319
+ socket.disconnect();
320
+ }
321
+ };
322
+ server.registerTool("delete_custom_property", {
323
+ title: "Delete Custom Property",
324
+ description: "Soft-delete a workspace custom property definition (by propertyId or name). Existing values are hidden.",
325
+ inputSchema: {
326
+ workspaceId: WorkspaceId.optional(),
327
+ property: z.string().min(1).describe("Property id or name"),
328
+ },
329
+ }, deleteCustomPropertyHandler);
330
+ // ---------------------------------------------------------------------------
331
+ // set_doc_property
332
+ // ---------------------------------------------------------------------------
333
+ /** Handle `set_doc_property`: validate, encode, and upsert a doc's property value. */
334
+ const setDocPropertyHandler = async (parsed) => {
335
+ const workspaceId = requireWorkspaceId(parsed.workspaceId);
336
+ const { endpoint, cookie, bearer } = getCookieAndEndpoint();
337
+ const socket = await connectWorkspaceSocket(wsUrlFromGraphQLEndpoint(endpoint), cookie, bearer);
338
+ try {
339
+ await joinWorkspace(socket, workspaceId);
340
+ await assertDocExists(socket, workspaceId, parsed.docId);
341
+ const { doc: infoDoc } = await loadSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID);
342
+ const defs = readPropertyDefinitions(infoDoc);
343
+ const def = resolveDefinition(defs, parsed.property);
344
+ if (!def) {
345
+ throw new Error(`No custom property matches "${parsed.property}". Create it first with create_custom_property.`);
346
+ }
347
+ if (!SUPPORTED_TYPES.includes(def.type)) {
348
+ throw new Error(`Property "${def.name || def.id}" has type "${def.type}", which set_doc_property cannot edit. Supported: ${SUPPORTED_TYPES.join(", ")}.`);
349
+ }
350
+ const encoded = encodeValue(def.type, parsed.value);
351
+ const { doc, prevSV } = await loadSubdoc(socket, workspaceId, DOC_PROPERTIES_GUID);
352
+ const record = doc.getMap(parsed.docId);
353
+ record.set("id", parsed.docId); // ORM keyField, required by find/observe
354
+ record.set(CUSTOM_PREFIX + def.id, encoded);
355
+ await pushSubdoc(socket, workspaceId, DOC_PROPERTIES_GUID, doc, prevSV);
356
+ return text({
357
+ workspaceId,
358
+ docId: parsed.docId,
359
+ propertyId: def.id,
360
+ name: def.name,
361
+ type: def.type,
362
+ value: decodeValue(def.type, encoded),
363
+ stored: encoded,
364
+ updated: true,
365
+ });
366
+ }
367
+ finally {
368
+ socket.disconnect();
369
+ }
370
+ };
371
+ server.registerTool("set_doc_property", {
372
+ title: "Set Document Property",
373
+ description: "Set a document's custom property value (property by id or name). Value is validated against the property type (text/number/checkbox/date).",
374
+ inputSchema: {
375
+ workspaceId: WorkspaceId.optional(),
376
+ docId: DocId,
377
+ property: z.string().min(1).describe("Property id or name"),
378
+ value: z
379
+ .union([z.string(), z.number(), z.boolean()])
380
+ .describe("Value; coerced per property type (checkbox->bool, number, date YYYY-MM-DD, text)"),
381
+ },
382
+ }, setDocPropertyHandler);
383
+ // ---------------------------------------------------------------------------
384
+ // clear_doc_property
385
+ // ---------------------------------------------------------------------------
386
+ /** Handle `clear_doc_property`: remove a doc's value for a property (by id or name). */
387
+ const clearDocPropertyHandler = async (parsed) => {
388
+ const workspaceId = requireWorkspaceId(parsed.workspaceId);
389
+ const { endpoint, cookie, bearer } = getCookieAndEndpoint();
390
+ const socket = await connectWorkspaceSocket(wsUrlFromGraphQLEndpoint(endpoint), cookie, bearer);
391
+ try {
392
+ await joinWorkspace(socket, workspaceId);
393
+ const { doc: infoDoc } = await loadSubdoc(socket, workspaceId, CUSTOM_PROPERTY_INFO_GUID);
394
+ const defs = readPropertyDefinitions(infoDoc);
395
+ const def = resolveDefinition(defs, parsed.property);
396
+ // Allow clearing by raw id even if the definition was already deleted.
397
+ const propertyId = def?.id ?? parsed.property;
398
+ const { doc, prevSV } = await loadSubdoc(socket, workspaceId, DOC_PROPERTIES_GUID);
399
+ let cleared = false;
400
+ if (doc.share.has(parsed.docId)) {
401
+ const record = doc.getMap(parsed.docId);
402
+ const key = CUSTOM_PREFIX + propertyId;
403
+ if (record.has(key)) {
404
+ record.delete(key);
405
+ cleared = true;
406
+ }
407
+ }
408
+ if (cleared) {
409
+ await pushSubdoc(socket, workspaceId, DOC_PROPERTIES_GUID, doc, prevSV);
410
+ }
411
+ return text({ workspaceId, docId: parsed.docId, propertyId, cleared });
412
+ }
413
+ finally {
414
+ socket.disconnect();
415
+ }
416
+ };
417
+ server.registerTool("clear_doc_property", {
418
+ title: "Clear Document Property",
419
+ description: "Remove a custom property value from a document (property by id or name).",
420
+ inputSchema: {
421
+ workspaceId: WorkspaceId.optional(),
422
+ docId: DocId,
423
+ property: z.string().min(1).describe("Property id or name"),
424
+ },
425
+ }, clearDocPropertyHandler);
426
+ }
@@ -0,0 +1,13 @@
1
+ import { text } from "../util/mcp.js";
2
+ export function registerUserTools(server, gql) {
3
+ const currentUserHandler = async () => {
4
+ const query = `query Me { currentUser { id name email emailVerified avatarUrl disabled } }`;
5
+ const data = await gql.request(query);
6
+ return text(data.currentUser);
7
+ };
8
+ server.registerTool("current_user", {
9
+ title: "Current User",
10
+ description: "Get current signed-in user.",
11
+ inputSchema: {}
12
+ }, currentUserHandler);
13
+ }
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { text } from "../util/mcp.js";
3
+ export function registerUserCRUDTools(server, gql) {
4
+ // UPDATE PROFILE
5
+ const updateProfileHandler = async ({ name, avatarUrl }) => {
6
+ try {
7
+ const mutation = `
8
+ mutation UpdateProfile($input: UpdateUserInput!) {
9
+ updateProfile(input: $input) {
10
+ id
11
+ name
12
+ avatarUrl
13
+ email
14
+ }
15
+ }
16
+ `;
17
+ const input = {};
18
+ if (name !== undefined)
19
+ input.name = name;
20
+ if (avatarUrl !== undefined)
21
+ input.avatarUrl = avatarUrl;
22
+ const data = await gql.request(mutation, { input });
23
+ return text(data.updateProfile);
24
+ }
25
+ catch (error) {
26
+ return text({ error: error.message });
27
+ }
28
+ };
29
+ server.registerTool("update_profile", {
30
+ title: "Update Profile",
31
+ description: "Update current user's profile information.",
32
+ inputSchema: {
33
+ name: z.string().optional().describe("Display name"),
34
+ avatarUrl: z.string().optional().describe("Avatar URL")
35
+ }
36
+ }, updateProfileHandler);
37
+ // UPDATE SETTINGS
38
+ const updateSettingsHandler = async ({ settings }) => {
39
+ try {
40
+ const mutation = `
41
+ mutation UpdateSettings($input: UpdateUserSettingsInput!) {
42
+ updateSettings(input: $input)
43
+ }
44
+ `;
45
+ const input = {};
46
+ if (typeof settings.receiveCommentEmail === 'boolean')
47
+ input.receiveCommentEmail = settings.receiveCommentEmail;
48
+ if (typeof settings.receiveInvitationEmail === 'boolean')
49
+ input.receiveInvitationEmail = settings.receiveInvitationEmail;
50
+ if (typeof settings.receiveMentionEmail === 'boolean')
51
+ input.receiveMentionEmail = settings.receiveMentionEmail;
52
+ if (Object.keys(input).length === 0) {
53
+ return text({
54
+ error: "settings must include at least one of: receiveCommentEmail, receiveInvitationEmail, receiveMentionEmail",
55
+ });
56
+ }
57
+ const data = await gql.request(mutation, {
58
+ input
59
+ });
60
+ return text({ success: data.updateSettings });
61
+ }
62
+ catch (error) {
63
+ return text({ error: error.message });
64
+ }
65
+ };
66
+ server.registerTool("update_settings", {
67
+ title: "Update Settings",
68
+ description: "Update user settings and preferences.",
69
+ inputSchema: {
70
+ settings: z.object({
71
+ receiveCommentEmail: z.boolean().optional(),
72
+ receiveInvitationEmail: z.boolean().optional(),
73
+ receiveMentionEmail: z.boolean().optional(),
74
+ }).describe("User notification settings")
75
+ }
76
+ }, updateSettingsHandler);
77
+ }