@wppoland/woocommerce-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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +184 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WPPoland (wppoland.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# woocommerce-mcp
|
|
2
|
+
|
|
3
|
+
A small, read-only [Model Context Protocol](https://modelcontextprotocol.io) server for **WordPress + WooCommerce**. It lets Claude (or any MCP client) answer questions about a live store — products, orders, sales, and blog posts — over the official REST APIs. No writes, no plugins to install on the store: it talks to the existing WordPress/WooCommerce REST endpoints.
|
|
4
|
+
|
|
5
|
+
Built and maintained by [WPPoland](https://wppoland.com/en/) — senior WordPress & WooCommerce engineering. If you need this wired into a real store stack, we build [WooCommerce ERP and API integrations](https://wppoland.com/en/woocommerce-erp-integration/) and [enterprise e-commerce architecture](https://wppoland.com/en/enterprise-solutions/) (headless, integrations, AI-ready data).
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Tool | What it does | Needs WooCommerce keys |
|
|
10
|
+
|------|--------------|:---:|
|
|
11
|
+
| `list_products` | List / search products (name, sku, price, stock, permalink) | yes |
|
|
12
|
+
| `get_product` | Full details for one product by id | yes |
|
|
13
|
+
| `list_orders` | Recent orders, newest first, optional status filter | yes |
|
|
14
|
+
| `sales_report` | Sales totals for a period (week / month / last_month / year) | yes |
|
|
15
|
+
| `search_posts` | Search published blog posts (public WP REST API) | no |
|
|
16
|
+
|
|
17
|
+
Everything is **read-only**. The server never creates, edits, or deletes anything in the store.
|
|
18
|
+
|
|
19
|
+
## Install & build
|
|
20
|
+
|
|
21
|
+
From npm (package name is scoped - the unscoped `woocommerce-mcp` name is locked on the registry):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @wppoland/woocommerce-mcp
|
|
25
|
+
# or: npx @wppoland/woocommerce-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
From source:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/wppoland/woocommerce-mcp.git
|
|
32
|
+
cd woocommerce-mcp
|
|
33
|
+
npm install
|
|
34
|
+
npm run build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configure
|
|
38
|
+
|
|
39
|
+
Set three environment variables:
|
|
40
|
+
|
|
41
|
+
| Var | Required | Example |
|
|
42
|
+
|-----|:---:|---------|
|
|
43
|
+
| `WP_URL` | yes | `https://shop.example.com` |
|
|
44
|
+
| `WC_CONSUMER_KEY` | for `wc_*` tools | `ck_xxx` |
|
|
45
|
+
| `WC_CONSUMER_SECRET` | for `wc_*` tools | `cs_xxx` |
|
|
46
|
+
|
|
47
|
+
Create the WooCommerce keys in **WooCommerce → Settings → Advanced → REST API → Add key** with **Read** permission. `search_posts` works without keys against any public WordPress site.
|
|
48
|
+
|
|
49
|
+
> The keys are sent to your own store over HTTPS as REST query auth. Use HTTPS, and give the key **Read** access only.
|
|
50
|
+
|
|
51
|
+
## Use with Claude Desktop / Claude Code
|
|
52
|
+
|
|
53
|
+
Add to your MCP client config (e.g. `claude_desktop_config.json`):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"woocommerce": {
|
|
59
|
+
"command": "node",
|
|
60
|
+
"args": ["/absolute/path/to/woocommerce-mcp/dist/index.js"],
|
|
61
|
+
"env": {
|
|
62
|
+
"WP_URL": "https://shop.example.com",
|
|
63
|
+
"WC_CONSUMER_KEY": "ck_xxx",
|
|
64
|
+
"WC_CONSUMER_SECRET": "cs_xxx"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then ask things like *"What were last month's WooCommerce sales?"* or *"List the 5 most recent orders that are on hold."*
|
|
72
|
+
|
|
73
|
+
## Articles (off-site)
|
|
74
|
+
|
|
75
|
+
Field notes published on DEV (not duplicates of wppoland.com pages):
|
|
76
|
+
|
|
77
|
+
- [A read-only MCP server for WooCommerce: what AI agents actually need from a store](https://dev.to/wppolandcom/a-read-only-mcp-server-for-woocommerce-what-ai-agents-actually-need-from-a-store-3fk6)
|
|
78
|
+
- [Syncing a wholesaler's API into WooCommerce without overselling or melting the server](https://dev.to/wppolandcom/syncing-a-wholesalers-api-into-woocommerce-without-overselling-or-melting-the-server-38ao)
|
|
79
|
+
- [Twelve months after migrating wppoland.com from WordPress to Astro on Cloudflare Pages](https://dev.to/wppolandcom/twelve-months-after-migrating-wppolandcom-from-wordpress-to-astro-on-cloudflare-pages-11fi)
|
|
80
|
+
|
|
81
|
+
Show HN: [discussion](https://news.ycombinator.com/item?id=48815903)
|
|
82
|
+
|
|
83
|
+
## Verify
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm run check # builds, then asserts all five tools register (no network/credentials needed)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Notes
|
|
90
|
+
|
|
91
|
+
- Node 18+ (uses the built-in `fetch`).
|
|
92
|
+
- Logs go to stderr so they never corrupt the stdio MCP protocol on stdout.
|
|
93
|
+
- API errors are surfaced with the store's message; credentials are never echoed.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT © [WPPoland](https://wppoland.com/en/)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* woocommerce-mcp — a Model Context Protocol server for WordPress + WooCommerce.
|
|
4
|
+
*
|
|
5
|
+
* Gives Claude (and any MCP client) read access to a store over the public
|
|
6
|
+
* WordPress and authenticated WooCommerce REST APIs: products, single product,
|
|
7
|
+
* recent orders, a sales report, and blog posts. Read-only by design — it never
|
|
8
|
+
* writes to the store.
|
|
9
|
+
*
|
|
10
|
+
* Configure with env vars:
|
|
11
|
+
* WP_URL e.g. https://shop.example.com (required)
|
|
12
|
+
* WC_CONSUMER_KEY WooCommerce REST API key (required for wc_* tools)
|
|
13
|
+
* WC_CONSUMER_SECRET WooCommerce REST API secret (required for wc_* tools)
|
|
14
|
+
*
|
|
15
|
+
* Built by wppoland.com — WordPress & WooCommerce engineering.
|
|
16
|
+
*/
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
export declare const VERSION = "0.1.0";
|
|
19
|
+
export declare function createServer(): McpServer;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* woocommerce-mcp — a Model Context Protocol server for WordPress + WooCommerce.
|
|
4
|
+
*
|
|
5
|
+
* Gives Claude (and any MCP client) read access to a store over the public
|
|
6
|
+
* WordPress and authenticated WooCommerce REST APIs: products, single product,
|
|
7
|
+
* recent orders, a sales report, and blog posts. Read-only by design — it never
|
|
8
|
+
* writes to the store.
|
|
9
|
+
*
|
|
10
|
+
* Configure with env vars:
|
|
11
|
+
* WP_URL e.g. https://shop.example.com (required)
|
|
12
|
+
* WC_CONSUMER_KEY WooCommerce REST API key (required for wc_* tools)
|
|
13
|
+
* WC_CONSUMER_SECRET WooCommerce REST API secret (required for wc_* tools)
|
|
14
|
+
*
|
|
15
|
+
* Built by wppoland.com — WordPress & WooCommerce engineering.
|
|
16
|
+
*/
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
export const VERSION = "0.1.0";
|
|
21
|
+
/** Read + validate env at call time (not import time) so tooling/tests don't need credentials. */
|
|
22
|
+
function loadConfig(requireWoo) {
|
|
23
|
+
const wpUrl = (process.env.WP_URL ?? "").trim().replace(/\/+$/, "");
|
|
24
|
+
if (!wpUrl)
|
|
25
|
+
throw new Error("Missing required env var WP_URL (e.g. https://shop.example.com)");
|
|
26
|
+
if (!/^https?:\/\//.test(wpUrl))
|
|
27
|
+
throw new Error("WP_URL must start with http:// or https://");
|
|
28
|
+
const key = (process.env.WC_CONSUMER_KEY ?? "").trim();
|
|
29
|
+
const secret = (process.env.WC_CONSUMER_SECRET ?? "").trim();
|
|
30
|
+
if (requireWoo && (!key || !secret)) {
|
|
31
|
+
throw new Error("This tool needs WC_CONSUMER_KEY and WC_CONSUMER_SECRET (WooCommerce > Settings > Advanced > REST API).");
|
|
32
|
+
}
|
|
33
|
+
return { wpUrl, key, secret };
|
|
34
|
+
}
|
|
35
|
+
async function apiGet(base, params) {
|
|
36
|
+
const url = new URL(base);
|
|
37
|
+
for (const [k, v] of Object.entries(params)) {
|
|
38
|
+
if (v !== undefined && v !== "")
|
|
39
|
+
url.searchParams.set(k, String(v));
|
|
40
|
+
}
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
throw new Error(`Network error reaching ${url.host}: ${e instanceof Error ? e.message : String(e)}`);
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
// Surface the API error message but never echo the credentials in the URL.
|
|
50
|
+
let detail = "";
|
|
51
|
+
try {
|
|
52
|
+
const body = (await res.json());
|
|
53
|
+
detail = body?.message ? ` — ${body.message}` : "";
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* ignore non-JSON error bodies */
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`API ${res.status} ${res.statusText}${detail}`);
|
|
59
|
+
}
|
|
60
|
+
return res.json();
|
|
61
|
+
}
|
|
62
|
+
function wc(cfg, path, params = {}) {
|
|
63
|
+
return apiGet(`${cfg.wpUrl}/wp-json/wc/v3/${path}`, {
|
|
64
|
+
consumer_key: cfg.key,
|
|
65
|
+
consumer_secret: cfg.secret,
|
|
66
|
+
...params,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function wp(cfg, path, params = {}) {
|
|
70
|
+
return apiGet(`${cfg.wpUrl}/wp-json/wp/v2/${path}`, params);
|
|
71
|
+
}
|
|
72
|
+
const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
|
|
73
|
+
const fail = (e) => ({
|
|
74
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
75
|
+
isError: true,
|
|
76
|
+
});
|
|
77
|
+
export function createServer() {
|
|
78
|
+
const server = new McpServer({ name: "woocommerce-mcp", version: VERSION });
|
|
79
|
+
server.registerTool("list_products", {
|
|
80
|
+
title: "List products",
|
|
81
|
+
description: "List or search WooCommerce products. Returns id, name, sku, price, stock status and permalink.",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
search: z.string().optional().describe("Search term to filter products by name/sku"),
|
|
84
|
+
per_page: z.number().int().min(1).max(100).optional().describe("Results per page (default 10)"),
|
|
85
|
+
status: z.enum(["any", "draft", "pending", "private", "publish"]).optional().describe("Product status filter"),
|
|
86
|
+
},
|
|
87
|
+
}, async ({ search, per_page, status }) => {
|
|
88
|
+
try {
|
|
89
|
+
const cfg = loadConfig(true);
|
|
90
|
+
const data = (await wc(cfg, "products", { search, per_page: per_page ?? 10, status }));
|
|
91
|
+
return ok(data.map((p) => ({
|
|
92
|
+
id: p.id, name: p.name, sku: p.sku, price: p.price,
|
|
93
|
+
stock_status: p.stock_status, permalink: p.permalink,
|
|
94
|
+
})));
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return fail(e);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
server.registerTool("get_product", {
|
|
101
|
+
title: "Get product",
|
|
102
|
+
description: "Get a single WooCommerce product by id, with full details.",
|
|
103
|
+
inputSchema: { id: z.number().int().positive().describe("Product id") },
|
|
104
|
+
}, async ({ id }) => {
|
|
105
|
+
try {
|
|
106
|
+
return ok(await wc(loadConfig(true), `products/${id}`));
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return fail(e);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
server.registerTool("list_orders", {
|
|
113
|
+
title: "List orders",
|
|
114
|
+
description: "List recent WooCommerce orders, newest first. Optionally filter by status.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
per_page: z.number().int().min(1).max(100).optional().describe("Results per page (default 10)"),
|
|
117
|
+
status: z
|
|
118
|
+
.enum(["any", "pending", "processing", "on-hold", "completed", "cancelled", "refunded", "failed"])
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Order status filter"),
|
|
121
|
+
},
|
|
122
|
+
}, async ({ per_page, status }) => {
|
|
123
|
+
try {
|
|
124
|
+
const cfg = loadConfig(true);
|
|
125
|
+
const data = (await wc(cfg, "orders", { per_page: per_page ?? 10, status, orderby: "date", order: "desc" }));
|
|
126
|
+
return ok(data.map((o) => ({
|
|
127
|
+
id: o.id, number: o.number, status: o.status, total: o.total, currency: o.currency,
|
|
128
|
+
date_created: o.date_created, customer: o.billing?.email,
|
|
129
|
+
})));
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
return fail(e);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
server.registerTool("sales_report", {
|
|
136
|
+
title: "Sales report",
|
|
137
|
+
description: "WooCommerce sales totals for a period (week, month, last_month, year). Gross sales, orders, items.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
period: z.enum(["week", "month", "last_month", "year"]).optional().describe("Reporting period (default week)"),
|
|
140
|
+
},
|
|
141
|
+
}, async ({ period }) => {
|
|
142
|
+
try {
|
|
143
|
+
return ok(await wc(loadConfig(true), "reports/sales", { period: period ?? "week" }));
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
return fail(e);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
server.registerTool("search_posts", {
|
|
150
|
+
title: "Search posts",
|
|
151
|
+
description: "Search published WordPress blog posts (public REST API, no WooCommerce keys required).",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
search: z.string().min(1).describe("Search term"),
|
|
154
|
+
per_page: z.number().int().min(1).max(50).optional().describe("Results per page (default 10)"),
|
|
155
|
+
},
|
|
156
|
+
}, async ({ search, per_page }) => {
|
|
157
|
+
try {
|
|
158
|
+
const cfg = loadConfig(false);
|
|
159
|
+
const data = (await wp(cfg, "posts", { search, per_page: per_page ?? 10, _fields: "id,link,title,date,excerpt" }));
|
|
160
|
+
return ok(data.map((p) => ({
|
|
161
|
+
id: p.id, link: p.link, date: p.date,
|
|
162
|
+
title: p.title?.rendered,
|
|
163
|
+
})));
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
return fail(e);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return server;
|
|
170
|
+
}
|
|
171
|
+
async function main() {
|
|
172
|
+
const server = createServer();
|
|
173
|
+
await server.connect(new StdioServerTransport());
|
|
174
|
+
// stdio transport: logs must go to stderr so they don't corrupt the protocol on stdout.
|
|
175
|
+
console.error(`woocommerce-mcp v${VERSION} ready (stdio)`);
|
|
176
|
+
}
|
|
177
|
+
// Only start the stdio server when run directly, not when imported (tests import createServer).
|
|
178
|
+
const invokedDirectly = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
|
|
179
|
+
if (invokedDirectly) {
|
|
180
|
+
main().catch((err) => {
|
|
181
|
+
console.error(err);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
});
|
|
184
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wppoland/woocommerce-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol (MCP) server for WordPress and WooCommerce. Gives Claude and other AI agents read access to products, orders, sales reports and posts over the REST API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.wppoland/woocommerce-mcp",
|
|
7
|
+
"bin": {
|
|
8
|
+
"woocommerce-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/wppoland/woocommerce-mcp.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/wppoland/woocommerce-mcp/issues"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"check": "npm run build && node test/smoke.mjs",
|
|
28
|
+
"start": "node dist/index.js",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"wordpress",
|
|
35
|
+
"woocommerce",
|
|
36
|
+
"claude",
|
|
37
|
+
"ai-agents",
|
|
38
|
+
"rest-api"
|
|
39
|
+
],
|
|
40
|
+
"author": "wppoland.com",
|
|
41
|
+
"homepage": "https://wppoland.com/en/",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.20.0",
|
|
49
|
+
"typescript": "^5.6.0"
|
|
50
|
+
}
|
|
51
|
+
}
|