@mangerik/wordpress-mcp 0.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 (45) hide show
  1. package/.env.example +32 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE +21 -0
  4. package/README.md +256 -0
  5. package/dist/config.d.ts +80 -0
  6. package/dist/config.js +84 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +145 -0
  9. package/dist/prompts.d.ts +7 -0
  10. package/dist/prompts.js +104 -0
  11. package/dist/resources.d.ts +13 -0
  12. package/dist/resources.js +64 -0
  13. package/dist/tools/batch.d.ts +14 -0
  14. package/dist/tools/batch.js +49 -0
  15. package/dist/tools/blocks.d.ts +4 -0
  16. package/dist/tools/blocks.js +202 -0
  17. package/dist/tools/comments.d.ts +4 -0
  18. package/dist/tools/comments.js +80 -0
  19. package/dist/tools/cpt.d.ts +16 -0
  20. package/dist/tools/cpt.js +97 -0
  21. package/dist/tools/jwt.d.ts +9 -0
  22. package/dist/tools/jwt.js +17 -0
  23. package/dist/tools/media.d.ts +4 -0
  24. package/dist/tools/media.js +101 -0
  25. package/dist/tools/multisite.d.ts +17 -0
  26. package/dist/tools/multisite.js +111 -0
  27. package/dist/tools/pages.d.ts +4 -0
  28. package/dist/tools/pages.js +101 -0
  29. package/dist/tools/posts.d.ts +4 -0
  30. package/dist/tools/posts.js +160 -0
  31. package/dist/tools/seo.d.ts +4 -0
  32. package/dist/tools/seo.js +269 -0
  33. package/dist/tools/site.d.ts +4 -0
  34. package/dist/tools/site.js +96 -0
  35. package/dist/tools/taxonomy.d.ts +4 -0
  36. package/dist/tools/taxonomy.js +147 -0
  37. package/dist/tools/users.d.ts +4 -0
  38. package/dist/tools/users.js +99 -0
  39. package/dist/tools/woocommerce.d.ts +4 -0
  40. package/dist/tools/woocommerce.js +400 -0
  41. package/dist/types.d.ts +26 -0
  42. package/dist/types.js +2 -0
  43. package/dist/wordpress-client.d.ts +223 -0
  44. package/dist/wordpress-client.js +519 -0
  45. package/package.json +67 -0
