@gravitykit/block-mcp 2.0.0-beta

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 (70) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +26 -0
  3. package/README.md +592 -0
  4. package/dist/index.cjs +52721 -0
  5. package/package.json +70 -0
  6. package/src/__tests__/fixtures/block-trees.ts +199 -0
  7. package/src/__tests__/fixtures/error-envelopes.ts +115 -0
  8. package/src/__tests__/fixtures/rest-responses.ts +280 -0
  9. package/src/__tests__/helpers/mock-client.ts +185 -0
  10. package/src/__tests__/helpers/request-matchers.ts +88 -0
  11. package/src/__tests__/helpers/schema-asserts.ts +132 -0
  12. package/src/__tests__/integration/concurrency.test.ts +129 -0
  13. package/src/__tests__/integration/dual-storage.test.ts +156 -0
  14. package/src/__tests__/integration/error-envelopes.test.ts +238 -0
  15. package/src/__tests__/integration/global-setup.ts +17 -0
  16. package/src/__tests__/integration/rate-limit.test.ts +88 -0
  17. package/src/__tests__/integration/read-edit-read.test.ts +141 -0
  18. package/src/__tests__/integration/ref-stability.test.ts +175 -0
  19. package/src/__tests__/integration/setup.ts +201 -0
  20. package/src/__tests__/tools/discovery/get_pattern.test.ts +58 -0
  21. package/src/__tests__/tools/discovery/get_post_info.test.ts +100 -0
  22. package/src/__tests__/tools/discovery/get_site_usage.test.ts +41 -0
  23. package/src/__tests__/tools/discovery/list_block_types.test.ts +103 -0
  24. package/src/__tests__/tools/discovery/list_patterns.test.ts +106 -0
  25. package/src/__tests__/tools/discovery/list_posts.test.ts +47 -0
  26. package/src/__tests__/tools/discovery/resolve_url.test.ts +69 -0
  27. package/src/__tests__/tools/discovery/scan_storage_modes.test.ts +34 -0
  28. package/src/__tests__/tools/media/upload_media.test.ts +123 -0
  29. package/src/__tests__/tools/mutate/edit_block_tree.test.ts +439 -0
  30. package/src/__tests__/tools/mutate/ref_routing.test.ts +105 -0
  31. package/src/__tests__/tools/patterns/insert_pattern.test.ts +117 -0
  32. package/src/__tests__/tools/posts/create_post.test.ts +84 -0
  33. package/src/__tests__/tools/posts/update_post.test.ts +93 -0
  34. package/src/__tests__/tools/read/get_block.test.ts +96 -0
  35. package/src/__tests__/tools/read/get_page_blocks.test.ts +184 -0
  36. package/src/__tests__/tools/read/persist_refs.test.ts +35 -0
  37. package/src/__tests__/tools/terms/list_terms.test.ts +91 -0
  38. package/src/__tests__/tools/write/delete_block.test.ts +91 -0
  39. package/src/__tests__/tools/write/insert_blocks.test.ts +149 -0
  40. package/src/__tests__/tools/write/ref_routing.test.ts +177 -0
  41. package/src/__tests__/tools/write/replace_block_range.test.ts +90 -0
  42. package/src/__tests__/tools/write/rewrite_post_blocks.test.ts +126 -0
  43. package/src/__tests__/tools/write/update_block.test.ts +206 -0
  44. package/src/__tests__/tools/write/update_blocks.test.ts +173 -0
  45. package/src/__tests__/tools/yoast/yoast_bulk_update_seo.test.ts +112 -0
  46. package/src/__tests__/tools/yoast/yoast_get_seo.test.ts +78 -0
  47. package/src/__tests__/tools/yoast/yoast_update_seo.test.ts +105 -0
  48. package/src/__tests__/unit/client/ref-endpoints.test.ts +232 -0
  49. package/src/__tests__/unit/enrichers/cbp-enricher.test.ts +457 -0
  50. package/src/__tests__/unit/error-translator/translate-wp-error.test.ts +318 -0
  51. package/src/__tests__/unit/instructions.test.ts +374 -0
  52. package/src/__tests__/unit/preferences/enrich-block-list.test.ts +175 -0
  53. package/src/__tests__/unit/preferences/enrich-pattern-list.test.ts +227 -0
  54. package/src/client.ts +964 -0
  55. package/src/connect.ts +877 -0
  56. package/src/enrichers.ts +348 -0
  57. package/src/error-translator.ts +156 -0
  58. package/src/index.ts +450 -0
  59. package/src/instructions.ts +270 -0
  60. package/src/preferences.ts +273 -0
  61. package/src/tools/discovery.ts +251 -0
  62. package/src/tools/media.ts +75 -0
  63. package/src/tools/mutate.ts +243 -0
  64. package/src/tools/patterns.ts +94 -0
  65. package/src/tools/posts.ts +200 -0
  66. package/src/tools/read.ts +201 -0
  67. package/src/tools/terms.ts +44 -0
  68. package/src/tools/write.ts +542 -0
  69. package/src/tools/yoast.ts +224 -0
  70. package/src/types.ts +862 -0
