@mseep/affine-mcp-server 2.3.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.
Files changed (43) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +270 -0
  3. package/bin/affine-mcp +5 -0
  4. package/dist/auth.js +61 -0
  5. package/dist/cli.js +726 -0
  6. package/dist/config.js +178 -0
  7. package/dist/edgeless/layout.js +222 -0
  8. package/dist/graphqlClient.js +116 -0
  9. package/dist/httpAuth.js +147 -0
  10. package/dist/httpDiagnostics.js +38 -0
  11. package/dist/index.js +209 -0
  12. package/dist/markdown/parse.js +559 -0
  13. package/dist/markdown/render.js +227 -0
  14. package/dist/markdown/types.js +1 -0
  15. package/dist/oauth.js +154 -0
  16. package/dist/sse.js +261 -0
  17. package/dist/toolSurface.js +349 -0
  18. package/dist/tools/accessTokens.js +45 -0
  19. package/dist/tools/auth.js +18 -0
  20. package/dist/tools/blobStorage.js +136 -0
  21. package/dist/tools/comments.js +104 -0
  22. package/dist/tools/docs.js +7478 -0
  23. package/dist/tools/history.js +22 -0
  24. package/dist/tools/icons.js +125 -0
  25. package/dist/tools/notifications.js +79 -0
  26. package/dist/tools/organize.js +1145 -0
  27. package/dist/tools/properties.js +426 -0
  28. package/dist/tools/user.js +13 -0
  29. package/dist/tools/userCRUD.js +77 -0
  30. package/dist/tools/workspaces.js +322 -0
  31. package/dist/util/explorerIcon.js +95 -0
  32. package/dist/util/mcp.js +28 -0
  33. package/dist/ws.js +113 -0
  34. package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
  35. package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
  36. package/docs/client-setup.md +174 -0
  37. package/docs/configuration-and-deployment.md +265 -0
  38. package/docs/edgeless-canvas-cookbook.md +226 -0
  39. package/docs/getting-started.md +229 -0
  40. package/docs/tool-reference.md +200 -0
  41. package/docs/workflow-recipes.md +147 -0
  42. package/package.json +118 -0
  43. package/tool-manifest.json +99 -0
