affine-mcp-server 2.0.0 → 2.1.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/README.md +3 -3
- package/dist/toolSurface.js +4 -0
- package/dist/tools/docs.js +113 -2
- package/dist/tools/organize.js +42 -33
- package/package.json +6 -4
- package/tool-manifest.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol (MCP) server for AFFiNE. It exposes AFFiNE workspaces and documents to AI assistants over stdio (default) or HTTP (`/mcp`) and supports both AFFiNE Cloud and self-hosted deployments.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/dawncr0w/affine-mcp-server/releases)
|
|
6
6
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
7
7
|
[](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
@@ -38,7 +38,7 @@ Highlights:
|
|
|
38
38
|
- Supports AFFiNE Cloud and self-hosted AFFiNE instances
|
|
39
39
|
- Supports stdio and HTTP transports
|
|
40
40
|
- Supports token, cookie, and email/password authentication
|
|
41
|
-
- Exposes
|
|
41
|
+
- Exposes 85 canonical MCP tools backed by AFFiNE GraphQL and WebSocket APIs
|
|
42
42
|
- Includes semantic page composition, native template instantiation, database intent composition, capability and fidelity reporting, and workspace blueprint helpers
|
|
43
43
|
- Includes Docker images, health probes, and end-to-end test coverage
|
|
44
44
|
|
|
@@ -48,7 +48,7 @@ Scope boundaries:
|
|
|
48
48
|
- Browser-local workspaces stored only in local storage are not available through AFFiNE server APIs
|
|
49
49
|
- AFFiNE Cloud requires API-token-based access for MCP usage; programmatic email/password sign-in is blocked by Cloudflare
|
|
50
50
|
|
|
51
|
-
> New in v2.
|
|
51
|
+
> New in v2.1.0: Added exact-title document lookup, folder placement for `create_doc`, and trusted npm publishing.
|
|
52
52
|
|
|
53
53
|
## Choose Your Path
|
|
54
54
|
| Goal | Start here |
|
package/dist/toolSurface.js
CHANGED
|
@@ -33,6 +33,7 @@ const ALL_TOOLS = [
|
|
|
33
33
|
"delete_workspace",
|
|
34
34
|
"export_doc_markdown",
|
|
35
35
|
"export_with_fidelity_report",
|
|
36
|
+
"find_doc_by_title",
|
|
36
37
|
"generate_access_token",
|
|
37
38
|
"get_capabilities",
|
|
38
39
|
"get_collection",
|
|
@@ -119,6 +120,7 @@ const TOOL_GROUPS = {
|
|
|
119
120
|
delete_workspace: ["workspaces", "workspaces.write", "admin", "destructive", "write"],
|
|
120
121
|
export_doc_markdown: ["docs", "docs.export", "docs.markdown", "docs.read", "read"],
|
|
121
122
|
export_with_fidelity_report: ["docs", "docs.export", "docs.markdown", "docs.read", "read"],
|
|
123
|
+
find_doc_by_title: ["docs", "docs.read", "read"],
|
|
122
124
|
generate_access_token: ["access_tokens", "access_tokens.write", "admin", "write"],
|
|
123
125
|
get_capabilities: ["docs", "docs.read", "read"],
|
|
124
126
|
get_collection: ["organize", "organize.collections", "organize.read", "read"],
|
|
@@ -175,6 +177,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
175
177
|
"current_user",
|
|
176
178
|
"export_doc_markdown",
|
|
177
179
|
"export_with_fidelity_report",
|
|
180
|
+
"find_doc_by_title",
|
|
178
181
|
"get_capabilities",
|
|
179
182
|
"get_collection",
|
|
180
183
|
"get_doc",
|
|
@@ -211,6 +214,7 @@ const CORE_TOOLS = new Set([
|
|
|
211
214
|
"create_doc_from_markdown",
|
|
212
215
|
"current_user",
|
|
213
216
|
"export_doc_markdown",
|
|
217
|
+
"find_doc_by_title",
|
|
214
218
|
"get_capabilities",
|
|
215
219
|
"get_doc",
|
|
216
220
|
"get_workspace",
|
package/dist/tools/docs.js
CHANGED
|
@@ -5,6 +5,7 @@ import { wsUrlFromGraphQLEndpoint, connectWorkspaceSocket, joinWorkspace, loadDo
|
|
|
5
5
|
import * as Y from "yjs";
|
|
6
6
|
import { parseMarkdownToOperations } from "../markdown/parse.js";
|
|
7
7
|
import { renderBlocksToMarkdown } from "../markdown/render.js";
|
|
8
|
+
import { addOrganizeLinkToFolder } from "./organize.js";
|
|
8
9
|
import { DEFAULT_NOTE_XYWH, DEFAULT_STACK_GAP_HORIZONTAL, DEFAULT_STACK_GAP_VERTICAL, SIDE_TO_NORMALIZED_POSITION, encloseBounds, estimateConnectorLabelXYWH, estimateNoteHeightForMarkdown, formatXywhString, parseXywhString, pickConnectorSides, pickFurthestInDirection, sortByFractionalIndex, stackRelativeTo, } from "../edgeless/layout.js";
|
|
9
10
|
const WorkspaceId = z.string().min(1, "workspaceId required");
|
|
10
11
|
const DocId = z.string().min(1, "docId required");
|
|
@@ -3387,6 +3388,88 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
3387
3388
|
sortDirection: z.enum(["asc", "desc"]).optional().describe("Sort direction for updatedAt sorting (default: desc)."),
|
|
3388
3389
|
},
|
|
3389
3390
|
}, searchDocsHandler);
|
|
3391
|
+
const findDocByTitleHandler = async (parsed) => {
|
|
3392
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3393
|
+
if (!workspaceId) {
|
|
3394
|
+
throw new Error("workspaceId is required. Provide it as a parameter or set AFFINE_WORKSPACE_ID in environment.");
|
|
3395
|
+
}
|
|
3396
|
+
const title = parsed.title;
|
|
3397
|
+
if (!title || title.length === 0) {
|
|
3398
|
+
throw new Error("title is required and must be non-empty.");
|
|
3399
|
+
}
|
|
3400
|
+
const limit = parsed.limit ?? 50;
|
|
3401
|
+
const caseInsensitive = parsed.caseInsensitive ?? false;
|
|
3402
|
+
const target = caseInsensitive ? title.toLocaleLowerCase() : title;
|
|
3403
|
+
const { endpoint, cookie, bearer } = await getCookieAndEndpoint();
|
|
3404
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
3405
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
3406
|
+
try {
|
|
3407
|
+
await joinWorkspace(socket, workspaceId);
|
|
3408
|
+
const snapshot = await loadDoc(socket, workspaceId, workspaceId);
|
|
3409
|
+
if (!snapshot.missing) {
|
|
3410
|
+
return text({
|
|
3411
|
+
query: title,
|
|
3412
|
+
caseInsensitive,
|
|
3413
|
+
matches: [],
|
|
3414
|
+
workspaceDocCount: 0,
|
|
3415
|
+
truncated: false,
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
const wsDoc = new Y.Doc();
|
|
3419
|
+
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
|
|
3420
|
+
const meta = wsDoc.getMap("meta");
|
|
3421
|
+
const pages = getWorkspacePageEntries(meta);
|
|
3422
|
+
const matches = [];
|
|
3423
|
+
let truncated = false;
|
|
3424
|
+
for (const page of pages) {
|
|
3425
|
+
const pageTitle = page.title ?? "";
|
|
3426
|
+
const candidate = caseInsensitive ? pageTitle.toLocaleLowerCase() : pageTitle;
|
|
3427
|
+
if (candidate !== target)
|
|
3428
|
+
continue;
|
|
3429
|
+
if (matches.length >= limit) {
|
|
3430
|
+
// We hit `limit` on a previous iteration and now found another match —
|
|
3431
|
+
// there are genuinely more matches than the cap.
|
|
3432
|
+
truncated = true;
|
|
3433
|
+
break;
|
|
3434
|
+
}
|
|
3435
|
+
// A doc that has never been edited after creation has no
|
|
3436
|
+
// `updatedDate` in workspace meta — fall back to `createDate` so
|
|
3437
|
+
// `updatedAt` is always populated, consistent with `search_docs`.
|
|
3438
|
+
const updatedTimestamp = page.updatedDate ?? page.createDate;
|
|
3439
|
+
matches.push({
|
|
3440
|
+
id: page.id,
|
|
3441
|
+
title: pageTitle,
|
|
3442
|
+
createdAt: page.createDate ? new Date(page.createDate).toISOString() : null,
|
|
3443
|
+
updatedAt: updatedTimestamp ? new Date(updatedTimestamp).toISOString() : null,
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
return text({
|
|
3447
|
+
query: title,
|
|
3448
|
+
caseInsensitive,
|
|
3449
|
+
matches,
|
|
3450
|
+
workspaceDocCount: pages.length,
|
|
3451
|
+
truncated,
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
finally {
|
|
3455
|
+
socket.disconnect();
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
server.registerTool("find_doc_by_title", {
|
|
3459
|
+
title: "Find Doc by Title",
|
|
3460
|
+
description: "Resolve docs by exact title. Returns ALL matches up to `limit` (callers handle ambiguity). " +
|
|
3461
|
+
"Case-sensitive by default; pass `caseInsensitive: true` to fold case. " +
|
|
3462
|
+
"Reads workspace metadata — fast, no per-doc fetch. " +
|
|
3463
|
+
"Unlike `search_docs` (which is always case-insensitive and capped at limit 20), this tool defaults to case-sensitive matching and returns up to `limit` matches (default 50, max 200). " +
|
|
3464
|
+
"Prefer this over `search_docs` when you know the exact title and want every match. " +
|
|
3465
|
+
"Returns: { query, caseInsensitive, matches: [{ id, title, createdAt, updatedAt }], workspaceDocCount, truncated }.",
|
|
3466
|
+
inputSchema: {
|
|
3467
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if AFFINE_WORKSPACE_ID is set)."),
|
|
3468
|
+
title: z.string().min(1).describe("The exact title to match."),
|
|
3469
|
+
caseInsensitive: z.boolean().optional().describe("If true, fold case for comparison (default: false)."),
|
|
3470
|
+
limit: z.number().int().positive().max(200).optional().describe("Max matches to return (default: 50)."),
|
|
3471
|
+
},
|
|
3472
|
+
}, findDocByTitleHandler);
|
|
3390
3473
|
server.registerTool("list_tags", {
|
|
3391
3474
|
title: "List Tags",
|
|
3392
3475
|
description: "List all tags in a workspace and the number of docs attached to each tag.",
|
|
@@ -4033,23 +4116,51 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
4033
4116
|
parentDocId: parsed.parentDocId,
|
|
4034
4117
|
context: "create_doc",
|
|
4035
4118
|
});
|
|
4119
|
+
const warnings = mergeWarnings(created.warnings ?? [], placement.warnings);
|
|
4120
|
+
let linkedFolderId = null;
|
|
4121
|
+
let folderNodeId = null;
|
|
4122
|
+
if (parsed.folderId) {
|
|
4123
|
+
const { endpoint, cookie, bearer } = await getCookieAndEndpoint();
|
|
4124
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
4125
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
4126
|
+
try {
|
|
4127
|
+
await joinWorkspace(socket, created.workspaceId);
|
|
4128
|
+
const link = await addOrganizeLinkToFolder(socket, created.workspaceId, {
|
|
4129
|
+
folderId: parsed.folderId,
|
|
4130
|
+
type: "doc",
|
|
4131
|
+
targetId: created.docId,
|
|
4132
|
+
});
|
|
4133
|
+
linkedFolderId = link.parentId;
|
|
4134
|
+
folderNodeId = link.id;
|
|
4135
|
+
}
|
|
4136
|
+
catch (err) {
|
|
4137
|
+
warnings.push(`Doc created but could not be placed in folder "${parsed.folderId}": ${err?.message ?? "unknown error"}`);
|
|
4138
|
+
}
|
|
4139
|
+
finally {
|
|
4140
|
+
socket.disconnect();
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4036
4143
|
return receipt("doc.create", {
|
|
4037
4144
|
workspaceId: created.workspaceId,
|
|
4038
4145
|
docId: created.docId,
|
|
4039
4146
|
title: created.title,
|
|
4040
4147
|
parentDocId: placement.parentDocId,
|
|
4041
4148
|
linkedToParent: placement.linkedToParent,
|
|
4042
|
-
|
|
4149
|
+
folderId: linkedFolderId,
|
|
4150
|
+
folderLinked: folderNodeId !== null,
|
|
4151
|
+
folderNodeId,
|
|
4152
|
+
warnings,
|
|
4043
4153
|
});
|
|
4044
4154
|
};
|
|
4045
4155
|
server.registerTool('create_doc', {
|
|
4046
4156
|
title: 'Create Document',
|
|
4047
|
-
description: 'Create a new AFFiNE document with optional content. If parentDocId is provided, the new doc is linked into the sidebar tree immediately.',
|
|
4157
|
+
description: 'Create a new AFFiNE document with optional content. If parentDocId is provided, the new doc is linked into the sidebar tree immediately. If folderId is provided, the doc is placed inside that folder in the sidebar.',
|
|
4048
4158
|
inputSchema: {
|
|
4049
4159
|
workspaceId: z.string().optional(),
|
|
4050
4160
|
title: z.string().optional(),
|
|
4051
4161
|
content: z.string().optional(),
|
|
4052
4162
|
parentDocId: z.string().optional().describe("Optional parent doc to link the new doc under in the sidebar."),
|
|
4163
|
+
folderId: z.string().optional().describe("Optional folder ID to place the doc in. Use list_organize_nodes to find folder IDs."),
|
|
4053
4164
|
},
|
|
4054
4165
|
}, createDocHandler);
|
|
4055
4166
|
const semanticSectionSchema = z.object({
|
package/dist/tools/organize.js
CHANGED
|
@@ -453,6 +453,43 @@ function nextOrganizeIndex(nodes, parentId) {
|
|
|
453
453
|
const last = siblings.at(-1);
|
|
454
454
|
return generateFractionalIndexingKeyBetween(last?.index ?? null, null);
|
|
455
455
|
}
|
|
456
|
+
async function loadFoldersDoc(socket, workspaceId) {
|
|
457
|
+
const docId = specialWorkspaceDbDocId(workspaceId, "folders");
|
|
458
|
+
const snapshot = await loadDoc(socket, workspaceId, docId);
|
|
459
|
+
const doc = new Y.Doc();
|
|
460
|
+
if (snapshot.missing) {
|
|
461
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
462
|
+
}
|
|
463
|
+
return { docId, doc, snapshot };
|
|
464
|
+
}
|
|
465
|
+
async function saveFoldersDoc(socket, workspaceId, docId, doc) {
|
|
466
|
+
const update = Y.encodeStateAsUpdate(doc);
|
|
467
|
+
await pushDocUpdate(socket, workspaceId, docId, Buffer.from(update).toString("base64"));
|
|
468
|
+
}
|
|
469
|
+
export async function addOrganizeLinkToFolder(socket, workspaceId, { folderId, type, targetId, index, }) {
|
|
470
|
+
const { docId, doc } = await loadFoldersDoc(socket, workspaceId);
|
|
471
|
+
const nodes = readOrganizeNodes(doc);
|
|
472
|
+
const nodeMap = organizeNodeMap(nodes);
|
|
473
|
+
ensureNodeIsFolder(nodeMap, folderId);
|
|
474
|
+
const linkId = generateId();
|
|
475
|
+
const nextIndex = index ?? nextOrganizeIndex(nodes, folderId);
|
|
476
|
+
const record = ensureRecord(doc, linkId);
|
|
477
|
+
record.set("id", linkId);
|
|
478
|
+
record.set("type", type);
|
|
479
|
+
record.set("data", targetId);
|
|
480
|
+
record.set("parentId", folderId);
|
|
481
|
+
record.set("index", nextIndex);
|
|
482
|
+
record.delete("$$DELETED");
|
|
483
|
+
await saveFoldersDoc(socket, workspaceId, docId, doc);
|
|
484
|
+
return {
|
|
485
|
+
id: linkId,
|
|
486
|
+
parentId: folderId,
|
|
487
|
+
type,
|
|
488
|
+
data: targetId,
|
|
489
|
+
index: nextIndex,
|
|
490
|
+
storageDocId: docId,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
456
493
|
export function registerOrganizeTools(server, gql, defaults) {
|
|
457
494
|
async function getSocketContext() {
|
|
458
495
|
const endpoint = gql.endpoint;
|
|
@@ -474,19 +511,6 @@ export function registerOrganizeTools(server, gql, defaults) {
|
|
|
474
511
|
const update = Y.encodeStateAsUpdate(doc);
|
|
475
512
|
await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(update).toString("base64"));
|
|
476
513
|
}
|
|
477
|
-
async function loadFoldersDoc(socket, workspaceId) {
|
|
478
|
-
const docId = specialWorkspaceDbDocId(workspaceId, "folders");
|
|
479
|
-
const snapshot = await loadDoc(socket, workspaceId, docId);
|
|
480
|
-
const doc = new Y.Doc();
|
|
481
|
-
if (snapshot.missing) {
|
|
482
|
-
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
483
|
-
}
|
|
484
|
-
return { docId, doc, snapshot };
|
|
485
|
-
}
|
|
486
|
-
async function saveFoldersDoc(socket, workspaceId, docId, doc) {
|
|
487
|
-
const update = Y.encodeStateAsUpdate(doc);
|
|
488
|
-
await pushDocUpdate(socket, workspaceId, docId, Buffer.from(update).toString("base64"));
|
|
489
|
-
}
|
|
490
514
|
function sleep(ms) {
|
|
491
515
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
492
516
|
}
|
|
@@ -1068,28 +1092,13 @@ export function registerOrganizeTools(server, gql, defaults) {
|
|
|
1068
1092
|
const { socket } = await getSocketContext();
|
|
1069
1093
|
try {
|
|
1070
1094
|
await joinWorkspace(socket, resolvedWorkspaceId);
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
const nodeMap = organizeNodeMap(nodes);
|
|
1074
|
-
ensureNodeIsFolder(nodeMap, folderId);
|
|
1075
|
-
const linkId = generateId();
|
|
1076
|
-
const nextIndex = index ?? nextOrganizeIndex(nodes, folderId);
|
|
1077
|
-
const record = ensureRecord(doc, linkId);
|
|
1078
|
-
record.set("id", linkId);
|
|
1079
|
-
record.set("type", type);
|
|
1080
|
-
record.set("data", targetId);
|
|
1081
|
-
record.set("parentId", folderId);
|
|
1082
|
-
record.set("index", nextIndex);
|
|
1083
|
-
record.delete("$$DELETED");
|
|
1084
|
-
await saveFoldersDoc(socket, resolvedWorkspaceId, docId, doc);
|
|
1085
|
-
return text({
|
|
1086
|
-
id: linkId,
|
|
1087
|
-
parentId: folderId,
|
|
1095
|
+
const link = await addOrganizeLinkToFolder(socket, resolvedWorkspaceId, {
|
|
1096
|
+
folderId,
|
|
1088
1097
|
type,
|
|
1089
|
-
|
|
1090
|
-
index
|
|
1091
|
-
storageDocId: docId,
|
|
1098
|
+
targetId,
|
|
1099
|
+
index,
|
|
1092
1100
|
});
|
|
1101
|
+
return text(link);
|
|
1093
1102
|
}
|
|
1094
1103
|
finally {
|
|
1095
1104
|
socket.disconnect();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "affine-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Model Context Protocol server for AFFiNE - enables AI assistants to interact with AFFiNE workspaces, documents, and collaboration features.",
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
11
|
+
"url": "git+https://github.com/DAWNCR0W/affine-mcp-server.git"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/DAWNCR0W/affine-mcp-server/issues"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.com/
|
|
16
|
+
"homepage": "https://github.com/DAWNCR0W/affine-mcp-server#readme",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"mcp",
|
|
19
19
|
"affine",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"test:db-schema": "node tests/test-database-schema.mjs",
|
|
48
48
|
"test:data-view": "node tests/test-data-view.mjs",
|
|
49
49
|
"test:doc-discovery": "node tests/test-doc-discovery.mjs",
|
|
50
|
+
"test:find-doc-by-title": "node tests/test-find-doc-by-title.mjs",
|
|
50
51
|
"test:create-placement": "node tests/test-create-placement.mjs",
|
|
51
52
|
"test:surface-elements": "node tests/test-surface-elements.mjs",
|
|
52
53
|
"test:surface-element-gating": "node scripts/verify-surface-element-gating.mjs",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"test:http-bearer": "node tests/test-http-bearer.mjs",
|
|
61
62
|
"test:oauth-http": "node tests/test-oauth-http.mjs",
|
|
62
63
|
"test:organize": "node tests/test-organize-tools.mjs",
|
|
64
|
+
"test:create-doc-folder-placement": "node tests/test-create-doc-folder-placement.mjs",
|
|
63
65
|
"test:supporting-tools": "node tests/test-supporting-tools.mjs",
|
|
64
66
|
"test:tag-visibility": "node tests/test-tag-visibility.mjs",
|
|
65
67
|
"test:markdown-roundtrip": "node tests/test-markdown-roundtrip.mjs",
|
package/tool-manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.1.0",
|
|
3
3
|
"tools": [
|
|
4
4
|
"add_database_column",
|
|
5
5
|
"add_database_row",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"delete_workspace",
|
|
36
36
|
"export_doc_markdown",
|
|
37
37
|
"export_with_fidelity_report",
|
|
38
|
+
"find_doc_by_title",
|
|
38
39
|
"generate_access_token",
|
|
39
40
|
"get_capabilities",
|
|
40
41
|
"get_collection",
|