package/src/index.ts ADDED
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GravityKit Block MCP Server
5
+ *
6
+ * Model Context Protocol server for WordPress block-level content management.
7
+ * Gives AI agents full block-level CRUD with smart preference-aware guidance:
8
+ *
9
+ * - Discover available block types and patterns with preference scoring
10
+ * - Read page blocks as structured JSON with legacy-block annotations
11
+ * - Surgically edit single blocks without rewriting entire post_content
12
+ * - Insert patterns (synced or inline) from the site's pattern library
13
+ * - Full page rewrites with validation and revision tracking
14
+ *
15
+ * Connects to the gk-block-api WordPress plugin via REST API with
16
+ * Application Password authentication.
17
+ *
18
+ * Sub-commands:
19
+ * connect — Browser-Approve handoff: opens WP admin authorize URL in the
20
+ * browser, receives credentials via loopback callback, writes the
21
+ * chosen AI client's MCP config. Credentials are never logged.
22
+ *
23
+ * @see AGENTS.md and docs/specs/ for architecture and endpoint documentation
24
+ */
25
+
26
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
27
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
28
+ // Read the version from package.json so npm/CI bumps automatically flow
29
+ // into the MCP handshake — keeps the runtime version in lockstep with the
30
+ // published release. esbuild inlines this at bundle time.
31
+ import pkg from '../package.json' with { type: 'json' };
32
+ import {
33
+ CallToolRequestSchema,
34
+ ListToolsRequestSchema,
35
+ ListResourcesRequestSchema,
36
+ ReadResourceRequestSchema,
37
+ ListPromptsRequestSchema,
38
+ GetPromptRequestSchema,
39
+ } from '@modelcontextprotocol/sdk/types.js';
40
+ import { WordPressBlockClient } from './client.js';
41
+ import { getInstructions } from './instructions.js';
42
+ import { DISCOVERY_TOOLS, handleDiscoveryTool } from './tools/discovery.js';
43
+ import { READ_TOOLS, handleReadTool } from './tools/read.js';
44
+ import { WRITE_TOOLS, handleWriteTool } from './tools/write.js';
45
+ import { PATTERN_TOOLS, handlePatternTool } from './tools/patterns.js';
46
+ import { MUTATE_TOOLS, handleMutateTool } from './tools/mutate.js';
47
+ import { POST_TOOLS, handlePostTool } from './tools/posts.js';
48
+ import { TERM_TOOLS, handleTermTool } from './tools/terms.js';
49
+ import { MEDIA_TOOLS, handleMediaTool } from './tools/media.js';
50
+ import { YOAST_TOOLS, handleYoastTool } from './tools/yoast.js';
51
+ import { runConnect } from './connect.js';
52
+
53
+ // Environment variables are passed by the parent process (Claude Code, Hermes, etc.)
54
+ // No dotenv.config() needed — it breaks esbuild ESM bundles due to CJS dynamic require('fs').
55
+
56
+ // ============================================
57
+ // Initialize WordPress client
58
+ // ============================================
59
+
60
+ // Primary names (recommended): WORDPRESS_URL / WORDPRESS_USER / WORDPRESS_APP_PASSWORD.
61
+ // Fall back to the legacy GK_-prefixed names so existing configs keep working;
62
+ // emit a deprecation notice to stderr when they're used. The legacy names will
63
+ // be removed in a future minor release.
64
+ function readEnv(primary: string, legacy: string): string | undefined {
65
+ const fromPrimary = process.env[primary];
66
+ if (fromPrimary) return fromPrimary;
67
+ const fromLegacy = process.env[legacy];
68
+ if (fromLegacy) {
69
+ console.error(`[block-mcp] DEPRECATED: ${legacy} is deprecated; rename to ${primary} in your MCP client config.`);
70
+ return fromLegacy;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ // ============================================
76
+ // Aggregate all tool definitions
77
+ // ============================================
78
+
79
+ const ALL_TOOLS = [
80
+ ...DISCOVERY_TOOLS,
81
+ ...READ_TOOLS,
82
+ ...WRITE_TOOLS,
83
+ ...PATTERN_TOOLS,
84
+ ...MUTATE_TOOLS,
85
+ ...POST_TOOLS,
86
+ ...TERM_TOOLS,
87
+ ...MEDIA_TOOLS,
88
+ ...YOAST_TOOLS,
89
+ ];
90
+
91
+ /**
92
+ * Tool dispatch table — name → handler function.
93
+ *
94
+ * Built once at startup from each tool group's `*_TOOLS` array paired
95
+ * with its `handle*Tool` dispatcher. A new tool added to any `*_TOOLS`
96
+ * array is automatically routable; an unrouted tool name produces a
97
+ * single point of failure (the Map lookup) instead of falling silently
98
+ * through an `else if` chain to "Unknown tool".
99
+ */
100
+ type ToolHandler = (
101
+ name: string,
102
+ args: Record<string, unknown>,
103
+ client: WordPressBlockClient,
104
+ ) => Promise<unknown>;
105
+
106
+ const TOOL_GROUPS: ReadonlyArray<{
107
+ tools: ReadonlyArray<{ name: string }>;
108
+ handle: ToolHandler;
109
+ }> = [
110
+ { tools: DISCOVERY_TOOLS, handle: handleDiscoveryTool },
111
+ { tools: READ_TOOLS, handle: handleReadTool },
112
+ { tools: WRITE_TOOLS, handle: handleWriteTool },
113
+ { tools: PATTERN_TOOLS, handle: handlePatternTool },
114
+ { tools: MUTATE_TOOLS, handle: handleMutateTool },
115
+ { tools: POST_TOOLS, handle: handlePostTool },
116
+ { tools: TERM_TOOLS, handle: handleTermTool },
117
+ { tools: MEDIA_TOOLS, handle: handleMediaTool },
118
+ { tools: YOAST_TOOLS, handle: handleYoastTool },
119
+ ];
120
+
121
+ const TOOL_DISPATCH = new Map<string, ToolHandler>();
122
+ for (const { tools, handle } of TOOL_GROUPS) {
123
+ for (const tool of tools) {
124
+ TOOL_DISPATCH.set(tool.name, handle);
125
+ }
126
+ }
127
+
128
+ // ============================================
129
+ // System prompt context resource
130
+ // ============================================
131
+
132
+ // Renamed from `block-mcp://block-preferences` — the content is workflow
133
+ // guidance, not the live policy itself (which lives in wp_options and is
134
+ // surfaced via list_block_types + per-block `preference` annotations).
135
+ const AGENT_GUIDE_RESOURCE_URI = 'block-mcp://agent-guide';
136
+ // Legacy alias — kept in the resources list for one release so existing
137
+ // integrations resolving the old URI don't 404.
138
+ const LEGACY_PREFERENCES_RESOURCE_URI = 'block-mcp://block-preferences';
139
+
140
+ const AGENT_GUIDE_CONTENT = `# Block MCP — Agent Guide
141
+
142
+ ## URL → post ID resolution
143
+
144
+ NEVER run curl, wget, or any bash/shell command to hit wp-json or resolve a URL to a post ID.
145
+ The MCP does this for you:
146
+
147
+ - \`get_page_blocks\` accepts \`url\` as an alternative to \`post_id\`. Pass the full URL or path; the server resolves it via \`url_to_postid\`.
148
+ - For explicit resolution (title, post_type, edit_url before editing), call \`resolve_url\`.
149
+
150
+ If the user says "change X on https://example.com/some-page/", your first tool call should be \`get_page_blocks({ url: "...", search: "keyword" })\` or \`resolve_url({ url: "..." })\` — not a shell command.
151
+
152
+ ## Moving / reordering blocks
153
+
154
+ NEVER do a move as separate \`insert_blocks\` + \`delete_block\` calls — if the delete is skipped or fails, the page ends up with an orphaned clone of the original. The atomic primitive is the \`move\` op on \`edit_block_tree\`:
155
+
156
+ - Target the source with \`ref\` (the \`gk_ref\` from \`get_page_blocks\`) or \`path\`. Prefer \`ref\` — it survives sibling shifts; paths go stale the moment any earlier block is inserted or removed.
157
+ - Express the destination with \`destination_ref\` or \`destination\` (path). For path destinations, use **pre-move** indexing — write the path as if the source were still in place; the server adjusts indices after the removal.
158
+ - Use \`count\` to move N consecutive siblings in a single op.
159
+ - The server rejects moves into the source itself or any of its descendants.
160
+ - The whole \`edit_block_tree\` call is one revision, reversible via \`revert_to_revision\`.
161
+
162
+ If you must fall back to the flat-index tools, do \`insert_blocks\` + \`delete_block\` in the same turn and re-fetch \`get_page_blocks\` afterward to confirm exactly one copy remains.
163
+
164
+ ## Verifying writes
165
+
166
+ Every write echoes the canonical post-save snapshot. Use it. Do not fetch the public page to verify what saved.
167
+
168
+ - \`update_block\` always returns \`saved.inner_html\` + \`saved.attributes\` — the exact content that just landed in post_content. The write call IS the verification round-trip.
169
+ - \`update_blocks\` returns per-result \`saved\` only when called with \`verbose: true\` (default false to keep batch responses compact). Pass \`verbose: true\` if you need to confirm each item without a re-read.
170
+ - For after-the-fact re-reads of a single known block, use \`get_block({ post_id, ref })\` — returns the same \`saved\` shape, lighter than \`get_page_blocks\`.
171
+
172
+ For dynamic blocks (\`saved.is_dynamic: true\`, e.g. shortcodes, query loops, latest-posts), \`saved.inner_html\` is the stored template that runs at render time — not the rendered HTML the visitor sees. That's expected; the canonical state is the template.
173
+
174
+ ## Block preferences (site-defined)
175
+
176
+ Block preference policy is configured per-site in the WordPress admin (the
177
+ gk-block-api Preferences option) and exposed dynamically. There is no
178
+ client-side hardcoded list of "good" vs "bad" namespaces.
179
+
180
+ How to discover the policy at runtime:
181
+
182
+ 1. \`list_block_types\` returns blocks grouped by tier (PREFERRED / ACCEPTABLE / AVOID / LEGACY) for the current site. Use this when you need the full picture.
183
+ 2. \`get_page_blocks\` annotates non-preferred blocks inline with \`preference.tier\` and (when configured) \`preference.suggested_replacement\`. Trust those fields — they reflect the live config.
184
+ 3. \`insert_blocks\` rejects legacy-tier blocks with a \`legacy_block\` error that includes the rejected namespace, the suggested replacement, and a pointer back to this resource.
185
+
186
+ How to behave:
187
+
188
+ - Prefer the highest-tier blocks for new content. Defer to the server's classification rather than guessing from a namespace prefix.
189
+ - Reuse existing patterns before building from scratch — call \`list_patterns\` first.
190
+ - For patterns that need per-page customization, use \`synced: false\` to inline them.
191
+ - When you encounter legacy blocks on a page during a read, note them but do not replace unless asked.`;
192
+
193
+ // ============================================
194
+ // Handler registration
195
+ //
196
+ // All request handlers run on the server passed in by main(). Keeping
197
+ // this in a function instead of running at module scope means the
198
+ // server can be constructed AFTER fetching the per-site instructions
199
+ // addendum (otherwise we'd have to mutate the SDK's private
200
+ // `_instructions` field — see the construction note above).
201
+ // ============================================
202
+
203
+ function registerHandlers(server: McpServer, client: WordPressBlockClient): void {
204
+
205
+ server.server.setRequestHandler(ListToolsRequestSchema, async () => {
206
+ return { tools: ALL_TOOLS };
207
+ });
208
+
209
+ // ============================================
210
+ // Handler: Call tool
211
+ // ============================================
212
+
213
+ server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
214
+ const { name, arguments: args } = request.params;
215
+ const toolArgs = (args ?? {}) as Record<string, unknown>;
216
+
217
+ try {
218
+ const handle = TOOL_DISPATCH.get(name);
219
+ if (!handle) {
220
+ throw new Error(`Unknown tool: ${name}`);
221
+ }
222
+ const result = await handle(name, toolArgs, client);
223
+
224
+ // Emit `structuredContent` alongside the text fallback when the tool
225
+ // declared an `outputSchema`. Clients that parse structured output
226
+ // (MCP Inspector, programmatic agents) get typed data; LLM clients
227
+ // still see the same JSON-stringified text.
228
+ const toolDef = ALL_TOOLS.find((t) => t.name === name) as { outputSchema?: unknown } | undefined;
229
+ const response: {
230
+ content: Array<{ type: string; text: string }>;
231
+ structuredContent?: unknown;
232
+ } = {
233
+ content: [
234
+ { type: 'text', text: JSON.stringify(result, null, 2) },
235
+ ],
236
+ };
237
+ if (toolDef && toolDef.outputSchema !== undefined && result !== null && typeof result === 'object') {
238
+ response.structuredContent = result;
239
+ }
240
+ return response;
241
+ } catch (error) {
242
+ // Surface the full WordPress error envelope (code, data, status) when
243
+ // the client decorated it. Critical for AI agents — `legacy_block`
244
+ // carries `suggested_replacement`, `dual_storage_requires_both`
245
+ // carries the policy_resource pointer, etc. Without forwarding `data`
246
+ // the agent only sees the prose message and re-prompts blindly.
247
+ const err = error as Error & {
248
+ wpCode?: string;
249
+ wpData?: unknown;
250
+ wpStatus?: number;
251
+ response?: { status?: number; data?: unknown };
252
+ };
253
+
254
+ return {
255
+ content: [
256
+ {
257
+ type: 'text',
258
+ text: JSON.stringify(
259
+ {
260
+ error: true,
261
+ tool: name,
262
+ message: err.message || 'Unknown error occurred',
263
+ code: err.wpCode,
264
+ statusCode: err.wpStatus ?? err.response?.status,
265
+ hint: err.wpData ?? null,
266
+ },
267
+ null,
268
+ 2
269
+ ),
270
+ },
271
+ ],
272
+ isError: true,
273
+ };
274
+ }
275
+ });
276
+
277
+ // ============================================
278
+ // Handler: List resources
279
+ // ============================================
280
+
281
+ server.server.setRequestHandler(ListResourcesRequestSchema, async () => {
282
+ return {
283
+ resources: [
284
+ {
285
+ uri: AGENT_GUIDE_RESOURCE_URI,
286
+ name: 'Block MCP — Agent Guide',
287
+ description:
288
+ 'Editing workflow + how to discover the live block-preference policy on this site. Read this before editing pages.',
289
+ mimeType: 'text/plain',
290
+ },
291
+ // Legacy alias kept for one release; resolves to the same content.
292
+ {
293
+ uri: LEGACY_PREFERENCES_RESOURCE_URI,
294
+ name: 'Block MCP — Agent Guide (legacy URI)',
295
+ description: 'Renamed to block-mcp://agent-guide. Same content; kept for backwards compatibility.',
296
+ mimeType: 'text/plain',
297
+ },
298
+ ],
299
+ };
300
+ });
301
+
302
+ // ============================================
303
+ // Handler: Read resource
304
+ // ============================================
305
+
306
+ server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
307
+ const { uri } = request.params;
308
+
309
+ if (uri === AGENT_GUIDE_RESOURCE_URI || uri === LEGACY_PREFERENCES_RESOURCE_URI) {
310
+ return {
311
+ contents: [
312
+ { uri, mimeType: 'text/plain', text: AGENT_GUIDE_CONTENT },
313
+ ],
314
+ };
315
+ }
316
+
317
+ throw new Error(`Unknown resource: ${uri}`);
318
+ });
319
+
320
+ // ============================================
321
+ // Handler: Prompts
322
+ // ============================================
323
+ //
324
+ // Single canonical prompt — `edit-block-page` — that bundles the editing
325
+ // workflow + a one-shot reminder to call get_page_blocks first. Saves a
326
+ // tool call per session for clients that surface prompts in the UI.
327
+
328
+ const PROMPTS = [
329
+ {
330
+ name: 'edit-block-page',
331
+ description:
332
+ 'Bundle: workflow guidance + reminder to call get_page_blocks first. Pass `url` to seed a specific page.',
333
+ arguments: [
334
+ {
335
+ name: 'url',
336
+ description: 'Optional. Full URL or path of the page being edited.',
337
+ required: false,
338
+ },
339
+ ],
340
+ },
341
+ ];
342
+
343
+ server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
344
+ prompts: PROMPTS,
345
+ }));
346
+
347
+ server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
348
+ const { name, arguments: args } = request.params;
349
+ if (name !== 'edit-block-page') {
350
+ throw new Error(`Unknown prompt: ${name}`);
351
+ }
352
+ const url = (args?.url as string | undefined) ?? '';
353
+ const seed = url
354
+ ? `Editing target: ${url}\n\nFirst tool call: get_page_blocks({ url: ${JSON.stringify(url)}, summary_only: true }) for cheap orientation, then re-fetch with search/block_name filters as needed.\n\n`
355
+ : '';
356
+ return {
357
+ description: 'Workflow primer for editing a WordPress page via block-mcp.',
358
+ messages: [
359
+ {
360
+ role: 'user',
361
+ content: {
362
+ type: 'text',
363
+ text: `${seed}${AGENT_GUIDE_CONTENT}`,
364
+ },
365
+ },
366
+ ],
367
+ };
368
+ });
369
+
370
+ } // end registerHandlers
371
+
372
+ // ============================================
373
+ // Start the server
374
+ // ============================================
375
+
376
+ async function main(): Promise<void> {
377
+ // ── Node version preflight ──────────────────────────────────────────────
378
+ // engines.node already warns at install time, but a non-technical user who
379
+ // runs the connector anyway should get a clear, actionable message rather than
380
+ // a cryptic runtime crash on an unsupported Node.
381
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
382
+ if (Number.isFinite(nodeMajor) && nodeMajor < 20) {
383
+ console.error(
384
+ `Block MCP requires Node.js 20 or newer — you are running ${process.version}. ` +
385
+ 'Please upgrade Node.js and try again: https://nodejs.org/'
386
+ );
387
+ process.exit(1);
388
+ }
389
+
390
+ // ── connect sub-command ─────────────────────────────────────────────────
391
+ // Checked first so `npx @gravitykit/block-mcp connect` works without the
392
+ // WORDPRESS_* env vars that the MCP server requires.
393
+ if (process.argv[2] === 'connect') {
394
+ await runConnect(process.argv.slice(3));
395
+ process.exit(0);
396
+ }
397
+
398
+ // ── env-var validation ──────────────────────────────────────────────────
399
+ const WORDPRESS_URL = readEnv('WORDPRESS_URL', 'GK_SITE_URL');
400
+ const WORDPRESS_USER = readEnv('WORDPRESS_USER', 'GK_BLOCK_API_USER');
401
+ const WORDPRESS_APP_PASSWORD = readEnv('WORDPRESS_APP_PASSWORD', 'GK_BLOCK_API_APP_PASSWORD');
402
+
403
+ if (!WORDPRESS_URL || !WORDPRESS_USER || !WORDPRESS_APP_PASSWORD) {
404
+ console.error(
405
+ 'Missing required environment variables: WORDPRESS_URL, WORDPRESS_USER, WORDPRESS_APP_PASSWORD'
406
+ );
407
+ process.exit(1);
408
+ }
409
+
410
+ const client = new WordPressBlockClient({
411
+ wordpress_url: WORDPRESS_URL,
412
+ auth: {
413
+ username: WORDPRESS_USER,
414
+ application_password: WORDPRESS_APP_PASSWORD,
415
+ },
416
+ });
417
+
418
+ // Fetch the per-site instructions addendum BEFORE constructing the
419
+ // server so the initialize handshake includes the combined string
420
+ // from the start — no post-construction mutation of SDK internals.
421
+ // `getInstructions` never throws: on any failure it logs to stderr
422
+ // and returns the baseline only.
423
+ const instructions = await getInstructions(WORDPRESS_URL);
424
+
425
+ const server = new McpServer(
426
+ {
427
+ name: 'block-mcp',
428
+ version: pkg.version,
429
+ },
430
+ {
431
+ capabilities: {
432
+ tools: {},
433
+ resources: {},
434
+ prompts: {},
435
+ },
436
+ instructions,
437
+ }
438
+ );
439
+
440
+ registerHandlers(server, client);
441
+
442
+ const transport = new StdioServerTransport();
443
+ await server.connect(transport);
444
+ console.error('Block MCP Server running on stdio');
445
+ }
446
+
447
+ main().catch((error) => {
448
+ console.error('Fatal error starting Block MCP Server:', error);
449
+ process.exit(1);
450
+ });