affine-mcp-server 1.13.0 → 2.0.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Model Context Protocol (MCP) server for AFFiNE. It exposes AFFiNE workspaces and documents to AI assistants over stdio (default) or HTTP (`/mcp`) and supports both AFFiNE Cloud and self-hosted deployments.
4
4
 
5
- [![Version](https://img.shields.io/badge/version-1.13.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.0.0-blue)](https://github.com/dawncr0w/affine-mcp-server/releases)
6
6
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.17.2-green)](https://github.com/modelcontextprotocol/typescript-sdk)
7
7
  [![CI](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
8
8
  [![License](https://img.shields.io/badge/license-MIT-yellow)](LICENSE)
@@ -29,7 +29,6 @@ A Model Context Protocol (MCP) server for AFFiNE. It exposes AFFiNE workspaces a
29
29
  ## Overview
30
30
 
31
31
  AFFiNE MCP Server is designed for three common scenarios:
32
-
33
32
  - Run a local stdio MCP server for Claude Code, Codex CLI, Cursor, or Claude Desktop
34
33
  - Expose a remote HTTP MCP endpoint for hosted or browser-connected clients
35
34
  - Automate AFFiNE workspace, document, database, organization, and comment workflows through a stable MCP tool surface
@@ -39,7 +38,7 @@ Highlights:
39
38
  - Supports AFFiNE Cloud and self-hosted AFFiNE instances
40
39
  - Supports stdio and HTTP transports
41
40
  - Supports token, cookie, and email/password authentication
42
- - Exposes 87 canonical MCP tools backed by AFFiNE GraphQL and WebSocket APIs
41
+ - Exposes 84 canonical MCP tools backed by AFFiNE GraphQL and WebSocket APIs
43
42
  - Includes semantic page composition, native template instantiation, database intent composition, capability and fidelity reporting, and workspace blueprint helpers
44
43
  - Includes Docker images, health probes, and end-to-end test coverage
45
44
 
@@ -49,10 +48,9 @@ Scope boundaries:
49
48
  - Browser-local workspaces stored only in local storage are not available through AFFiNE server APIs
50
49
  - AFFiNE Cloud requires API-token-based access for MCP usage; programmatic email/password sign-in is blocked by Cloudflare
51
50
 
52
- > New in v1.13.0: Added high-level semantic page, native template, fidelity, and workspace blueprint workflows, plus structured receipts and productized setup docs.
51
+ > New in v2.0.0: Added native edgeless canvas tools and shipped a slimmer 84-tool public surface with least-privilege profiles for read-only, core, and authoring deployments.
53
52
 
54
53
  ## Choose Your Path
55
-
56
54
  | Goal | Start here |
57
55
  | --- | --- |
58
56
  | Set up a local stdio server with the least friction | [docs/getting-started.md](docs/getting-started.md) |
@@ -172,14 +170,16 @@ Domains:
172
170
 
173
171
  - Workspace: create, inspect, update, delete, and traverse workspaces
174
172
  - Organization: collections, collection-rule sync, workspace blueprints, and experimental organize or folder helpers
175
- - Documents: search, read, create, publish, move, tag, import/export, semantic composition, template inspection and native instantiation, capability and fidelity reporting, and text mutation
176
- - Databases: create columns, add rows, update cells, inspect schema, and compose database structures from intent
177
- - Comments: list, create, update, delete, resolve, and list unresolved threads
173
+ - Documents: search, read, create, publish, move, tag, import/export, semantic composition, template inspection and native instantiation, capability and fidelity reporting, and block-level mutation
174
+ - Databases: create columns, add rows, update rows, inspect schema, and compose database structures from intent
175
+ - Comments: list, create, update, delete, and resolve
178
176
  - History: version history listing
179
177
  - Users and tokens: current user, sign-in, profile/settings, personal access tokens
180
178
  - Notifications: list and mark notifications as read
181
179
  - Blob storage: upload, delete, and cleanup blobs
182
180
 
181
+ Use `AFFINE_TOOL_PROFILE=read_only`, `core`, or `authoring` when a deployment should expose a smaller surface than the complete `full` default. You can also combine profiles with `AFFINE_DISABLED_GROUPS` such as `docs.database`, `destructive`, or `admin` for finer control.
182
+
183
183
  For the grouped catalog, notes, and operational caveats, see [docs/tool-reference.md](docs/tool-reference.md).
184
184
 
185
185
  ## Documentation Map
@@ -191,6 +191,7 @@ For the grouped catalog, notes, and operational caveats, see [docs/tool-referenc
191
191
  | [docs/configuration-and-deployment.md](docs/configuration-and-deployment.md) | Environment variables, auth modes, Docker, HTTP mode, and deployment guidance |
192
192
  | [docs/workflow-recipes.md](docs/workflow-recipes.md) | End-to-end workflows and example tool sequences |
193
193
  | [docs/tool-reference.md](docs/tool-reference.md) | Tool catalog grouped by domain |
194
+ | [docs/edgeless-canvas-cookbook.md](docs/edgeless-canvas-cookbook.md) | Edgeless canvas layout helpers and surface elements, worked end-to-end |
194
195
  | [CONTRIBUTING.md](CONTRIBUTING.md) | Contributor workflow |
195
196
  | [SECURITY.md](SECURITY.md) | Security reporting |
196
197
 
@@ -0,0 +1,222 @@
1
+ /** Pure-function edgeless-canvas layout helpers. No Y.Doc, no MCP wiring. */
2
+ /** Only these four positions carry tangent vectors in BlockSuite's connector model. */
3
+ export const SIDE_TO_NORMALIZED_POSITION = {
4
+ top: [0.5, 0],
5
+ bottom: [0.5, 1],
6
+ left: [0, 0.5],
7
+ right: [1, 0.5],
8
+ };
9
+ /** Pick connector sides for a src/tgt pair. Single-axis → that axis; diagonal →
10
+ * dominant by center displacement; overlap → 4×4 midpoint minimization. Ports
11
+ * BlockSuite's `getNearestConnectableAnchor` (`connector-manager.ts:174-190`). */
12
+ export function pickConnectorSides(src, tgt) {
13
+ const srcBottom = src.y + src.h;
14
+ const srcRight = src.x + src.w;
15
+ const tgtBottom = tgt.y + tgt.h;
16
+ const tgtRight = tgt.x + tgt.w;
17
+ const above = srcBottom <= tgt.y;
18
+ const below = tgtBottom <= src.y;
19
+ const leftOf = srcRight <= tgt.x;
20
+ const rightOf = tgtRight <= src.x;
21
+ const vSeparated = above || below;
22
+ const hSeparated = leftOf || rightOf;
23
+ if (vSeparated && !hSeparated) {
24
+ return above ? { from: "bottom", to: "top" } : { from: "top", to: "bottom" };
25
+ }
26
+ if (hSeparated && !vSeparated) {
27
+ return leftOf ? { from: "right", to: "left" } : { from: "left", to: "right" };
28
+ }
29
+ if (vSeparated && hSeparated) {
30
+ const dx = (tgt.x + tgt.w / 2) - (src.x + src.w / 2);
31
+ const dy = (tgt.y + tgt.h / 2) - (src.y + src.h / 2);
32
+ if (Math.abs(dx) >= Math.abs(dy)) {
33
+ return dx >= 0 ? { from: "right", to: "left" } : { from: "left", to: "right" };
34
+ }
35
+ return dy >= 0 ? { from: "bottom", to: "top" } : { from: "top", to: "bottom" };
36
+ }
37
+ const anchors = (b) => [
38
+ { side: "top", x: b.x + b.w / 2, y: b.y },
39
+ { side: "bottom", x: b.x + b.w / 2, y: b.y + b.h },
40
+ { side: "left", x: b.x, y: b.y + b.h / 2 },
41
+ { side: "right", x: b.x + b.w, y: b.y + b.h / 2 },
42
+ ];
43
+ const srcA = anchors(src);
44
+ const tgtA = anchors(tgt);
45
+ let best = {
46
+ from: "bottom",
47
+ to: "top",
48
+ dist: Infinity,
49
+ };
50
+ for (const a of srcA) {
51
+ for (const b of tgtA) {
52
+ const dx = b.x - a.x;
53
+ const dy = b.y - a.y;
54
+ const dist = dx * dx + dy * dy;
55
+ if (dist < best.dist)
56
+ best = { from: a.side, to: b.side, dist };
57
+ }
58
+ }
59
+ return { from: best.from, to: best.to };
60
+ }
61
+ /** Enclosing bound of `children`, expanded by `padding` and `titleBand` (extra on top for a frame title). */
62
+ export function encloseBounds(children, opts = {}) {
63
+ if (children.length === 0)
64
+ return null;
65
+ const padding = opts.padding ?? 40;
66
+ const titleBand = opts.titleBand ?? 60;
67
+ let minX = Infinity;
68
+ let minY = Infinity;
69
+ let maxX = -Infinity;
70
+ let maxY = -Infinity;
71
+ for (const c of children) {
72
+ minX = Math.min(minX, c.x);
73
+ minY = Math.min(minY, c.y);
74
+ maxX = Math.max(maxX, c.x + c.w);
75
+ maxY = Math.max(maxY, c.y + c.h);
76
+ }
77
+ return {
78
+ x: Math.floor(minX - padding),
79
+ y: Math.floor(minY - padding - titleBand),
80
+ w: Math.max(1, Math.ceil(maxX - minX + padding * 2)),
81
+ h: Math.max(1, Math.ceil(maxY - minY + padding * 2 + titleBand)),
82
+ };
83
+ }
84
+ /** Pick the bound furthest along `direction` (bottommost for `"down"`, etc). */
85
+ export function pickFurthestInDirection(candidates, direction) {
86
+ if (candidates.length === 0)
87
+ return null;
88
+ let chosen = candidates[0];
89
+ for (let i = 1; i < candidates.length; i++) {
90
+ const c = candidates[i];
91
+ if (direction === "down" && c.y + c.h > chosen.y + chosen.h)
92
+ chosen = c;
93
+ else if (direction === "up" && c.y < chosen.y)
94
+ chosen = c;
95
+ else if (direction === "right" && c.x + c.w > chosen.x + chosen.w)
96
+ chosen = c;
97
+ else if (direction === "left" && c.x < chosen.x)
98
+ chosen = c;
99
+ }
100
+ return chosen;
101
+ }
102
+ /** Parse BlockSuite's `[x,y,w,h]` string. Returns null if the input isn't well-formed. */
103
+ export function parseXywhString(value) {
104
+ if (typeof value !== "string")
105
+ return null;
106
+ const m = value.match(/^\s*\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]\s*$/);
107
+ if (!m)
108
+ return null;
109
+ return { x: Number(m[1]), y: Number(m[2]), width: Number(m[3]), height: Number(m[4]) };
110
+ }
111
+ /** Inverse of `parseXywhString`. */
112
+ export function formatXywhString(x, y, width, height) {
113
+ return `[${x},${y},${width},${height}]`;
114
+ }
115
+ /** Asymmetric defaults: notes default to wide/short, so equal gaps feel tight horizontally. */
116
+ export const DEFAULT_STACK_GAP_VERTICAL = 40;
117
+ export const DEFAULT_STACK_GAP_HORIZONTAL = 80;
118
+ /** BlockSuite's createDefaultDoc constants (packages/affine/model/src/consts/note.ts). */
119
+ export const DEFAULT_PAGE_BLOCK_WIDTH = 800;
120
+ export const DEFAULT_NOTE_HEIGHT = 92;
121
+ export const DEFAULT_NOTE_XYWH = `[0,0,${DEFAULT_PAGE_BLOCK_WIDTH},${DEFAULT_NOTE_HEIGHT}]`;
122
+ /** Position a new block relative to `ref` along `direction`. The orthogonal axis
123
+ * inherits from `ref` unless `preserveX` / `preserveY` is supplied. */
124
+ export function stackRelativeTo(ref, newSize, opts = {}) {
125
+ const direction = opts.direction ?? "down";
126
+ const isHorizontal = direction === "left" || direction === "right";
127
+ const gap = opts.gap ?? (isHorizontal ? DEFAULT_STACK_GAP_HORIZONTAL : DEFAULT_STACK_GAP_VERTICAL);
128
+ if (direction === "down") {
129
+ return { x: opts.preserveX ?? ref.x, y: ref.y + ref.h + gap };
130
+ }
131
+ if (direction === "up") {
132
+ return { x: opts.preserveX ?? ref.x, y: ref.y - gap - newSize.h };
133
+ }
134
+ if (direction === "right") {
135
+ return { x: ref.x + ref.w + gap, y: opts.preserveY ?? ref.y };
136
+ }
137
+ return { x: ref.x - gap - newSize.w, y: opts.preserveY ?? ref.y };
138
+ }
139
+ /** Over-estimate note height from markdown. BlockSuite's `EdgelessNoteMask`
140
+ * `ResizeObserver` corrects `prop:xywh.h` to the DOM-measured height on first render. */
141
+ export function estimateNoteHeightForMarkdown(markdown, widthPx) {
142
+ const NOTE_V_PADDING = 64;
143
+ const BODY_LINE_H = 34;
144
+ const CHAR_WIDTH = 8;
145
+ const H_PADDING = 52;
146
+ const H1_LINE_H = 58;
147
+ const H2_LINE_H = 48;
148
+ const H3_LINE_H = 40;
149
+ const CODE_LINE_H = 32;
150
+ const CODE_FENCE_PAD = 30;
151
+ const CODE_BLOCK_EXTRA = 20;
152
+ const BLANK_LINE_H = 14;
153
+ const usableWidth = Math.max(80, widthPx - H_PADDING);
154
+ const charsPerLine = Math.max(16, Math.floor(usableWidth / CHAR_WIDTH));
155
+ let total = NOTE_V_PADDING;
156
+ let inCode = false;
157
+ const lines = markdown.split("\n");
158
+ for (const raw of lines) {
159
+ const line = raw.trim();
160
+ if (line.startsWith("```")) {
161
+ inCode = !inCode;
162
+ total += CODE_FENCE_PAD;
163
+ if (inCode)
164
+ total += CODE_BLOCK_EXTRA;
165
+ continue;
166
+ }
167
+ if (inCode) {
168
+ total += CODE_LINE_H;
169
+ continue;
170
+ }
171
+ if (line === "") {
172
+ total += BLANK_LINE_H;
173
+ continue;
174
+ }
175
+ let lineHeight = BODY_LINE_H;
176
+ let prefixChars = 0;
177
+ if (/^#\s/.test(line)) {
178
+ lineHeight = H1_LINE_H;
179
+ prefixChars = 2;
180
+ }
181
+ else if (/^##\s/.test(line)) {
182
+ lineHeight = H2_LINE_H;
183
+ prefixChars = 3;
184
+ }
185
+ else if (/^###\s/.test(line)) {
186
+ lineHeight = H3_LINE_H;
187
+ prefixChars = 4;
188
+ }
189
+ else if (/^[-*]\s/.test(line)) {
190
+ prefixChars = 2;
191
+ }
192
+ else if (/^\d+\.\s/.test(line)) {
193
+ prefixChars = 3;
194
+ }
195
+ const contentChars = Math.max(1, line.length - prefixChars);
196
+ const wraps = Math.max(1, Math.ceil(contentChars / charsPerLine));
197
+ total += lineHeight * wraps;
198
+ }
199
+ return Math.max(120, Math.ceil(total));
200
+ }
201
+ /** Initial `labelXYWH` at source→target midpoint so BlockSuite's `hasLabel()` gate passes on first render. */
202
+ export function estimateConnectorLabelXYWH(labelText, fontSize, midpoint, maxWidth) {
203
+ const charWidth = fontSize * 0.55;
204
+ const estimatedW = Math.max(16, Math.ceil(labelText.length * charWidth));
205
+ const w = Math.min(estimatedW, maxWidth);
206
+ const h = Math.ceil(fontSize + 4);
207
+ if (!midpoint)
208
+ return [0, 0, Math.max(w, 16), Math.max(h, 16)];
209
+ return [Math.round(midpoint.x - w / 2), Math.round(midpoint.y - h / 2), w, h];
210
+ }
211
+ /** Sort by fractional `index` string ascending. Y.Map iteration order is not stable across reloads. */
212
+ export function sortByFractionalIndex(entries) {
213
+ return entries.slice().sort((a, b) => {
214
+ const ai = typeof a.index === "string" ? a.index : "";
215
+ const bi = typeof b.index === "string" ? b.index : "";
216
+ if (ai < bi)
217
+ return -1;
218
+ if (ai > bi)
219
+ return 1;
220
+ return 0;
221
+ });
222
+ }
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { runCli } from "./cli.js";
18
18
  import { startHttpMcpServer } from "./sse.js";
19
19
  import { existsSync } from "fs";
20
20
  import { CONFIG_FILE } from "./config.js";
21
+ import { createToolFilter, toolFilterRequiresRegisterTool } from "./toolSurface.js";
21
22
  // CLI commands: affine-mcp login|status|logout|version
22
23
  const rawArgs = process.argv.slice(2);
23
24
  const cliArgs = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
@@ -43,21 +44,8 @@ if (subcommand) {
43
44
  const config = loadConfig();
44
45
  const transportMode = (process.env.MCP_TRANSPORT || "stdio").toLowerCase();
45
46
  const useHttpTransport = transportMode === "sse" || transportMode === "http" || transportMode === "streamable";
46
- // ---------------------------------------------------------------------------
47
- // Tool filtering — parsed once at module load (not per-session in HTTP mode)
48
- // ---------------------------------------------------------------------------
49
- const KNOWN_GROUPS = new Set([
50
- "workspaces", "docs", "comments", "history", "organize",
51
- "users", "access_tokens", "blobs", "notifications",
52
- ]);
53
- const DISABLED_GROUPS = new Set((process.env.AFFINE_DISABLED_GROUPS || "")
54
- .split(",")
55
- .map((s) => s.trim().toLowerCase())
56
- .filter(Boolean));
57
- const DISABLED_TOOLS = new Set((process.env.AFFINE_DISABLED_TOOLS || "")
58
- .split(",")
59
- .map((s) => s.trim())
60
- .filter(Boolean));
47
+ // Tool filtering is parsed once at module load (not per-session in HTTP mode).
48
+ const toolFilter = createToolFilter(process.env);
61
49
  // Startup diagnostics (visible in Claude Code MCP server logs via stderr)
62
50
  console.error(`[affine-mcp] Config: ${CONFIG_FILE} (${existsSync(CONFIG_FILE) ? 'found' : 'missing'})`);
63
51
  console.error(`[affine-mcp] Endpoint: ${config.baseUrl}${config.graphqlPath}`);
@@ -70,12 +58,8 @@ if (hasAuth && config.baseUrl.startsWith("http://")
70
58
  console.error("WARNING: Credentials configured over plain HTTP. Use HTTPS for remote servers.");
71
59
  }
72
60
  console.error(`[affine-mcp] Workspace: ${config.defaultWorkspaceId ? 'set' : '(none)'}`);
73
- // Warn about unknown group names (likely typos) before they silently do nothing
74
- for (const g of DISABLED_GROUPS) {
75
- if (!KNOWN_GROUPS.has(g)) {
76
- console.error(`[affine-mcp] WARNING: Unknown group "${g}" in AFFINE_DISABLED_GROUPS — ` +
77
- `valid groups: ${[...KNOWN_GROUPS].join(", ")}`);
78
- }
61
+ for (const warning of toolFilter.warnings) {
62
+ console.error(`[affine-mcp] WARNING: ${warning}`);
79
63
  }
80
64
  if (config.authMode === "oauth" && !useHttpTransport) {
81
65
  throw new Error("AFFINE_MCP_AUTH_MODE=oauth requires MCP_TRANSPORT=http (or streamable/sse).");
@@ -155,52 +139,40 @@ async function buildServer() {
155
139
  console.error("WARNING: No authentication configured. Some operations may fail.");
156
140
  console.error("Set AFFINE_API_TOKEN or run: affine-mcp login");
157
141
  }
158
- // ---------------------------------------------------------------------------
159
- // Per-tool blacklist: patch registerTool on this server instance so individual
160
- // tools in AFFINE_DISABLED_TOOLS are silently skipped during registration.
161
- // All tool files use server.registerTool exclusively — no need to patch server.tool.
162
- // ---------------------------------------------------------------------------
163
- if (DISABLED_TOOLS.size > 0) {
164
- const originalRegisterTool = server.registerTool?.bind(server);
165
- if (typeof originalRegisterTool !== "function") {
166
- console.error("[affine-mcp] WARNING: server.registerTool not found — " +
167
- "AFFINE_DISABLED_TOOLS will have no effect. " +
168
- "The MCP SDK API may have changed.");
169
- }
170
- else {
171
- server.registerTool = (name, options, handler) => {
172
- if (DISABLED_TOOLS.has(name))
173
- return;
174
- return originalRegisterTool(name, options, handler);
175
- };
142
+ const originalRegisterTool = server.registerTool?.bind(server);
143
+ if (typeof originalRegisterTool !== "function") {
144
+ const message = "[affine-mcp] server.registerTool not found - tool filtering cannot be enforced. " +
145
+ "The MCP SDK API may have changed.";
146
+ if (toolFilterRequiresRegisterTool(toolFilter)) {
147
+ throw new Error(`${message} Refusing to start because AFFINE_TOOL_PROFILE is not "full" ` +
148
+ "or AFFINE_DISABLED_GROUPS/AFFINE_DISABLED_TOOLS is configured.");
176
149
  }
150
+ console.error(`[affine-mcp] WARNING: ${message} Continuing with the full tool surface.`);
177
151
  }
178
- // Log filters directly from environment variables
152
+ else {
153
+ server.registerTool = (name, options, handler) => {
154
+ if (!toolFilter.isEnabled(name))
155
+ return;
156
+ return originalRegisterTool(name, options, handler);
157
+ };
158
+ }
159
+ console.error(`[affine-mcp] Tool profile: ${toolFilter.profile}`);
179
160
  console.error(`[affine-mcp] Disabled groups: ${process.env.AFFINE_DISABLED_GROUPS || "(none)"}`);
180
161
  console.error(`[affine-mcp] Disabled tools: ${process.env.AFFINE_DISABLED_TOOLS || "(none)"}`);
181
- if (!DISABLED_GROUPS.has("workspaces"))
182
- registerWorkspaceTools(server, gql);
183
- if (!DISABLED_GROUPS.has("docs"))
184
- registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
185
- if (!DISABLED_GROUPS.has("comments"))
186
- registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
187
- if (!DISABLED_GROUPS.has("history"))
188
- registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
189
- if (!DISABLED_GROUPS.has("organize"))
190
- registerOrganizeTools(server, gql, { workspaceId: config.defaultWorkspaceId });
191
- if (!DISABLED_GROUPS.has("users")) {
192
- registerUserTools(server, gql);
193
- registerUserCRUDTools(server, gql);
194
- if (config.authMode !== "oauth") {
195
- registerAuthTools(server, gql, config.baseUrl);
196
- }
162
+ console.error(`[affine-mcp] Enabled tools: ${toolFilter.enabledTools.length}/${toolFilter.totalToolCount}`);
163
+ registerWorkspaceTools(server, gql);
164
+ registerDocTools(server, gql, { workspaceId: config.defaultWorkspaceId });
165
+ registerCommentTools(server, gql, { workspaceId: config.defaultWorkspaceId });
166
+ registerHistoryTools(server, gql, { workspaceId: config.defaultWorkspaceId });
167
+ registerOrganizeTools(server, gql, { workspaceId: config.defaultWorkspaceId });
168
+ registerUserTools(server, gql);
169
+ registerUserCRUDTools(server, gql);
170
+ if (config.authMode !== "oauth") {
171
+ registerAuthTools(server, gql, config.baseUrl);
197
172
  }
198
- if (!DISABLED_GROUPS.has("access_tokens"))
199
- registerAccessTokenTools(server, gql);
200
- if (!DISABLED_GROUPS.has("blobs"))
201
- registerBlobTools(server, gql);
202
- if (!DISABLED_GROUPS.has("notifications"))
203
- registerNotificationTools(server, gql);
173
+ registerAccessTokenTools(server, gql);
174
+ registerBlobTools(server, gql);
175
+ registerNotificationTools(server, gql);
204
176
  return server;
205
177
  }
206
178
  async function start() {