@gravitykit/block-mcp 2.0.1 → 2.1.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 (42) hide show
  1. package/dist/index.cjs +311 -195
  2. package/package.json +8 -4
  3. package/src/__tests__/coerce.test.ts +57 -0
  4. package/src/__tests__/fixtures/error-envelopes.ts +19 -7
  5. package/src/__tests__/integration/error-envelopes.test.ts +2 -2
  6. package/src/__tests__/tools/discovery/list_patterns.test.ts +25 -6
  7. package/src/__tests__/tools/mutate/edit_block_tree.test.ts +53 -0
  8. package/src/__tests__/tools/patterns/insert_pattern.test.ts +16 -0
  9. package/src/__tests__/tools/posts/update_post.test.ts +14 -0
  10. package/src/__tests__/tools/read/get_block.test.ts +35 -0
  11. package/src/__tests__/tools/read/get_page_blocks.test.ts +18 -0
  12. package/src/__tests__/tools/write/delete_block.test.ts +18 -0
  13. package/src/__tests__/tools/write/insert_blocks.test.ts +21 -0
  14. package/src/__tests__/tools/write/replace_block_range.test.ts +71 -0
  15. package/src/__tests__/tools/write/rewrite_post_blocks.test.ts +39 -0
  16. package/src/__tests__/tools/write/update_block.test.ts +16 -0
  17. package/src/__tests__/tools/write/update_blocks.test.ts +21 -0
  18. package/src/__tests__/tools/yoast/yoast_get_seo.test.ts +11 -3
  19. package/src/__tests__/tools/yoast/yoast_update_seo.test.ts +16 -0
  20. package/src/__tests__/unit/enrichers/cbp-enricher.test.ts +21 -0
  21. package/src/__tests__/unit/error-translator/translate-wp-error.test.ts +100 -29
  22. package/src/__tests__/unit/instructions.test.ts +3 -3
  23. package/src/__tests__/unit/preferences/enrich-pattern-list.test.ts +26 -10
  24. package/src/__tests__/unit/rest-url.test.ts +23 -0
  25. package/src/client.ts +53 -18
  26. package/src/coerce.ts +41 -0
  27. package/src/config.ts +96 -0
  28. package/src/connect.ts +17 -3
  29. package/src/enrichers.ts +6 -2
  30. package/src/error-translator.ts +62 -10
  31. package/src/index.ts +35 -27
  32. package/src/instructions.ts +3 -3
  33. package/src/preferences.ts +56 -43
  34. package/src/rest-url.ts +18 -0
  35. package/src/tools/discovery.ts +10 -14
  36. package/src/tools/mutate.ts +18 -12
  37. package/src/tools/patterns.ts +3 -2
  38. package/src/tools/posts.ts +26 -26
  39. package/src/tools/read.ts +7 -6
  40. package/src/tools/write.ts +26 -31
  41. package/src/tools/yoast.ts +30 -31
  42. package/src/types.ts +23 -13
package/dist/index.cjs CHANGED
@@ -7,7 +7,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __commonJS = (cb, mod) => function __require() {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ try {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ } catch (e) {
13
+ throw mod = 0, e;
14
+ }
11
15
  };
