mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(server): add document write tools for mcp (#14245)
## Summary This PR adds write capabilities to AFFiNE's MCP (Model Context Protocol) integration, enabling external tools (Claude, GPT, etc.) to create and modify documents programmatically. **New MCP Tools:** - `create_document` - Create new documents from markdown content - `update_document` - Update document content using structural diffing for minimal changes (preserves document history and enables real-time collaboration) **Implementation:** - `markdown_to_ydoc.rs` - Converts markdown to AFFiNE-compatible y-octo binary format - `markdown_utils.rs` - Shared markdown parsing utilities (used by both ydoc-to-md and md-to-ydoc) - `update_ydoc.rs` - Structural diffing implementation for updating existing documents - `DocWriter` service - TypeScript service for document operations - Exposes `markdownToDocBinary` and `updateDocBinary` via napi bindings **Supported Markdown Elements:** - Headings (H1-H6) - Paragraphs - Bullet lists and numbered lists - Code blocks (with language detection) - Blockquotes - Horizontal dividers - Todo items (checkboxes) **y-octo Changes:** This PR reverts the y-octo sync (ca2462f,a5b60cf) which introduced a concurrency bug causing hangs when creating documents with many nested block structures. It also ports the improved `get_node_index` binary search fix from upstream that prevents divide-by-zero panics when decoding documents. ## Test Results ✅ ### Unit Tests (47/47 passing) | Test Suite | Tests | Status | |------------|-------|--------| | markdown_to_ydoc | 16/16 | ✅ Pass | | markdown_utils | 11/11 | ✅ Pass | | update_ydoc | 13/13 | ✅ Pass | | delta_markdown | 2/2 | ✅ Pass | | affine (doc parser) | 5/5 | ✅ Pass | ### End-to-End MCP Testing ✅ Tested against local AFFiNE server with real MCP client requests: | Tool | Result | Notes | |------|--------|-------| | `tools/list` | ✅ Pass | Returns all 5 tools with correct schemas | | `create_document` | ✅ Pass | Successfully created test documents | | `update_document` | ✅ Pass | Successfully updated documents with structural diffing | | `read_document` | ✅ Pass | Existing tool, works correctly | | `keyword_search` | ✅ Pass | Existing tool, works correctly | **E2E Test Details:** - Started local AFFiNE server with PostgreSQL, Redis, and Manticore - Created test user and workspace via seed/GraphQL - Verified MCP endpoint at `/api/workspaces/:workspaceId/mcp` - Tested JSON-RPC calls with proper SSE streaming - Confirmed documents are stored and indexed correctly (verified via server logs) ## Test Plan - [x] All Rust unit tests pass (47 tests) - [x] Native bindings build successfully (release mode) - [x] Document creation via MCP works end-to-end - [x] Document update via MCP works end-to-end - [x] CodeRabbit feedback addressed - [ ] Integration testing with Claude/GPT MCP clients Closes #14161 --- **Requested by:** @realies **Key guidance from:** @darkskygit (use y-octo instead of yjs for memory efficiency) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Create documents from Markdown: generate new documents directly from Markdown content with automatic title extraction * Update documents with Markdown: modify existing documents using Markdown as the source with automatic diff calculation for efficient updates * Copilot integration: new tools for document creation and updates through Copilot's interface <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
40
packages/backend/native/index.d.ts
vendored
40
packages/backend/native/index.d.ts
vendored
@@ -4,6 +4,20 @@ export declare class Tokenizer {
|
||||
count(content: string, allowedSpecial?: Array<string> | undefined | null): number
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a document ID to the workspace root doc's meta.pages array.
|
||||
* This registers the document in the workspace so it appears in the UI.
|
||||
*
|
||||
* # Arguments
|
||||
* * `root_doc_bin` - The current root doc binary (workspaceId doc)
|
||||
* * `doc_id` - The document ID to add
|
||||
* * `title` - Optional title for the document
|
||||
*
|
||||
* # Returns
|
||||
* A Buffer containing the y-octo update binary to apply to the root doc
|
||||
*/
|
||||
export declare function addDocToRootDoc(rootDocBin: Buffer, docId: string, title?: string | undefined | null): Buffer
|
||||
|
||||
export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
|
||||
|
||||
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||
@@ -19,6 +33,18 @@ export declare function getMime(input: Uint8Array): string
|
||||
|
||||
export declare function htmlSanitize(input: string): string
|
||||
|
||||
/**
|
||||
* Converts markdown content to AFFiNE-compatible y-octo document binary.
|
||||
*
|
||||
* # Arguments
|
||||
* * `markdown` - The markdown content to convert
|
||||
* * `doc_id` - The document ID to use for the y-octo doc
|
||||
*
|
||||
* # Returns
|
||||
* A Buffer containing the y-octo document update binary
|
||||
*/
|
||||
export declare function markdownToDocBinary(markdown: string, docId: string): Buffer
|
||||
|
||||
/**
|
||||
* Merge updates in form like `Y.applyUpdate(doc, update)` way and return the
|
||||
* result binary.
|
||||
@@ -77,4 +103,18 @@ export declare function parseWorkspaceDoc(docBin: Buffer): NativeWorkspaceDocCon
|
||||
|
||||
export declare function readAllDocIdsFromRootDoc(docBin: Buffer, includeTrash?: boolean | undefined | null): Array<string>
|
||||
|
||||
/**
|
||||
* Updates an existing document with new markdown content.
|
||||
* Uses structural and text-level diffing to apply minimal changes.
|
||||
*
|
||||
* # Arguments
|
||||
* * `existing_binary` - The current document binary
|
||||
* * `new_markdown` - The new markdown content to apply
|
||||
* * `doc_id` - The document ID
|
||||
*
|
||||
* # Returns
|
||||
* A Buffer containing only the delta (changes) as a y-octo update binary
|
||||
*/
|
||||
export declare function updateDocWithMarkdown(existingBinary: Buffer, newMarkdown: string, docId: string): Buffer
|
||||
|
||||
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
|
||||
|
||||
@@ -132,3 +132,52 @@ pub fn read_all_doc_ids_from_root_doc(doc_bin: Buffer, include_trash: Option<boo
|
||||
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Converts markdown content to AFFiNE-compatible y-octo document binary.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `markdown` - The markdown content to convert
|
||||
/// * `doc_id` - The document ID to use for the y-octo doc
|
||||
///
|
||||
/// # Returns
|
||||
/// A Buffer containing the y-octo document update binary
|
||||
#[napi]
|
||||
pub fn markdown_to_doc_binary(markdown: String, doc_id: String) -> Result<Buffer> {
|
||||
let result =
|
||||
doc_parser::markdown_to_ydoc(&markdown, &doc_id).map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
||||
Ok(Buffer::from(result))
|
||||
}
|
||||
|
||||
/// Updates an existing document with new markdown content.
|
||||
/// Uses structural and text-level diffing to apply minimal changes.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `existing_binary` - The current document binary
|
||||
/// * `new_markdown` - The new markdown content to apply
|
||||
/// * `doc_id` - The document ID
|
||||
///
|
||||
/// # Returns
|
||||
/// A Buffer containing only the delta (changes) as a y-octo update binary
|
||||
#[napi]
|
||||
pub fn update_doc_with_markdown(existing_binary: Buffer, new_markdown: String, doc_id: String) -> Result<Buffer> {
|
||||
let result = doc_parser::update_ydoc(&existing_binary, &new_markdown, &doc_id)
|
||||
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
||||
Ok(Buffer::from(result))
|
||||
}
|
||||
|
||||
/// Adds a document ID to the workspace root doc's meta.pages array.
|
||||
/// This registers the document in the workspace so it appears in the UI.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `root_doc_bin` - The current root doc binary (workspaceId doc)
|
||||
/// * `doc_id` - The document ID to add
|
||||
/// * `title` - Optional title for the document
|
||||
///
|
||||
/// # Returns
|
||||
/// A Buffer containing the y-octo update binary to apply to the root doc
|
||||
#[napi]
|
||||
pub fn add_doc_to_root_doc(root_doc_bin: Buffer, doc_id: String, title: Option<String>) -> Result<Buffer> {
|
||||
let result = doc_parser::add_doc_to_root_doc(root_doc_bin.into(), &doc_id, title.as_deref())
|
||||
.map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?;
|
||||
Ok(Buffer::from(result))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user