affine-mcp-server 1.6.0 → 1.7.1
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 +81 -9
- package/dist/config.js +33 -42
- package/dist/index.js +29 -6
- package/dist/sse.js +284 -0
- package/dist/tools/docs.js +87 -39
- package/dist/tools/workspaces.js +6 -6
- package/dist/ws.js +55 -33
- package/package.json +6 -1
- package/dist/markdown/index.js +0 -3
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AFFiNE MCP Server
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted or cloud). It exposes AFFiNE workspaces and documents to AI assistants over stdio.
|
|
3
|
+
A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted or cloud). It exposes AFFiNE workspaces and documents to AI assistants over stdio (default) or HTTP (`/mcp`).
|
|
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)
|
|
@@ -14,12 +14,12 @@ A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted
|
|
|
14
14
|
## Overview
|
|
15
15
|
|
|
16
16
|
- Purpose: Manage AFFiNE workspaces and documents through MCP
|
|
17
|
-
- Transport: stdio
|
|
17
|
+
- Transport: stdio (default) and optional HTTP (`/mcp`) for remote MCP deployments
|
|
18
18
|
- Auth: Token, Cookie, or Email/Password (priority order)
|
|
19
19
|
- Tools: 43 focused tools with WebSocket-based document editing
|
|
20
20
|
- Status: Active
|
|
21
21
|
|
|
22
|
-
> New in v1.
|
|
22
|
+
> New in v1.7.1: Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling) and callout text rendering, plus regression coverage for UI visibility paths.
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
@@ -102,7 +102,7 @@ You can also configure via environment variables (they override the config file)
|
|
|
102
102
|
|
|
103
103
|
- Required: `AFFINE_BASE_URL`
|
|
104
104
|
- Auth (choose one): `AFFINE_API_TOKEN` | `AFFINE_COOKIE` | `AFFINE_EMAIL` + `AFFINE_PASSWORD`
|
|
105
|
-
- Optional: `AFFINE_GRAPHQL_PATH` (default `/graphql`), `AFFINE_WORKSPACE_ID`, `AFFINE_LOGIN_AT_START` (
|
|
105
|
+
- Optional: `AFFINE_GRAPHQL_PATH` (default `/graphql`), `AFFINE_WORKSPACE_ID`, `AFFINE_LOGIN_AT_START` (set `sync` only when you must block startup)
|
|
106
106
|
|
|
107
107
|
Authentication priority:
|
|
108
108
|
1) `AFFINE_API_TOKEN` → 2) `AFFINE_COOKIE` → 3) `AFFINE_EMAIL` + `AFFINE_PASSWORD`
|
|
@@ -175,8 +175,7 @@ Or with email/password for self-hosted instances (not supported on AFFiNE Cloud
|
|
|
175
175
|
"env": {
|
|
176
176
|
"AFFINE_BASE_URL": "https://your-self-hosted-affine.com",
|
|
177
177
|
"AFFINE_EMAIL": "you@example.com",
|
|
178
|
-
"AFFINE_PASSWORD": "secret!"
|
|
179
|
-
"AFFINE_LOGIN_AT_START": "async"
|
|
178
|
+
"AFFINE_PASSWORD": "secret!"
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
181
|
}
|
|
@@ -198,7 +197,7 @@ Register the MCP server with Codex:
|
|
|
198
197
|
- `codex mcp add affine --env AFFINE_BASE_URL=https://app.affine.pro --env AFFINE_API_TOKEN=ut_xxx -- affine-mcp`
|
|
199
198
|
|
|
200
199
|
- With email/password (self-hosted only):
|
|
201
|
-
- `codex mcp add affine --env AFFINE_BASE_URL=https://your-self-hosted-affine.com --env 'AFFINE_EMAIL=you@example.com' --env 'AFFINE_PASSWORD=secret!' --
|
|
200
|
+
- `codex mcp add affine --env AFFINE_BASE_URL=https://your-self-hosted-affine.com --env 'AFFINE_EMAIL=you@example.com' --env 'AFFINE_PASSWORD=secret!' -- affine-mcp`
|
|
202
201
|
|
|
203
202
|
### Cursor
|
|
204
203
|
|
|
@@ -237,6 +236,70 @@ If you prefer `npx`:
|
|
|
237
236
|
}
|
|
238
237
|
```
|
|
239
238
|
|
|
239
|
+
### Remote Server
|
|
240
|
+
|
|
241
|
+
If you want to host the server remotely (e.g., using Render, Railway, Docker, or a VPS) and connect via HTTP MCP (Streamable HTTP on `/mcp`) instead of local `stdio`, run the server in HTTP mode.
|
|
242
|
+
|
|
243
|
+
#### Environment variables (HTTP mode)
|
|
244
|
+
|
|
245
|
+
Required:
|
|
246
|
+
- `MCP_TRANSPORT=http`
|
|
247
|
+
- `AFFINE_BASE_URL` (example: `https://app.affine.pro`)
|
|
248
|
+
- One auth method:
|
|
249
|
+
- `AFFINE_API_TOKEN` (recommended), or `AFFINE_COOKIE`, or `AFFINE_EMAIL` + `AFFINE_PASSWORD`
|
|
250
|
+
|
|
251
|
+
Recommended for remote/public deployments:
|
|
252
|
+
- `AFFINE_MCP_HTTP_HOST=0.0.0.0`
|
|
253
|
+
- `AFFINE_MCP_HTTP_TOKEN=<strong-random-token>` (protects `/mcp`, `/sse`, `/messages`)
|
|
254
|
+
- `AFFINE_MCP_HTTP_ALLOWED_ORIGINS=<comma-separated-origins>` (for browser clients)
|
|
255
|
+
|
|
256
|
+
Optional:
|
|
257
|
+
- `PORT` (defaults to `3000`; many platforms like Render inject this automatically)
|
|
258
|
+
- `AFFINE_WORKSPACE_ID`
|
|
259
|
+
- `AFFINE_GRAPHQL_PATH` (defaults to `/graphql`)
|
|
260
|
+
- `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS=true` (testing only)
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# Export your configuration first
|
|
264
|
+
export MCP_TRANSPORT=http
|
|
265
|
+
export AFFINE_API_TOKEN="your_token..."
|
|
266
|
+
export AFFINE_MCP_HTTP_HOST="0.0.0.0" # Default: 127.0.0.1
|
|
267
|
+
export AFFINE_MCP_HTTP_TOKEN="your-super-secret-token"
|
|
268
|
+
export PORT=3000
|
|
269
|
+
|
|
270
|
+
# Start in HTTP mode (Streamable HTTP on /mcp)
|
|
271
|
+
npm run start:http
|
|
272
|
+
# OR manually:
|
|
273
|
+
# MCP_TRANSPORT=http node dist/index.js
|
|
274
|
+
# ("sse" is still accepted at /sse)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Recommended presets
|
|
278
|
+
|
|
279
|
+
Local testing (HTTP mode):
|
|
280
|
+
- `MCP_TRANSPORT=http`
|
|
281
|
+
- `AFFINE_MCP_HTTP_HOST=127.0.0.1`
|
|
282
|
+
- `AFFINE_MCP_HTTP_TOKEN=<token>` (recommended even locally)
|
|
283
|
+
- `AFFINE_MCP_HTTP_ALLOWED_ORIGINS=http://localhost:3000` (if testing from a browser app)
|
|
284
|
+
|
|
285
|
+
Docker / container runtime:
|
|
286
|
+
- `MCP_TRANSPORT=http`
|
|
287
|
+
- `AFFINE_MCP_HTTP_HOST=0.0.0.0`
|
|
288
|
+
- `PORT=3000` (or container/platform port)
|
|
289
|
+
- `AFFINE_MCP_HTTP_TOKEN=<strong-token>`
|
|
290
|
+
- `AFFINE_MCP_HTTP_ALLOWED_ORIGINS=<your app origin(s)>`
|
|
291
|
+
|
|
292
|
+
Render / Railway / VPS (public endpoint):
|
|
293
|
+
- `MCP_TRANSPORT=http`
|
|
294
|
+
- `AFFINE_MCP_HTTP_HOST=0.0.0.0`
|
|
295
|
+
- `AFFINE_MCP_HTTP_TOKEN=<strong-token>`
|
|
296
|
+
- `AFFINE_MCP_HTTP_ALLOWED_ORIGINS=<your client origin(s)>`
|
|
297
|
+
|
|
298
|
+
Endpoints currently available:
|
|
299
|
+
- `/mcp` - MCP server (Streamable HTTP)
|
|
300
|
+
- `/sse` - SSE endpoint (old protocol compatible)
|
|
301
|
+
- `/messages` - Messages endpoint (old protocol compatible)
|
|
302
|
+
|
|
240
303
|
## Available Tools
|
|
241
304
|
|
|
242
305
|
### Workspace
|
|
@@ -309,6 +372,7 @@ npm run pack:check
|
|
|
309
372
|
|
|
310
373
|
- `tool-manifest.json` is the source of truth for publicly exposed tool names.
|
|
311
374
|
- CI validates that `registerTool(...)` declarations match the manifest exactly.
|
|
375
|
+
- For full tool-surface verification, run `npm run test:comprehensive`.
|
|
312
376
|
- For full environment verification, run `npm run test:e2e` (Docker + MCP + Playwright).
|
|
313
377
|
- Additional focused runners: `npm run test:db-create`, `npm run test:bearer`, `npm run test:playwright`.
|
|
314
378
|
|
|
@@ -345,6 +409,14 @@ Workspace visibility
|
|
|
345
409
|
|
|
346
410
|
## Version History
|
|
347
411
|
|
|
412
|
+
### 1.7.0 (2026‑02‑27)
|
|
413
|
+
- Added Streamable HTTP MCP support on `/mcp` for remote hosting while keeping legacy SSE compatibility paths (`/sse`, `/messages`)
|
|
414
|
+
- Added HTTP deployment controls: `AFFINE_MCP_HTTP_HOST`, `AFFINE_MCP_HTTP_TOKEN`, `AFFINE_MCP_HTTP_ALLOWED_ORIGINS`, `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS`
|
|
415
|
+
- Added `npm run start:http` for one-command HTTP mode startup
|
|
416
|
+
- Hardened HTTP request handling with explicit 50MB parser application and case-insensitive Bearer auth parsing
|
|
417
|
+
- Expanded docs with remote deployment/security presets (Docker, Render, Railway, VPS)
|
|
418
|
+
- Verified full release checks with `npm run ci`, `npm run test:e2e`, and `npm run test:comprehensive`
|
|
419
|
+
|
|
348
420
|
### 1.6.0 (2026‑02‑24)
|
|
349
421
|
- Added 11 document workflow tools: tags (`list_tags`, `list_docs_by_tag`, `create_tag`, `add_tag_to_doc`, `remove_tag_from_doc`), markdown roundtrip (`export_doc_markdown`, `create_doc_from_markdown`, `append_markdown`, `replace_doc_with_markdown`), and database operations (`add_database_column`, `add_database_row`)
|
|
350
422
|
- Added interactive CLI commands: `affine-mcp login`, `affine-mcp status`, `affine-mcp logout`
|
|
@@ -375,7 +447,7 @@ Workspace visibility
|
|
|
375
447
|
|
|
376
448
|
### 1.2.1 (2025‑09‑17)
|
|
377
449
|
- Default to asynchronous email/password login after MCP stdio handshake
|
|
378
|
-
-
|
|
450
|
+
- `AFFINE_LOGIN_AT_START` supports `sync` when you need blocking startup (default is non-blocking)
|
|
379
451
|
- Expanded docs for Codex/Claude using npm, npx, and local clone
|
|
380
452
|
|
|
381
453
|
### 1.2.0 (2025‑09‑16)
|
package/dist/config.js
CHANGED
|
@@ -5,20 +5,8 @@ import { createRequire } from "module";
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
6
|
const pkg = require("../package.json");
|
|
7
7
|
export const VERSION = pkg.version;
|
|
8
|
-
const defaultEndpoints = {
|
|
9
|
-
listWorkspaces: { method: "GET", path: "/api/workspaces" },
|
|
10
|
-
listDocs: { method: "GET", path: "/api/workspaces/:workspaceId/docs" },
|
|
11
|
-
getDoc: { method: "GET", path: "/api/docs/:docId" },
|
|
12
|
-
createDoc: { method: "POST", path: "/api/workspaces/:workspaceId/docs" },
|
|
13
|
-
updateDoc: { method: "PATCH", path: "/api/docs/:docId" },
|
|
14
|
-
deleteDoc: { method: "DELETE", path: "/api/docs/:docId" },
|
|
15
|
-
searchDocs: {
|
|
16
|
-
method: "GET",
|
|
17
|
-
path: "/api/workspaces/:workspaceId/search"
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
8
|
/** Config file location: ~/.config/affine-mcp/config */
|
|
21
|
-
|
|
9
|
+
const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "affine-mcp");
|
|
22
10
|
export const CONFIG_FILE = path.join(CONFIG_DIR, "config");
|
|
23
11
|
/** Read key=value config file, returns empty object if missing */
|
|
24
12
|
export function loadConfigFile() {
|
|
@@ -98,44 +86,47 @@ export function validateBaseUrl(input) {
|
|
|
98
86
|
function env(name, file, fallback) {
|
|
99
87
|
return process.env[name] || file[name] || fallback;
|
|
100
88
|
}
|
|
89
|
+
function parseHeadersJson(raw) {
|
|
90
|
+
if (!raw)
|
|
91
|
+
return undefined;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
95
|
+
console.warn("Failed to parse AFFINE_HEADERS_JSON; expected a JSON object of string headers.");
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const headers = {};
|
|
99
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
100
|
+
if (typeof value !== "string") {
|
|
101
|
+
console.warn(`Ignoring non-string AFFINE_HEADERS_JSON value for header '${key}'.`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
headers[key] = value;
|
|
105
|
+
}
|
|
106
|
+
const sensitiveKeys = Object.keys(headers).filter((k) => /^(authorization|cookie)$/i.test(k));
|
|
107
|
+
if (sensitiveKeys.length) {
|
|
108
|
+
console.warn(`WARNING: AFFINE_HEADERS_JSON contains sensitive key(s): ${sensitiveKeys.join(", ")}. ` +
|
|
109
|
+
`These may conflict with built-in auth and are not protected by debug-logging guards.`);
|
|
110
|
+
}
|
|
111
|
+
return headers;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
console.warn("Failed to parse AFFINE_HEADERS_JSON; ignoring.");
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
101
118
|
export function loadConfig() {
|
|
102
119
|
const file = loadConfigFile();
|
|
103
|
-
const baseUrl = env("AFFINE_BASE_URL", file, "http://localhost:3010")
|
|
120
|
+
const baseUrl = validateBaseUrl(env("AFFINE_BASE_URL", file, "http://localhost:3010"));
|
|
104
121
|
const apiToken = env("AFFINE_API_TOKEN", file);
|
|
105
122
|
const cookie = env("AFFINE_COOKIE", file);
|
|
106
123
|
const email = env("AFFINE_EMAIL", file);
|
|
107
124
|
const password = env("AFFINE_PASSWORD", file);
|
|
108
|
-
let headers =
|
|
109
|
-
const headersJson = process.env.AFFINE_HEADERS_JSON;
|
|
110
|
-
if (headersJson) {
|
|
111
|
-
try {
|
|
112
|
-
headers = JSON.parse(headersJson);
|
|
113
|
-
if (headers) {
|
|
114
|
-
const sensitiveKeys = Object.keys(headers).filter((k) => /^(authorization|cookie)$/i.test(k));
|
|
115
|
-
if (sensitiveKeys.length) {
|
|
116
|
-
console.warn(`WARNING: AFFINE_HEADERS_JSON contains sensitive key(s): ${sensitiveKeys.join(", ")}. ` +
|
|
117
|
-
`These may conflict with built-in auth and are not protected by debug-logging guards.`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (e) {
|
|
122
|
-
console.warn("Failed to parse AFFINE_HEADERS_JSON; ignoring.");
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
+
let headers = parseHeadersJson(process.env.AFFINE_HEADERS_JSON);
|
|
125
126
|
if (cookie) {
|
|
126
127
|
headers = { ...(headers || {}), Cookie: cookie };
|
|
127
128
|
}
|
|
128
129
|
const graphqlPath = env("AFFINE_GRAPHQL_PATH", file, "/graphql");
|
|
129
130
|
const defaultWorkspaceId = env("AFFINE_WORKSPACE_ID", file);
|
|
130
|
-
|
|
131
|
-
const endpointsJson = process.env.AFFINE_ENDPOINTS_JSON;
|
|
132
|
-
if (endpointsJson) {
|
|
133
|
-
try {
|
|
134
|
-
endpoints = { ...defaultEndpoints, ...JSON.parse(endpointsJson) };
|
|
135
|
-
}
|
|
136
|
-
catch (e) {
|
|
137
|
-
console.warn("Failed to parse AFFINE_ENDPOINTS_JSON; using defaults.");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return { baseUrl, apiToken, cookie, headers, graphqlPath, email, password, defaultWorkspaceId, endpoints };
|
|
131
|
+
return { baseUrl, apiToken, cookie, headers, graphqlPath, email, password, defaultWorkspaceId };
|
|
141
132
|
}
|
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 { runCli } from "./cli.js";
|
|
17
|
+
import { startHttpMcpServer } from "./sse.js";
|
|
17
18
|
// CLI subcommands: affine-mcp login|status|logout
|
|
18
19
|
const subcommand = process.argv[2];
|
|
19
20
|
if (subcommand && await runCli(subcommand)) {
|
|
@@ -101,12 +102,34 @@ async function buildServer() {
|
|
|
101
102
|
return server;
|
|
102
103
|
}
|
|
103
104
|
async function start() {
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
// MCP_TRANSPORT aliases:
|
|
106
|
+
// - "stdio" (default): local desktop MCP clients
|
|
107
|
+
// - "http" / "streamable": HTTP MCP server exposing /mcp (preferred)
|
|
108
|
+
// - "sse": legacy alias retained for backward compatibility
|
|
109
|
+
const transportMode = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
|
|
110
|
+
const useHttpTransport = transportMode === "sse" || transportMode === "http" || transportMode === "streamable";
|
|
111
|
+
if (useHttpTransport) {
|
|
112
|
+
const DEFAULT_PORT = 3000;
|
|
113
|
+
const portEnvValue = process.env.PORT;
|
|
114
|
+
let port = DEFAULT_PORT;
|
|
115
|
+
// Validate the HTTP server port if provided.
|
|
116
|
+
if (portEnvValue != null && portEnvValue.trim() !== "") {
|
|
117
|
+
const parsedPort = Number(portEnvValue);
|
|
118
|
+
if (Number.isInteger(parsedPort) && parsedPort >= 0 && parsedPort <= 65535) {
|
|
119
|
+
port = parsedPort;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.warn(`[affine-mcp] Invalid PORT "${portEnvValue}" (expected 0..65535 integer). Falling back to ${DEFAULT_PORT}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await startHttpMcpServer(buildServer, port);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// stdio transport is the default for typical desktop MCP clients
|
|
129
|
+
const server = await buildServer();
|
|
130
|
+
const transport = new StdioServerTransport();
|
|
131
|
+
await server.connect(transport);
|
|
132
|
+
}
|
|
110
133
|
}
|
|
111
134
|
start().catch((err) => {
|
|
112
135
|
console.error("Failed to start affine-mcp server:", err);
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
export async function startHttpMcpServer(createMcpServer, port) {
|
|
8
|
+
// --- HTTP host binding ---
|
|
9
|
+
// AFFINE_MCP_HTTP_HOST: network interface to bind (default: "127.0.0.1" — loopback only).
|
|
10
|
+
// Set to "0.0.0.0" for Docker / remote deployments (Render, Railway, etc.).
|
|
11
|
+
const host = (process.env.AFFINE_MCP_HTTP_HOST || "127.0.0.1").trim();
|
|
12
|
+
// --- Bearer Token guard (AFFINE_MCP_HTTP_TOKEN) ---
|
|
13
|
+
// When set, all requests to /mcp, /sse and /messages must include:
|
|
14
|
+
// Authorization: Bearer <token> OR ?token=<token> (fallback for limited clients)
|
|
15
|
+
// When the server is bound to 0.0.0.0 without a token, a startup warning is emitted.
|
|
16
|
+
const httpAuthToken = process.env.AFFINE_MCP_HTTP_TOKEN?.trim();
|
|
17
|
+
if (!httpAuthToken && host === "0.0.0.0") {
|
|
18
|
+
console.warn("[affine-mcp] WARNING: HTTP MCP server is bound to 0.0.0.0 without AFFINE_MCP_HTTP_TOKEN. " +
|
|
19
|
+
"The endpoint is unprotected. Set AFFINE_MCP_HTTP_TOKEN for public deployments.");
|
|
20
|
+
}
|
|
21
|
+
// Use a plain Express app here so it can fully control JSON parser ordering/limits.
|
|
22
|
+
// `createMcpExpressApp()` installs its own JSON parser first, which can enforce
|
|
23
|
+
// a smaller default limit before the intended 50mb parser runs on /mcp.
|
|
24
|
+
const app = express();
|
|
25
|
+
const jsonBody = express.json({ limit: "50mb" });
|
|
26
|
+
// --- CORS origin allowlist ---
|
|
27
|
+
// AFFINE_MCP_HTTP_ALLOWED_ORIGINS: comma-separated list, e.g. "https://app.example.com,http://localhost:3000".
|
|
28
|
+
// AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS=true: explicit opt-in to allow any origin (use with caution).
|
|
29
|
+
// Default (no env set): only loopback addresses (localhost / 127.0.0.1 / ::1) are allowed.
|
|
30
|
+
//
|
|
31
|
+
// CORS is applied per-route (/mcp, /sse, /messages) — not globally — to minimise attack surface.
|
|
32
|
+
const allowAnyOrigin = process.env.AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS === "true";
|
|
33
|
+
const allowedOrigins = (process.env.AFFINE_MCP_HTTP_ALLOWED_ORIGINS || "")
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((o) => o.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
// Returns true if origin is a loopback address (http or https, any port).
|
|
38
|
+
const isLoopbackOrigin = (origin) => {
|
|
39
|
+
try {
|
|
40
|
+
const { protocol, hostname } = new URL(origin);
|
|
41
|
+
if (protocol !== "http:" && protocol !== "https:")
|
|
42
|
+
return false;
|
|
43
|
+
return (hostname === "localhost" ||
|
|
44
|
+
hostname === "127.0.0.1" ||
|
|
45
|
+
hostname === "::1");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const corsOptions = {
|
|
52
|
+
origin: (origin, callback) => {
|
|
53
|
+
// Non-browser clients (curl, MCP Inspector, server-to-server) send no Origin header.
|
|
54
|
+
// CORS is a browser mechanism only; the token guard covers programmatic access.
|
|
55
|
+
if (!origin)
|
|
56
|
+
return callback(null, true);
|
|
57
|
+
if (allowAnyOrigin)
|
|
58
|
+
return callback(null, true);
|
|
59
|
+
const allowed = allowedOrigins.length > 0
|
|
60
|
+
? allowedOrigins.includes(origin)
|
|
61
|
+
: isLoopbackOrigin(origin);
|
|
62
|
+
return allowed
|
|
63
|
+
? callback(null, true)
|
|
64
|
+
: callback(new Error("Origin not allowed"));
|
|
65
|
+
},
|
|
66
|
+
methods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
67
|
+
allowedHeaders: ["Content-Type", "Authorization", "mcp-session-id"],
|
|
68
|
+
exposedHeaders: ["mcp-session-id"],
|
|
69
|
+
};
|
|
70
|
+
// Wraps cors() to return an explicit 403 on rejected origins (rather than silently
|
|
71
|
+
// withholding CORS headers, which still lets the request reach the handler).
|
|
72
|
+
const corsMiddleware = (req, res, next) => {
|
|
73
|
+
cors(corsOptions)(req, res, (err) => {
|
|
74
|
+
if (err) {
|
|
75
|
+
if (!res.headersSent)
|
|
76
|
+
res.status(403).send("Forbidden: Origin not allowed");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (res.headersSent || res.writableEnded)
|
|
80
|
+
return;
|
|
81
|
+
next();
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
// Validates the Bearer token on all non-preflight requests.
|
|
85
|
+
// The auth scheme match is case-insensitive for client compatibility.
|
|
86
|
+
// OPTIONS is allowed through so CORS preflight can complete before auth is checked.
|
|
87
|
+
const authMiddleware = (req, res, next) => {
|
|
88
|
+
if (req.method === "OPTIONS")
|
|
89
|
+
return next();
|
|
90
|
+
if (!httpAuthToken)
|
|
91
|
+
return next();
|
|
92
|
+
const authHeader = req.headers["authorization"];
|
|
93
|
+
const queryToken = typeof req.query.token === "string" ? req.query.token : undefined;
|
|
94
|
+
let token;
|
|
95
|
+
if (authHeader !== undefined) {
|
|
96
|
+
// Normalize string | string[] to string (HTTP spec allows duplicate headers).
|
|
97
|
+
const raw = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
98
|
+
const bearerMatch = /^Bearer\s+(.+)$/i.exec(raw);
|
|
99
|
+
if (bearerMatch) {
|
|
100
|
+
token = bearerMatch[1];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Strictly reject non-Bearer schemes to avoid accidentally accepting Basic / Digest.
|
|
104
|
+
console.warn("[affine-mcp] Authorization header is not Bearer scheme. " +
|
|
105
|
+
"Expected: 'Authorization: Bearer <token>'.");
|
|
106
|
+
res
|
|
107
|
+
.status(401)
|
|
108
|
+
.send("Unauthorized: Use 'Authorization: Bearer <token>'");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (queryToken !== undefined) {
|
|
113
|
+
token = queryToken;
|
|
114
|
+
}
|
|
115
|
+
if (token !== httpAuthToken) {
|
|
116
|
+
res.status(401).send("Unauthorized: Invalid or missing token");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
next();
|
|
120
|
+
};
|
|
121
|
+
// Explicit preflight handlers for the legacy SSE routes.
|
|
122
|
+
app.options("/sse", corsMiddleware);
|
|
123
|
+
app.options("/messages", corsMiddleware);
|
|
124
|
+
const transports = {};
|
|
125
|
+
// ===========================================================================
|
|
126
|
+
// STREAMABLE HTTP TRANSPORT — MCP protocol 2025-03-26
|
|
127
|
+
// Single endpoint /mcp (GET / POST / DELETE) replaces the old two-endpoint SSE
|
|
128
|
+
// pattern. Use this for all new integrations.
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
app.all("/mcp", corsMiddleware, authMiddleware, async (req, res) => {
|
|
131
|
+
console.error(`[affine-mcp] Received ${req.method} request to /mcp`);
|
|
132
|
+
try {
|
|
133
|
+
// mcp-session-id header can technically be string | string[]; normalise.
|
|
134
|
+
const sidHeader = req.headers["mcp-session-id"];
|
|
135
|
+
const sessionId = Array.isArray(sidHeader) ? sidHeader[0] : sidHeader;
|
|
136
|
+
let transport;
|
|
137
|
+
const existing = sessionId ? transports[sessionId] : undefined;
|
|
138
|
+
if (existing instanceof StreamableHTTPServerTransport) {
|
|
139
|
+
transport = existing;
|
|
140
|
+
}
|
|
141
|
+
else if (!sessionId && req.method === "POST") {
|
|
142
|
+
// Parse body only for the initialize POST (lazy — avoids consuming the stream early).
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
jsonBody(req, res, (err) => (err ? reject(err) : resolve()));
|
|
145
|
+
});
|
|
146
|
+
if (!isInitializeRequest(req.body)) {
|
|
147
|
+
res.status(400).json({
|
|
148
|
+
jsonrpc: "2.0",
|
|
149
|
+
error: {
|
|
150
|
+
code: -32000,
|
|
151
|
+
message: "Bad Request: Not an initialize request",
|
|
152
|
+
},
|
|
153
|
+
id: null,
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
transport = new StreamableHTTPServerTransport({
|
|
158
|
+
sessionIdGenerator: () => randomUUID(),
|
|
159
|
+
onsessioninitialized: (sid) => {
|
|
160
|
+
console.error(`[affine-mcp] StreamableHTTP session initialized: ${sid}`);
|
|
161
|
+
transports[sid] = transport;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
transport.onclose = () => {
|
|
165
|
+
const sid = transport.sessionId;
|
|
166
|
+
if (sid && transports[sid]) {
|
|
167
|
+
console.error(`[affine-mcp] StreamableHTTP session closed: ${sid}`);
|
|
168
|
+
delete transports[sid];
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const server = await createMcpServer();
|
|
172
|
+
await server.connect(transport);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
res.status(400).json({
|
|
176
|
+
jsonrpc: "2.0",
|
|
177
|
+
error: {
|
|
178
|
+
code: -32000,
|
|
179
|
+
message: "Bad Request: No valid session ID or not an initialize request",
|
|
180
|
+
},
|
|
181
|
+
id: null,
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Ensure JSON body is available for subsequent POST requests within the session.
|
|
186
|
+
if (req.method === "POST" && req.body === undefined) {
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
jsonBody(req, res, (err) => (err ? reject(err) : resolve()));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
await transport.handleRequest(req, res, req.body);
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
console.error("[affine-mcp] Error handling /mcp request:", e);
|
|
195
|
+
if (!res.headersSent) {
|
|
196
|
+
res.status(500).json({
|
|
197
|
+
jsonrpc: "2.0",
|
|
198
|
+
error: { code: -32603, message: "Internal server error" },
|
|
199
|
+
id: null,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
// LEGACY HTTP+SSE TRANSPORT — MCP protocol 2024-11-05
|
|
206
|
+
// Kept for backward compatibility with older MCP clients that have not yet
|
|
207
|
+
// migrated to the Streamable HTTP transport above.
|
|
208
|
+
// @deprecated — SSEServerTransport is deprecated by the SDK; use /mcp for new clients.
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
app.get("/sse", corsMiddleware, authMiddleware, async (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
// @ts-ignore — intentional: SSEServerTransport retained for backward compat only
|
|
213
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
214
|
+
const sessionId = transport.sessionId;
|
|
215
|
+
transports[sessionId] = transport;
|
|
216
|
+
res.on("close", () => {
|
|
217
|
+
console.error(`[affine-mcp] Legacy SSE session closed: ${sessionId}`);
|
|
218
|
+
delete transports[sessionId];
|
|
219
|
+
});
|
|
220
|
+
const server = await createMcpServer();
|
|
221
|
+
await server.connect(transport);
|
|
222
|
+
console.error(`[affine-mcp] Legacy SSE session established: ${sessionId}`);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
console.error("[affine-mcp] Error establishing legacy SSE stream:", e);
|
|
226
|
+
if (!res.headersSent)
|
|
227
|
+
res.status(500).send("Error establishing SSE stream");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
app.post("/messages", corsMiddleware, authMiddleware, jsonBody, async (req, res) => {
|
|
231
|
+
const sessionId = typeof req.query.sessionId === "string"
|
|
232
|
+
? req.query.sessionId
|
|
233
|
+
: undefined;
|
|
234
|
+
if (!sessionId) {
|
|
235
|
+
res.status(400).send("Missing sessionId parameter");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const transport = transports[sessionId];
|
|
239
|
+
if (!(transport instanceof SSEServerTransport)) {
|
|
240
|
+
res.status(400).json({
|
|
241
|
+
jsonrpc: "2.0",
|
|
242
|
+
error: {
|
|
243
|
+
code: -32000,
|
|
244
|
+
message: "Bad Request: Session uses a different transport protocol",
|
|
245
|
+
},
|
|
246
|
+
id: null,
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
// @ts-ignore — intentional: SSEServerTransport retained for backward compat only
|
|
252
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
console.error("[affine-mcp] Error handling legacy SSE message:", e);
|
|
256
|
+
if (!res.headersSent)
|
|
257
|
+
res.status(500).send("Error handling POST message");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const server = app.listen(port, host, () => {
|
|
261
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
262
|
+
console.error(`[affine-mcp] MCP server listening on ${host}:${port}`);
|
|
263
|
+
console.error(`[affine-mcp] Streamable HTTP (2025-03-26): http://${displayHost}:${port}/mcp`);
|
|
264
|
+
console.error(`[affine-mcp] Legacy SSE (2024-11-05): http://${displayHost}:${port}/sse`);
|
|
265
|
+
});
|
|
266
|
+
// Graceful shutdown: stop accepting new connections, then close active transports.
|
|
267
|
+
const shutdown = async (signal) => {
|
|
268
|
+
console.error(`[affine-mcp] ${signal} received - shutting down gracefully`);
|
|
269
|
+
server.close(() => {
|
|
270
|
+
void (async () => {
|
|
271
|
+
for (const sessionId in transports) {
|
|
272
|
+
try {
|
|
273
|
+
await transports[sessionId].close();
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
delete transports[sessionId];
|
|
277
|
+
}
|
|
278
|
+
process.exit(0);
|
|
279
|
+
})();
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
283
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
284
|
+
}
|
package/dist/tools/docs.js
CHANGED
|
@@ -219,7 +219,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
219
219
|
const noteId = generateId();
|
|
220
220
|
const note = new Y.Map();
|
|
221
221
|
setSysFields(note, noteId, "affine:note");
|
|
222
|
-
note.set("sys:parent",
|
|
222
|
+
note.set("sys:parent", null);
|
|
223
223
|
note.set("sys:children", new Y.Array());
|
|
224
224
|
note.set("prop:xywh", "[0,0,800,95]");
|
|
225
225
|
note.set("prop:index", "a0");
|
|
@@ -251,7 +251,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
251
251
|
const surfaceId = generateId();
|
|
252
252
|
const surface = new Y.Map();
|
|
253
253
|
setSysFields(surface, surfaceId, "affine:surface");
|
|
254
|
-
surface.set("sys:parent",
|
|
254
|
+
surface.set("sys:parent", null);
|
|
255
255
|
surface.set("sys:children", new Y.Array());
|
|
256
256
|
const elements = new Y.Map();
|
|
257
257
|
elements.set("type", "$blocksuite:internal:native$");
|
|
@@ -601,6 +601,30 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
601
601
|
});
|
|
602
602
|
return index;
|
|
603
603
|
}
|
|
604
|
+
function findParentIdByChild(blocks, childId) {
|
|
605
|
+
for (const [id, value] of blocks) {
|
|
606
|
+
if (!(value instanceof Y.Map)) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const childIds = childIdsFrom(value.get("sys:children"));
|
|
610
|
+
if (childIds.includes(childId)) {
|
|
611
|
+
return String(id);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
function resolveBlockParentId(blocks, blockId) {
|
|
617
|
+
const block = findBlockById(blocks, blockId);
|
|
618
|
+
if (!block) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
const rawParentId = block.get("sys:parent");
|
|
622
|
+
if (typeof rawParentId === "string" && rawParentId.trim().length > 0) {
|
|
623
|
+
return rawParentId;
|
|
624
|
+
}
|
|
625
|
+
// AFFiNE UI commonly stores sys:parent as null and derives hierarchy from sys:children.
|
|
626
|
+
return findParentIdByChild(blocks, blockId);
|
|
627
|
+
}
|
|
604
628
|
function resolveInsertContext(blocks, normalized) {
|
|
605
629
|
const placement = normalized.placement;
|
|
606
630
|
let parentId;
|
|
@@ -612,8 +636,8 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
612
636
|
const referenceBlock = findBlockById(blocks, referenceBlockId);
|
|
613
637
|
if (!referenceBlock)
|
|
614
638
|
throw new Error(`placement.afterBlockId '${referenceBlockId}' was not found.`);
|
|
615
|
-
const refParentId =
|
|
616
|
-
if (
|
|
639
|
+
const refParentId = resolveBlockParentId(blocks, referenceBlockId);
|
|
640
|
+
if (!refParentId) {
|
|
617
641
|
throw new Error(`Block '${referenceBlockId}' has no parent.`);
|
|
618
642
|
}
|
|
619
643
|
parentId = refParentId;
|
|
@@ -624,8 +648,8 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
624
648
|
const referenceBlock = findBlockById(blocks, referenceBlockId);
|
|
625
649
|
if (!referenceBlock)
|
|
626
650
|
throw new Error(`placement.beforeBlockId '${referenceBlockId}' was not found.`);
|
|
627
|
-
const refParentId =
|
|
628
|
-
if (
|
|
651
|
+
const refParentId = resolveBlockParentId(blocks, referenceBlockId);
|
|
652
|
+
if (!refParentId) {
|
|
629
653
|
throw new Error(`Block '${referenceBlockId}' has no parent.`);
|
|
630
654
|
}
|
|
631
655
|
parentId = refParentId;
|
|
@@ -688,16 +712,17 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
688
712
|
}
|
|
689
713
|
return { parentId, parentBlock, children, insertIndex };
|
|
690
714
|
}
|
|
691
|
-
function createBlock(
|
|
715
|
+
function createBlock(normalized) {
|
|
692
716
|
const blockId = generateId();
|
|
693
717
|
const block = new Y.Map();
|
|
694
718
|
const content = normalized.text;
|
|
719
|
+
// Keep parity with AFFiNE UI-created docs: sys:parent stays null and hierarchy is represented by sys:children.
|
|
695
720
|
switch (normalized.type) {
|
|
696
721
|
case "paragraph":
|
|
697
722
|
case "heading":
|
|
698
723
|
case "quote": {
|
|
699
724
|
setSysFields(block, blockId, "affine:paragraph");
|
|
700
|
-
block.set("sys:parent",
|
|
725
|
+
block.set("sys:parent", null);
|
|
701
726
|
block.set("sys:children", new Y.Array());
|
|
702
727
|
const blockType = normalized.type === "heading"
|
|
703
728
|
? `h${normalized.headingLevel}`
|
|
@@ -710,7 +735,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
710
735
|
}
|
|
711
736
|
case "list": {
|
|
712
737
|
setSysFields(block, blockId, "affine:list");
|
|
713
|
-
block.set("sys:parent",
|
|
738
|
+
block.set("sys:parent", null);
|
|
714
739
|
block.set("sys:children", new Y.Array());
|
|
715
740
|
block.set("prop:type", normalized.listStyle);
|
|
716
741
|
block.set("prop:checked", normalized.listStyle === "todo" ? normalized.checked : false);
|
|
@@ -719,7 +744,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
719
744
|
}
|
|
720
745
|
case "code": {
|
|
721
746
|
setSysFields(block, blockId, "affine:code");
|
|
722
|
-
block.set("sys:parent",
|
|
747
|
+
block.set("sys:parent", null);
|
|
723
748
|
block.set("sys:children", new Y.Array());
|
|
724
749
|
block.set("prop:language", normalized.language);
|
|
725
750
|
if (normalized.caption) {
|
|
@@ -730,22 +755,35 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
730
755
|
}
|
|
731
756
|
case "divider": {
|
|
732
757
|
setSysFields(block, blockId, "affine:divider");
|
|
733
|
-
block.set("sys:parent",
|
|
758
|
+
block.set("sys:parent", null);
|
|
734
759
|
block.set("sys:children", new Y.Array());
|
|
735
760
|
return { blockId, block, flavour: "affine:divider" };
|
|
736
761
|
}
|
|
737
762
|
case "callout": {
|
|
738
763
|
setSysFields(block, blockId, "affine:callout");
|
|
739
|
-
block.set("sys:parent",
|
|
740
|
-
|
|
764
|
+
block.set("sys:parent", null);
|
|
765
|
+
const calloutChildren = new Y.Array();
|
|
766
|
+
const textBlockId = generateId();
|
|
767
|
+
const textBlock = new Y.Map();
|
|
768
|
+
setSysFields(textBlock, textBlockId, "affine:paragraph");
|
|
769
|
+
textBlock.set("sys:parent", null);
|
|
770
|
+
textBlock.set("sys:children", new Y.Array());
|
|
771
|
+
textBlock.set("prop:type", "text");
|
|
772
|
+
textBlock.set("prop:text", makeText(content));
|
|
773
|
+
calloutChildren.push([textBlockId]);
|
|
774
|
+
block.set("sys:children", calloutChildren);
|
|
741
775
|
block.set("prop:icon", { type: "emoji", unicode: "💡" });
|
|
742
776
|
block.set("prop:backgroundColorName", "grey");
|
|
743
|
-
|
|
744
|
-
|
|
777
|
+
return {
|
|
778
|
+
blockId,
|
|
779
|
+
block,
|
|
780
|
+
flavour: "affine:callout",
|
|
781
|
+
extraBlocks: [{ blockId: textBlockId, block: textBlock }],
|
|
782
|
+
};
|
|
745
783
|
}
|
|
746
784
|
case "latex": {
|
|
747
785
|
setSysFields(block, blockId, "affine:latex");
|
|
748
|
-
block.set("sys:parent",
|
|
786
|
+
block.set("sys:parent", null);
|
|
749
787
|
block.set("sys:children", new Y.Array());
|
|
750
788
|
block.set("prop:xywh", "[0,0,16,16]");
|
|
751
789
|
block.set("prop:index", "a0");
|
|
@@ -757,7 +795,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
757
795
|
}
|
|
758
796
|
case "table": {
|
|
759
797
|
setSysFields(block, blockId, "affine:table");
|
|
760
|
-
block.set("sys:parent",
|
|
798
|
+
block.set("sys:parent", null);
|
|
761
799
|
block.set("sys:children", new Y.Array());
|
|
762
800
|
const rows = {};
|
|
763
801
|
const columns = {};
|
|
@@ -792,7 +830,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
792
830
|
}
|
|
793
831
|
case "bookmark": {
|
|
794
832
|
setSysFields(block, blockId, "affine:bookmark");
|
|
795
|
-
block.set("sys:parent",
|
|
833
|
+
block.set("sys:parent", null);
|
|
796
834
|
block.set("sys:children", new Y.Array());
|
|
797
835
|
block.set("prop:style", normalized.bookmarkStyle);
|
|
798
836
|
block.set("prop:url", normalized.url);
|
|
@@ -810,7 +848,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
810
848
|
}
|
|
811
849
|
case "image": {
|
|
812
850
|
setSysFields(block, blockId, "affine:image");
|
|
813
|
-
block.set("sys:parent",
|
|
851
|
+
block.set("sys:parent", null);
|
|
814
852
|
block.set("sys:children", new Y.Array());
|
|
815
853
|
block.set("prop:caption", normalized.caption ?? "");
|
|
816
854
|
block.set("prop:sourceId", normalized.sourceId);
|
|
@@ -825,7 +863,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
825
863
|
}
|
|
826
864
|
case "attachment": {
|
|
827
865
|
setSysFields(block, blockId, "affine:attachment");
|
|
828
|
-
block.set("sys:parent",
|
|
866
|
+
block.set("sys:parent", null);
|
|
829
867
|
block.set("sys:children", new Y.Array());
|
|
830
868
|
block.set("prop:name", normalized.name);
|
|
831
869
|
block.set("prop:size", normalized.size);
|
|
@@ -843,7 +881,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
843
881
|
}
|
|
844
882
|
case "embed_youtube": {
|
|
845
883
|
setSysFields(block, blockId, "affine:embed-youtube");
|
|
846
|
-
block.set("sys:parent",
|
|
884
|
+
block.set("sys:parent", null);
|
|
847
885
|
block.set("sys:children", new Y.Array());
|
|
848
886
|
block.set("prop:index", "a0");
|
|
849
887
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -863,7 +901,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
863
901
|
}
|
|
864
902
|
case "embed_github": {
|
|
865
903
|
setSysFields(block, blockId, "affine:embed-github");
|
|
866
|
-
block.set("sys:parent",
|
|
904
|
+
block.set("sys:parent", null);
|
|
867
905
|
block.set("sys:children", new Y.Array());
|
|
868
906
|
block.set("prop:index", "a0");
|
|
869
907
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -887,7 +925,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
887
925
|
}
|
|
888
926
|
case "embed_figma": {
|
|
889
927
|
setSysFields(block, blockId, "affine:embed-figma");
|
|
890
|
-
block.set("sys:parent",
|
|
928
|
+
block.set("sys:parent", null);
|
|
891
929
|
block.set("sys:children", new Y.Array());
|
|
892
930
|
block.set("prop:index", "a0");
|
|
893
931
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -902,7 +940,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
902
940
|
}
|
|
903
941
|
case "embed_loom": {
|
|
904
942
|
setSysFields(block, blockId, "affine:embed-loom");
|
|
905
|
-
block.set("sys:parent",
|
|
943
|
+
block.set("sys:parent", null);
|
|
906
944
|
block.set("sys:children", new Y.Array());
|
|
907
945
|
block.set("prop:index", "a0");
|
|
908
946
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -919,7 +957,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
919
957
|
}
|
|
920
958
|
case "embed_html": {
|
|
921
959
|
setSysFields(block, blockId, "affine:embed-html");
|
|
922
|
-
block.set("sys:parent",
|
|
960
|
+
block.set("sys:parent", null);
|
|
923
961
|
block.set("sys:children", new Y.Array());
|
|
924
962
|
block.set("prop:index", "a0");
|
|
925
963
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -933,7 +971,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
933
971
|
}
|
|
934
972
|
case "embed_linked_doc": {
|
|
935
973
|
setSysFields(block, blockId, "affine:embed-linked-doc");
|
|
936
|
-
block.set("sys:parent",
|
|
974
|
+
block.set("sys:parent", null);
|
|
937
975
|
block.set("sys:children", new Y.Array());
|
|
938
976
|
block.set("prop:index", "a0");
|
|
939
977
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -949,7 +987,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
949
987
|
}
|
|
950
988
|
case "embed_synced_doc": {
|
|
951
989
|
setSysFields(block, blockId, "affine:embed-synced-doc");
|
|
952
|
-
block.set("sys:parent",
|
|
990
|
+
block.set("sys:parent", null);
|
|
953
991
|
block.set("sys:children", new Y.Array());
|
|
954
992
|
block.set("prop:index", "a0");
|
|
955
993
|
block.set("prop:xywh", "[0,0,800,100]");
|
|
@@ -966,7 +1004,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
966
1004
|
}
|
|
967
1005
|
case "embed_iframe": {
|
|
968
1006
|
setSysFields(block, blockId, "affine:embed-iframe");
|
|
969
|
-
block.set("sys:parent",
|
|
1007
|
+
block.set("sys:parent", null);
|
|
970
1008
|
block.set("sys:children", new Y.Array());
|
|
971
1009
|
block.set("prop:index", "a0");
|
|
972
1010
|
block.set("prop:xywh", "[0,0,0,0]");
|
|
@@ -983,7 +1021,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
983
1021
|
}
|
|
984
1022
|
case "database": {
|
|
985
1023
|
setSysFields(block, blockId, "affine:database");
|
|
986
|
-
block.set("sys:parent",
|
|
1024
|
+
block.set("sys:parent", null);
|
|
987
1025
|
block.set("sys:children", new Y.Array());
|
|
988
1026
|
// Create a default table view so AFFiNE UI renders the database
|
|
989
1027
|
const defaultView = new Y.Map();
|
|
@@ -1008,7 +1046,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1008
1046
|
// AFFiNE 0.26.x currently crashes on raw affine:data-view render path.
|
|
1009
1047
|
// Keep API compatibility for type="data_view" by mapping it to the stable database block.
|
|
1010
1048
|
setSysFields(block, blockId, "affine:database");
|
|
1011
|
-
block.set("sys:parent",
|
|
1049
|
+
block.set("sys:parent", null);
|
|
1012
1050
|
block.set("sys:children", new Y.Array());
|
|
1013
1051
|
const dvDefaultView = new Y.Map();
|
|
1014
1052
|
dvDefaultView.set("id", generateId());
|
|
@@ -1030,7 +1068,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1030
1068
|
}
|
|
1031
1069
|
case "surface_ref": {
|
|
1032
1070
|
setSysFields(block, blockId, "affine:surface-ref");
|
|
1033
|
-
block.set("sys:parent",
|
|
1071
|
+
block.set("sys:parent", null);
|
|
1034
1072
|
block.set("sys:children", new Y.Array());
|
|
1035
1073
|
block.set("prop:reference", normalized.reference);
|
|
1036
1074
|
block.set("prop:caption", normalized.caption ?? "");
|
|
@@ -1040,7 +1078,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1040
1078
|
}
|
|
1041
1079
|
case "frame": {
|
|
1042
1080
|
setSysFields(block, blockId, "affine:frame");
|
|
1043
|
-
block.set("sys:parent",
|
|
1081
|
+
block.set("sys:parent", null);
|
|
1044
1082
|
block.set("sys:children", new Y.Array());
|
|
1045
1083
|
block.set("prop:title", makeText(content || "Frame"));
|
|
1046
1084
|
block.set("prop:background", normalized.background);
|
|
@@ -1053,7 +1091,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1053
1091
|
}
|
|
1054
1092
|
case "edgeless_text": {
|
|
1055
1093
|
setSysFields(block, blockId, "affine:edgeless-text");
|
|
1056
|
-
block.set("sys:parent",
|
|
1094
|
+
block.set("sys:parent", null);
|
|
1057
1095
|
block.set("sys:children", new Y.Array());
|
|
1058
1096
|
block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
|
|
1059
1097
|
block.set("prop:index", "a0");
|
|
@@ -1071,7 +1109,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1071
1109
|
}
|
|
1072
1110
|
case "note": {
|
|
1073
1111
|
setSysFields(block, blockId, "affine:note");
|
|
1074
|
-
block.set("sys:parent",
|
|
1112
|
+
block.set("sys:parent", null);
|
|
1075
1113
|
block.set("sys:children", new Y.Array());
|
|
1076
1114
|
block.set("prop:xywh", `[0,0,${normalized.width},${normalized.height}]`);
|
|
1077
1115
|
block.set("prop:background", normalized.background);
|
|
@@ -1110,8 +1148,13 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1110
1148
|
const prevSV = Y.encodeStateVector(doc);
|
|
1111
1149
|
const blocks = doc.getMap("blocks");
|
|
1112
1150
|
const context = resolveInsertContext(blocks, normalized);
|
|
1113
|
-
const { blockId, block, flavour, blockType } = createBlock(
|
|
1151
|
+
const { blockId, block, flavour, blockType, extraBlocks } = createBlock(normalized);
|
|
1114
1152
|
blocks.set(blockId, block);
|
|
1153
|
+
if (Array.isArray(extraBlocks)) {
|
|
1154
|
+
for (const extra of extraBlocks) {
|
|
1155
|
+
blocks.set(extra.blockId, extra.block);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1115
1158
|
if (context.insertIndex >= context.children.length) {
|
|
1116
1159
|
context.children.push([blockId]);
|
|
1117
1160
|
}
|
|
@@ -1444,8 +1487,13 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1444
1487
|
try {
|
|
1445
1488
|
const normalized = normalizeAppendBlockInput(appendInput);
|
|
1446
1489
|
const context = resolveInsertContext(blocks, normalized);
|
|
1447
|
-
const { blockId, block } = createBlock(
|
|
1490
|
+
const { blockId, block, extraBlocks } = createBlock(normalized);
|
|
1448
1491
|
blocks.set(blockId, block);
|
|
1492
|
+
if (Array.isArray(extraBlocks)) {
|
|
1493
|
+
for (const extra of extraBlocks) {
|
|
1494
|
+
blocks.set(extra.blockId, extra.block);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1449
1497
|
if (context.insertIndex >= context.children.length) {
|
|
1450
1498
|
context.children.push([blockId]);
|
|
1451
1499
|
}
|
|
@@ -1499,7 +1547,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1499
1547
|
const surfaceId = generateId();
|
|
1500
1548
|
const surface = new Y.Map();
|
|
1501
1549
|
setSysFields(surface, surfaceId, "affine:surface");
|
|
1502
|
-
surface.set("sys:parent",
|
|
1550
|
+
surface.set("sys:parent", null);
|
|
1503
1551
|
surface.set("sys:children", new Y.Array());
|
|
1504
1552
|
const elements = new Y.Map();
|
|
1505
1553
|
elements.set("type", "$blocksuite:internal:native$");
|
|
@@ -1510,7 +1558,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1510
1558
|
const noteId = generateId();
|
|
1511
1559
|
const note = new Y.Map();
|
|
1512
1560
|
setSysFields(note, noteId, "affine:note");
|
|
1513
|
-
note.set("sys:parent",
|
|
1561
|
+
note.set("sys:parent", null);
|
|
1514
1562
|
note.set("prop:displayMode", "both");
|
|
1515
1563
|
note.set("prop:xywh", "[0,0,800,95]");
|
|
1516
1564
|
note.set("prop:index", "a0");
|
|
@@ -1527,7 +1575,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1527
1575
|
const paraId = generateId();
|
|
1528
1576
|
const para = new Y.Map();
|
|
1529
1577
|
setSysFields(para, paraId, "affine:paragraph");
|
|
1530
|
-
para.set("sys:parent",
|
|
1578
|
+
para.set("sys:parent", null);
|
|
1531
1579
|
para.set("sys:children", new Y.Array());
|
|
1532
1580
|
para.set("prop:type", "text");
|
|
1533
1581
|
const paragraphText = new Y.Text();
|
package/dist/tools/workspaces.js
CHANGED
|
@@ -14,13 +14,13 @@ function generateDocId() {
|
|
|
14
14
|
return id;
|
|
15
15
|
}
|
|
16
16
|
// Create initial workspace data with a document
|
|
17
|
-
function createInitialWorkspaceData(workspaceName = 'New Workspace') {
|
|
17
|
+
function createInitialWorkspaceData(workspaceName = 'New Workspace', avatar = '') {
|
|
18
18
|
// Create workspace root YDoc
|
|
19
19
|
const rootDoc = new Y.Doc();
|
|
20
20
|
// Set workspace metadata
|
|
21
21
|
const meta = rootDoc.getMap('meta');
|
|
22
22
|
meta.set('name', workspaceName);
|
|
23
|
-
meta.set('avatar',
|
|
23
|
+
meta.set('avatar', avatar);
|
|
24
24
|
// Create pages array with initial document
|
|
25
25
|
const pages = new Y.Array();
|
|
26
26
|
const firstDocId = generateDocId();
|
|
@@ -58,7 +58,7 @@ function createInitialWorkspaceData(workspaceName = 'New Workspace') {
|
|
|
58
58
|
const surfaceBlock = new Y.Map();
|
|
59
59
|
surfaceBlock.set('sys:id', surfaceId);
|
|
60
60
|
surfaceBlock.set('sys:flavour', 'affine:surface');
|
|
61
|
-
surfaceBlock.set('sys:parent',
|
|
61
|
+
surfaceBlock.set('sys:parent', null);
|
|
62
62
|
surfaceBlock.set('sys:children', new Y.Array());
|
|
63
63
|
blocks.set(surfaceId, surfaceBlock);
|
|
64
64
|
pageChildren.push([surfaceId]);
|
|
@@ -67,7 +67,7 @@ function createInitialWorkspaceData(workspaceName = 'New Workspace') {
|
|
|
67
67
|
const noteBlock = new Y.Map();
|
|
68
68
|
noteBlock.set('sys:id', noteId);
|
|
69
69
|
noteBlock.set('sys:flavour', 'affine:note');
|
|
70
|
-
noteBlock.set('sys:parent',
|
|
70
|
+
noteBlock.set('sys:parent', null);
|
|
71
71
|
noteBlock.set('prop:displayMode', 'DocAndEdgeless');
|
|
72
72
|
noteBlock.set('prop:xywh', '[0,0,800,600]');
|
|
73
73
|
noteBlock.set('prop:index', 'a0');
|
|
@@ -81,7 +81,7 @@ function createInitialWorkspaceData(workspaceName = 'New Workspace') {
|
|
|
81
81
|
const paragraphBlock = new Y.Map();
|
|
82
82
|
paragraphBlock.set('sys:id', paragraphId);
|
|
83
83
|
paragraphBlock.set('sys:flavour', 'affine:paragraph');
|
|
84
|
-
paragraphBlock.set('sys:parent',
|
|
84
|
+
paragraphBlock.set('sys:parent', null);
|
|
85
85
|
paragraphBlock.set('sys:children', new Y.Array());
|
|
86
86
|
paragraphBlock.set('prop:type', 'text');
|
|
87
87
|
const paragraphText = new Y.Text();
|
|
@@ -158,7 +158,7 @@ export function registerWorkspaceTools(server, gql) {
|
|
|
158
158
|
const cookie = gql.cookie;
|
|
159
159
|
const bearer = gql.bearer;
|
|
160
160
|
// Create initial workspace data
|
|
161
|
-
const { workspaceUpdate, firstDocId, docUpdate } = createInitialWorkspaceData(name);
|
|
161
|
+
const { workspaceUpdate, firstDocId, docUpdate } = createInitialWorkspaceData(name, avatar || '');
|
|
162
162
|
// Only send workspace update - document will be created separately
|
|
163
163
|
const initData = Buffer.from(workspaceUpdate);
|
|
164
164
|
// Create multipart form
|
package/dist/ws.js
CHANGED
|
@@ -2,6 +2,35 @@ import { io } from "socket.io-client";
|
|
|
2
2
|
const DEFAULT_WS_CLIENT_VERSION = process.env.AFFINE_WS_CLIENT_VERSION || '0.26.0';
|
|
3
3
|
const WS_CONNECT_TIMEOUT_MS = Number(process.env.AFFINE_WS_CONNECT_TIMEOUT_MS || 10000);
|
|
4
4
|
const WS_ACK_TIMEOUT_MS = Number(process.env.AFFINE_WS_ACK_TIMEOUT_MS || 10000);
|
|
5
|
+
function ackErrorMessage(ack, fallback) {
|
|
6
|
+
const message = ack?.error?.message;
|
|
7
|
+
if (typeof message === "string" && message.trim())
|
|
8
|
+
return message;
|
|
9
|
+
return ack?.error ? fallback : null;
|
|
10
|
+
}
|
|
11
|
+
function emitWithAck(socket, event, payload, onAck) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
reject(new Error(`${event} timeout after ${WS_ACK_TIMEOUT_MS}ms`));
|
|
19
|
+
}, WS_ACK_TIMEOUT_MS);
|
|
20
|
+
socket.emit(event, payload, (ack) => {
|
|
21
|
+
if (settled)
|
|
22
|
+
return;
|
|
23
|
+
settled = true;
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
try {
|
|
26
|
+
resolve(onAck(ack));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
reject(err);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
5
34
|
export function wsUrlFromGraphQLEndpoint(endpoint) {
|
|
6
35
|
return endpoint
|
|
7
36
|
.replace('https://', 'wss://')
|
|
@@ -10,6 +39,7 @@ export function wsUrlFromGraphQLEndpoint(endpoint) {
|
|
|
10
39
|
}
|
|
11
40
|
export async function connectWorkspaceSocket(wsUrl, cookie, bearer) {
|
|
12
41
|
return new Promise((resolve, reject) => {
|
|
42
|
+
let settled = false;
|
|
13
43
|
const extraHeaders = {};
|
|
14
44
|
if (cookie)
|
|
15
45
|
extraHeaders['Cookie'] = cookie;
|
|
@@ -22,16 +52,25 @@ export async function connectWorkspaceSocket(wsUrl, cookie, bearer) {
|
|
|
22
52
|
autoConnect: true
|
|
23
53
|
});
|
|
24
54
|
const timeout = setTimeout(() => {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
25
58
|
cleanup();
|
|
26
59
|
socket.disconnect();
|
|
27
60
|
reject(new Error(`socket connect timeout after ${WS_CONNECT_TIMEOUT_MS}ms`));
|
|
28
61
|
}, WS_CONNECT_TIMEOUT_MS);
|
|
29
62
|
const onError = (err) => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
30
66
|
cleanup();
|
|
31
67
|
socket.disconnect();
|
|
32
68
|
reject(err);
|
|
33
69
|
};
|
|
34
70
|
const onConnect = () => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
35
74
|
cleanup();
|
|
36
75
|
resolve(socket);
|
|
37
76
|
};
|
|
@@ -45,45 +84,28 @@ export async function connectWorkspaceSocket(wsUrl, cookie, bearer) {
|
|
|
45
84
|
});
|
|
46
85
|
}
|
|
47
86
|
export async function joinWorkspace(socket, workspaceId, clientVersion = DEFAULT_WS_CLIENT_VERSION) {
|
|
48
|
-
return
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
socket.emit('space:join', { spaceType: 'workspace', spaceId: workspaceId, clientVersion }, (ack) => {
|
|
53
|
-
clearTimeout(timeout);
|
|
54
|
-
if (ack?.error)
|
|
55
|
-
return reject(new Error(ack.error.message || 'join failed'));
|
|
56
|
-
resolve();
|
|
57
|
-
});
|
|
87
|
+
return emitWithAck(socket, 'space:join', { spaceType: 'workspace', spaceId: workspaceId, clientVersion }, (ack) => {
|
|
88
|
+
const message = ackErrorMessage(ack, "join failed");
|
|
89
|
+
if (message)
|
|
90
|
+
throw new Error(message);
|
|
58
91
|
});
|
|
59
92
|
}
|
|
60
93
|
export async function loadDoc(socket, workspaceId, docId) {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (ack.error.name === 'DOC_NOT_FOUND')
|
|
69
|
-
return resolve({});
|
|
70
|
-
return reject(new Error(ack.error.message || 'load-doc failed'));
|
|
71
|
-
}
|
|
72
|
-
resolve(ack?.data || {});
|
|
73
|
-
});
|
|
94
|
+
return emitWithAck(socket, 'space:load-doc', { spaceType: 'workspace', spaceId: workspaceId, docId }, (ack) => {
|
|
95
|
+
if (ack?.error) {
|
|
96
|
+
if (ack.error.name === 'DOC_NOT_FOUND')
|
|
97
|
+
return {};
|
|
98
|
+
throw new Error(ackErrorMessage(ack, "load-doc failed") || "load-doc failed");
|
|
99
|
+
}
|
|
100
|
+
return ack?.data || {};
|
|
74
101
|
});
|
|
75
102
|
}
|
|
76
103
|
export async function pushDocUpdate(socket, workspaceId, docId, updateBase64) {
|
|
77
|
-
return
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
clearTimeout(timeout);
|
|
83
|
-
if (ack?.error)
|
|
84
|
-
return reject(new Error(ack.error.message || 'push-doc-update failed'));
|
|
85
|
-
resolve(ack?.data?.timestamp || Date.now());
|
|
86
|
-
});
|
|
104
|
+
return emitWithAck(socket, 'space:push-doc-update', { spaceType: 'workspace', spaceId: workspaceId, docId, update: updateBase64 }, (ack) => {
|
|
105
|
+
const message = ackErrorMessage(ack, "push-doc-update failed");
|
|
106
|
+
if (message)
|
|
107
|
+
throw new Error(message);
|
|
108
|
+
return ack?.data?.timestamp || Date.now();
|
|
87
109
|
});
|
|
88
110
|
}
|
|
89
111
|
export function deleteDoc(socket, workspaceId, docId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "affine-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
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.",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"build": "npm run clean && tsc -p tsconfig.json",
|
|
32
32
|
"dev": "tsx watch src/index.ts",
|
|
33
33
|
"start": "node dist/index.js",
|
|
34
|
+
"start:http": "MCP_TRANSPORT=http node dist/index.js",
|
|
34
35
|
"test": "npm run test:tool-manifest",
|
|
35
36
|
"test:tool-manifest": "node scripts/verify-tool-manifest.mjs",
|
|
36
37
|
"test:comprehensive": "node test-comprehensive.mjs",
|
|
@@ -56,6 +57,8 @@
|
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"@modelcontextprotocol/sdk": "^1.17.2",
|
|
60
|
+
"cors": "^2.8.6",
|
|
61
|
+
"express": "^5.2.1",
|
|
59
62
|
"form-data": "^4.0.4",
|
|
60
63
|
"markdown-it": "^14.1.0",
|
|
61
64
|
"node-fetch": "^3.3.2",
|
|
@@ -66,6 +69,8 @@
|
|
|
66
69
|
},
|
|
67
70
|
"devDependencies": {
|
|
68
71
|
"@playwright/test": "^1.50.0",
|
|
72
|
+
"@types/cors": "^2.8.19",
|
|
73
|
+
"@types/express": "^5.0.6",
|
|
69
74
|
"@types/markdown-it": "^14.1.2",
|
|
70
75
|
"@types/node": "^25.2.3",
|
|
71
76
|
"tsx": "^4.16.2",
|
package/dist/markdown/index.js
DELETED
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|