@mokoconsulting/mcp-mokosuite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +19 -0
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
- package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogit/actions/resolve-source-dir/action.yml +108 -0
- package/.mokogit/workflows/auto-assign.yml +76 -0
- package/.mokogit/workflows/auto-bump.yml +78 -0
- package/.mokogit/workflows/auto-dev-issue.yml +207 -0
- package/.mokogit/workflows/auto-release.yml +596 -0
- package/.mokogit/workflows/branch-cleanup.yml +60 -0
- package/.mokogit/workflows/cascade-dev.yml +198 -0
- package/.mokogit/workflows/changelog-validation.yml +101 -0
- package/.mokogit/workflows/ci-generic.yml +203 -0
- package/.mokogit/workflows/ci-issue-reporter.yml +75 -0
- package/.mokogit/workflows/cleanup.yml +87 -0
- package/.mokogit/workflows/gitleaks.yml +94 -0
- package/.mokogit/workflows/issue-branch.yml +81 -0
- package/.mokogit/workflows/notify.yml +73 -0
- package/.mokogit/workflows/npm-build-test.yml +83 -0
- package/.mokogit/workflows/npm-publish.yml +126 -0
- package/.mokogit/workflows/npm-sdk-check.yml +110 -0
- package/.mokogit/workflows/npm-tool-inventory.yml +80 -0
- package/.mokogit/workflows/pr-branch-check.yml +90 -0
- package/.mokogit/workflows/pr-check.yml +565 -0
- package/.mokogit/workflows/pre-release.yml +413 -0
- package/.mokogit/workflows/push-notify.yml +43 -0
- package/.mokogit/workflows/rc-revert.yml +72 -0
- package/.mokogit/workflows/repo-health.yml +700 -0
- package/.mokogit/workflows/repository-cleanup.yml +525 -0
- package/.mokogit/workflows/standards-compliance.yml +2507 -0
- package/.mokogit/workflows/sync-version-on-merge.yml +130 -0
- package/.mokogit/workflows/version-set.yml +131 -0
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +52 -0
- package/CODE_OF_CONDUCT.md +70 -0
- package/CONTRIBUTING.md +161 -0
- package/LICENSE +696 -0
- package/Makefile +70 -0
- package/README.md +83 -0
- package/SECURITY.md +34 -0
- package/config.example.json +21 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +124 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +53 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +181 -0
- package/dist/signing.d.ts +14 -0
- package/dist/signing.js +62 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +16 -0
- package/docs/API.md +63 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/INSTALLATION.md +102 -0
- package/docs/index.md +12 -0
- package/package.json +35 -0
- package/src/client.ts +142 -0
- package/src/config.ts +63 -0
- package/src/index.ts +336 -0
- package/src/signing.ts +67 -0
- package/src/types.ts +63 -0
- package/tsconfig.json +19 -0
package/Makefile
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# MCP Server Makefile
|
|
2
|
+
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
PROJECT_NAME := mcp-mokosuite
|
|
6
|
+
PROJECT_VERSION := 1.0.0
|
|
7
|
+
|
|
8
|
+
NPM := npm
|
|
9
|
+
|
|
10
|
+
COLOR_RESET := \033[0m
|
|
11
|
+
COLOR_GREEN := \033[32m
|
|
12
|
+
COLOR_BLUE := \033[34m
|
|
13
|
+
|
|
14
|
+
.PHONY: help
|
|
15
|
+
help: ## Show this help message
|
|
16
|
+
@echo "$(COLOR_BLUE)$(PROJECT_NAME) v$(PROJECT_VERSION)$(COLOR_RESET)"
|
|
17
|
+
@echo ""
|
|
18
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
|
19
|
+
@echo ""
|
|
20
|
+
|
|
21
|
+
.PHONY: install-deps
|
|
22
|
+
install-deps: ## Install dependencies
|
|
23
|
+
@$(NPM) install
|
|
24
|
+
@echo "$(COLOR_GREEN)✓ Dependencies installed$(COLOR_RESET)"
|
|
25
|
+
|
|
26
|
+
MOKO_PLATFORM ?= $(or $(wildcard ../mokocli),$(wildcard $(HOME)/mokocli),$(wildcard /opt/mokocli))
|
|
27
|
+
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
|
28
|
+
|
|
29
|
+
.PHONY: minify
|
|
30
|
+
minify: ## Minify CSS/JS assets
|
|
31
|
+
@echo "Minifying assets..."
|
|
32
|
+
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
|
33
|
+
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
|
34
|
+
elif [ -f "scripts/minify.js" ]; then \
|
|
35
|
+
node scripts/minify.js; \
|
|
36
|
+
else \
|
|
37
|
+
echo "No minify script found"; \
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
.PHONY: build
|
|
41
|
+
build: minify ## Build TypeScript
|
|
42
|
+
@$(NPM) run build
|
|
43
|
+
@echo "$(COLOR_GREEN)✓ Build complete$(COLOR_RESET)"
|
|
44
|
+
|
|
45
|
+
.PHONY: dev
|
|
46
|
+
dev: ## Watch and rebuild on changes
|
|
47
|
+
@$(NPM) run dev
|
|
48
|
+
|
|
49
|
+
.PHONY: clean
|
|
50
|
+
clean: ## Clean build artifacts
|
|
51
|
+
@rm -rf dist
|
|
52
|
+
@echo "$(COLOR_GREEN)✓ Cleaned$(COLOR_RESET)"
|
|
53
|
+
|
|
54
|
+
.PHONY: setup
|
|
55
|
+
setup: ## Run interactive setup wizard
|
|
56
|
+
@$(NPM) run setup
|
|
57
|
+
|
|
58
|
+
.PHONY: start
|
|
59
|
+
start: ## Start the MCP server
|
|
60
|
+
@$(NPM) run start
|
|
61
|
+
|
|
62
|
+
.PHONY: lint
|
|
63
|
+
lint: ## Run linter
|
|
64
|
+
@$(NPM) run lint
|
|
65
|
+
|
|
66
|
+
.PHONY: ci
|
|
67
|
+
ci: install-deps build ## Run CI pipeline
|
|
68
|
+
@echo "$(COLOR_GREEN)✓ CI pipeline complete$(COLOR_RESET)"
|
|
69
|
+
|
|
70
|
+
.DEFAULT_GOAL := help
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# mcp-mokosuite
|
|
2
|
+
|
|
3
|
+
  
|
|
4
|
+
|
|
5
|
+
Model Context Protocol (MCP) server for the **MokoSuite** platform. It lets AI assistants like Claude operate MokoSuite client sites through the MokoSuiteClient Joomla Web Services API — health, dashboards, extensions, plugins, snapshots, cache/updates, content sync, and (via HQ credentials) provisioning, remote login, and fleet-wide user operations.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **24 tools** across the platform surface — reads, actions, and RSA-signed remote-control.
|
|
10
|
+
- **Multi-site** — every named connection is a MokoSuite site (or the HQ control plane); all tools accept an optional `connection` parameter.
|
|
11
|
+
- **Two credential tiers** — a Joomla Web Services Bearer token for read/CRUD, plus an optional `health_api_token` + HQ RSA private key for the remote-control tier.
|
|
12
|
+
- **Safety guards** — destructive fleet-wide operations require an explicit `confirm: true`.
|
|
13
|
+
- **TypeScript-first**, ESM, `node:https` transport (reliable self-signed cert support).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @mokoconsulting/mcp-mokosuite
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or run on demand with `npx`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @mokoconsulting/mcp-mokosuite
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Create `~/.mcp_mokosuite.json` (or point `$MOKOSUITE_CONFIG` at a file) — see [`config.example.json`](config.example.json):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"defaultConnection": "moko-live",
|
|
32
|
+
"connections": {
|
|
33
|
+
"moko-live": { "baseUrl": "https://mokoconsulting.tech", "apiToken": "your-joomla-api-token" }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `apiToken` — a Joomla **Web Services** API token (Users → Manage → API Tokens) for the read/CRUD tools.
|
|
39
|
+
- `healthApiToken` + `rsaPrivateKeyPath` + `keyVersion` *(optional)* — the site's `health_api_token` and HQ's RSA-2048 signing key, required only for the remote-control tools (provision-reset, remote-login, mass user ops).
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Register the server with your MCP client (e.g. Claude Code / Claude Desktop):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"mokosuite": { "command": "npx", "args": ["-y", "@mokoconsulting/mcp-mokosuite"] }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then ask the assistant to use the tools, e.g. *"check the health of the moko-dev site"* → `client_get_health`. Representative tools:
|
|
54
|
+
|
|
55
|
+
| Tier | Tools |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Foundation | `mokosuite_ping`, `list_connections`, `api_request` |
|
|
58
|
+
| Reads | `client_get_health`, `client_get_dashboard`, `client_list_extensions`, `client_list_plugins`, `client_list_snapshots`, `client_list_users`, `client_export_users` |
|
|
59
|
+
| Actions | `client_clear_cache`, `client_check_updates`, `client_install_extension`, `client_toggle_plugin`, `client_create_snapshot`, `client_reset_snapshot`, `client_sync_push` |
|
|
60
|
+
| Remote-control (RSA-signed) | `client_provision_reset`, `client_remote_login`, `client_users_reset_passwords`, `client_users_reset_2fa`, `client_users_disable_all`, `client_users_enable_all`, `client_users_force_logout` |
|
|
61
|
+
|
|
62
|
+
All tools target the default connection unless a `connection` argument names another site. Remote-control tools require the second credential tier and destructive ones require `confirm: true`.
|
|
63
|
+
|
|
64
|
+
## Development
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install # install dependencies
|
|
68
|
+
npm run build # compile TypeScript to dist/
|
|
69
|
+
npm run dev # watch mode
|
|
70
|
+
npm start # run the compiled server
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Contributing
|
|
74
|
+
|
|
75
|
+
Contributions follow [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki/Home). See [CONTRIBUTING.md](CONTRIBUTING.md). Develop on feature branches, use `Authored-by: Moko Consulting` in commits, and open a PR to `main`.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
Licensed under the GNU General Public License v3.0 or later — see [LICENSE](LICENSE).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
*[Moko Consulting](https://mokoconsulting.tech) — [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki/Home)*
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
# FILE INFORMATION
|
|
6
|
+
DEFGROUP: mcp-mokosuite.Documentation
|
|
7
|
+
PATH: /SECURITY.md
|
|
8
|
+
VERSION: 01.00.00
|
|
9
|
+
BRIEF: Security policy and vulnerability disclosure
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
# Security Policy
|
|
13
|
+
|
|
14
|
+
## Supported Versions
|
|
15
|
+
|
|
16
|
+
The latest published `@mokoconsulting/mcp-mokosuite` release receives security updates. Please upgrade before reporting issues against older versions.
|
|
17
|
+
|
|
18
|
+
## Reporting a Vulnerability
|
|
19
|
+
|
|
20
|
+
Do **not** open a public issue for security vulnerabilities. Instead, email **[hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech)** with:
|
|
21
|
+
|
|
22
|
+
- a description of the vulnerability and its impact,
|
|
23
|
+
- steps to reproduce (proof of concept if possible),
|
|
24
|
+
- affected version(s).
|
|
25
|
+
|
|
26
|
+
You can expect an acknowledgement within 5 business days and a remediation plan once the report is triaged. Please allow a reasonable disclosure window before making details public.
|
|
27
|
+
|
|
28
|
+
## Handling Credentials
|
|
29
|
+
|
|
30
|
+
This server connects to MokoSuite sites using Joomla Web Services API tokens and, for the remote-control tier, a `health_api_token` plus an RSA private key. Treat the config file (`~/.mcp_mokosuite.json`) and any key material as secrets:
|
|
31
|
+
|
|
32
|
+
- never commit tokens, `health_api_token`s, or private keys to source control;
|
|
33
|
+
- scope Joomla API tokens to the minimum required (`core.manage`);
|
|
34
|
+
- the RSA private key is HQ-only — keep it off client machines that only need read access.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaultConnection": "moko-live",
|
|
3
|
+
"connections": {
|
|
4
|
+
"moko-live": {
|
|
5
|
+
"baseUrl": "https://mokoconsulting.tech",
|
|
6
|
+
"apiToken": "your-joomla-api-token"
|
|
7
|
+
},
|
|
8
|
+
"cfurs": {
|
|
9
|
+
"baseUrl": "https://clarksvillefurs.com",
|
|
10
|
+
"apiToken": "your-joomla-api-token",
|
|
11
|
+
"healthApiToken": "site-health-api-token-64hex",
|
|
12
|
+
"rsaPrivateKeyPath": "/path/to/hq-signing-key.pem",
|
|
13
|
+
"keyVersion": 1
|
|
14
|
+
},
|
|
15
|
+
"local-dev": {
|
|
16
|
+
"baseUrl": "https://localhost",
|
|
17
|
+
"apiToken": "your-joomla-api-token",
|
|
18
|
+
"insecure": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ApiConnection, ApiResponse } from './types.js';
|
|
2
|
+
export declare class ApiClient {
|
|
3
|
+
private readonly conn;
|
|
4
|
+
private readonly base_url;
|
|
5
|
+
private readonly headers;
|
|
6
|
+
private readonly insecure;
|
|
7
|
+
constructor(conn: ApiConnection);
|
|
8
|
+
get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse>;
|
|
9
|
+
post(endpoint: string, body?: unknown): Promise<ApiResponse>;
|
|
10
|
+
put(endpoint: string, body: unknown): Promise<ApiResponse>;
|
|
11
|
+
patch(endpoint: string, body: unknown): Promise<ApiResponse>;
|
|
12
|
+
delete(endpoint: string): Promise<ApiResponse>;
|
|
13
|
+
/**
|
|
14
|
+
* Remote-control tier: swap the auth to the site's `health_api_token` and attach the
|
|
15
|
+
* RSA signature headers HQ uses, then issue a GET or POST. Requires the connection to
|
|
16
|
+
* carry `healthApiToken` + `rsaPrivateKeyPath`.
|
|
17
|
+
*/
|
|
18
|
+
remoteControl(method: 'GET' | 'POST', endpoint: string, body?: unknown, params?: Record<string, string>): Promise<ApiResponse>;
|
|
19
|
+
private buildUrl;
|
|
20
|
+
private request;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
*
|
|
3
|
+
* This file is part of a Moko Consulting project.
|
|
4
|
+
*
|
|
5
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*
|
|
7
|
+
* FILE INFORMATION
|
|
8
|
+
* DEFGROUP: mcp-mokosuite.Client
|
|
9
|
+
* INGROUP: mcp-mokosuite
|
|
10
|
+
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite
|
|
11
|
+
* PATH: /src/client.ts
|
|
12
|
+
* VERSION: 01.00.00
|
|
13
|
+
* BRIEF: HTTP client for the MokoSuite (Joomla Web Services) API
|
|
14
|
+
*/
|
|
15
|
+
import * as https from 'node:https';
|
|
16
|
+
import * as http from 'node:http';
|
|
17
|
+
import { buildRemoteControlHeaders } from './signing.js';
|
|
18
|
+
// ── Endpoint conventions ────────────────────────────────────────────────
|
|
19
|
+
// Joomla Web Services live under /api/index.php; MokoSuiteClient routes are
|
|
20
|
+
// registered at /v1/mokosuiteclient/* (see MSC_BASE in index.ts).
|
|
21
|
+
const API_PREFIX = '/api/index.php';
|
|
22
|
+
const TIMEOUT_MS = 30_000;
|
|
23
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
24
|
+
export class ApiClient {
|
|
25
|
+
conn;
|
|
26
|
+
base_url;
|
|
27
|
+
headers;
|
|
28
|
+
insecure;
|
|
29
|
+
constructor(conn) {
|
|
30
|
+
this.conn = conn;
|
|
31
|
+
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
|
32
|
+
// Read/CRUD tier: Joomla Web Services authenticate via a Bearer API token.
|
|
33
|
+
this.headers = {
|
|
34
|
+
'Authorization': `Bearer ${conn.apiToken}`,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Accept': 'application/vnd.api+json',
|
|
37
|
+
};
|
|
38
|
+
this.insecure = conn.insecure ?? false;
|
|
39
|
+
}
|
|
40
|
+
async get(endpoint, params) {
|
|
41
|
+
return this.request(this.buildUrl(endpoint, params), 'GET');
|
|
42
|
+
}
|
|
43
|
+
async post(endpoint, body) {
|
|
44
|
+
return this.request(this.buildUrl(endpoint), 'POST', body);
|
|
45
|
+
}
|
|
46
|
+
async put(endpoint, body) {
|
|
47
|
+
return this.request(this.buildUrl(endpoint), 'PUT', body);
|
|
48
|
+
}
|
|
49
|
+
async patch(endpoint, body) {
|
|
50
|
+
return this.request(this.buildUrl(endpoint), 'PATCH', body);
|
|
51
|
+
}
|
|
52
|
+
async delete(endpoint) {
|
|
53
|
+
return this.request(this.buildUrl(endpoint), 'DELETE');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remote-control tier: swap the auth to the site's `health_api_token` and attach the
|
|
57
|
+
* RSA signature headers HQ uses, then issue a GET or POST. Requires the connection to
|
|
58
|
+
* carry `healthApiToken` + `rsaPrivateKeyPath`.
|
|
59
|
+
*/
|
|
60
|
+
async remoteControl(method, endpoint, body, params) {
|
|
61
|
+
const signed = {
|
|
62
|
+
...buildRemoteControlHeaders(this.conn),
|
|
63
|
+
'Authorization': `Bearer ${this.conn.healthApiToken}`,
|
|
64
|
+
};
|
|
65
|
+
return this.request(this.buildUrl(endpoint, params), method, body, signed);
|
|
66
|
+
}
|
|
67
|
+
buildUrl(endpoint, params) {
|
|
68
|
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
69
|
+
const url = new URL(`${this.base_url}${path}`);
|
|
70
|
+
if (params) {
|
|
71
|
+
for (const [key, value] of Object.entries(params)) {
|
|
72
|
+
url.searchParams.set(key, value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return url.toString();
|
|
76
|
+
}
|
|
77
|
+
request(url, method, body, extraHeaders) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const parsed = new URL(url);
|
|
80
|
+
const is_https = parsed.protocol === 'https:';
|
|
81
|
+
const transport = is_https ? https : http;
|
|
82
|
+
const options = {
|
|
83
|
+
hostname: parsed.hostname,
|
|
84
|
+
port: parsed.port || (is_https ? 443 : 80),
|
|
85
|
+
path: parsed.pathname + parsed.search,
|
|
86
|
+
method,
|
|
87
|
+
headers: { ...this.headers, ...(extraHeaders ?? {}) },
|
|
88
|
+
timeout: TIMEOUT_MS,
|
|
89
|
+
};
|
|
90
|
+
if (this.insecure && is_https) {
|
|
91
|
+
options.rejectUnauthorized = false;
|
|
92
|
+
}
|
|
93
|
+
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
|
94
|
+
if (payload) {
|
|
95
|
+
options.headers['Content-Length'] = Buffer.byteLength(payload).toString();
|
|
96
|
+
}
|
|
97
|
+
const req = transport.request(options, (res) => {
|
|
98
|
+
const chunks = [];
|
|
99
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
100
|
+
res.on('end', () => {
|
|
101
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
102
|
+
let data;
|
|
103
|
+
try {
|
|
104
|
+
data = JSON.parse(raw);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
data = raw;
|
|
108
|
+
}
|
|
109
|
+
resolve({ status: res.statusCode ?? 0, data });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
req.on('error', (err) => reject(err));
|
|
113
|
+
req.on('timeout', () => {
|
|
114
|
+
req.destroy();
|
|
115
|
+
reject(new Error('Request timed out'));
|
|
116
|
+
});
|
|
117
|
+
if (payload) {
|
|
118
|
+
req.write(payload);
|
|
119
|
+
}
|
|
120
|
+
req.end();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=client.js.map
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
*
|
|
3
|
+
* This file is part of a Moko Consulting project.
|
|
4
|
+
*
|
|
5
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
6
|
+
*
|
|
7
|
+
* FILE INFORMATION
|
|
8
|
+
* DEFGROUP: mcp-mokosuite.Config
|
|
9
|
+
* INGROUP: mcp-mokosuite
|
|
10
|
+
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite
|
|
11
|
+
* PATH: /src/config.ts
|
|
12
|
+
* VERSION: 01.00.00
|
|
13
|
+
* BRIEF: Configuration loader for MokoSuite MCP connections
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
// ── Customize this ──────────────────────────────────────────────────────
|
|
19
|
+
// Change the filename to match your project (e.g. ".dolibarr-api-mcp.json")
|
|
20
|
+
const CONFIG_FILENAME = '.mcp_mokosuite.json';
|
|
21
|
+
// Change the env var name to match your project
|
|
22
|
+
const CONFIG_ENV_VAR = 'MOKOSUITE_CONFIG';
|
|
23
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
24
|
+
export async function loadConfig() {
|
|
25
|
+
const config_path = process.env[CONFIG_ENV_VAR]
|
|
26
|
+
? resolve(process.env[CONFIG_ENV_VAR])
|
|
27
|
+
: resolve(homedir(), CONFIG_FILENAME);
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(config_path, 'utf-8');
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
|
32
|
+
throw new Error('No connections defined in config');
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
connections: parsed.connections,
|
|
36
|
+
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
throw new Error(`Failed to load config from ${config_path}: ${message}\n` +
|
|
42
|
+
`Create ${config_path} — see config.example.json for format.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getConnection(config, name) {
|
|
46
|
+
const key = name ?? config.defaultConnection;
|
|
47
|
+
const conn = config.connections[key];
|
|
48
|
+
if (!conn) {
|
|
49
|
+
throw new Error(`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
return conn;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=config.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
*
|
|
4
|
+
* This file is part of a Moko Consulting project.
|
|
5
|
+
*
|
|
6
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
7
|
+
*
|
|
8
|
+
* FILE INFORMATION
|
|
9
|
+
* DEFGROUP: mcp-mokosuite.Server
|
|
10
|
+
* INGROUP: mcp-mokosuite
|
|
11
|
+
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite
|
|
12
|
+
* PATH: /src/index.ts
|
|
13
|
+
* VERSION: 01.00.00
|
|
14
|
+
* BRIEF: MCP server entry point — registers MokoSuite (Layer 0 / Platform) tools
|
|
15
|
+
*/
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { loadConfig, getConnection } from './config.js';
|
|
20
|
+
import { ApiClient } from './client.js';
|
|
21
|
+
import { hasRemoteControl } from './signing.js';
|
|
22
|
+
/** Base path for MokoSuiteClient Web Services (appended after /api/index.php). */
|
|
23
|
+
const MSC = '/v1/mokosuiteclient';
|
|
24
|
+
let config;
|
|
25
|
+
function clientFor(connection) {
|
|
26
|
+
return new ApiClient(getConnection(config, connection));
|
|
27
|
+
}
|
|
28
|
+
function formatResponse(res) {
|
|
29
|
+
if (res.status >= 400) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: 'text', text: `Error: HTTP ${res.status}: ${JSON.stringify(res.data, null, 2)}` }],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function text(message) {
|
|
39
|
+
return { content: [{ type: 'text', text: message }] };
|
|
40
|
+
}
|
|
41
|
+
// ── Shared parameter definitions ────────────────────────────────────────
|
|
42
|
+
const ConnectionParam = {
|
|
43
|
+
connection: z.string().optional().describe('Named MokoSuite site/connection from config (uses default if omitted)'),
|
|
44
|
+
};
|
|
45
|
+
const ConfirmParam = {
|
|
46
|
+
confirm: z.boolean().describe('Must be true — this operates on ALL users of the target site and is not reversible'),
|
|
47
|
+
};
|
|
48
|
+
function requireConfirm(confirm) {
|
|
49
|
+
return confirm ? null : text('Refused: set `confirm: true` to run this fleet-wide destructive operation.');
|
|
50
|
+
}
|
|
51
|
+
// ── Server ──────────────────────────────────────────────────────────────
|
|
52
|
+
const server = new McpServer({
|
|
53
|
+
name: 'mcp-mokosuite',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
});
|
|
56
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
57
|
+
// FOUNDATION — connectivity & generic access
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
59
|
+
server.tool('mokosuite_ping', 'Check reachability of a MokoSuite site (calls its dashboard endpoint and reports HTTP status).', { ...ConnectionParam }, async ({ connection }) => {
|
|
60
|
+
const client = clientFor(connection);
|
|
61
|
+
try {
|
|
62
|
+
const res = await client.get(`${MSC}/dashboard`);
|
|
63
|
+
return text(`Reachable — HTTP ${res.status} from ${MSC}/dashboard`);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return text(`Unreachable: ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
server.tool('list_connections', 'List configured MokoSuite connections, their role, and which credential tiers are available.', {}, async () => {
|
|
70
|
+
const lines = Object.entries(config.connections).map(([name, conn]) => {
|
|
71
|
+
const is_default = name === config.defaultConnection ? ' (default)' : '';
|
|
72
|
+
const role = conn.role ?? 'client';
|
|
73
|
+
const tiers = ['read/CRUD'];
|
|
74
|
+
if (hasRemoteControl(conn))
|
|
75
|
+
tiers.push('remote-control');
|
|
76
|
+
return ` ${name}${is_default} [${role}]: ${conn.baseUrl} — tiers: ${tiers.join(', ')}`;
|
|
77
|
+
});
|
|
78
|
+
return text(`Configured MokoSuite connections:\n${lines.join('\n')}`);
|
|
79
|
+
});
|
|
80
|
+
server.tool('api_request', 'Make a raw request to any MokoSuite/Joomla Web Services endpoint (escape hatch). Path is relative to /api/index.php.', {
|
|
81
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),
|
|
82
|
+
endpoint: z.string().describe('Path relative to /api/index.php (e.g. "/v1/mokosuiteclient/health")'),
|
|
83
|
+
body: z.record(z.string(), z.unknown()).optional().describe('Request body for POST/PUT/PATCH'),
|
|
84
|
+
params: z.record(z.string(), z.string()).optional().describe('Query parameters'),
|
|
85
|
+
...ConnectionParam,
|
|
86
|
+
}, async ({ method, endpoint, body, params, connection }) => {
|
|
87
|
+
const client = clientFor(connection);
|
|
88
|
+
switch (method) {
|
|
89
|
+
case 'GET': return formatResponse(await client.get(endpoint, params));
|
|
90
|
+
case 'POST': return formatResponse(await client.post(endpoint, body));
|
|
91
|
+
case 'PUT': return formatResponse(await client.put(endpoint, body));
|
|
92
|
+
case 'PATCH': return formatResponse(await client.patch(endpoint, body));
|
|
93
|
+
case 'DELETE': return formatResponse(await client.delete(endpoint));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
97
|
+
// LAYER 0 — Platform (MokoSuiteClient) — READS (Joomla API token)
|
|
98
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
99
|
+
server.tool('client_get_health', 'Get the full 16-check health diagnostics for a MokoSuite site (db, filesystem, cache, extensions, backup, security, ssl, cron, errors, content, users, mail, seo, template, config).', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/health`)));
|
|
100
|
+
server.tool('client_get_dashboard', 'Get the summarized dashboard for a MokoSuite site (health summary, Joomla/PHP versions, plugin states).', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/dashboard`)));
|
|
101
|
+
server.tool('client_list_extensions', 'List installed extensions on a MokoSuite site, with versions and available update info.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/extensions`)));
|
|
102
|
+
server.tool('client_list_plugins', 'List the MokoSuite feature plugins on a site and their enabled/disabled state.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/plugins`)));
|
|
103
|
+
server.tool('client_list_snapshots', 'List the baseline snapshots available on a MokoSuite site.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/snapshot`)));
|
|
104
|
+
server.tool('client_list_users', 'List Joomla users on a MokoSuite site.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/users`)));
|
|
105
|
+
server.tool('client_export_users', 'Export the users of a MokoSuite site.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).get(`${MSC}/users/export`)));
|
|
106
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
107
|
+
// LAYER 0 — Platform — ACTIONS (Joomla API token, core.manage)
|
|
108
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
109
|
+
server.tool('client_clear_cache', 'Clear all Joomla caches and opcache on a MokoSuite site.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).post(`${MSC}/cache`)));
|
|
110
|
+
server.tool('client_check_updates', 'Run the Joomla update finder on a MokoSuite site and report the number of available updates.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).post(`${MSC}/update`)));
|
|
111
|
+
server.tool('client_install_extension', 'Install an extension on a MokoSuite site from a remote ZIP URL (64MB cap). Privileged — use trusted URLs only.', {
|
|
112
|
+
url: z.string().describe('HTTPS URL of the extension ZIP to install'),
|
|
113
|
+
...ConnectionParam,
|
|
114
|
+
}, async ({ url, connection }) => formatResponse(await clientFor(connection).post(`${MSC}/install`, { url })));
|
|
115
|
+
server.tool('client_toggle_plugin', 'Enable or disable a MokoSuite feature plugin on a site by its extension id.', {
|
|
116
|
+
extension_id: z.number().describe('Joomla extension id of the plugin to toggle'),
|
|
117
|
+
...ConnectionParam,
|
|
118
|
+
}, async ({ extension_id, connection }) => formatResponse(await clientFor(connection).post(`${MSC}/plugins`, { task: 'toggle', extension_id })));
|
|
119
|
+
server.tool('client_create_snapshot', 'Create a baseline snapshot on a MokoSuite site.', {
|
|
120
|
+
label: z.string().optional().describe('Optional label for the snapshot'),
|
|
121
|
+
...ConnectionParam,
|
|
122
|
+
}, async ({ label, connection }) => formatResponse(await clientFor(connection).post(`${MSC}/snapshot`, label ? { label } : {})));
|
|
123
|
+
server.tool('client_reset_snapshot', 'Restore a named baseline snapshot on a MokoSuite site.', {
|
|
124
|
+
name: z.string().describe('Name of the baseline snapshot to restore'),
|
|
125
|
+
...ConnectionParam,
|
|
126
|
+
}, async ({ name, connection }) => formatResponse(await clientFor(connection).post(`${MSC}/reset`, { name })));
|
|
127
|
+
server.tool('client_sync_push', 'Push a content sync from this MokoSuite site to a target site.', {
|
|
128
|
+
target: z.string().optional().describe('Target site identifier/URL for the sync (per site sync config)'),
|
|
129
|
+
...ConnectionParam,
|
|
130
|
+
}, async ({ target, connection }) => formatResponse(await clientFor(connection).post(`${MSC}/sync`, target ? { target } : {})));
|
|
131
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
132
|
+
// LAYER 0 — Platform — REMOTE-CONTROL (RSA-signed; requires HQ credentials)
|
|
133
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
134
|
+
server.tool('client_provision_reset', 'Reset a MokoSuite site for a new client (clears hits, purges version history, regenerates tokens, revokes user tokens). RSA-signed remote-control tier.', { ...ConfirmParam, ...ConnectionParam }, async ({ confirm, connection }) => {
|
|
135
|
+
const guard = requireConfirm(confirm);
|
|
136
|
+
if (guard)
|
|
137
|
+
return guard;
|
|
138
|
+
return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/provision-reset`));
|
|
139
|
+
});
|
|
140
|
+
server.tool('client_remote_login', 'Get a one-time master login URL (60s TTL) for a MokoSuite site. RSA-signed remote-control tier.', { ...ConnectionParam }, async ({ connection }) => formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/remote-login`)));
|
|
141
|
+
server.tool('client_users_reset_passwords', 'Reset passwords for ALL users on a MokoSuite site. RSA-signed remote-control tier.', { ...ConfirmParam, ...ConnectionParam }, async ({ confirm, connection }) => {
|
|
142
|
+
const guard = requireConfirm(confirm);
|
|
143
|
+
if (guard)
|
|
144
|
+
return guard;
|
|
145
|
+
return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/users/reset-passwords`));
|
|
146
|
+
});
|
|
147
|
+
server.tool('client_users_reset_2fa', 'Reset two-factor authentication for ALL users on a MokoSuite site. RSA-signed remote-control tier.', { ...ConfirmParam, ...ConnectionParam }, async ({ confirm, connection }) => {
|
|
148
|
+
const guard = requireConfirm(confirm);
|
|
149
|
+
if (guard)
|
|
150
|
+
return guard;
|
|
151
|
+
return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/users/reset-2fa`));
|
|
152
|
+
});
|
|
153
|
+
server.tool('client_users_disable_all', 'Block ALL users on a MokoSuite site. RSA-signed remote-control tier.', { ...ConfirmParam, ...ConnectionParam }, async ({ confirm, connection }) => {
|
|
154
|
+
const guard = requireConfirm(confirm);
|
|
155
|
+
if (guard)
|
|
156
|
+
return guard;
|
|
157
|
+
return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/users/disable-all`));
|
|
158
|
+
});
|
|
159
|
+
server.tool('client_users_enable_all', 'Unblock ALL users on a MokoSuite site. RSA-signed remote-control tier.', { ...ConfirmParam, ...ConnectionParam }, async ({ confirm, connection }) => {
|
|
160
|
+
const guard = requireConfirm(confirm);
|
|
161
|
+
if (guard)
|
|
162
|
+
return guard;
|
|
163
|
+
return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/users/enable-all`));
|
|
164
|
+
});
|
|
165
|
+
server.tool('client_users_force_logout', 'Terminate ALL user sessions on a MokoSuite site. RSA-signed remote-control tier.', { ...ConfirmParam, ...ConnectionParam }, async ({ confirm, connection }) => {
|
|
166
|
+
const guard = requireConfirm(confirm);
|
|
167
|
+
if (guard)
|
|
168
|
+
return guard;
|
|
169
|
+
return formatResponse(await clientFor(connection).remoteControl('POST', `${MSC}/users/force-logout`));
|
|
170
|
+
});
|
|
171
|
+
// ── Start Server ────────────────────────────────────────────────────────
|
|
172
|
+
async function main() {
|
|
173
|
+
config = await loadConfig();
|
|
174
|
+
const transport = new StdioServerTransport();
|
|
175
|
+
await server.connect(transport);
|
|
176
|
+
}
|
|
177
|
+
main().catch((err) => {
|
|
178
|
+
process.stderr.write(`Fatal: ${err}\n`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
181
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ApiConnection } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* MokoSuiteClient's remote-control routes (provision-reset, remote-login, mass user ops)
|
|
4
|
+
* are `public` but require defence-in-depth headers that only MokoSuiteHQ can generate.
|
|
5
|
+
* HQ signs the payload `domain|timestamp|token` with its RSA-2048 private key (SHA-256);
|
|
6
|
+
* the client verifies against a baked-in public keyring within a 300-second freshness
|
|
7
|
+
* window. This reproduces that scheme so the MCP can act as HQ.
|
|
8
|
+
*
|
|
9
|
+
* @throws if the connection is missing the remote-control credentials or the key is unreadable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildRemoteControlHeaders(conn: ApiConnection): Record<string, string>;
|
|
12
|
+
/** True when a connection carries the credentials needed for the remote-control tier. */
|
|
13
|
+
export declare function hasRemoteControl(conn: ApiConnection): boolean;
|
|
14
|
+
//# sourceMappingURL=signing.d.ts.map
|