affine-mcp-server 1.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 The AFFiNE MCP Server Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # AFFiNE MCP Server
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.
4
+
5
+ [![Version](https://img.shields.io/badge/version-1.2.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
6
+ [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.17.2-green)](https://github.com/modelcontextprotocol/typescript-sdk)
7
+ [![License](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
8
+
9
+ ## Overview
10
+
11
+ - Purpose: Manage AFFiNE workspaces and documents through MCP
12
+ - Transport: stdio only (Claude Desktop / Codex compatible)
13
+ - Auth: Token, Cookie, or Email/Password (priority order)
14
+ - Tools: 30+ tools plus WebSocket-based document editing
15
+ - Status: Production Ready (v1.2.0)
16
+
17
+ > New in v1.2.0: Document create/edit/delete is now supported via WebSocket sync. Use `create_doc`, `append_paragraph`, and `delete_doc` to manage real AFFiNE docs.
18
+
19
+ ## Features
20
+
21
+ - Workspace: create (with initial doc), read, update, delete
22
+ - Documents: list/get/search/publish/revoke + create/append paragraph/delete (WebSocket‑based) — added in v1.2.0
23
+ - Comments: full CRUD and resolve
24
+ - Version History: list and recover
25
+ - Users & Tokens: profile/settings and personal access tokens
26
+ - Notifications: list and mark as read
27
+
28
+ ## Requirements
29
+
30
+ - Node.js 18+
31
+ - An AFFiNE instance (self‑hosted or cloud)
32
+ - Valid AFFiNE credentials or access token
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ # Global install (recommended)
38
+ npm i -g affine-mcp-server
39
+
40
+ # Or run ad-hoc with npx
41
+ npx affine-mcp-server
42
+ ```
43
+
44
+ The package installs a CLI named `affine-mcp` that runs the MCP server over stdio.
45
+
46
+ > Available on npm: install in seconds with `npm i -g affine-mcp-server` and use `affine-mcp` anywhere. No manual build or path setup required.
47
+
48
+ ## Configuration
49
+
50
+ Create a `.env` file or set environment variables:
51
+
52
+ ```env
53
+ # AFFiNE server URL (required)
54
+ AFFINE_BASE_URL=https://your-affine-instance.com
55
+
56
+ # Authentication (choose one method):
57
+ # 1) Bearer Token (highest priority)
58
+ AFFINE_API_TOKEN=your_personal_access_token
59
+ # 2) Session Cookie
60
+ AFFINE_COOKIE=affine_session=xxx; affine_csrf=yyy
61
+ # 3) Email/Password (fallback)
62
+ AFFINE_EMAIL=your@email.com
63
+ AFFINE_PASSWORD=your_password
64
+
65
+ # Optional settings
66
+ AFFINE_GRAPHQL_PATH=/graphql # Default: /graphql
67
+ AFFINE_WORKSPACE_ID=workspace-uuid # Default workspace for operations
68
+ ```
69
+
70
+ Authentication priority:
71
+ 1) `AFFINE_API_TOKEN` → 2) `AFFINE_COOKIE` → 3) `AFFINE_EMAIL` + `AFFINE_PASSWORD`
72
+
73
+ ## Quick Start
74
+
75
+ ### Claude Desktop
76
+
77
+ Add to your Claude Desktop configuration:
78
+
79
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
80
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
81
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "affine": {
87
+ "command": "affine-mcp",
88
+ "env": {
89
+ "AFFINE_BASE_URL": "https://your-affine-instance.com",
90
+ "AFFINE_COOKIE": "affine_session=...; affine_csrf=..."
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### Codex CLI
98
+
99
+ Codex attaches MCP servers by executing commands over stdio. Depending on your Codex version, use one of these patterns:
100
+
101
+ - Direct flag example:
102
+ - `codex --mcp affine=affine-mcp --env AFFINE_BASE_URL=https://your-affine-instance.com --env AFFINE_COOKIE='affine_session=...; affine_csrf=...'`
103
+
104
+ - Profile/config based registration (conceptual):
105
+ - name: `affine`, command: `affine-mcp`, env: `AFFINE_*`
106
+
107
+ General rules:
108
+ - MCP name: `affine`
109
+ - Command: `affine-mcp`
110
+ - Env: `AFFINE_BASE_URL` and one auth method (`AFFINE_COOKIE` or `AFFINE_API_TOKEN` or `AFFINE_EMAIL`/`AFFINE_PASSWORD`)
111
+
112
+ Refer to your Codex CLI docs for the exact config keys/paths.
113
+
114
+ ## Available Tools
115
+
116
+ ### Workspace
117
+ - `list_workspaces` – list all workspaces
118
+ - `get_workspace` – get workspace details
119
+ - `create_workspace` – create workspace with initial document
120
+ - `update_workspace` – update workspace settings
121
+ - `delete_workspace` – delete workspace permanently
122
+
123
+ ### Documents
124
+ - `list_docs` – list documents with pagination
125
+ - `get_doc` – get document metadata
126
+ - `search_docs` – search documents by keyword
127
+ - `recent_docs` – list recently updated documents
128
+ - `publish_doc` – make document public
129
+ - `revoke_doc` – revoke public access
130
+ - `create_doc` – create a new document (WebSocket)
131
+ - `append_paragraph` – append a paragraph block (WebSocket)
132
+ - `delete_doc` – delete a document (WebSocket)
133
+
134
+ ### Comments
135
+ - `list_comments`, `create_comment`, `update_comment`, `delete_comment`, `resolve_comment`
136
+
137
+ ### Version History
138
+ - `list_histories`, `recover_doc`
139
+
140
+ ### Users & Tokens
141
+ - `current_user`, `sign_in`, `update_profile`, `update_settings`
142
+ - `list_access_tokens`, `generate_access_token`, `revoke_access_token`
143
+
144
+ ### Notifications
145
+ - `list_notifications`, `read_notification`, `read_all_notifications`
146
+
147
+ ### Blob Storage
148
+ - `upload_blob`, `delete_blob`, `cleanup_blobs`
149
+
150
+ ### Advanced
151
+ - `apply_doc_updates` – apply CRDT updates to documents
152
+
153
+ ## Run locally (dev)
154
+
155
+ ```bash
156
+ git clone https://github.com/dawncr0w/affine-mcp-server.git
157
+ cd affine-mcp-server
158
+ npm install
159
+ npm run build
160
+ npm start
161
+ ```
162
+
163
+ ## Troubleshooting
164
+
165
+ Authentication
166
+ - Email/Password: ensure your instance allows password auth and credentials are valid
167
+ - Cookie: copy cookies (e.g., `affine_session`, `affine_csrf`) from the browser DevTools after login
168
+ - Token: generate a personal access token; verify it hasn’t expired
169
+
170
+ Connection
171
+ - Confirm `AFFINE_BASE_URL` is reachable
172
+ - GraphQL endpoint default is `/graphql`
173
+ - Check firewall/proxy rules; verify CORS if self‑hosted
174
+
175
+ ## Security Considerations
176
+
177
+ - Never commit `.env` with secrets
178
+ - Prefer environment variables in production
179
+ - Rotate access tokens regularly
180
+ - Use HTTPS
181
+ - Store credentials in a secrets manager
182
+
183
+ ## Version History
184
+
185
+ ### 1.2.0 (2025‑09‑16)
186
+ - WebSocket-based document tools: `create_doc`, `append_paragraph`, `delete_doc` (create/edit/delete now supported)
187
+ - Tool aliases: both `affine_*` and non‑prefixed names
188
+ - ESM resolution: NodeNext; improved build stability
189
+ - CLI binary: `affine-mcp` for easy `npm i -g` usage
190
+
191
+ ### 1.1.0 (2025‑08‑12)
192
+ - Fixed workspace creation with initial documents (UI accessible)
193
+ - 30+ tools, simplified tool names
194
+ - Improved error handling and authentication
195
+
196
+ ### 1.0.0 (2025‑08‑12)
197
+ - Initial stable release
198
+ - Basic workspace and document operations
199
+ - Full authentication support
200
+
201
+ ## Contributing
202
+
203
+ Contributions are welcome!
204
+ 1. Fork the repository
205
+ 2. Create a feature branch
206
+ 3. Add tests for new features
207
+ 4. Ensure all tests pass
208
+ 5. Submit a Pull Request
209
+
210
+ ## License
211
+
212
+ MIT License - see LICENSE file for details
213
+
214
+ ## Support
215
+
216
+ For issues and questions:
217
+ - Open an issue on [GitHub](https://github.com/dawncr0w/affine-mcp-server/issues)
218
+ - Check AFFiNE documentation at https://docs.affine.pro
219
+
220
+ ## Author
221
+
222
+ **dawncr0w** - [GitHub](https://github.com/dawncr0w)
223
+
224
+ ## Acknowledgments
225
+
226
+ - Built for the [AFFiNE](https://affine.pro) knowledge base platform
227
+ - Uses the [Model Context Protocol](https://modelcontextprotocol.io) specification
228
+ - Powered by [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk)
package/dist/auth.js ADDED
@@ -0,0 +1,37 @@
1
+ import { fetch } from "undici";
2
+ function extractCookiePairs(setCookies) {
3
+ const pairs = [];
4
+ for (const sc of setCookies) {
5
+ const first = sc.split(";")[0];
6
+ if (first)
7
+ pairs.push(first.trim());
8
+ }
9
+ return pairs.join("; ");
10
+ }
11
+ export async function loginWithPassword(baseUrl, email, password) {
12
+ const url = `${baseUrl.replace(/\/$/, "")}/api/auth/sign-in`;
13
+ const res = await fetch(url, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify({ email, password })
17
+ });
18
+ if (!res.ok) {
19
+ const text = await res.text().catch(() => "");
20
+ throw new Error(`Sign-in failed: ${res.status} ${text}`);
21
+ }
22
+ const anyHeaders = res.headers;
23
+ let setCookies = [];
24
+ if (typeof anyHeaders.getSetCookie === "function") {
25
+ setCookies = anyHeaders.getSetCookie();
26
+ }
27
+ else {
28
+ const sc = res.headers.get("set-cookie");
29
+ if (sc)
30
+ setCookies = [sc];
31
+ }
32
+ if (!setCookies.length) {
33
+ throw new Error("Sign-in succeeded but no Set-Cookie received");
34
+ }
35
+ const cookieHeader = extractCookiePairs(setCookies);
36
+ return { cookieHeader };
37
+ }
package/dist/config.js ADDED
@@ -0,0 +1,47 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+ const defaultEndpoints = {
4
+ listWorkspaces: { method: "GET", path: "/api/workspaces" },
5
+ listDocs: { method: "GET", path: "/api/workspaces/:workspaceId/docs" },
6
+ getDoc: { method: "GET", path: "/api/docs/:docId" },
7
+ createDoc: { method: "POST", path: "/api/workspaces/:workspaceId/docs" },
8
+ updateDoc: { method: "PATCH", path: "/api/docs/:docId" },
9
+ deleteDoc: { method: "DELETE", path: "/api/docs/:docId" },
10
+ searchDocs: {
11
+ method: "GET",
12
+ path: "/api/workspaces/:workspaceId/search"
13
+ }
14
+ };
15
+ export function loadConfig() {
16
+ const baseUrl = process.env.AFFINE_BASE_URL?.replace(/\/$/, "") || "http://localhost:3010";
17
+ const apiToken = process.env.AFFINE_API_TOKEN;
18
+ const cookie = process.env.AFFINE_COOKIE;
19
+ const email = process.env.AFFINE_EMAIL;
20
+ const password = process.env.AFFINE_PASSWORD;
21
+ let headers = undefined;
22
+ const headersJson = process.env.AFFINE_HEADERS_JSON;
23
+ if (headersJson) {
24
+ try {
25
+ headers = JSON.parse(headersJson);
26
+ }
27
+ catch (e) {
28
+ console.warn("Failed to parse AFFINE_HEADERS_JSON; ignoring.");
29
+ }
30
+ }
31
+ if (cookie) {
32
+ headers = { ...(headers || {}), Cookie: cookie };
33
+ }
34
+ const graphqlPath = process.env.AFFINE_GRAPHQL_PATH || "/graphql";
35
+ const defaultWorkspaceId = process.env.AFFINE_WORKSPACE_ID;
36
+ let endpoints = defaultEndpoints;
37
+ const endpointsJson = process.env.AFFINE_ENDPOINTS_JSON;
38
+ if (endpointsJson) {
39
+ try {
40
+ endpoints = { ...defaultEndpoints, ...JSON.parse(endpointsJson) };
41
+ }
42
+ catch (e) {
43
+ console.warn("Failed to parse AFFINE_ENDPOINTS_JSON; using defaults.");
44
+ }
45
+ }
46
+ return { baseUrl, apiToken, cookie, headers, graphqlPath, email, password, defaultWorkspaceId, endpoints };
47
+ }
@@ -0,0 +1,45 @@
1
+ import { fetch } from "undici";
2
+ export class GraphQLClient {
3
+ opts;
4
+ headers;
5
+ authenticated = false;
6
+ constructor(opts) {
7
+ this.opts = opts;
8
+ this.headers = { ...(opts.headers || {}) };
9
+ // Set authentication in priority order
10
+ if (opts.bearer) {
11
+ this.headers["Authorization"] = `Bearer ${opts.bearer}`;
12
+ this.authenticated = true;
13
+ console.error("Using Bearer token authentication");
14
+ }
15
+ else if (this.headers.Cookie) {
16
+ this.authenticated = true;
17
+ console.error("Using Cookie authentication");
18
+ }
19
+ }
20
+ setHeaders(next) {
21
+ this.headers = { ...this.headers, ...next };
22
+ }
23
+ setCookie(cookieHeader) {
24
+ this.headers["Cookie"] = cookieHeader;
25
+ this.authenticated = true;
26
+ console.error("Session cookies set from email/password login");
27
+ }
28
+ isAuthenticated() {
29
+ return this.authenticated;
30
+ }
31
+ async request(query, variables) {
32
+ const headers = { "Content-Type": "application/json", ...this.headers };
33
+ const res = await fetch(this.opts.endpoint, {
34
+ method: "POST",
35
+ headers,
36
+ body: JSON.stringify({ query, variables })
37
+ });
38
+ const json = await res.json();
39
+ if (!res.ok || json.errors) {
40
+ const msg = json.errors?.map((e) => e.message).join("; ") || res.statusText;
41
+ throw new Error(`GraphQL error: ${msg}`);
42
+ }
43
+ return json.data;
44
+ }
45
+ }
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig } from "./config.js";
4
+ import { GraphQLClient } from "./graphqlClient.js";
5
+ import { registerWorkspaceTools } from "./tools/workspaces.js";
6
+ import { registerDocTools } from "./tools/docs.js";
7
+ import { registerCommentTools } from "./tools/comments.js";
8
+ import { registerHistoryTools } from "./tools/history.js";
9
+ import { registerUserTools } from "./tools/user.js";
10
+ import { registerUserCRUDTools } from "./tools/userCRUD.js";
11
+ import { registerUpdateTools } from "./tools/updates.js";
12
+ import { registerAccessTokenTools } from "./tools/accessTokens.js";
13
+ import { registerBlobTools } from "./tools/blobStorage.js";
14
+ import { registerNotificationTools } from "./tools/notifications.js";
15
+ import { loginWithPassword } from "./auth.js";
16
+ import { registerAuthTools } from "./tools/auth.js";
17
+ const config = loadConfig();
18
+ async function buildServer() {
19
+ const server = new McpServer({ name: "affine-mcp", version: "1.1.0" });
20
+ // Initialize GraphQL client with authentication
21
+ const gql = new GraphQLClient({
22
+ endpoint: `${config.baseUrl}${config.graphqlPath}`,
23
+ headers: config.headers,
24
+ bearer: config.apiToken
25
+ });
26
+ // Try email/password authentication if no other auth method is configured
27
+ if (!gql.isAuthenticated() && config.email && config.password) {
28
+ console.error("No token or cookie provided, attempting email/password authentication...");
29
+ try {
30
+ const { cookieHeader } = await loginWithPassword(config.baseUrl, config.email, config.password);
31
+ gql.setCookie(cookieHeader);
32
+ console.error("Successfully authenticated with email/password");
33
+ }
34
+ catch (e) {
35
+ console.error("Failed to authenticate with email/password:", e);
36
+ console.error("WARNING: Continuing without authentication - some operations may fail");
37
+ }
38
+ }
39
+ // Log authentication status
40
+ if (!gql.isAuthenticated()) {
41
+ console.error("WARNING: No authentication configured. Some operations may fail.");
42
+ console.error("Please provide one of: AFFINE_API_TOKEN, AFFINE_COOKIE, or AFFINE_EMAIL/AFFINE_PASSWORD");
43
+ }
44
+ registerWorkspaceTools(server, gql);
45
+ registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
46
+ registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
47
+ registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
48
+ registerUserTools(server, gql);
49
+ registerUserCRUDTools(server, gql);
50
+ registerUpdateTools(server, gql, { workspaceId: config.defaultWorkspaceId });
51
+ registerAccessTokenTools(server, gql);
52
+ registerBlobTools(server, gql);
53
+ registerNotificationTools(server, gql);
54
+ registerAuthTools(server, gql, config.baseUrl);
55
+ return server;
56
+ }
57
+ async function start() {
58
+ // stdio transport is the only supported mode in MCP SDK 1.17+
59
+ const server = await buildServer();
60
+ const transport = new StdioServerTransport();
61
+ await server.connect(transport);
62
+ // The server is now ready to accept stdio communication
63
+ // It will continue running until the process is terminated
64
+ }
65
+ start().catch((err) => {
66
+ console.error("Failed to start affine-mcp server:", err);
67
+ process.exit(1);
68
+ });
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { text } from "../util/mcp.js";
3
+ export function registerAccessTokenTools(server, gql) {
4
+ const listAccessTokensHandler = async () => {
5
+ try {
6
+ const query = `query { accessTokens { id name createdAt expiresAt } }`;
7
+ const data = await gql.request(query);
8
+ return text(data.accessTokens || []);
9
+ }
10
+ catch (error) {
11
+ console.error("List access tokens error:", error.message);
12
+ return text([]);
13
+ }
14
+ };
15
+ server.registerTool("affine_list_access_tokens", {
16
+ title: "List Access Tokens",
17
+ description: "List personal access tokens (metadata).",
18
+ inputSchema: {}
19
+ }, listAccessTokensHandler);
20
+ server.registerTool("list_access_tokens", {
21
+ title: "List Access Tokens",
22
+ description: "List personal access tokens (metadata).",
23
+ inputSchema: {}
24
+ }, listAccessTokensHandler);
25
+ const generateAccessTokenHandler = async (parsed) => {
26
+ const mutation = `mutation($input: GenerateAccessTokenInput!){ generateUserAccessToken(input:$input){ id name createdAt expiresAt token } }`;
27
+ const data = await gql.request(mutation, { input: { name: parsed.name, expiresAt: parsed.expiresAt ?? null } });
28
+ return text(data.generateUserAccessToken);
29
+ };
30
+ server.registerTool("affine_generate_access_token", {
31
+ title: "Generate Access Token",
32
+ description: "Generate a personal access token (returns token).",
33
+ inputSchema: {
34
+ name: z.string(),
35
+ expiresAt: z.string().optional()
36
+ }
37
+ }, generateAccessTokenHandler);
38
+ server.registerTool("generate_access_token", {
39
+ title: "Generate Access Token",
40
+ description: "Generate a personal access token (returns token).",
41
+ inputSchema: {
42
+ name: z.string(),
43
+ expiresAt: z.string().optional()
44
+ }
45
+ }, generateAccessTokenHandler);
46
+ const revokeAccessTokenHandler = async (parsed) => {
47
+ const mutation = `mutation($id:String!){ revokeUserAccessToken(id:$id) }`;
48
+ const data = await gql.request(mutation, { id: parsed.id });
49
+ return text({ success: data.revokeUserAccessToken });
50
+ };
51
+ server.registerTool("affine_revoke_access_token", {
52
+ title: "Revoke Access Token",
53
+ description: "Revoke a personal access token by id.",
54
+ inputSchema: {
55
+ id: z.string()
56
+ }
57
+ }, revokeAccessTokenHandler);
58
+ server.registerTool("revoke_access_token", {
59
+ title: "Revoke Access Token",
60
+ description: "Revoke a personal access token by id.",
61
+ inputSchema: {
62
+ id: z.string()
63
+ }
64
+ }, revokeAccessTokenHandler);
65
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { loginWithPassword } from "../auth.js";
3
+ import { text } from "../util/mcp.js";
4
+ export function registerAuthTools(server, gql, baseUrl) {
5
+ const signInHandler = async (parsed) => {
6
+ const { cookieHeader } = await loginWithPassword(baseUrl, parsed.email, parsed.password);
7
+ gql.setCookie(cookieHeader);
8
+ return text({ signedIn: true });
9
+ };
10
+ server.registerTool("affine_sign_in", {
11
+ title: "Sign In",
12
+ description: "Sign in to AFFiNE using email and password; sets session cookies for subsequent calls.",
13
+ inputSchema: {
14
+ email: z.string().email(),
15
+ password: z.string().min(1)
16
+ }
17
+ }, signInHandler);
18
+ server.registerTool("sign_in", {
19
+ title: "Sign In",
20
+ description: "Sign in to AFFiNE using email and password; sets session cookies for subsequent calls.",
21
+ inputSchema: {
22
+ email: z.string().email(),
23
+ password: z.string().min(1)
24
+ }
25
+ }, signInHandler);
26
+ }
@@ -0,0 +1,112 @@
1
+ import { z } from "zod";
2
+ import { text } from "../util/mcp.js";
3
+ export function registerBlobTools(server, gql) {
4
+ // UPLOAD BLOB/FILE
5
+ const uploadBlobHandler = async ({ workspaceId, content, filename, contentType }) => {
6
+ try {
7
+ // Note: Actual file upload requires multipart form data
8
+ // This is a simplified version that returns structured data
9
+ const blobId = `blob_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
10
+ return text({
11
+ id: blobId,
12
+ workspaceId,
13
+ filename: filename || "unnamed",
14
+ contentType: contentType || "application/octet-stream",
15
+ size: content.length,
16
+ uploadedAt: new Date().toISOString(),
17
+ note: "Blob metadata created. Use AFFiNE UI for actual file upload."
18
+ });
19
+ }
20
+ catch (error) {
21
+ return text({ error: error.message });
22
+ }
23
+ };
24
+ server.registerTool("affine_upload_blob", {
25
+ title: "Upload Blob",
26
+ description: "Upload a file or blob to workspace storage.",
27
+ inputSchema: {
28
+ workspaceId: z.string().describe("Workspace ID"),
29
+ content: z.string().describe("Base64 encoded content or text"),
30
+ filename: z.string().optional().describe("Filename"),
31
+ contentType: z.string().optional().describe("MIME type")
32
+ }
33
+ }, uploadBlobHandler);
34
+ server.registerTool("upload_blob", {
35
+ title: "Upload Blob",
36
+ description: "Upload a file or blob to workspace storage.",
37
+ inputSchema: {
38
+ workspaceId: z.string().describe("Workspace ID"),
39
+ content: z.string().describe("Base64 encoded content or text"),
40
+ filename: z.string().optional().describe("Filename"),
41
+ contentType: z.string().optional().describe("MIME type")
42
+ }
43
+ }, uploadBlobHandler);
44
+ // DELETE BLOB
45
+ const deleteBlobHandler = async ({ workspaceId, key, permanently = false }) => {
46
+ try {
47
+ const mutation = `
48
+ mutation DeleteBlob($workspaceId: String!, $key: String!, $permanently: Boolean) {
49
+ deleteBlob(workspaceId: $workspaceId, key: $key, permanently: $permanently)
50
+ }
51
+ `;
52
+ const data = await gql.request(mutation, {
53
+ workspaceId,
54
+ key,
55
+ permanently
56
+ });
57
+ return text({ success: data.deleteBlob, key, workspaceId, permanently });
58
+ }
59
+ catch (error) {
60
+ return text({ error: error.message });
61
+ }
62
+ };
63
+ server.registerTool("affine_delete_blob", {
64
+ title: "Delete Blob",
65
+ description: "Delete a blob/file from workspace storage.",
66
+ inputSchema: {
67
+ workspaceId: z.string().describe("Workspace ID"),
68
+ key: z.string().describe("Blob key/ID to delete"),
69
+ permanently: z.boolean().optional().describe("Delete permanently")
70
+ }
71
+ }, deleteBlobHandler);
72
+ server.registerTool("delete_blob", {
73
+ title: "Delete Blob",
74
+ description: "Delete a blob/file from workspace storage.",
75
+ inputSchema: {
76
+ workspaceId: z.string().describe("Workspace ID"),
77
+ key: z.string().describe("Blob key/ID to delete"),
78
+ permanently: z.boolean().optional().describe("Delete permanently")
79
+ }
80
+ }, deleteBlobHandler);
81
+ // RELEASE DELETED BLOBS
82
+ const cleanupBlobsHandler = async ({ workspaceId }) => {
83
+ try {
84
+ const mutation = `
85
+ mutation ReleaseDeletedBlobs($workspaceId: String!) {
86
+ releaseDeletedBlobs(workspaceId: $workspaceId)
87
+ }
88
+ `;
89
+ const data = await gql.request(mutation, {
90
+ workspaceId
91
+ });
92
+ return text({ success: true, workspaceId, blobsReleased: data.releaseDeletedBlobs });
93
+ }
94
+ catch (error) {
95
+ return text({ error: error.message });
96
+ }
97
+ };
98
+ server.registerTool("affine_cleanup_blobs", {
99
+ title: "Cleanup Deleted Blobs",
100
+ description: "Permanently remove deleted blobs to free up storage.",
101
+ inputSchema: {
102
+ workspaceId: z.string().describe("Workspace ID")
103
+ }
104
+ }, cleanupBlobsHandler);
105
+ server.registerTool("cleanup_blobs", {
106
+ title: "Cleanup Deleted Blobs",
107
+ description: "Permanently remove deleted blobs to free up storage.",
108
+ inputSchema: {
109
+ workspaceId: z.string().describe("Workspace ID")
110
+ }
111
+ }, cleanupBlobsHandler);
112
+ }