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 +22 -0
- package/README.md +228 -0
- package/dist/auth.js +37 -0
- package/dist/config.js +47 -0
- package/dist/graphqlClient.js +45 -0
- package/dist/index.js +68 -0
- package/dist/tools/accessTokens.js +65 -0
- package/dist/tools/auth.js +26 -0
- package/dist/tools/blobStorage.js +112 -0
- package/dist/tools/comments.js +128 -0
- package/dist/tools/docs.js +449 -0
- package/dist/tools/history.js +58 -0
- package/dist/tools/notifications.js +108 -0
- package/dist/tools/updates.js +32 -0
- package/dist/tools/user.js +18 -0
- package/dist/tools/userCRUD.js +209 -0
- package/dist/tools/workspaces.js +373 -0
- package/dist/types.js +1 -0
- package/dist/util/mcp.js +4 -0
- package/dist/ws.js +64 -0
- package/package.json +57 -0
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
|
+
[](https://github.com/dawncr0w/affine-mcp-server/releases)
|
|
6
|
+
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
7
|
+
[](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
|
+
}
|