12
16
  var __export = (target, all4) => {
13
17
  for (var name in all4)
@@ -16854,6 +16858,9 @@ var require_form_data = __commonJS({
16854
16858
  var setToStringTag = require_es_set_tostringtag();
16855
16859
  var hasOwn = require_hasown();
16856
16860
  var populate = require_populate();
16861
+ function escapeHeaderParam(str) {
16862
+ return String(str).replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/"/g, "%22");
16863
+ }
16857
16864
  function FormData3(options) {
16858
16865
  if (!(this instanceof FormData3)) {
16859
16866
  return new FormData3(options);
@@ -16943,7 +16950,7 @@ var require_form_data = __commonJS({
16943
16950
  var contents = "";
16944
16951
  var headers = {
16945
16952
  // add custom disposition as third element or keep it two elements if not
16946
- "Content-Disposition": ["form-data", 'name="' + field + '"'].concat(contentDisposition || []),
16953
+ "Content-Disposition": ["form-data", 'name="' + escapeHeaderParam(field) + '"'].concat(contentDisposition || []),
16947
16954
  // if no content type. allow it to be empty array
16948
16955
  "Content-Type": [].concat(contentType || [])
16949
16956
  };
@@ -16977,7 +16984,7 @@ var require_form_data = __commonJS({
16977
16984
  filename = path2.basename(value.client._httpMessage.path || "");
16978
16985
  }
16979
16986
  if (filename) {
16980
- return 'filename="' + filename + '"';
16987
+ return 'filename="' + escapeHeaderParam(filename) + '"';
16981
16988
  }
16982
16989
  };
16983
16990
  FormData3.prototype._getContentType = function(value, options) {
@@ -35208,7 +35215,7 @@ var StdioServerTransport = class {
35208
35215
  // package.json
35209
35216
  var package_default = {
35210
35217
  name: "@gravitykit/block-mcp",
35211
- version: "2.0.1",
35218
+ version: "2.1.0",
35212
35219
  description: "MCP server for WordPress block-level content management with preference-aware editing",
35213
35220
  main: "dist/index.cjs",
35214
35221
  bin: {
@@ -35225,6 +35232,7 @@ var package_default = {
35225
35232
  "test:watch": "vitest",
35226
35233
  "test:integration": "vitest run --config vitest.integration.config.ts",
35227
35234
  "test:docs": "playwright test --config tests/docs/playwright.config.ts",
35235
+ "test:e2e": "playwright test --config tests/e2e/playwright.config.ts",
35228
35236
  eval: "tsx tests/evals/lib/runner.ts",
35229
35237
  "eval:fixture-refresh": "tsx tests/evals/scripts/fetch-fixture.ts",
35230
35238
  prepare: "npm run build",
@@ -35250,7 +35258,7 @@ var package_default = {
35250
35258
  },
35251
35259
  dependencies: {
35252
35260
  "@modelcontextprotocol/sdk": "^1.0.0",
35253
- axios: "^1.7.9",
35261
+ axios: "^1.16.0",
35254
35262
  "form-data": "^4.0.1"
35255
35263
  },
35256
35264
  devDependencies: {
@@ -35259,12 +35267,12 @@ var package_default = {
35259
35267
  "@shikijs/engine-javascript": "^4.0.2",
35260
35268
  "@shikijs/langs": "^4.0.2",
35261
35269
  "@types/node": "^22.0.0",
35262
- esbuild: "^0.27.3",
35270
+ esbuild: "^0.28.1",
35263
35271
  sharp: "^0.33.5",
35264
35272
  shiki: "^4.0.2",
35265
35273
  tsx: "^4.21.0",
35266
35274
  typescript: "^5.7.0",
35267
- vitest: "^3.2.4"
35275
+ vitest: "^3.2.6"
35268
35276
  },
35269
35277
  engines: {
35270
35278
  node: ">=20.0.0"
@@ -35278,6 +35286,9 @@ var package_default = {
35278
35286
  ],
35279
35287
  publishConfig: {
35280
35288
  access: "public"
35289
+ },
35290
+ overrides: {
35291
+ esbuild: "$esbuild"
35281
35292
  }
35282
35293
  };
35283
35294
 
@@ -39920,6 +39931,11 @@ function extractHints(data) {
39920
39931
  if (typeof d2.block_name === "string") hints.block_name = d2.block_name;
39921
39932
  if (typeof d2.suggested_replacement === "string") hints.suggested_replacement = d2.suggested_replacement;
39922
39933
  if (typeof d2.status === "number") hints.status = d2.status;
39934
+ if (typeof d2.flat_index === "number") hints.flat_index = d2.flat_index;
39935
+ if (typeof d2.expected_revision === "number") hints.expected_revision = d2.expected_revision;
39936
+ if (typeof d2.current_revision === "number") hints.current_revision = d2.current_revision;
39937
+ if (typeof d2.max_depth === "number") hints.max_depth = d2.max_depth;
39938
+ if (typeof d2.actual_depth === "number") hints.actual_depth = d2.actual_depth;
39923
39939
  return hints;
39924
39940
  }
39925
39941
  function translateWpError(code, data) {
@@ -39938,23 +39954,30 @@ function translateWpError(code, data) {
39938
39954
  case "rest_authentication_required":
39939
39955
  return "Authentication failed. Confirm WORDPRESS_USER and WORDPRESS_APP_PASSWORD are set to a valid Application Password (not a regular login password).";
39940
39956
  // ── Post lookup ────────────────────────────────────────────────
39957
+ // `post_not_found` is the code the plugin actually emits;
39958
+ // `rest_post_invalid_id` is WordPress core's equivalent, kept as an
39959
+ // alias in case a core route ever surfaces it.
39941
39960
  case "rest_post_invalid_id":
39942
- case "invalid_post_id": {
39961
+ case "post_not_found": {
39943
39962
  const target = hints.post_id !== void 0 ? `Post ${hints.post_id}` : "Post";
39944
39963
  return `${target} not found. List pages with \`list_posts\` to find the right ID.`;
39945
39964
  }
39946
39965
  case "not_found":
39947
39966
  return hints.post_id ? `Post ${hints.post_id} not found. It may have been deleted, or the ID is wrong.` : "Resource not found. It may have been deleted, or the ID is wrong.";
39948
39967
  // ── Block ref / path resolution ────────────────────────────────
39949
- case "gk_block_api_invalid_ref":
39950
39968
  case "invalid_ref":
39951
- return `Block ref \`${hints.ref ?? "?"}\` not found in post ${hints.post_id ?? "?"}. The post may have been edited since you last fetched it \u2014 call \`get_page_blocks\` again to get the current refs.`;
39952
- case "path_not_found":
39969
+ return "Ref must be a non-empty string. Use the `ref` value returned by `get_page_blocks` \u2014 not a made-up ID.";
39970
+ case "ref_stale": {
39971
+ const where = hints.post_id !== void 0 ? ` in post ${hints.post_id}` : "";
39972
+ return `Block ref \`${hints.ref ?? "?"}\`${where} no longer resolves to a block. It may have been deleted, or the ref is from an older snapshot \u2014 call \`get_page_blocks\` again to get current refs.`;
39973
+ }
39953
39974
  case "invalid_path":
39954
- return `Block path ${formatPath(hints.path)} doesn't address an existing block. Re-fetch the post with \`get_page_blocks\` to get current paths \u2014 paths shift when blocks are added or removed.`;
39955
- case "path_out_of_bounds":
39956
- return `Block path ${formatPath(hints.path)} is out of bounds. The post has fewer blocks than expected \u2014 re-fetch with \`get_page_blocks\` for current state.`;
39957
- // ── Block tier / preference enforcement ────────────────────────
39975
+ return `Block path ${formatPath(hints.path)} doesn't address an existing block (or isn't a valid array of non-negative integers). Re-fetch the post with \`get_page_blocks\` to get current paths \u2014 paths shift when blocks are added or removed.`;
39976
+ case "invalid_index": {
39977
+ const idx = typeof hints.flat_index === "number" ? ` ${hints.flat_index}` : "";
39978
+ return `Block index${idx} out of range. Re-fetch the post with \`get_page_blocks\` to get current indices \u2014 they shift when blocks are added or removed.`;
39979
+ }
39980
+ // ── Block tier / preference / storage enforcement ───────────────
39958
39981
  case "legacy_block":
39959
39982
  return blockName ? `${blockName} is in a namespace this site has configured as legacy. Use ${hints.suggested_replacement ?? "a core block instead"}.` : "Legacy block rejected. Use a core block (or a higher-tier alternative) instead.";
39960
39983
  case "inner_html_required": {
@@ -39963,11 +39986,28 @@ function translateWpError(code, data) {
39963
39986
  }
39964
39987
  case "static_markup_stale_risk":
39965
39988
  return "Updating attributes on a static block without new innerHTML may leave its rendered markup stale. Pass `innerHTML` alongside `attributes`, or use a dynamic block.";
39989
+ case "dual_storage_requires_both":
39990
+ return blockName ? `${blockName} is dual-storage: \`attributes\` and \`innerHTML\` carry the same data and must be sent together \u2014 sending only one silently desyncs the other. Pass both fields in the same call.` : "This block is dual-storage: `attributes` and `innerHTML` carry the same data and must be sent together \u2014 sending only one silently desyncs the other. Pass both fields in the same call.";
39991
+ case "block_depth_exceeded": {
39992
+ const depth = typeof hints.max_depth === "number" && typeof hints.actual_depth === "number" ? ` (max ${hints.max_depth}, got ${hints.actual_depth})` : "";
39993
+ return `Block tree exceeds the maximum nesting depth${depth}. Flatten the structure \u2014 split deeply nested groups into separate top-level blocks.`;
39994
+ }
39995
+ // ── Concurrency / staleness ──────────────────────────────────────
39996
+ case "edit_conflict":
39997
+ return "The post content changed since it was read (a concurrent write raced this one). Re-fetch the page with `get_page_blocks` and retry your edit against the current content.";
39998
+ case "stale_revision": {
39999
+ const rev = typeof hints.current_revision === "number" ? ` (current revision: ${hints.current_revision})` : "";
40000
+ return `The post has changed since you fetched it${rev}. Re-fetch with \`get_page_blocks\` and retry.`;
40001
+ }
39966
40002
  // ── Rate limiting ──────────────────────────────────────────────
39967
40003
  case "rate_limit_exceeded": {
39968
40004
  const where = hints.post_id !== void 0 ? `on post ${hints.post_id} ` : "";
39969
40005
  return `Too many writes ${where}in the last minute. Wait ~60s before retrying, or batch your edits into a single \`edit_block_tree\` call.`;
39970
40006
  }
40007
+ case "rate_limit_locked": {
40008
+ const where = hints.post_id !== void 0 ? ` on post ${hints.post_id}` : "";
40009
+ return `Another write${where} is in progress. Retry in a moment; the lock clears within a second. To avoid contention, batch edits into a single \`edit_block_tree\` call.`;
40010
+ }
39971
40011
  // ── v1.2 post lifecycle ────────────────────────────────────────
39972
40012
  case "mixed_trash_payload":
39973
40013
  return '`status: "trash"` cannot be combined with other fields. Trash the post in one call, then update other fields after.';
@@ -39975,6 +40015,8 @@ function translateWpError(code, data) {
39975
40015
  return "Post type not allowed by this site's gk_block_api_post_types_allowlist option. Ask the site admin to add it, or pick a supported type.";
39976
40016
  case "invalid_status":
39977
40017
  return 'Post status not allowed. Valid values: draft, pending, publish, future, private. To trash, call update_post with status:"trash" (on its own, not combined with other fields).';
40018
+ case "trash_disabled":
40019
+ return "Moving posts to trash is turned off for this site. A site administrator can enable it under Block MCP \u2192 Settings, or use update_post with a different status.";
39978
40020
  // ── Media uploads ──────────────────────────────────────────────
39979
40021
  case "invalid_url":
39980
40022
  return "URL rejected by SSRF guard. Hostnames pointing at private/loopback/cloud-metadata IPs are blocked. Use a publicly reachable URL.";
@@ -39989,6 +40031,29 @@ function formatPath(path2) {
39989
40031
  return `[${path2.join(", ")}]`;
39990
40032
  }
39991
40033
 
40034
+ // src/rest-url.ts
40035
+ function restRouteUrl(siteUrl, routeSuffix = "") {
40036
+ const trimmed = siteUrl.replace(/\/+$/, "");
40037
+ return `${trimmed}/?rest_route=/gk-block-api/v1${routeSuffix}`;
40038
+ }
40039
+
40040
+ // src/coerce.ts
40041
+ function coercePostId(value, label) {
40042
+ if (value === void 0 || value === null) {
40043
+ return void 0;
40044
+ }
40045
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
40046
+ return value;
40047
+ }
40048
+ if (typeof value === "string" && /^[0-9]+$/.test(value)) {
40049
+ const parsed = parseInt(value, 10);
40050
+ if (parsed > 0 && Number.isSafeInteger(parsed)) {
40051
+ return parsed;
40052
+ }
40053
+ }
40054
+ throw new Error(`${label}: post_id must be a positive integer`);
40055
+ }
40056
+
39992
40057
  // src/client.ts
39993
40058
  function mimeForFilename(filename) {
39994
40059
  const ext = filename.toLowerCase().split(".").pop() ?? "";
@@ -40053,8 +40118,7 @@ var WordPressBlockClient = class {
40053
40118
  const credentials = Buffer.from(
40054
40119
  `${auth.username}:${auth.application_password}`
40055
40120
  ).toString("base64");
40056
- const trimmed = wordpress_url.replace(/\/+$/, "");
40057
- const baseURL = `${trimmed}/wp-json/gk-block-api/v1`;
40121
+ const baseURL = restRouteUrl(wordpress_url);
40058
40122
  this.client = axios_default.create({
40059
40123
  baseURL,
40060
40124
  headers: {
@@ -40252,9 +40316,11 @@ var WordPressBlockClient = class {
40252
40316
  * @returns Array of parsed blocks
40253
40317
  */
40254
40318
  async getPageBlocks(postId, params) {
40255
- if (postId === void 0 || postId === null) {
40319
+ const coercedPostId = coercePostId(postId, "get_page_blocks");
40320
+ if (coercedPostId === void 0) {
40256
40321
  throw new Error("Post ID is required");
40257
40322
  }
40323
+ postId = coercedPostId;
40258
40324
  const queryParams = {};
40259
40325
  if (params?.fields) queryParams.fields = params.fields;
40260
40326
  if (params?.render) queryParams.render = "true";
@@ -40321,7 +40387,9 @@ var WordPressBlockClient = class {
40321
40387
  * @returns Updated block details with revision ID
40322
40388
  */
40323
40389
  async updateBlock(postId, index, data) {
40324
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40390
+ const coercedPostId = coercePostId(postId, "update_block");
40391
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40392
+ postId = coercedPostId;
40325
40393
  if (index < 0) throw new Error("Block index must be non-negative");
40326
40394
  if (!data.attributes && !data.innerHTML) {
40327
40395
  throw new Error("At least one of attributes or innerHTML must be provided");
@@ -40342,7 +40410,9 @@ var WordPressBlockClient = class {
40342
40410
  * @returns 404 ref_stale if the ref no longer matches any block.
40343
40411
  */
40344
40412
  async updateBlockByRef(postId, ref, data) {
40345
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40413
+ const coercedPostId = coercePostId(postId, "update_block");
40414
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40415
+ postId = coercedPostId;
40346
40416
  if (!ref || typeof ref !== "string") throw new Error("Ref is required");
40347
40417
  if (!data.attributes && !data.innerHTML) {
40348
40418
  throw new Error("At least one of attributes or innerHTML must be provided");
@@ -40370,7 +40440,9 @@ var WordPressBlockClient = class {
40370
40440
  * @returns Per-item results plus the single revision ID.
40371
40441
  */
40372
40442
  async updateBlocksBatch(postId, updates, options = {}) {
40373
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40443
+ const coercedPostId = coercePostId(postId, "update_blocks");
40444
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40445
+ postId = coercedPostId;
40374
40446
  if (!Array.isArray(updates) || updates.length === 0) {
40375
40447
  throw new Error("updates must be a non-empty array");
40376
40448
  }
@@ -40396,7 +40468,9 @@ var WordPressBlockClient = class {
40396
40468
  * @returns { success, saved } where `saved` mirrors update_block's saved.
40397
40469
  */
40398
40470
  async getBlock(postId, target) {
40399
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40471
+ const coercedPostId = coercePostId(postId, "get_block");
40472
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40473
+ postId = coercedPostId;
40400
40474
  const hasRef = typeof target.ref === "string" && target.ref !== "";
40401
40475
  const hasIdx = typeof target.flatIndex === "number";
40402
40476
  if (hasRef === hasIdx) {
@@ -40416,7 +40490,9 @@ var WordPressBlockClient = class {
40416
40490
  * @returns Inserted blocks with new indices, warnings, and revision ID
40417
40491
  */
40418
40492
  async insertBlocks(postId, data) {
40419
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40493
+ const coercedPostId = coercePostId(postId, "insert_blocks");
40494
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40495
+ postId = coercedPostId;
40420
40496
  if (!data.blocks || data.blocks.length === 0) {
40421
40497
  throw new Error("At least one block is required");
40422
40498
  }
@@ -40435,7 +40511,9 @@ var WordPressBlockClient = class {
40435
40511
  * @returns Deletion confirmation with revision ID
40436
40512
  */
40437
40513
  async deleteBlock(postId, index, count) {
40438
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40514
+ const coercedPostId = coercePostId(postId, "delete_block");
40515
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40516
+ postId = coercedPostId;
40439
40517
  if (index < 0) throw new Error("Block index must be non-negative");
40440
40518
  const params = {};
40441
40519
  if (count && count > 1) params.count = String(count);
@@ -40453,7 +40531,9 @@ var WordPressBlockClient = class {
40453
40531
  * @param count - Consecutive blocks to remove (default 1)
40454
40532
  */
40455
40533
  async deleteBlockByRef(postId, ref, count) {
40456
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40534
+ const coercedPostId = coercePostId(postId, "delete_block");
40535
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40536
+ postId = coercedPostId;
40457
40537
  if (!ref || typeof ref !== "string") throw new Error("Ref is required");
40458
40538
  const params = {};
40459
40539
  if (count && count > 1) params.count = String(count);
@@ -40473,7 +40553,9 @@ var WordPressBlockClient = class {
40473
40553
  * @returns Result with `removed`, `inserted[]`, warnings, revision IDs
40474
40554
  */
40475
40555
  async replaceBlocksRange(postId, data) {
40476
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40556
+ const coercedPostId = coercePostId(postId, "replace_block_range");
40557
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40558
+ postId = coercedPostId;
40477
40559
  if (typeof data.start !== "number" || data.start < 0) {
40478
40560
  throw new Error("start must be a non-negative integer");
40479
40561
  }
@@ -40500,7 +40582,9 @@ var WordPressBlockClient = class {
40500
40582
  * @returns Written blocks with revision ID
40501
40583
  */
40502
40584
  async replaceAllBlocks(postId, blocks) {
40503
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40585
+ const coercedPostId = coercePostId(postId, "rewrite_post_blocks");
40586
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40587
+ postId = coercedPostId;
40504
40588
  if (!blocks || blocks.length === 0) {
40505
40589
  throw new Error("At least one block is required for a full rewrite");
40506
40590
  }
@@ -40521,9 +40605,11 @@ var WordPressBlockClient = class {
40521
40605
  * @returns Mutation result with revision IDs and optional warnings
40522
40606
  */
40523
40607
  async mutateBlockTree(postId, data) {
40524
- if (postId === void 0 || postId === null) {
40608
+ const coercedPostId = coercePostId(postId, "edit_block_tree");
40609
+ if (coercedPostId === void 0) {
40525
40610
  throw new Error("Post ID is required");
40526
40611
  }
40612
+ postId = coercedPostId;
40527
40613
  const response = await this.client.post(
40528
40614
  `/posts/${postId}/mutate`,
40529
40615
  data
@@ -40541,9 +40627,11 @@ var WordPressBlockClient = class {
40541
40627
  * @returns Revert result with revision IDs
40542
40628
  */
40543
40629
  async revertToRevision(postId, revisionId) {
40544
- if (postId === void 0 || postId === null) {
40630
+ const coercedPostId = coercePostId(postId, "revert_to_revision");
40631
+ if (coercedPostId === void 0) {
40545
40632
  throw new Error("Post ID is required");
40546
40633
  }
40634
+ postId = coercedPostId;
40547
40635
  const response = await this.client.post(`/posts/${postId}/revert`, { revision_id: revisionId });
40548
40636
  return response.data;
40549
40637
  }
@@ -40558,7 +40646,9 @@ var WordPressBlockClient = class {
40558
40646
  * @returns Inserted pattern details with revision ID
40559
40647
  */
40560
40648
  async insertPattern(postId, data) {
40561
- if (postId === void 0 || postId === null) throw new Error("Post ID is required");
40649
+ const coercedPostId = coercePostId(postId, "insert_pattern");
40650
+ if (coercedPostId === void 0) throw new Error("Post ID is required");
40651
+ postId = coercedPostId;
40562
40652
  if (data.pattern_id === void 0 || data.pattern_id === null) throw new Error("Pattern ID is required");
40563
40653
  const response = await this.client.post(
40564
40654
  `/posts/${postId}/insert-pattern`,
@@ -40593,9 +40683,11 @@ var WordPressBlockClient = class {
40593
40683
  * Use `status: trash` to trash; any non-trash status untrashes a trashed post.
40594
40684
  */
40595
40685
  async updatePost(postId, data) {
40596
- if (postId === void 0 || postId === null) {
40686
+ const coercedPostId = coercePostId(postId, "update_post");
40687
+ if (coercedPostId === void 0) {
40597
40688
  throw new Error("update_post: post_id is required");
40598
40689
  }
40690
+ postId = coercedPostId;
40599
40691
  const response = await this.client.patch(`/posts/${postId}`, data);
40600
40692
  return response.data;
40601
40693
  }
@@ -40655,17 +40747,21 @@ var WordPressBlockClient = class {
40655
40747
  // ──────────────────────────────────────────────────────────
40656
40748
  /** Read all Yoast SEO metadata for a post. */
40657
40749
  async getYoastSEO(postId) {
40658
- if (postId === void 0 || postId === null) {
40750
+ const coercedPostId = coercePostId(postId, "yoast_get_seo");
40751
+ if (coercedPostId === void 0) {
40659
40752
  throw new Error("yoast_get_seo: post_id is required");
40660
40753
  }
40754
+ postId = coercedPostId;
40661
40755
  const response = await this.client.get(`/yoast/${postId}`);
40662
40756
  return response.data;
40663
40757
  }
40664
40758
  /** Partial update of Yoast SEO fields on a single post. */
40665
40759
  async updateYoastSEO(postId, fields) {
40666
- if (postId === void 0 || postId === null) {
40760
+ const coercedPostId = coercePostId(postId, "yoast_update_seo");
40761
+ if (coercedPostId === void 0) {
40667
40762
  throw new Error("yoast_update_seo: post_id is required");
40668
40763
  }
40764
+ postId = coercedPostId;
40669
40765
  const response = await this.client.patch(`/yoast/${postId}`, fields);
40670
40766
  return response.data;
40671
40767
  }
@@ -40679,6 +40775,51 @@ var WordPressBlockClient = class {
40679
40775
  }
40680
40776
  };
40681
40777
 
40778
+ // src/config.ts
40779
+ function readEnv(env, primary, legacy) {
40780
+ const fromPrimary = env[primary];
40781
+ if (fromPrimary && fromPrimary.trim() !== "") {
40782
+ return fromPrimary;
40783
+ }
40784
+ const fromLegacy = env[legacy];
40785
+ if (fromLegacy && fromLegacy.trim() !== "") {
40786
+ console.error(`[block-mcp] DEPRECATED: ${legacy} is deprecated; rename to ${primary} in your MCP client config.`);
40787
+ return fromLegacy;
40788
+ }
40789
+ return void 0;
40790
+ }
40791
+ function resolveWordPressConfig(env) {
40792
+ const url3 = readEnv(env, "WORDPRESS_URL", "GK_SITE_URL");
40793
+ const user = readEnv(env, "WORDPRESS_USER", "GK_BLOCK_API_USER");
40794
+ const password = readEnv(env, "WORDPRESS_APP_PASSWORD", "GK_BLOCK_API_APP_PASSWORD");
40795
+ const missing = [];
40796
+ if (!url3) {
40797
+ missing.push("WORDPRESS_URL");
40798
+ }
40799
+ if (!user) {
40800
+ missing.push("WORDPRESS_USER");
40801
+ }
40802
+ if (!password) {
40803
+ missing.push("WORDPRESS_APP_PASSWORD");
40804
+ }
40805
+ if (missing.length > 0) {
40806
+ const message = `Block MCP is not configured: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} missing. Set them in the "env" block of the block-mcp server entry in your MCP client config. Your site's Settings \u2192 Block MCP \u2192 Connect generates a ready-made config for you.`;
40807
+ return { ok: false, missing, message };
40808
+ }
40809
+ return { ok: true, config: { url: url3, user, password } };
40810
+ }
40811
+ function buildNotConfiguredResult(message) {
40812
+ return {
40813
+ content: [
40814
+ {
40815
+ type: "text",
40816
+ text: JSON.stringify({ error: true, code: "not_configured", message }, null, 2)
40817
+ }
40818
+ ],
40819
+ isError: true
40820
+ };
40821
+ }
40822
+
40682
40823
  // src/instructions.ts
40683
40824
  var BASELINE = `Block-level WordPress CRUD. URL \u2192 post_id is resolved server-side \u2014 pass URLs directly to get_page_blocks / resolve_url; never shell out to curl or wp-json.
40684
40825
 
@@ -40717,8 +40858,7 @@ async function fetchAddendum(wordpressUrl) {
40717
40858
  if (process.env[OFF_ENV_VAR] === "1") {
40718
40859
  return "";
40719
40860
  }
40720
- const base = wordpressUrl.replace(/\/+$/, "");
40721
- const url3 = `${base}/wp-json/gk-block-api/v1/instructions`;
40861
+ const url3 = restRouteUrl(wordpressUrl, "/instructions");
40722
40862
  try {
40723
40863
  const response = await axios_default.get(url3, {
40724
40864
  timeout: FETCH_TIMEOUT_MS,
@@ -40819,7 +40959,9 @@ function enrichPatternList(patterns) {
40819
40959
  const sorted = [...patterns].sort(
40820
40960
  (a2, b3) => b3.preference.score - a2.preference.score
40821
40961
  );
40822
- const recommended = sorted.filter((p2) => p2.preference.tier === "recommended");
40962
+ const recommended = sorted.filter(
40963
+ (p2) => p2.preference.tier === "preferred" || p2.preference.tier === "acceptable"
40964
+ );
40823
40965
  const avoid = sorted.filter((p2) => p2.preference.tier === "avoid" || p2.preference.tier === "legacy");
40824
40966
  const lines = [];
40825
40967
  if (recommended.length > 0) {
@@ -40867,51 +41009,39 @@ function enrichBlockTypes(types) {
40867
41009
  }
40868
41010
  }
40869
41011
  const lines = [];
40870
- if (preferred.length > 0) {
40871
- const grouped = groupByNamespace(preferred);
40872
- for (const [ns, blocks] of Object.entries(grouped)) {
40873
- const names = blocks.map((t) => getShortName(t.name)).join(", ");
40874
- lines.push(`PREFERRED (${ns}/): ${names}`);
40875
- }
40876
- }
40877
- if (acceptable.length > 0) {
40878
- const grouped = groupByNamespace(acceptable);
40879
- for (const [ns, blocks] of Object.entries(grouped)) {
40880
- const names = blocks.map((t) => getShortName(t.name)).join(", ");
40881
- lines.push(`ACCEPTABLE (${ns}/): ${names}`);
40882
- }
40883
- }
40884
- if (avoid.length > 0) {
40885
- const grouped = groupByNamespace(avoid);
40886
- for (const [ns, blocks] of Object.entries(grouped)) {
40887
- const mappings = blocks.map((t) => {
40888
- const replacement = t.preference.replacement;
40889
- const shortName = getShortName(t.name);
40890
- return replacement ? `${shortName} -> use ${replacement}` : shortName;
40891
- });
40892
- lines.push(`AVOID (${ns}/): ${mappings.join(", ")}`);
40893
- }
40894
- }
40895
- if (legacy.length > 0) {
40896
- const grouped = groupByNamespace(legacy);
40897
- for (const [ns, blocks] of Object.entries(grouped)) {
40898
- const mappings = blocks.map((t) => {
40899
- const replacement = t.preference.replacement;
40900
- const shortName = getShortName(t.name);
40901
- return replacement ? `${shortName} -> use ${replacement}` : shortName;
40902
- });
40903
- lines.push(`LEGACY \u2014 DO NOT USE (${ns}/): ${mappings.join(", ")}`);
40904
- }
40905
- }
41012
+ pushTierLines(lines, preferred, "PREFERRED", false);
41013
+ pushTierLines(lines, acceptable, "ACCEPTABLE", false);
41014
+ pushTierLines(lines, avoid, "AVOID", true);
41015
+ pushTierLines(lines, legacy, "LEGACY \u2014 DO NOT USE", true);
40906
41016
  const guidance = lines.join("\n");
40907
41017
  return { block_types: types, guidance };
40908
41018
  }
41019
+ function pushTierLines(lines, bucket, label, withReplacement) {
41020
+ if (bucket.length === 0) return;
41021
+ const grouped = groupByNamespace(bucket);
41022
+ for (const [ns, blocks] of Object.entries(grouped)) {
41023
+ const names = blocks.map((t) => {
41024
+ const shortName = getShortName(t.name);
41025
+ if (!withReplacement) return shortName;
41026
+ const replacement = t.preference.replacement;
41027
+ return replacement ? `${shortName} -> use ${replacement}` : shortName;
41028
+ });
41029
+ lines.push(`${label} (${ns}/): ${names.join(", ")}`);
41030
+ }
41031
+ }
40909
41032
  function formatPreferenceWarning(warning) {
40910
41033
  if (warning.suggested_replacement) {
40911
41034
  return `WARNING: ${warning.block} is non-preferred. Use ${warning.suggested_replacement} instead.`;
40912
41035
  }
40913
41036
  return `WARNING: ${warning.message}`;
40914
41037
  }
41038
+ function withFormattedWarnings(result, formatWarning) {
41039
+ if (result.warnings && result.warnings.length > 0) {
41040
+ const warnings = result.warnings;
41041
+ return { ...result, formatted_warnings: warnings.map(formatWarning) };
41042
+ }
41043
+ return result;
41044
+ }
40915
41045
  function groupByNamespace(types) {
40916
41046
  const groups = {};
40917
41047
  for (const t of types) {
@@ -41075,8 +41205,6 @@ async function handleDiscoveryTool(toolName, args, client) {
41075
41205
  q: args.search,
41076
41206
  synced: args.synced,
41077
41207
  min_score: args.min_score,
41078
- // Fetch enough to honor offset+limit. Server caps respond too.
41079
- limit: offset + limit,
41080
41208
  refresh: args.refresh
41081
41209
  });
41082
41210
  const enriched = enrichPatternList(response.patterns);
@@ -41120,14 +41248,7 @@ async function handleDiscoveryTool(toolName, args, client) {
41120
41248
  if ((postId === void 0 || postId === null) && (typeof url3 !== "string" || url3.length === 0) && (typeof slug !== "string" || slug.length === 0)) {
41121
41249
  throw new Error("get_post_info requires one of: post_id, url, or slug");
41122
41250
  }
41123
- let normalizedPostId;
41124
- if (typeof postId === "number" && Number.isInteger(postId) && postId > 0) {
41125
- normalizedPostId = postId;
41126
- } else if (typeof postId === "string" && /^[0-9]+$/.test(postId)) {
41127
- normalizedPostId = parseInt(postId, 10);
41128
- } else if (postId !== void 0 && postId !== null) {
41129
- throw new Error("get_post_info: post_id must be a positive integer");
41130
- }
41251
+ const normalizedPostId = coercePostId(postId, "get_post_info");
41131
41252
  return await client.getPostInfo({
41132
41253
  post_id: normalizedPostId,
41133
41254
  url: typeof url3 === "string" ? url3 : void 0,
@@ -41251,7 +41372,7 @@ var READ_TOOLS = [
41251
41372
  async function handleReadTool(toolName, args, client) {
41252
41373
  switch (toolName) {
41253
41374
  case "get_page_blocks": {
41254
- let postId = args.post_id;
41375
+ let postId = coercePostId(args.post_id, "get_page_blocks");
41255
41376
  const url3 = args.url;
41256
41377
  const fields = args.fields;
41257
41378
  const render = args.render;
@@ -41263,10 +41384,10 @@ async function handleReadTool(toolName, args, client) {
41263
41384
  const persistRefs = args.persist_refs;
41264
41385
  const limit = args.limit;
41265
41386
  const cursor = args.cursor;
41266
- if ((postId === void 0 || postId === null) && !url3) {
41387
+ if (postId === void 0 && !url3) {
41267
41388
  throw new Error("Either post_id or url is required");
41268
41389
  }
41269
- if (postId === void 0 || postId === null) {
41390
+ if (postId === void 0) {
41270
41391
  const resolved = await client.resolveUrl(url3);
41271
41392
  postId = resolved.post_id;
41272
41393
  }
@@ -41300,10 +41421,10 @@ async function handleReadTool(toolName, args, client) {
41300
41421
  };
41301
41422
  }
41302
41423
  case "get_block": {
41303
- const postId = args.post_id;
41424
+ const postId = coercePostId(args.post_id, "get_block");
41304
41425
  const ref = typeof args.ref === "string" && args.ref.length > 0 ? args.ref : void 0;
41305
- const flatIndex = typeof args.flat_index === "number" && Number.isFinite(args.flat_index) ? args.flat_index : void 0;
41306
- if (postId === void 0 || postId === null) {
41426
+ const flatIndex = typeof args.flat_index === "number" && Number.isInteger(args.flat_index) && args.flat_index >= 0 ? args.flat_index : void 0;
41427
+ if (postId === void 0) {
41307
41428
  throw new Error("post_id is required");
41308
41429
  }
41309
41430
  const hasRef = ref !== void 0;
@@ -52027,7 +52148,7 @@ async function shikiHighlight(code, language, themeName) {
52027
52148
  const lang29 = langs.has(language) ? language : "plaintext";
52028
52149
  let html5 = hl.codeToHtml(code, { lang: lang29, theme: "css-variables" }).replace(/var\(--shiki-foreground\)/g, "var(--shiki-color-text)").replace(/var\(--shiki-background\)/g, "var(--shiki-color-background)");
52029
52150
  if (themeName && themeName !== "css-variables") {
52030
- html5 = html5.replace(/(<pre[^>]*class="shiki) css-variables(")/, `$1 ${themeName}$2`);
52151
+ html5 = html5.replace(/(<pre[^>]*class="shiki) css-variables(")/, (_m, p1, p2) => `${p1} ${themeName}${p2}`);
52031
52152
  }
52032
52153
  return html5;
52033
52154
  }
@@ -52089,7 +52210,7 @@ registerBlockEnricher("kevinbatdorf/code-block-pro", async (block) => {
52089
52210
  if (incomingInnerHTML !== "") {
52090
52211
  updatedInnerHTML = incomingInnerHTML.replace(
52091
52212
  /<pre class="shiki[\s\S]*?<\/pre>/,
52092
- codeHTML
52213
+ () => codeHTML
52093
52214
  );
52094
52215
  updatedInnerHTML = updatedInnerHTML.replace(
52095
52216
  /(<textarea[^>]*>)([\s\S]*?)(<\/textarea>)/,
@@ -52164,7 +52285,9 @@ var WRITE_TOOLS = [
52164
52285
  // idempotentHint is false: every call creates a new revision, and
52165
52286
  // revision history is observable to other readers. Same-input/same-state
52166
52287
  // is true at the block level but not at the post level.
52167
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, title: "Update one block" },
52288
+ // destructiveHint is true: attributes/innerHTML overwrite the block's
52289
+ // existing content rather than merging additively.
52290
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, title: "Update one block" },
52168
52291
  outputSchema: {
52169
52292
  type: "object",
52170
52293
  properties: {
@@ -52211,7 +52334,8 @@ var WRITE_TOOLS = [
52211
52334
  {
52212
52335
  name: "update_blocks",
52213
52336
  description: "Update N independent blocks atomically in ONE revision. Each item targets one block by `ref` (recommended) or `flat_index`, with `attributes` and/or `innerHTML`. Validation is all-or-nothing: any stale ref / out-of-range index / dual-storage rejection / duplicate target aborts the batch with itemized errors \u2014 no partial writes hit disk. Max 50 items per call. Counts as ONE write against the per-post rate limit. Use this instead of looping update_block when fixing multiple blocks on the same post \u2014 keeps revision history clean. Pass `verbose: true` to include `saved.inner_html` + `saved.attributes` per result for per-item verification without a re-read.",
52214
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, title: "Batch-update blocks" },
52337
+ // destructiveHint is true: same overwrite semantics as update_block, applied per item.
52338
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, title: "Batch-update blocks" },
52215
52339
  outputSchema: {
52216
52340
  type: "object",
52217
52341
  properties: {
@@ -52405,13 +52529,13 @@ var WRITE_TOOLS = [
52405
52529
  async function handleWriteTool(toolName, args, client) {
52406
52530
  switch (toolName) {
52407
52531
  case "update_block": {
52408
- const postId = args.post_id;
52532
+ const postId = coercePostId(args.post_id, "update_block");
52409
52533
  const flatIndex = args.flat_index;
52410
52534
  const ref = args.ref;
52411
52535
  const blockName = args.block_name;
52412
52536
  let attributes = args.attributes;
52413
52537
  let innerHTML = args.innerHTML;
52414
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52538
+ if (postId === void 0) throw new Error("post_id is required");
52415
52539
  const hasIndex = typeof flatIndex === "number" && Number.isFinite(flatIndex) && flatIndex >= 0;
52416
52540
  const hasRef = typeof ref === "string" && ref.length > 0;
52417
52541
  if (!hasIndex && !hasRef) {
@@ -52435,9 +52559,9 @@ async function handleWriteTool(toolName, args, client) {
52435
52559
  return await client.updateBlock(postId, flatIndex, { attributes, innerHTML });
52436
52560
  }
52437
52561
  case "update_blocks": {
52438
- const postId = args.post_id;
52562
+ const postId = coercePostId(args.post_id, "update_blocks");
52439
52563
  const updates = args.updates;
52440
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52564
+ if (postId === void 0) throw new Error("post_id is required");
52441
52565
  if (!Array.isArray(updates) || updates.length === 0) {
52442
52566
  throw new Error("updates must be a non-empty array");
52443
52567
  }
@@ -52481,13 +52605,13 @@ async function handleWriteTool(toolName, args, client) {
52481
52605
  return await client.updateBlocksBatch(postId, normalized);
52482
52606
  }
52483
52607
  case "insert_blocks": {
52484
- const postId = args.post_id;
52608
+ const postId = coercePostId(args.post_id, "insert_blocks");
52485
52609
  const after = args.after_top_level;
52486
52610
  const before = args.before_top_level;
52487
52611
  const afterRef = args.after_ref;
52488
52612
  const beforeRef = args.before_ref;
52489
52613
  const blocks = args.blocks;
52490
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52614
+ if (postId === void 0) throw new Error("post_id is required");
52491
52615
  if (!blocks || blocks.length === 0) throw new Error("At least one block is required in the blocks array");
52492
52616
  const result = await client.insertBlocks(postId, {
52493
52617
  after,
@@ -52496,17 +52620,14 @@ async function handleWriteTool(toolName, args, client) {
52496
52620
  ...beforeRef ? { before_ref: beforeRef } : {},
52497
52621
  blocks: await enrichBlocks(blocks)
52498
52622
  });
52499
- if (result.warnings && result.warnings.length > 0) {
52500
- return { ...result, formatted_warnings: result.warnings.map(formatPreferenceWarning) };
52501
- }
52502
- return result;
52623
+ return withFormattedWarnings(result, formatPreferenceWarning);
52503
52624
  }
52504
52625
  case "delete_block": {
52505
- const postId = args.post_id;
52626
+ const postId = coercePostId(args.post_id, "delete_block");
52506
52627
  const topLevelCounter = args.top_level_counter;
52507
52628
  const ref = args.ref;
52508
52629
  const count = args.count;
52509
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52630
+ if (postId === void 0) throw new Error("post_id is required");
52510
52631
  const hasCounter = typeof topLevelCounter === "number" && Number.isFinite(topLevelCounter) && topLevelCounter >= 0;
52511
52632
  const hasRef = typeof ref === "string" && ref.length > 0;
52512
52633
  if (!hasCounter && !hasRef) {
@@ -52526,35 +52647,29 @@ async function handleWriteTool(toolName, args, client) {
52526
52647
  return await client.deleteBlock(postId, topLevelCounter, count);
52527
52648
  }
52528
52649
  case "replace_block_range": {
52529
- const postId = args.post_id;
52650
+ const postId = coercePostId(args.post_id, "replace_block_range");
52530
52651
  const start = args.start;
52531
52652
  const count = args.count;
52532
52653
  const blocks = args.blocks;
52533
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52534
- if (typeof start !== "number" || start < 0) throw new Error("start must be a non-negative integer");
52535
- if (typeof count !== "number" || count < 0) throw new Error("count must be a non-negative integer");
52654
+ if (postId === void 0) throw new Error("post_id is required");
52655
+ if (!Number.isSafeInteger(start) || start < 0) throw new Error("start must be a non-negative integer");
52656
+ if (!Number.isSafeInteger(count) || count < 0) throw new Error("count must be a non-negative integer");
52536
52657
  if (!Array.isArray(blocks)) throw new Error("blocks must be an array (may be empty for a pure delete)");
52537
52658
  const result = await client.replaceBlocksRange(postId, { start, count, blocks: await enrichBlocks(blocks) });
52538
- if (result.warnings && result.warnings.length > 0) {
52539
- return { ...result, formatted_warnings: result.warnings.map(formatPreferenceWarning) };
52540
- }
52541
- return result;
52659
+ return withFormattedWarnings(result, formatPreferenceWarning);
52542
52660
  }
52543
52661
  case "rewrite_post_blocks": {
52544
- const postId = args.post_id;
52662
+ const postId = coercePostId(args.post_id, "rewrite_post_blocks");
52545
52663
  const blocks = args.blocks;
52546
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52664
+ if (postId === void 0) throw new Error("post_id is required");
52547
52665
  if (!blocks || blocks.length === 0) throw new Error("At least one block is required for a full page rewrite");
52548
52666
  const result = await client.replaceAllBlocks(postId, await enrichBlocks(blocks));
52549
- if (result.warnings && result.warnings.length > 0) {
52550
- return { ...result, formatted_warnings: result.warnings.map(formatPreferenceWarning) };
52551
- }
52552
- return result;
52667
+ return withFormattedWarnings(result, formatPreferenceWarning);
52553
52668
  }
52554
52669
  case "revert_to_revision": {
52555
- const postId = args.post_id;
52670
+ const postId = coercePostId(args.post_id, "revert_to_revision");
52556
52671
  const revisionId = args.revision_id;
52557
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52672
+ if (postId === void 0) throw new Error("post_id is required");
52558
52673
  if (revisionId === void 0 || revisionId === null) throw new Error("revision_id is required");
52559
52674
  return await client.revertToRevision(postId, revisionId);
52560
52675
  }
@@ -52600,12 +52715,12 @@ var PATTERN_TOOLS = [
52600
52715
  async function handlePatternTool(toolName, args, client) {
52601
52716
  switch (toolName) {
52602
52717
  case "insert_pattern": {
52603
- const postId = args.post_id;
52718
+ const postId = coercePostId(args.post_id, "insert_pattern");
52604
52719
  const patternId = args.pattern_id;
52605
52720
  const after = args.after_top_level;
52606
52721
  const before = args.before_top_level;
52607
52722
  const synced = args.synced;
52608
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52723
+ if (postId === void 0) throw new Error("post_id is required");
52609
52724
  if (patternId === void 0 || patternId === null) throw new Error("pattern_id is required");
52610
52725
  const result = await client.insertPattern(postId, {
52611
52726
  pattern_id: patternId,
@@ -52711,11 +52826,11 @@ async function handleMutateTool(toolName, args, client) {
52711
52826
  if (toolName !== "edit_block_tree") {
52712
52827
  throw new Error(`Unknown mutate tool: ${toolName}`);
52713
52828
  }
52714
- const postId = args.post_id;
52829
+ const postId = coercePostId(args.post_id, "edit_block_tree");
52715
52830
  const op = args.op;
52716
52831
  const path2 = args.path;
52717
52832
  const ref = args.ref;
52718
- if (postId === void 0 || postId === null) throw new Error("post_id is required");
52833
+ if (postId === void 0) throw new Error("post_id is required");
52719
52834
  if (!op || !OPS.includes(op)) {
52720
52835
  throw new Error(`op must be one of: ${OPS.join(", ")}. Got: ${JSON.stringify(op)}`);
52721
52836
  }
@@ -52763,10 +52878,16 @@ async function handleMutateTool(toolName, args, client) {
52763
52878
  requestBody.block = block;
52764
52879
  if (op === "insert-child" && args.position !== void 0) {
52765
52880
  const position = args.position;
52766
- if (typeof position === "number" && Number.isInteger(position)) {
52881
+ if (typeof position === "number" && Number.isInteger(position) && position >= -1) {
52767
52882
  requestBody.position = position;
52768
52883
  } else if (position === "start" || position === "end") {
52769
52884
  requestBody.position = position;
52885
+ } else if (typeof position === "string" && /^-?\d+$/.test(position)) {
52886
+ const parsedPosition = parseInt(position, 10);
52887
+ if (parsedPosition < -1) {
52888
+ throw new Error('position must be an integer >= -1, "start", or "end"');
52889
+ }
52890
+ requestBody.position = parsedPosition;
52770
52891
  } else {
52771
52892
  throw new Error('position must be an integer, "start", or "end"');
52772
52893
  }
@@ -52808,14 +52929,10 @@ async function handleMutateTool(toolName, args, client) {
52808
52929
  }
52809
52930
  }
52810
52931
  const result = await client.mutateBlockTree(postId, requestBody);
52811
- if (result.warnings && result.warnings.length > 0) {
52812
- const formattedWarnings = result.warnings.map((warning) => {
52813
- if (isStaticBlockWarning(warning)) return formatStaticBlockWarning(warning);
52814
- return formatPreferenceWarning(warning);
52815
- });
52816
- return { ...result, formatted_warnings: formattedWarnings };
52817
- }
52818
- return result;
52932
+ return withFormattedWarnings(
52933
+ result,
52934
+ (warning) => isStaticBlockWarning(warning) ? formatStaticBlockWarning(warning) : formatPreferenceWarning(warning)
52935
+ );
52819
52936
  }
52820
52937
 
52821
52938
  // src/tools/posts.ts
@@ -52921,10 +53038,12 @@ async function handlePostTool(toolName, args, client) {
52921
53038
  return client.createPost(create3);
52922
53039
  }
52923
53040
  case "update_post": {
52924
- if (typeof args.post_id !== "number") {
52925
- throw new Error('update_post: "post_id" (number) is required');
53041
+ const postId = coercePostId(args.post_id, "update_post");
53042
+ if (postId === void 0) {
53043
+ throw new Error('update_post: "post_id" is required');
52926
53044
  }
52927
- const { post_id: postId, ...rest } = args;
53045
+ const { post_id: _omit, ...rest } = args;
53046
+ void _omit;
52928
53047
  if (Object.keys(rest).length === 0) {
52929
53048
  throw new Error("update_post: provide at least one mutating field besides post_id");
52930
53049
  }
@@ -52935,12 +53054,8 @@ async function handlePostTool(toolName, args, client) {
52935
53054
  throw new Error(`Unknown post tool: ${toolName}`);
52936
53055
  }
52937
53056
  }
52938
- function narrowCreatePost(input) {
52939
- const out = { title: input.title };
52940
- if (typeof input.post_type === "string") out.post_type = input.post_type;
52941
- if (typeof input.status === "string") out.status = input.status;
52942
- if (typeof input.content === "string") out.content = input.content;
52943
- if (Array.isArray(input.blocks)) out.blocks = input.blocks;
53057
+ function narrowCommonPostFields(input) {
53058
+ const out = {};
52944
53059
  if (typeof input.slug === "string") out.slug = input.slug;
52945
53060
  if (typeof input.parent === "number") out.parent = input.parent;
52946
53061
  if (typeof input.excerpt === "string") out.excerpt = input.excerpt;
@@ -52957,24 +53072,18 @@ function narrowCreatePost(input) {
52957
53072
  if (typeof input.author === "number") out.author = input.author;
52958
53073
  return out;
52959
53074
  }
53075
+ function narrowCreatePost(input) {
53076
+ const out = { title: input.title, ...narrowCommonPostFields(input) };
53077
+ if (typeof input.post_type === "string") out.post_type = input.post_type;
53078
+ if (typeof input.status === "string") out.status = input.status;
53079
+ if (typeof input.content === "string") out.content = input.content;
53080
+ if (Array.isArray(input.blocks)) out.blocks = input.blocks;
53081
+ return out;
53082
+ }
52960
53083
  function narrowUpdatePost(input) {
52961
- const out = {};
53084
+ const out = narrowCommonPostFields(input);
52962
53085
  if (typeof input.title === "string") out.title = input.title;
52963
53086
  if (typeof input.status === "string") out.status = input.status;
52964
- if (typeof input.slug === "string") out.slug = input.slug;
52965
- if (typeof input.parent === "number") out.parent = input.parent;
52966
- if (typeof input.excerpt === "string") out.excerpt = input.excerpt;
52967
- if (typeof input.featured_media === "number") out.featured_media = input.featured_media;
52968
- if (Array.isArray(input.categories)) out.categories = input.categories.filter((n) => typeof n === "number");
52969
- if (Array.isArray(input.tags)) out.tags = input.tags.filter((n) => typeof n === "number");
52970
- if (input.terms && typeof input.terms === "object" && !Array.isArray(input.terms)) {
52971
- out.terms = narrowTermsMap(input.terms);
52972
- }
52973
- if (typeof input.date === "string") out.date = input.date;
52974
- if (typeof input.menu_order === "number") out.menu_order = input.menu_order;
52975
- if (input.comment_status === "open" || input.comment_status === "closed") out.comment_status = input.comment_status;
52976
- if (input.ping_status === "open" || input.ping_status === "closed") out.ping_status = input.ping_status;
52977
- if (typeof input.author === "number") out.author = input.author;
52978
53087
  return out;
52979
53088
  }
52980
53089
  function narrowTermsMap(input) {
@@ -53084,8 +53193,8 @@ async function handleMediaTool(toolName, args, client) {
53084
53193
  }
53085
53194
  }
53086
53195
 
53087
- // src/tools/yoast.ts
53088
- var SCHEMA_PAGE_TYPES = [
53196
+ // src/types.ts
53197
+ var YOAST_SCHEMA_PAGE_TYPES = [
53089
53198
  "WebPage",
53090
53199
  "ItemPage",
53091
53200
  "AboutPage",
@@ -53099,7 +53208,7 @@ var SCHEMA_PAGE_TYPES = [
53099
53208
  "RealEstateListing",
53100
53209
  "SearchResultsPage"
53101
53210
  ];
53102
- var SCHEMA_ARTICLE_TYPES = [
53211
+ var YOAST_SCHEMA_ARTICLE_TYPES = [
53103
53212
  "Article",
53104
53213
  "BlogPosting",
53105
53214
  "SocialMediaPosting",
@@ -53111,15 +53220,20 @@ var SCHEMA_ARTICLE_TYPES = [
53111
53220
  "Report",
53112
53221
  "None"
53113
53222
  ];
53114
- var ROBOTS_ADVANCED = ["noimageindex", "noarchive", "nosnippet"];
53223
+ var YOAST_ROBOTS_ADVANCED = ["noimageindex", "noarchive", "nosnippet"];
53224
+
53225
+ // src/tools/yoast.ts
53226
+ var SCHEMA_PAGE_TYPES = YOAST_SCHEMA_PAGE_TYPES;
53227
+ var SCHEMA_ARTICLE_TYPES = YOAST_SCHEMA_ARTICLE_TYPES;
53228
+ var ROBOTS_ADVANCED = YOAST_ROBOTS_ADVANCED;
53115
53229
  var YOAST_FIELD_PROPERTIES = {
53116
53230
  title: { type: "string", description: "SEO title (supports Yoast variables like %%title%%)." },
53117
53231
  description: { type: "string", description: "Meta description." },
53118
53232
  canonical: { type: "string", description: "Canonical URL override." },
53119
53233
  focus_keyword: { type: "string", description: "Focus keyphrase." },
53120
53234
  noindex: {
53121
- type: ["boolean", "null"],
53122
- description: "Tri-state: true=noindex, false=explicit index, null=post-type default."
53235
+ type: "boolean",
53236
+ description: 'true=noindex, false=explicit index. Pass null to reset to the post-type default; omit to leave the current value unchanged. (Advertised as a single boolean type because some AI clients \u2014 e.g. Google Gemini \u2014 reject a "null" member in a type array; the handler still accepts an explicit null.)'
53123
53237
  },
53124
53238
  nofollow: { type: "boolean", description: "true=nofollow, false=follow." },
53125
53239
  robots_advanced: {
@@ -53203,16 +53317,16 @@ var YOAST_TOOLS = [
53203
53317
  async function handleYoastTool(toolName, args, client) {
53204
53318
  switch (toolName) {
53205
53319
  case "yoast_get_seo": {
53206
- const postId = args.post_id;
53207
- if (typeof postId !== "number") {
53208
- throw new Error('yoast_get_seo: "post_id" (number) is required');
53320
+ const postId = coercePostId(args.post_id, "yoast_get_seo");
53321
+ if (postId === void 0) {
53322
+ throw new Error('yoast_get_seo: "post_id" is required');
53209
53323
  }
53210
53324
  return client.getYoastSEO(postId);
53211
53325
  }
53212
53326
  case "yoast_update_seo": {
53213
- const postId = args.post_id;
53214
- if (typeof postId !== "number") {
53215
- throw new Error('yoast_update_seo: "post_id" (number) is required');
53327
+ const postId = coercePostId(args.post_id, "yoast_update_seo");
53328
+ if (postId === void 0) {
53329
+ throw new Error('yoast_update_seo: "post_id" is required');
53216
53330
  }
53217
53331
  const { post_id: _omit, ...rest } = args;
53218
53332
  void _omit;
@@ -53232,10 +53346,12 @@ async function handleYoastTool(toolName, args, client) {
53232
53346
  throw new Error("yoast_bulk_update_seo: each item in `posts` must be an object");
53233
53347
  }
53234
53348
  const obj = raw2;
53235
- if (typeof obj.post_id !== "number") {
53236
- throw new Error("yoast_bulk_update_seo: each item requires `post_id` (number)");
53349
+ const id = coercePostId(obj.post_id, "yoast_bulk_update_seo");
53350
+ if (id === void 0) {
53351
+ throw new Error("yoast_bulk_update_seo: each item requires `post_id`");
53237
53352
  }
53238
- const { post_id: id, ...rest } = obj;
53353
+ const { post_id: _omit, ...rest } = obj;
53354
+ void _omit;
53239
53355
  items.push({ post_id: id, ...narrowYoastFields(rest) });
53240
53356
  }
53241
53357
  return client.bulkUpdateYoastSEO(items);
@@ -53609,7 +53725,6 @@ function describeExchangeFetchError(url3, err) {
53609
53725
  return `Exchange failed: could not reach ${url3} (${reason}).${hint}`;
53610
53726
  }
53611
53727
  async function exchangeCode(site, code, fetchFn = fetch, timeoutMs = EXCHANGE_FETCH_TIMEOUT_MS) {
53612
- const origin2 = new URL(site).origin;
53613
53728
  let url3 = `${site}/?rest_route=/gk-block-api/v1/connect/exchange`;
53614
53729
  const controller = new AbortController();
53615
53730
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -53636,8 +53751,11 @@ async function exchangeCode(site, code, fetchFn = fetch, timeoutMs = EXCHANGE_FE
53636
53751
  `Exchange failed: the site redirected the request (HTTP ${res.status}); ensure --site is the canonical site URL.`
53637
53752
  );
53638
53753
  }
53754
+ const current = new URL(url3);
53639
53755
  const next = new URL(location, url3);
53640
- if (next.origin !== origin2) {
53756
+ const upgradesToStandardHttpsPort = next.port === "" || next.port === "443";
53757
+ const sameHostHttpsUpgrade = current.protocol === "http:" && next.protocol === "https:" && next.hostname === current.hostname && upgradesToStandardHttpsPort;
53758
+ if (next.origin !== current.origin && !sameHostHttpsUpgrade) {
53641
53759
  throw new Error(
53642
53760
  `Exchange failed: the site redirected to a different origin (${next.origin}); refusing to send the credential off-site.`
53643
53761
  );
@@ -53962,16 +54080,6 @@ Fall back \u2014 add this to your Claude Code MCP config manually:`);
53962
54080
  }
53963
54081
 
53964
54082
  // src/index.ts
53965
- function readEnv(primary, legacy) {
53966
- const fromPrimary = process.env[primary];
53967
- if (fromPrimary) return fromPrimary;
53968
- const fromLegacy = process.env[legacy];
53969
- if (fromLegacy) {
53970
- console.error(`[block-mcp] DEPRECATED: ${legacy} is deprecated; rename to ${primary} in your MCP client config.`);
53971
- return fromLegacy;
53972
- }
53973
- return void 0;
53974
- }
53975
54083
  var ALL_TOOLS = [
53976
54084
  ...DISCOVERY_TOOLS,
53977
54085
  ...READ_TOOLS,
@@ -54002,13 +54110,16 @@ for (const { tools, handle: handle2 } of TOOL_GROUPS) {
54002
54110
  }
54003
54111
  var AGENT_GUIDE_RESOURCE_URI = "block-mcp://agent-guide";
54004
54112
  var LEGACY_PREFERENCES_RESOURCE_URI = "block-mcp://block-preferences";
54005
- function registerHandlers(server, client) {
54113
+ function registerHandlers(server, client, notConfiguredMessage) {
54006
54114
  server.server.setRequestHandler(ListToolsRequestSchema, async () => {
54007
54115
  return { tools: ALL_TOOLS };
54008
54116
  });
54009
54117
  server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
54010
54118
  const { name, arguments: args } = request.params;
54011
54119
  const toolArgs = args ?? {};
54120
+ if (!client) {
54121
+ return buildNotConfiguredResult(notConfiguredMessage ?? "Block MCP is not configured.");
54122
+ }
54012
54123
  try {
54013
54124
  const handle2 = TOOL_DISPATCH.get(name);
54014
54125
  if (!handle2) {
@@ -54134,23 +54245,28 @@ async function main() {
54134
54245
  await runConnect(process.argv.slice(3));
54135
54246
  process.exit(0);
54136
54247
  }
54137
- const WORDPRESS_URL = readEnv("WORDPRESS_URL", "GK_SITE_URL");
54138
- const WORDPRESS_USER = readEnv("WORDPRESS_USER", "GK_BLOCK_API_USER");
54139
- const WORDPRESS_APP_PASSWORD = readEnv("WORDPRESS_APP_PASSWORD", "GK_BLOCK_API_APP_PASSWORD");
54140
- if (!WORDPRESS_URL || !WORDPRESS_USER || !WORDPRESS_APP_PASSWORD) {
54248
+ const cfg = resolveWordPressConfig(process.env);
54249
+ if (!cfg.ok) {
54250
+ console.error(`Block MCP: ${cfg.message}`);
54251
+ const degraded = new McpServer(
54252
+ { name: "block-mcp", version: package_default.version },
54253
+ { capabilities: { tools: {}, resources: {}, prompts: {} } }
54254
+ );
54255
+ registerHandlers(degraded, null, cfg.message);
54256
+ await degraded.connect(new StdioServerTransport());
54141
54257
  console.error(
54142
- "Missing required environment variables: WORDPRESS_URL, WORDPRESS_USER, WORDPRESS_APP_PASSWORD"
54258
+ "Block MCP Server running on stdio (unconfigured \u2014 tool calls return a configuration error)"
54143
54259
  );
54144
- process.exit(1);
54260
+ return;
54145
54261
  }
54146
54262
  const client = new WordPressBlockClient({
54147
- wordpress_url: WORDPRESS_URL,
54263
+ wordpress_url: cfg.config.url,
54148
54264
  auth: {
54149
- username: WORDPRESS_USER,
54150
- application_password: WORDPRESS_APP_PASSWORD
54265
+ username: cfg.config.user,
54266
+ application_password: cfg.config.password
54151
54267
  }
54152
54268
  });
54153
- const instructions = await getInstructions(WORDPRESS_URL);
54269
+ const instructions = await getInstructions(cfg.config.url);
54154
54270
  const server = new McpServer(
54155
54271
  {
54156
54272
  name: "block-mcp",