affine-mcp-server 2.1.0 → 2.2.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 +4 -4
- package/dist/index.js +2 -0
- package/dist/toolSurface.js +11 -0
- package/dist/tools/docs.js +42 -17
- package/dist/tools/properties.js +426 -0
- package/docs/tool-reference.md +11 -1
- package/package.json +4 -1
- package/tool-manifest.json +6 -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 90 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.2.0: Added document custom-property tools, `read_doc` LinkedPage reference IDs, and table ordering fixes.
|
|
52
52
|
|
|
53
53
|
## Choose Your Path
|
|
54
54
|
| Goal | Start here |
|
|
@@ -170,7 +170,7 @@ Domains:
|
|
|
170
170
|
|
|
171
171
|
- Workspace: create, inspect, update, delete, and traverse workspaces
|
|
172
172
|
- Organization: collections, collection-rule sync, workspace blueprints, and experimental organize or folder helpers
|
|
173
|
-
- Documents: search, read, create, publish, move, tag, import/export, semantic composition, template inspection and native instantiation, capability and fidelity reporting, and block-level mutation
|
|
173
|
+
- Documents: search, read, create, publish, move, tag, custom properties, import/export, semantic composition, template inspection and native instantiation, capability and fidelity reporting, and block-level mutation
|
|
174
174
|
- Databases: create columns, add rows, update rows, inspect schema, and compose database structures from intent
|
|
175
175
|
- Comments: list, create, update, delete, and resolve
|
|
176
176
|
- History: version history listing
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerNotificationTools } from "./tools/notifications.js";
|
|
|
14
14
|
import { loginWithPassword } from "./auth.js";
|
|
15
15
|
import { registerAuthTools } from "./tools/auth.js";
|
|
16
16
|
import { registerOrganizeTools } from "./tools/organize.js";
|
|
17
|
+
import { registerPropertyTools } from "./tools/properties.js";
|
|
17
18
|
import { runCli } from "./cli.js";
|
|
18
19
|
import { startHttpMcpServer } from "./sse.js";
|
|
19
20
|
import { existsSync } from "fs";
|
|
@@ -165,6 +166,7 @@ async function buildServer() {
|
|
|
165
166
|
registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
166
167
|
registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
167
168
|
registerOrganizeTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
169
|
+
registerPropertyTools(server, gql, { workspaceId: config.defaultWorkspaceId });
|
|
168
170
|
registerUserTools(server, gql);
|
|
169
171
|
registerUserCRUDTools(server, gql);
|
|
170
172
|
if (config.authMode !== "oauth") {
|
package/dist/toolSurface.js
CHANGED
|
@@ -10,9 +10,11 @@ const ALL_TOOLS = [
|
|
|
10
10
|
"append_markdown",
|
|
11
11
|
"append_semantic_section",
|
|
12
12
|
"cleanup_blobs",
|
|
13
|
+
"clear_doc_property",
|
|
13
14
|
"compose_database_from_intent",
|
|
14
15
|
"create_collection",
|
|
15
16
|
"create_comment",
|
|
17
|
+
"create_custom_property",
|
|
16
18
|
"create_doc",
|
|
17
19
|
"create_doc_from_markdown",
|
|
18
20
|
"create_folder",
|
|
@@ -25,6 +27,7 @@ const ALL_TOOLS = [
|
|
|
25
27
|
"delete_block",
|
|
26
28
|
"delete_collection",
|
|
27
29
|
"delete_comment",
|
|
30
|
+
"delete_custom_property",
|
|
28
31
|
"delete_database_row",
|
|
29
32
|
"delete_doc",
|
|
30
33
|
"delete_folder",
|
|
@@ -47,6 +50,7 @@ const ALL_TOOLS = [
|
|
|
47
50
|
"list_children",
|
|
48
51
|
"list_collections",
|
|
49
52
|
"list_comments",
|
|
53
|
+
"list_doc_properties",
|
|
50
54
|
"list_docs",
|
|
51
55
|
"list_docs_by_tag",
|
|
52
56
|
"list_histories",
|
|
@@ -71,6 +75,7 @@ const ALL_TOOLS = [
|
|
|
71
75
|
"revoke_access_token",
|
|
72
76
|
"revoke_doc",
|
|
73
77
|
"search_docs",
|
|
78
|
+
"set_doc_property",
|
|
74
79
|
"sign_in",
|
|
75
80
|
"update_collection",
|
|
76
81
|
"update_collection_rules",
|
|
@@ -97,9 +102,11 @@ const TOOL_GROUPS = {
|
|
|
97
102
|
append_markdown: ["docs", "docs.markdown", "docs.write", "write"],
|
|
98
103
|
append_semantic_section: ["docs", "docs.semantic", "docs.write", "write"],
|
|
99
104
|
cleanup_blobs: ["blobs", "blobs.write", "cleanup", "destructive", "write"],
|
|
105
|
+
clear_doc_property: ["docs", "docs.properties", "docs.write", "write"],
|
|
100
106
|
compose_database_from_intent: ["docs", "docs.database", "docs.intent", "docs.write", "write"],
|
|
101
107
|
create_collection: ["organize", "organize.collections", "organize.write", "write"],
|
|
102
108
|
create_comment: ["comments", "comments.write", "write"],
|
|
109
|
+
create_custom_property: ["docs", "docs.properties", "docs.write", "write"],
|
|
103
110
|
create_doc: ["docs", "docs.write", "write"],
|
|
104
111
|
create_doc_from_markdown: ["docs", "docs.markdown", "docs.write", "write"],
|
|
105
112
|
create_folder: ["organize", "organize.folders", "organize.write", "experimental", "write"],
|
|
@@ -112,6 +119,7 @@ const TOOL_GROUPS = {
|
|
|
112
119
|
delete_block: ["docs", "docs.edgeless", "docs.write", "destructive", "write"],
|
|
113
120
|
delete_collection: ["organize", "organize.collections", "organize.write", "destructive", "write"],
|
|
114
121
|
delete_comment: ["comments", "comments.write", "destructive", "write"],
|
|
122
|
+
delete_custom_property: ["docs", "docs.properties", "docs.write", "destructive", "write"],
|
|
115
123
|
delete_database_row: ["docs", "docs.database", "docs.write", "destructive", "write"],
|
|
116
124
|
delete_doc: ["docs", "docs.write", "destructive", "write"],
|
|
117
125
|
delete_folder: ["organize", "organize.folders", "organize.write", "destructive", "experimental", "write"],
|
|
@@ -134,6 +142,7 @@ const TOOL_GROUPS = {
|
|
|
134
142
|
list_children: ["docs", "docs.tree", "docs.read", "read"],
|
|
135
143
|
list_collections: ["organize", "organize.collections", "organize.read", "read"],
|
|
136
144
|
list_comments: ["comments", "comments.read", "read"],
|
|
145
|
+
list_doc_properties: ["docs", "docs.properties", "docs.read", "read"],
|
|
137
146
|
list_docs: ["docs", "docs.read", "read"],
|
|
138
147
|
list_docs_by_tag: ["docs", "docs.tags", "docs.read", "read"],
|
|
139
148
|
list_histories: ["history", "history.read", "read"],
|
|
@@ -158,6 +167,7 @@ const TOOL_GROUPS = {
|
|
|
158
167
|
revoke_access_token: ["access_tokens", "access_tokens.write", "admin", "destructive", "write"],
|
|
159
168
|
revoke_doc: ["docs", "docs.share", "docs.write", "destructive", "write"],
|
|
160
169
|
search_docs: ["docs", "docs.read", "read"],
|
|
170
|
+
set_doc_property: ["docs", "docs.properties", "docs.write", "write"],
|
|
161
171
|
sign_in: ["users", "users.auth", "auth", "write"],
|
|
162
172
|
update_collection: ["organize", "organize.collections", "organize.write", "write"],
|
|
163
173
|
update_collection_rules: ["organize", "organize.collections", "organize.write", "write"],
|
|
@@ -189,6 +199,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
189
199
|
"list_children",
|
|
190
200
|
"list_collections",
|
|
191
201
|
"list_comments",
|
|
202
|
+
"list_doc_properties",
|
|
192
203
|
"list_docs",
|
|
193
204
|
"list_docs_by_tag",
|
|
194
205
|
"list_histories",
|
package/dist/tools/docs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { generateKeyBetween } from "fractional-indexing";
|
|
2
|
+
import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
|
|
3
3
|
import { receipt, text } from "../util/mcp.js";
|
|
4
4
|
import { wsUrlFromGraphQLEndpoint, connectWorkspaceSocket, joinWorkspace, loadDoc, pushDocUpdate, deleteDoc as wsDeleteDoc } from "../ws.js";
|
|
5
5
|
import * as Y from "yjs";
|
|
@@ -166,22 +166,30 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
166
166
|
return makeText(delta);
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
|
-
* Extract
|
|
170
|
-
*
|
|
169
|
+
* Extract inline LinkedPage reference IDs from a Y.Text value. AFFiNE stores
|
|
170
|
+
* @-mentions as zero-width text deltas whose page id lives in attributes.
|
|
171
171
|
*/
|
|
172
|
-
function
|
|
173
|
-
const propText = rowBlock.get("prop:text");
|
|
172
|
+
function extractLinkedPageRefs(propText) {
|
|
174
173
|
if (!(propText instanceof Y.Text))
|
|
175
|
-
return
|
|
174
|
+
return [];
|
|
176
175
|
const delta = propText.toDelta();
|
|
177
176
|
if (!Array.isArray(delta))
|
|
178
|
-
return
|
|
177
|
+
return [];
|
|
178
|
+
const refs = [];
|
|
179
179
|
for (const d of delta) {
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
const reference = d.attributes?.reference;
|
|
181
|
+
if (reference?.type === "LinkedPage" && typeof reference.pageId === "string" && reference.pageId.length > 0) {
|
|
182
|
+
refs.push(reference.pageId);
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
|
-
return
|
|
185
|
+
return refs;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Extract a linked-doc page ID from a database row block's prop:text,
|
|
189
|
+
* if it contains a LinkedPage reference delta. Returns null otherwise.
|
|
190
|
+
*/
|
|
191
|
+
function readLinkedDocId(rowBlock) {
|
|
192
|
+
return extractLinkedPageRefs(rowBlock.get("prop:text"))[0] ?? null;
|
|
185
193
|
}
|
|
186
194
|
function asText(value) {
|
|
187
195
|
if (value instanceof Y.Text)
|
|
@@ -1340,16 +1348,23 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1340
1348
|
const rowIds = [];
|
|
1341
1349
|
const columnIds = [];
|
|
1342
1350
|
const tableData = normalized.tableData ?? [];
|
|
1351
|
+
// Row/column `order` must be valid fractional-indexing keys. AFFiNE's
|
|
1352
|
+
// editor computes the next order with generateKeyBetween(prevOrder, ...)
|
|
1353
|
+
// when inserting a row/column; plain strings like "r0000" render but make
|
|
1354
|
+
// that call throw "invalid order key", so rows/columns cannot be added in
|
|
1355
|
+
// the UI. Use real keys (a0, a1, ...) so insertion works.
|
|
1356
|
+
const rowOrders = generateNKeysBetween(null, null, normalized.rows);
|
|
1357
|
+
const columnOrders = generateNKeysBetween(null, null, normalized.columns);
|
|
1343
1358
|
for (let i = 0; i < normalized.rows; i++) {
|
|
1344
1359
|
const rowId = generateId();
|
|
1345
1360
|
block.set(`prop:rows.${rowId}.rowId`, rowId);
|
|
1346
|
-
block.set(`prop:rows.${rowId}.order`,
|
|
1361
|
+
block.set(`prop:rows.${rowId}.order`, rowOrders[i]);
|
|
1347
1362
|
rowIds.push(rowId);
|
|
1348
1363
|
}
|
|
1349
1364
|
for (let i = 0; i < normalized.columns; i++) {
|
|
1350
1365
|
const columnId = generateId();
|
|
1351
1366
|
block.set(`prop:columns.${columnId}.columnId`, columnId);
|
|
1352
|
-
block.set(`prop:columns.${columnId}.order`,
|
|
1367
|
+
block.set(`prop:columns.${columnId}.order`, columnOrders[i]);
|
|
1353
1368
|
columnIds.push(columnId);
|
|
1354
1369
|
}
|
|
1355
1370
|
for (let rowIndex = 0; rowIndex < rowIds.length; rowIndex += 1) {
|
|
@@ -2074,6 +2089,13 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2074
2089
|
return [];
|
|
2075
2090
|
}
|
|
2076
2091
|
function extractTableData(block) {
|
|
2092
|
+
const compareOrder = (left, right) => {
|
|
2093
|
+
if (left < right)
|
|
2094
|
+
return -1;
|
|
2095
|
+
if (left > right)
|
|
2096
|
+
return 1;
|
|
2097
|
+
return 0;
|
|
2098
|
+
};
|
|
2077
2099
|
const rowsValue = block.get("prop:rows");
|
|
2078
2100
|
const columnsValue = block.get("prop:columns");
|
|
2079
2101
|
const cellsValue = block.get("prop:cells");
|
|
@@ -2084,7 +2106,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2084
2106
|
? payload.order
|
|
2085
2107
|
: rowId,
|
|
2086
2108
|
}))
|
|
2087
|
-
.sort((a, b) => a.order
|
|
2109
|
+
.sort((a, b) => compareOrder(a.order, b.order));
|
|
2088
2110
|
let columnEntries = mapEntries(columnsValue)
|
|
2089
2111
|
.map(([columnId, payload]) => ({
|
|
2090
2112
|
columnId,
|
|
@@ -2092,7 +2114,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2092
2114
|
? payload.order
|
|
2093
2115
|
: columnId,
|
|
2094
2116
|
}))
|
|
2095
|
-
.sort((a, b) => a.order
|
|
2117
|
+
.sort((a, b) => compareOrder(a.order, b.order));
|
|
2096
2118
|
let cells = new Map();
|
|
2097
2119
|
if (rowEntries.length === 0 || columnEntries.length === 0) {
|
|
2098
2120
|
// Fallback: AFFiNE self-hosted stores table props as flat dot-notation keys
|
|
@@ -2122,10 +2144,10 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2122
2144
|
if (flatRows.size > 0 && flatColumns.size > 0) {
|
|
2123
2145
|
rowEntries = Array.from(flatRows.entries())
|
|
2124
2146
|
.map(([rowId, order]) => ({ rowId, order }))
|
|
2125
|
-
.sort((a, b) => a.order
|
|
2147
|
+
.sort((a, b) => compareOrder(a.order, b.order));
|
|
2126
2148
|
columnEntries = Array.from(flatColumns.entries())
|
|
2127
2149
|
.map(([columnId, order]) => ({ columnId, order }))
|
|
2128
|
-
.sort((a, b) => a.order
|
|
2150
|
+
.sort((a, b) => compareOrder(a.order, b.order));
|
|
2129
2151
|
cells = flatCells;
|
|
2130
2152
|
}
|
|
2131
2153
|
}
|
|
@@ -3788,7 +3810,9 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
3788
3810
|
const flavour = raw.get("sys:flavour");
|
|
3789
3811
|
const parentId = raw.get("sys:parent");
|
|
3790
3812
|
const type = raw.get("prop:type");
|
|
3791
|
-
const
|
|
3813
|
+
const propText = raw.get("prop:text");
|
|
3814
|
+
const textValue = asText(propText);
|
|
3815
|
+
const linkedDocIds = extractLinkedPageRefs(propText);
|
|
3792
3816
|
const language = raw.get("prop:language");
|
|
3793
3817
|
const checked = raw.get("prop:checked");
|
|
3794
3818
|
const childIds = childIdsFrom(raw.get("sys:children"));
|
|
@@ -3804,6 +3828,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
3804
3828
|
flavour: typeof flavour === "string" ? flavour : null,
|
|
3805
3829
|
type: typeof type === "string" ? type : null,
|
|
3806
3830
|
text: textValue.length > 0 ? textValue : null,
|
|
3831
|
+
linkedDocIds,
|
|
3807
3832
|
checked: typeof checked === "boolean" ? checked : null,
|
|
3808
3833
|
language: typeof language === "string" ? language : null,
|
|
3809
3834
|
childIds,
|
|
@@ -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
|
+
}
|
package/docs/tool-reference.md
CHANGED
|
@@ -55,7 +55,7 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
|
|
|
55
55
|
| `search_docs` | Search titles with substring, prefix, or exact matching | Supports tag filter and updatedAt sorting |
|
|
56
56
|
| `list_docs_by_tag` | List documents with a specific tag | |
|
|
57
57
|
| `get_doc` | Read document metadata | |
|
|
58
|
-
| `read_doc` | Read block content and plain text snapshot | WebSocket-backed |
|
|
58
|
+
| `read_doc` | Read block content and plain text snapshot | WebSocket-backed; block rows include `linkedDocIds` for inline LinkedPage references |
|
|
59
59
|
| `get_capabilities` | Inspect the server's high-level authoring and fidelity capabilities | Useful for adaptive clients |
|
|
60
60
|
| `analyze_doc_fidelity` | Analyze how a document maps to Markdown and which native AFFiNE structures are lossy | Good before export or migration |
|
|
61
61
|
| `list_children` | List direct child docs linked from a document | |
|
|
@@ -97,6 +97,16 @@ Use this document as a grouped catalog. For exact schemas, your MCP client shoul
|
|
|
97
97
|
| `add_tag_to_doc` | Attach a tag to a document | |
|
|
98
98
|
| `remove_tag_from_doc` | Detach a tag from a document | |
|
|
99
99
|
|
|
100
|
+
### Custom properties
|
|
101
|
+
|
|
102
|
+
| Tool | Purpose | Notes |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `list_doc_properties` | List workspace custom-property definitions and a document's current values | WebSocket-backed; reads the `db$docProperties` / `db$docCustomPropertyInfo` sub-docs |
|
|
105
|
+
| `create_custom_property` | Create a workspace-wide custom property definition | Types: `text`, `number`, `checkbox`, `date`. Returns the `propertyId` |
|
|
106
|
+
| `delete_custom_property` | Soft-delete a custom property definition by id or name | Destructive; existing values are hidden |
|
|
107
|
+
| `set_doc_property` | Set a document's custom property value by property id or name | Value validated per type (`checkbox` boolean, `number`, `date` `YYYY-MM-DD`, `text`) |
|
|
108
|
+
| `clear_doc_property` | Remove a custom property value from a document | |
|
|
109
|
+
|
|
100
110
|
### Markdown export
|
|
101
111
|
|
|
102
112
|
| Tool | Purpose | Notes |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "affine-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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.",
|
|
@@ -43,10 +43,13 @@
|
|
|
43
43
|
"test:e2e": "bash tests/run-e2e.sh",
|
|
44
44
|
"test:db-create": "node tests/test-database-creation.mjs",
|
|
45
45
|
"test:db-cells": "node tests/test-database-cells.mjs",
|
|
46
|
+
"test:db-linked-doc": "node tests/test-database-linked-doc.mjs",
|
|
46
47
|
"test:db-ui-rows": "node tests/test-database-ui-rows.mjs",
|
|
47
48
|
"test:db-schema": "node tests/test-database-schema.mjs",
|
|
48
49
|
"test:data-view": "node tests/test-data-view.mjs",
|
|
49
50
|
"test:doc-discovery": "node tests/test-doc-discovery.mjs",
|
|
51
|
+
"test:doc-properties": "node tests/test-doc-properties.mjs",
|
|
52
|
+
"test:read-doc-linked-refs": "node tests/test-read-doc-linked-refs.mjs",
|
|
50
53
|
"test:find-doc-by-title": "node tests/test-find-doc-by-title.mjs",
|
|
51
54
|
"test:create-placement": "node tests/test-create-placement.mjs",
|
|
52
55
|
"test:surface-elements": "node tests/test-surface-elements.mjs",
|
package/tool-manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.2.0",
|
|
3
3
|
"tools": [
|
|
4
4
|
"add_database_column",
|
|
5
5
|
"add_database_row",
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
"append_markdown",
|
|
13
13
|
"append_semantic_section",
|
|
14
14
|
"cleanup_blobs",
|
|
15
|
+
"clear_doc_property",
|
|
15
16
|
"compose_database_from_intent",
|
|
16
17
|
"create_collection",
|
|
17
18
|
"create_comment",
|
|
19
|
+
"create_custom_property",
|
|
18
20
|
"create_doc",
|
|
19
21
|
"create_doc_from_markdown",
|
|
20
22
|
"create_folder",
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
"delete_block",
|
|
28
30
|
"delete_collection",
|
|
29
31
|
"delete_comment",
|
|
32
|
+
"delete_custom_property",
|
|
30
33
|
"delete_database_row",
|
|
31
34
|
"delete_doc",
|
|
32
35
|
"delete_folder",
|
|
@@ -49,6 +52,7 @@
|
|
|
49
52
|
"list_children",
|
|
50
53
|
"list_collections",
|
|
51
54
|
"list_comments",
|
|
55
|
+
"list_doc_properties",
|
|
52
56
|
"list_docs",
|
|
53
57
|
"list_docs_by_tag",
|
|
54
58
|
"list_histories",
|
|
@@ -73,6 +77,7 @@
|
|
|
73
77
|
"revoke_access_token",
|
|
74
78
|
"revoke_doc",
|
|
75
79
|
"search_docs",
|
|
80
|
+
"set_doc_property",
|
|
76
81
|
"sign_in",
|
|
77
82
|
"update_collection",
|
|
78
83
|
"update_collection_rules",
|