@@ -0,0 +1,38 @@
1
+ import { getOAuthResourceUrl, probeOAuthReadiness } from "./oauth.js";
2
+ export function registerHttpDiagnosticsRoutes(app, config, authState, corsMiddleware) {
3
+ app.get("/healthz", corsMiddleware, (_req, res) => {
4
+ res.json({
5
+ status: "ok",
6
+ authMode: config.authMode,
7
+ protected: config.authMode === "oauth" || Boolean(authState.httpAuthToken),
8
+ });
9
+ });
10
+ app.get("/readyz", corsMiddleware, async (_req, res) => {
11
+ if (config.authMode === "oauth" && authState.oauthConfig) {
12
+ try {
13
+ const readiness = await probeOAuthReadiness(authState.oauthConfig);
14
+ res.json({
15
+ status: "ok",
16
+ authMode: "oauth",
17
+ issuer: readiness.issuer,
18
+ jwksUri: readiness.jwksUri,
19
+ resource: getOAuthResourceUrl(authState.oauthConfig.publicBaseUrl),
20
+ });
21
+ return;
22
+ }
23
+ catch (error) {
24
+ const message = error instanceof Error ? error.message : "OAuth readiness check failed.";
25
+ res.status(503).json({
26
+ status: "not_ready",
27
+ authMode: "oauth",
28
+ error: message,
29
+ });
30
+ return;
31
+ }
32
+ }
33
+ res.json({
34
+ status: "ok",
35
+ authMode: config.authMode,
36
+ });
37
+ });
38
+ }
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig, VERSION } 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 { registerAccessTokenTools } from "./tools/accessTokens.js";
12
+ import { registerBlobTools } from "./tools/blobStorage.js";
13
+ import { registerNotificationTools } from "./tools/notifications.js";
14
+ import { loginWithPassword } from "./auth.js";
15
+ import { registerAuthTools } from "./tools/auth.js";
16
+ import { registerOrganizeTools } from "./tools/organize.js";
17
+ import { registerPropertyTools } from "./tools/properties.js";
18
+ import { registerIconTools } from "./tools/icons.js";
19
+ import { runCli } from "./cli.js";
20
+ import { startHttpMcpServer } from "./sse.js";
21
+ import { existsSync } from "fs";
22
+ import { CONFIG_FILE } from "./config.js";
23
+ import { createToolFilter, toolFilterRequiresRegisterTool } from "./toolSurface.js";
24
+ // CLI commands: affine-mcp login|status|logout|version
25
+ const rawArgs = process.argv.slice(2);
26
+ const cliArgs = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
27
+ const subcommand = cliArgs[0];
28
+ if (subcommand === "--version" || subcommand === "-v" || subcommand === "version") {
29
+ console.log(VERSION);
30
+ process.exit(0);
31
+ }
32
+ if (subcommand === "--help" || subcommand === "-h") {
33
+ await runCli("help");
34
+ process.exit(0);
35
+ }
36
+ if (subcommand) {
37
+ const handled = await runCli(subcommand, cliArgs.slice(1));
38
+ if (!handled) {
39
+ console.error(`Unknown command: ${subcommand}`);
40
+ await runCli("help");
41
+ process.exit(1);
42
+ }
43
+ process.exit(0);
44
+ }
45
+ // MCP server mode (default)
46
+ const config = loadConfig();
47
+ const transportMode = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
48
+ const useHttpTransport = transportMode === "sse" || transportMode === "http" || transportMode === "streamable";
49
+ // Tool filtering is parsed once at module load (not per-session in HTTP mode).
50
+ const toolFilter = createToolFilter(process.env);
51
+ // Startup diagnostics (visible in Claude Code MCP server logs via stderr)
52
+ console.error(`[affine-mcp] Config: ${CONFIG_FILE} (${existsSync(CONFIG_FILE) ? 'found' : 'missing'})`);
53
+ console.error(`[affine-mcp] Endpoint: ${config.baseUrl}${config.graphqlPath}`);
54
+ const hasAuth = !!(config.apiToken || config.cookie || (config.email && config.password));
55
+ console.error(`[affine-mcp] Auth: ${hasAuth ? 'configured' : 'not configured'}`);
56
+ console.error(`[affine-mcp] HTTP auth mode: ${config.authMode}`);
57
+ if (hasAuth && config.baseUrl.startsWith("http://")
58
+ && !config.baseUrl.includes("localhost")
59
+ && !config.baseUrl.includes("127.0.0.1")) {
60
+ console.error("WARNING: Credentials configured over plain HTTP. Use HTTPS for remote servers.");
61
+ }
62
+ console.error(`[affine-mcp] Workspace: ${config.defaultWorkspaceId ? 'set' : '(none)'}`);
63
+ for (const warning of toolFilter.warnings) {
64
+ console.error(`[affine-mcp] WARNING: ${warning}`);
65
+ }
66
+ if (config.authMode === "oauth" && !useHttpTransport) {
67
+ throw new Error("AFFINE_MCP_AUTH_MODE=oauth requires MCP_TRANSPORT=http (or streamable/sse).");
68
+ }
69
+ async function buildServer() {
70
+ const server = new McpServer({ name: "affine-mcp", version: VERSION });
71
+ const gqlHeaders = { ...(config.headers || {}) };
72
+ const gqlBearer = config.apiToken;
73
+ if (config.authMode === "oauth") {
74
+ if (!gqlBearer) {
75
+ throw new Error("AFFINE_API_TOKEN is required when AFFINE_MCP_AUTH_MODE=oauth.");
76
+ }
77
+ if (config.cookie || config.email || config.password) {
78
+ console.error("[affine-mcp] OAuth mode uses the configured AFFINE_API_TOKEN service credential. " +
79
+ "Ignoring AFFINE_COOKIE / AFFINE_EMAIL / AFFINE_PASSWORD.");
80
+ }
81
+ delete gqlHeaders.Cookie;
82
+ if (process.env.AFFINE_LOGIN_AT_START) {
83
+ console.error("[affine-mcp] AFFINE_LOGIN_AT_START is ignored when AFFINE_MCP_AUTH_MODE=oauth.");
84
+ }
85
+ }
86
+ // Initialize GraphQL client with authentication
87
+ const gql = new GraphQLClient({
88
+ endpoint: `${config.baseUrl}${config.graphqlPath}`,
89
+ headers: gqlHeaders,
90
+ bearer: gqlBearer
91
+ });
92
+ // Try email/password authentication if no other auth method is configured.
93
+ // To avoid startup timeouts in MCP clients, default to async login after the stdio handshake.
94
+ if (config.authMode !== "oauth" && !gql.isAuthenticated() && config.email && config.password) {
95
+ const mode = (process.env.AFFINE_LOGIN_AT_START || "async").toLowerCase();
96
+ // In HTTP transport mode, buildServer() is called per session, so credentials
97
+ // must be retained for subsequent sessions. Only clear in stdio mode (single session).
98
+ const isHttpTransport = ["sse", "http", "streamable"].includes((process.env.MCP_TRANSPORT || "stdio").toLowerCase());
99
+ if (mode === "sync") {
100
+ console.error("No token/cookie; performing synchronous email/password authentication at startup...");
101
+ try {
102
+ const { cookieHeader } = await loginWithPassword(config.baseUrl, config.email, config.password);
103
+ gql.setCookie(cookieHeader);
104
+ console.error("Successfully authenticated with email/password");
105
+ }
106
+ catch (e) {
107
+ console.error("Failed to authenticate with email/password:", e);
108
+ console.error("WARNING: Continuing without authentication - some operations may fail");
109
+ }
110
+ finally {
111
+ if (!isHttpTransport) {
112
+ config.password = undefined;
113
+ config.email = undefined;
114
+ }
115
+ }
116
+ }
117
+ else {
118
+ console.error("No token/cookie; deferring email/password authentication (async after connect)...");
119
+ // Capture credentials before clearing — async login needs them.
120
+ const loginEmail = config.email;
121
+ const loginPassword = config.password;
122
+ if (!isHttpTransport) {
123
+ config.password = undefined;
124
+ config.email = undefined;
125
+ }
126
+ // Fire-and-forget async login so stdio handshake is not delayed.
127
+ (async () => {
128
+ try {
129
+ const { cookieHeader } = await loginWithPassword(config.baseUrl, loginEmail, loginPassword);
130
+ gql.setCookie(cookieHeader);
131
+ console.error("Successfully authenticated with email/password (async)");
132
+ }
133
+ catch (e) {
134
+ console.error("Failed to authenticate with email/password (async):", e);
135
+ }
136
+ })();
137
+ }
138
+ }
139
+ // Log authentication status
140
+ if (!gql.isAuthenticated()) {
141
+ console.error("WARNING: No authentication configured. Some operations may fail.");
142
+ console.error("Set AFFINE_API_TOKEN or run: affine-mcp login");
143
+ }
144
+ const originalRegisterTool = server.registerTool?.bind(server);
145
+ if (typeof originalRegisterTool !== "function") {
146
+ const message = "[affine-mcp] server.registerTool not found - tool filtering cannot be enforced. " +
147
+ "The MCP SDK API may have changed.";
148
+ if (toolFilterRequiresRegisterTool(toolFilter)) {
149
+ throw new Error(`${message} Refusing to start because AFFINE_TOOL_PROFILE is not "full" ` +
150
+ "or AFFINE_DISABLED_GROUPS/AFFINE_DISABLED_TOOLS is configured.");
151
+ }
152
+ console.error(`[affine-mcp] WARNING: ${message} Continuing with the full tool surface.`);
153
+ }
154
+ else {
155
+ server.registerTool = (name, options, handler) => {
156
+ if (!toolFilter.isEnabled(name))
157
+ return;
158
+ return originalRegisterTool(name, options, handler);
159
+ };
160
+ }
161
+ console.error(`[affine-mcp] Tool profile: ${toolFilter.profile}`);
162
+ console.error(`[affine-mcp] Disabled groups: ${process.env.AFFINE_DISABLED_GROUPS || "(none)"}`);
163
+ console.error(`[affine-mcp] Disabled tools: ${process.env.AFFINE_DISABLED_TOOLS || "(none)"}`);
164
+ console.error(`[affine-mcp] Enabled tools: ${toolFilter.enabledTools.length}/${toolFilter.totalToolCount}`);
165
+ registerWorkspaceTools(server, gql);
166
+ registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
167
+ registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
168
+ registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
169
+ registerOrganizeTools(server, gql, { workspaceId: config.defaultWorkspaceId });
170
+ registerPropertyTools(server, gql, { workspaceId: config.defaultWorkspaceId });
171
+ registerIconTools(server, gql, { workspaceId: config.defaultWorkspaceId });
172
+ registerUserTools(server, gql);
173
+ registerUserCRUDTools(server, gql);
174
+ if (config.authMode !== "oauth") {
175
+ registerAuthTools(server, gql, config.baseUrl);
176
+ }
177
+ registerAccessTokenTools(server, gql);
178
+ registerBlobTools(server, gql);
179
+ registerNotificationTools(server, gql);
180
+ return server;
181
+ }
182
+ async function start() {
183
+ if (useHttpTransport) {
184
+ const DEFAULT_PORT = 3000;
185
+ const portEnvValue = process.env.PORT;
186
+ let port = DEFAULT_PORT;
187
+ // Validate the HTTP server port if provided.
188
+ if (portEnvValue != null && portEnvValue.trim() !== "") {
189
+ const parsedPort = Number(portEnvValue);
190
+ if (Number.isInteger(parsedPort) && parsedPort >= 0 && parsedPort <= 65535) {
191
+ port = parsedPort;
192
+ }
193
+ else {
194
+ console.warn(`[affine-mcp] Invalid PORT "${portEnvValue}" (expected 0..65535 integer). Falling back to ${DEFAULT_PORT}.`);
195
+ }
196
+ }
197
+ await startHttpMcpServer(buildServer, port, config);
198
+ }
199
+ else {
200
+ // stdio transport is the default for typical desktop MCP clients
201
+ const server = await buildServer();
202
+ const transport = new StdioServerTransport();
203
+ await server.connect(transport);
204
+ }
205
+ }
206
+ start().catch((err) => {
207
+ console.error("Failed to start affine-mcp server:", err);
208
+ process.exit(1);
209
+ });