@@ -0,0 +1,519 @@
1
+ import axios from "axios";
2
+ import https from "node:https";
3
+ import FormData from "form-data";
4
+ import { readFileSync } from "node:fs";
5
+ import { basename } from "node:path";
6
+ const RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
7
+ export class WordPressClient {
8
+ baseUrl;
9
+ client;
10
+ maxRetries;
11
+ userAgent;
12
+ wcKey;
13
+ wcSecret;
14
+ authMode;
15
+ jwtNamespace;
16
+ username;
17
+ password;
18
+ jwtToken;
19
+ constructor(config) {
20
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
21
+ this.maxRetries = config.maxRetries ?? 3;
22
+ this.userAgent = config.userAgent ?? "WordPress-MCP-Server/1.0";
23
+ this.wcKey = config.wcConsumerKey;
24
+ this.wcSecret = config.wcConsumerSecret;
25
+ this.authMode = config.authMode ?? "application_password";
26
+ this.jwtNamespace = (config.jwtNamespace ?? "jwt-auth/v1").replace(/^\/|\/$/g, "");
27
+ this.username = config.username;
28
+ this.password = config.password;
29
+ this.jwtToken = config.jwtToken;
30
+ let authHeader;
31
+ if (this.authMode === "application_password") {
32
+ if (!config.username || !config.appPassword) {
33
+ throw new Error("application_password mode requires both username and appPassword");
34
+ }
35
+ const token = Buffer.from(`${config.username}:${config.appPassword}`).toString("base64");
36
+ authHeader = `Basic ${token}`;
37
+ }
38
+ else if (this.jwtToken) {
39
+ authHeader = `Bearer ${this.jwtToken}`;
40
+ }
41
+ // If JWT mode without a pre-issued token, the header is set lazily
42
+ // after `ensureJwt()` runs.
43
+ this.client = axios.create({
44
+ baseURL: `${this.baseUrl}/wp-json`,
45
+ headers: {
46
+ ...(authHeader ? { Authorization: authHeader } : {}),
47
+ "User-Agent": this.userAgent,
48
+ Accept: "application/json",
49
+ },
50
+ timeout: config.timeoutMs ?? 30_000,
51
+ httpsAgent: config.verifySsl === false
52
+ ? new https.Agent({ rejectUnauthorized: false })
53
+ : undefined,
54
+ // Don't throw on 4xx — we map errors ourselves for clearer messages.
55
+ validateStatus: () => true,
56
+ });
57
+ }
58
+ /**
59
+ * Ensure a usable JWT is loaded. Called once at startup (or on demand)
60
+ * by callers in JWT mode without a pre-issued token.
61
+ */
62
+ async ensureJwt() {
63
+ if (this.authMode !== "jwt")
64
+ return;
65
+ if (this.jwtToken) {
66
+ this.client.defaults.headers["Authorization"] = `Bearer ${this.jwtToken}`;
67
+ return;
68
+ }
69
+ if (!this.username || !this.password) {
70
+ throw new WPError("JWT mode without pre-issued token requires username + password", { status: 0, code: "jwt_missing_credentials" });
71
+ }
72
+ // Fetch token. We deliberately don't go through this.request() here
73
+ // because that path would try to attach Authorization.
74
+ const url = `${this.baseUrl}/wp-json/${this.jwtNamespace}/token`;
75
+ const res = await axios.post(url, { username: this.username, password: this.password }, {
76
+ timeout: 15_000,
77
+ headers: { "User-Agent": this.userAgent, "Content-Type": "application/json" },
78
+ validateStatus: () => true,
79
+ httpsAgent: this.client.defaults.httpsAgent,
80
+ });
81
+ if (res.status !== 200 || !res.data?.token) {
82
+ throw new WPError(`JWT token fetch failed: HTTP ${res.status} ${res.data?.message ?? ""} — ` +
83
+ `verify the JWT plugin is installed at namespace '${this.jwtNamespace}' ` +
84
+ `and that JWT_AUTH_SECRET_KEY is set in wp-config.php.`, {
85
+ status: res.status,
86
+ code: res.data?.code ?? "jwt_token_fetch_failed",
87
+ details: res.data,
88
+ });
89
+ }
90
+ this.jwtToken = res.data.token;
91
+ this.client.defaults.headers["Authorization"] = `Bearer ${this.jwtToken}`;
92
+ }
93
+ // ─── Internals ────────────────────────────────────────────────────────────
94
+ buildQuery(params) {
95
+ if (!params)
96
+ return {};
97
+ const query = {};
98
+ for (const [key, value] of Object.entries(params)) {
99
+ if (value === undefined || value === null)
100
+ continue;
101
+ if (Array.isArray(value)) {
102
+ query[key] = value.join(",");
103
+ }
104
+ else if (typeof value === "boolean") {
105
+ query[key] = value ? 1 : 0;
106
+ }
107
+ else if (typeof value === "string" || typeof value === "number") {
108
+ query[key] = value;
109
+ }
110
+ }
111
+ return query;
112
+ }
113
+ /**
114
+ * Wrap axios request with retry on transient errors and structured error
115
+ * formatting that surfaces WordPress' own `code`/`message` fields.
116
+ */
117
+ async request(config) {
118
+ // For WooCommerce routes, prefer Consumer Key/Secret if provided.
119
+ const finalConfig = this.applyWcAuth(config);
120
+ let attempt = 0;
121
+ // eslint-disable-next-line no-constant-condition
122
+ while (true) {
123
+ attempt++;
124
+ try {
125
+ const res = await this.client.request(finalConfig);
126
+ if (res.status >= 200 && res.status < 300) {
127
+ return {
128
+ data: res.data,
129
+ headers: (res.headers ?? {}),
130
+ };
131
+ }
132
+ // Non-2xx
133
+ const shouldRetry = attempt <= this.maxRetries && RETRY_STATUSES.has(res.status);
134
+ if (shouldRetry) {
135
+ await this.sleep(this.backoffMs(attempt, res.headers));
136
+ continue;
137
+ }
138
+ throw this.toError(res.status, res.data, finalConfig);
139
+ }
140
+ catch (err) {
141
+ if (err instanceof WPError)
142
+ throw err;
143
+ const ax = err;
144
+ const transient = ax.code === "ECONNRESET" ||
145
+ ax.code === "ETIMEDOUT" ||
146
+ ax.code === "EAI_AGAIN" ||
147
+ ax.code === "ECONNABORTED";
148
+ if (transient && attempt <= this.maxRetries) {
149
+ await this.sleep(this.backoffMs(attempt));
150
+ continue;
151
+ }
152
+ throw new WPError(`WordPress request failed: ${ax.message ?? String(err)}`, { cause: err, status: 0, code: ax.code ?? "network_error" });
153
+ }
154
+ }
155
+ }
156
+ backoffMs(attempt, headers) {
157
+ // Honor Retry-After header if present.
158
+ if (headers && typeof headers === "object") {
159
+ const h = headers;
160
+ const retryAfter = h["retry-after"] ?? h["Retry-After"];
161
+ if (retryAfter) {
162
+ const secs = Number(retryAfter);
163
+ if (!Number.isNaN(secs))
164
+ return secs * 1000;
165
+ }
166
+ }
167
+ // Exponential backoff with jitter, capped at 8s.
168
+ return Math.min(8000, 250 * 2 ** (attempt - 1)) + Math.random() * 200;
169
+ }
170
+ sleep(ms) {
171
+ return new Promise((r) => setTimeout(r, ms));
172
+ }
173
+ /**
174
+ * If the request targets WooCommerce (/wc/* or /wc-analytics/*) AND
175
+ * Consumer Key/Secret are configured, attach them as query params and
176
+ * strip the Authorization header. WC accepts CK/CS over HTTPS as basic
177
+ * params; we always assume HTTPS and rely on TLS for confidentiality.
178
+ */
179
+ applyWcAuth(config) {
180
+ if (!this.wcKey || !this.wcSecret)
181
+ return config;
182
+ const url = config.url ?? "";
183
+ const isWc = url.startsWith("/wc/") || url.startsWith("/wc-analytics/");
184
+ if (!isWc)
185
+ return config;
186
+ return {
187
+ ...config,
188
+ params: {
189
+ ...(config.params ?? {}),
190
+ consumer_key: this.wcKey,
191
+ consumer_secret: this.wcSecret,
192
+ },
193
+ headers: {
194
+ ...(config.headers ?? {}),
195
+ // Avoid sending double credentials.
196
+ Authorization: undefined,
197
+ },
198
+ };
199
+ }
200
+ toError(status, body, config) {
201
+ const url = `${config.method?.toUpperCase() ?? "GET"} ${config.url}`;
202
+ if (body &&
203
+ typeof body === "object" &&
204
+ "message" in body &&
205
+ "code" in body) {
206
+ const b = body;
207
+ return new WPError(`${b.message} (${b.code}) [${url}]`, {
208
+ status,
209
+ code: b.code,
210
+ details: b.data,
211
+ });
212
+ }
213
+ return new WPError(`HTTP ${status} from ${url}`, {
214
+ status,
215
+ code: "http_error",
216
+ details: body,
217
+ });
218
+ }
219
+ // ─── Generic core (used by CPT/taxonomy generic tools) ────────────────────
220
+ /**
221
+ * Generic list call. `route` is e.g. "wp/v2/posts" or "wc/v3/products".
222
+ */
223
+ async list(route, params) {
224
+ const { data, headers } = await this.request({
225
+ method: "GET",
226
+ url: `/${route}`,
227
+ params: this.buildQuery(params),
228
+ });
229
+ return {
230
+ data,
231
+ total: Number(headers["x-wp-total"] ?? data.length ?? 0),
232
+ pages: Number(headers["x-wp-totalpages"] ?? 1),
233
+ };
234
+ }
235
+ async get(route, id, params) {
236
+ const { data } = await this.request({
237
+ method: "GET",
238
+ url: `/${route}/${id}`,
239
+ params: this.buildQuery(params),
240
+ });
241
+ return data;
242
+ }
243
+ async create(route, body) {
244
+ const { data } = await this.request({
245
+ method: "POST",
246
+ url: `/${route}`,
247
+ data: body,
248
+ headers: { "Content-Type": "application/json" },
249
+ });
250
+ return data;
251
+ }
252
+ async update(route, id, body) {
253
+ const { data } = await this.request({
254
+ method: "POST", // WP accepts POST for updates and many hosts strip PUT.
255
+ url: `/${route}/${id}`,
256
+ data: body,
257
+ headers: { "Content-Type": "application/json" },
258
+ });
259
+ return data;
260
+ }
261
+ async remove(route, id, force = false) {
262
+ const { data } = await this.request({
263
+ method: "DELETE",
264
+ url: `/${route}/${id}`,
265
+ params: { force },
266
+ });
267
+ return data;
268
+ }
269
+ /**
270
+ * Low-level escape hatch: send an arbitrary GET / POST to a fully
271
+ * specified path. Used by tools that work with string IDs (e.g. block
272
+ * templates) or non-`wp/v2` routes that don't fit list/get/create/update.
273
+ */
274
+ async raw(opts) {
275
+ const { data } = await this.request({
276
+ method: opts.method ?? "GET",
277
+ url: opts.path.startsWith("/") ? opts.path : `/${opts.path}`,
278
+ params: opts.query ? this.buildQuery(opts.query) : undefined,
279
+ data: opts.body,
280
+ headers: opts.body ? { "Content-Type": "application/json" } : undefined,
281
+ });
282
+ return data;
283
+ }
284
+ // ─── Convenience wrappers (wp/v2 namespace) ───────────────────────────────
285
+ posts = {
286
+ list: (p) => this.list("wp/v2/posts", p),
287
+ get: (id, p) => this.get("wp/v2/posts", id, p),
288
+ create: (body) => this.create("wp/v2/posts", body),
289
+ update: (id, body) => this.update("wp/v2/posts", id, body),
290
+ remove: (id, force = false) => this.remove("wp/v2/posts", id, force),
291
+ revisions: (id, p) => this.list(`wp/v2/posts/${id}/revisions`, p),
292
+ };
293
+ pages = {
294
+ list: (p) => this.list("wp/v2/pages", p),
295
+ get: (id, p) => this.get("wp/v2/pages", id, p),
296
+ create: (body) => this.create("wp/v2/pages", body),
297
+ update: (id, body) => this.update("wp/v2/pages", id, body),
298
+ remove: (id, force = false) => this.remove("wp/v2/pages", id, force),
299
+ };
300
+ media = {
301
+ list: (p) => this.list("wp/v2/media", p),
302
+ get: (id) => this.get("wp/v2/media", id),
303
+ update: (id, body) => this.update("wp/v2/media", id, body),
304
+ remove: (id, force = true) => this.remove("wp/v2/media", id, force),
305
+ };
306
+ categories = {
307
+ list: (p) => this.list("wp/v2/categories", p),
308
+ get: (id) => this.get("wp/v2/categories", id),
309
+ create: (body) => this.create("wp/v2/categories", body),
310
+ update: (id, body) => this.update("wp/v2/categories", id, body),
311
+ remove: (id, force = false) => this.remove("wp/v2/categories", id, force),
312
+ };
313
+ tags = {
314
+ list: (p) => this.list("wp/v2/tags", p),
315
+ get: (id) => this.get("wp/v2/tags", id),
316
+ create: (body) => this.create("wp/v2/tags", body),
317
+ update: (id, body) => this.update("wp/v2/tags", id, body),
318
+ remove: (id, force = false) => this.remove("wp/v2/tags", id, force),
319
+ };
320
+ comments = {
321
+ list: (p) => this.list("wp/v2/comments", p),
322
+ get: (id) => this.get("wp/v2/comments", id),
323
+ create: (body) => this.create("wp/v2/comments", body),
324
+ update: (id, body) => this.update("wp/v2/comments", id, body),
325
+ remove: (id, force = false) => this.remove("wp/v2/comments", id, force),
326
+ };
327
+ users = {
328
+ list: (p) => this.list("wp/v2/users", p),
329
+ get: (id) => this.get("wp/v2/users", id),
330
+ me: async () => {
331
+ const { data } = await this.request({ method: "GET", url: "/wp/v2/users/me" });
332
+ return data;
333
+ },
334
+ create: (body) => this.create("wp/v2/users", body),
335
+ update: (id, body) => this.update("wp/v2/users", id, body),
336
+ remove: async (id, reassignTo) => {
337
+ const { data } = await this.request({
338
+ method: "DELETE",
339
+ url: `/wp/v2/users/${id}`,
340
+ params: { force: true, reassign: reassignTo },
341
+ });
342
+ return data;
343
+ },
344
+ };
345
+ // ─── Site / discovery ─────────────────────────────────────────────────────
346
+ async siteInfo() {
347
+ // The /wp-json index does not require auth. Use a clean axios call so we
348
+ // don't accidentally send Basic auth to a public endpoint.
349
+ const res = await axios.get(`${this.baseUrl}/wp-json`, {
350
+ timeout: 15_000,
351
+ headers: { "User-Agent": this.userAgent },
352
+ validateStatus: () => true,
353
+ });
354
+ if (res.status !== 200) {
355
+ throw new WPError(`Cannot reach REST root: HTTP ${res.status}`, {
356
+ status: res.status,
357
+ code: "rest_root_unreachable",
358
+ });
359
+ }
360
+ return res.data;
361
+ }
362
+ async settings() {
363
+ const { data } = await this.request({
364
+ method: "GET",
365
+ url: "/wp/v2/settings",
366
+ });
367
+ return data;
368
+ }
369
+ async updateSettings(body) {
370
+ const { data } = await this.request({
371
+ method: "POST",
372
+ url: "/wp/v2/settings",
373
+ data: body,
374
+ });
375
+ return data;
376
+ }
377
+ async types() {
378
+ const { data } = await this.request({ method: "GET", url: "/wp/v2/types" });
379
+ return data;
380
+ }
381
+ async taxonomies() {
382
+ const { data } = await this.request({
383
+ method: "GET",
384
+ url: "/wp/v2/taxonomies",
385
+ });
386
+ return data;
387
+ }
388
+ async search(query, params) {
389
+ const { data, headers } = await this.request({
390
+ method: "GET",
391
+ url: "/wp/v2/search",
392
+ params: { search: query, ...params },
393
+ });
394
+ return {
395
+ data,
396
+ total: Number(headers["x-wp-total"] ?? 0),
397
+ };
398
+ }
399
+ // ─── Media upload (multipart) ─────────────────────────────────────────────
400
+ async uploadMediaFromFile(filePath, metadata) {
401
+ const fileContent = readFileSync(filePath);
402
+ const fileName = basename(filePath);
403
+ const form = new FormData();
404
+ form.append("file", fileContent, fileName);
405
+ if (metadata?.title)
406
+ form.append("title", metadata.title);
407
+ if (metadata?.alt_text)
408
+ form.append("alt_text", metadata.alt_text);
409
+ if (metadata?.caption)
410
+ form.append("caption", metadata.caption);
411
+ if (metadata?.description)
412
+ form.append("description", metadata.description);
413
+ if (metadata?.post != null)
414
+ form.append("post", String(metadata.post));
415
+ const { data } = await this.request({
416
+ method: "POST",
417
+ url: "/wp/v2/media",
418
+ data: form,
419
+ headers: {
420
+ ...form.getHeaders(),
421
+ "Content-Disposition": `attachment; filename="${fileName}"`,
422
+ },
423
+ maxBodyLength: Infinity,
424
+ maxContentLength: Infinity,
425
+ });
426
+ return data;
427
+ }
428
+ async uploadMediaFromUrl(fileUrl, metadata) {
429
+ const res = await axios.get(fileUrl, {
430
+ responseType: "arraybuffer",
431
+ timeout: 60_000,
432
+ });
433
+ const buf = Buffer.from(res.data);
434
+ const fileName = basename(new URL(fileUrl).pathname) || "upload.bin";
435
+ const form = new FormData();
436
+ form.append("file", buf, fileName);
437
+ if (metadata?.title)
438
+ form.append("title", metadata.title);
439
+ if (metadata?.alt_text)
440
+ form.append("alt_text", metadata.alt_text);
441
+ if (metadata?.caption)
442
+ form.append("caption", metadata.caption);
443
+ if (metadata?.description)
444
+ form.append("description", metadata.description);
445
+ if (metadata?.post != null)
446
+ form.append("post", String(metadata.post));
447
+ const { data } = await this.request({
448
+ method: "POST",
449
+ url: "/wp/v2/media",
450
+ data: form,
451
+ headers: {
452
+ ...form.getHeaders(),
453
+ "Content-Disposition": `attachment; filename="${fileName}"`,
454
+ },
455
+ maxBodyLength: Infinity,
456
+ maxContentLength: Infinity,
457
+ });
458
+ return data;
459
+ }
460
+ // ─── Batch (/batch/v1) — WP 5.6+ ─────────────────────────────────────────
461
+ /**
462
+ * Discover the batch endpoint capabilities. Returns the parsed OPTIONS body
463
+ * which contains `endpoints[0].args.requests.maxItems` (default 25).
464
+ */
465
+ async batchOptions() {
466
+ const { data } = await this.request({
467
+ method: "OPTIONS",
468
+ url: "/batch/v1",
469
+ });
470
+ return data;
471
+ }
472
+ /**
473
+ * Send a batch of write operations to /batch/v1.
474
+ * GET is not supported by core; only POST/PUT/PATCH/DELETE.
475
+ */
476
+ async batch(requests, validation = "normal") {
477
+ const { data } = await this.request({
478
+ method: "POST",
479
+ url: "/batch/v1",
480
+ data: { validation, requests },
481
+ headers: { "Content-Type": "application/json" },
482
+ });
483
+ return data;
484
+ }
485
+ // ─── JWT helper ──────────────────────────────────────────────────────────
486
+ /** Validate the current JWT against /token/validate. */
487
+ async validateJwt() {
488
+ if (this.authMode !== "jwt") {
489
+ throw new WPError("Server is not running in JWT mode", {
490
+ status: 0,
491
+ code: "wrong_auth_mode",
492
+ });
493
+ }
494
+ const { data } = await this.request({
495
+ method: "POST",
496
+ url: `/${this.jwtNamespace}/token/validate`,
497
+ });
498
+ return data;
499
+ }
500
+ }
501
+ /**
502
+ * Custom error class that carries the WP REST API error code & status.
503
+ * Tool runners convert this to MCP `isError` results.
504
+ */
505
+ export class WPError extends Error {
506
+ status;
507
+ code;
508
+ details;
509
+ constructor(message, opts) {
510
+ super(message);
511
+ this.name = "WPError";
512
+ this.status = opts.status;
513
+ this.code = opts.code;
514
+ this.details = opts.details;
515
+ if (opts.cause)
516
+ this.cause = opts.cause;
517
+ }
518
+ }
519
+ //# sourceMappingURL=wordpress-client.js.map
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@mangerik/wordpress-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP Server for WordPress REST API — connect AI agents to WordPress (posts, pages, media, users, custom post types, WooCommerce, Yoast / Rank Math, block themes, multisite, batch).",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "wordpress-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*.js",
12
+ "dist/**/*.d.ts",
13
+ "README.md",
14
+ "LICENSE",
15
+ "CHANGELOG.md",
16
+ ".env.example"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc && node scripts/postbuild.mjs",
20
+ "start": "node dist/index.js",
21
+ "dev": "tsx src/index.ts",
22
+ "watch": "tsc --watch",
23
+ "typecheck": "tsc --noEmit",
24
+ "smoke": "node scripts/smoke-test.mjs",
25
+ "prepublishOnly": "npm run typecheck && npm run build && npm run smoke"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.0.0",
29
+ "axios": "^1.7.0",
30
+ "form-data": "^4.0.0",
31
+ "zod": "^3.23.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "tsx": "^4.0.0",
36
+ "typescript": "^5.4.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "keywords": [
42
+ "mcp",
43
+ "model-context-protocol",
44
+ "wordpress",
45
+ "wp-rest-api",
46
+ "woocommerce",
47
+ "yoast",
48
+ "rank-math",
49
+ "ai",
50
+ "agent",
51
+ "claude",
52
+ "kiro",
53
+ "llm"
54
+ ],
55
+ "homepage": "https://github.com/mangerik/wordpress-mcp#readme",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/mangerik/wordpress-mcp.git"
59
+ },
60
+ "bugs": {
61
+ "url": "https://github.com/mangerik/wordpress-mcp/issues"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "license": "MIT"
67
+ }