@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/dist/signing.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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.Signing
|
|
9
|
+
* INGROUP: mcp-mokosuite
|
|
10
|
+
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite
|
|
11
|
+
* PATH: /src/signing.ts
|
|
12
|
+
* VERSION: 01.00.00
|
|
13
|
+
* BRIEF: RSA request-signing for the MokoSuiteClient remote-control tier
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { createSign } from 'node:crypto';
|
|
17
|
+
/**
|
|
18
|
+
* MokoSuiteClient's remote-control routes (provision-reset, remote-login, mass user ops)
|
|
19
|
+
* are `public` but require defence-in-depth headers that only MokoSuiteHQ can generate.
|
|
20
|
+
* HQ signs the payload `domain|timestamp|token` with its RSA-2048 private key (SHA-256);
|
|
21
|
+
* the client verifies against a baked-in public keyring within a 300-second freshness
|
|
22
|
+
* window. This reproduces that scheme so the MCP can act as HQ.
|
|
23
|
+
*
|
|
24
|
+
* @throws if the connection is missing the remote-control credentials or the key is unreadable.
|
|
25
|
+
*/
|
|
26
|
+
export function buildRemoteControlHeaders(conn) {
|
|
27
|
+
if (!conn.healthApiToken) {
|
|
28
|
+
throw new Error('Remote-control requires "healthApiToken" in the connection config.');
|
|
29
|
+
}
|
|
30
|
+
if (!conn.rsaPrivateKeyPath) {
|
|
31
|
+
throw new Error('Remote-control requires "rsaPrivateKeyPath" (HQ RSA-2048 private key) in the connection config.');
|
|
32
|
+
}
|
|
33
|
+
const domain = new URL(conn.baseUrl).hostname;
|
|
34
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
35
|
+
const payload = `${domain}|${timestamp}|${conn.healthApiToken}`;
|
|
36
|
+
let privateKey;
|
|
37
|
+
try {
|
|
38
|
+
privateKey = readFileSync(conn.rsaPrivateKeyPath, 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
throw new Error(`Cannot read RSA private key at ${conn.rsaPrivateKeyPath}: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
let signature;
|
|
45
|
+
try {
|
|
46
|
+
signature = createSign('RSA-SHA256').update(payload).sign(privateKey, 'base64');
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
throw new Error(`Failed to RSA-sign remote-control payload: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
'X-MokoSuite-Signature': signature,
|
|
54
|
+
'X-MokoSuite-Timestamp': timestamp,
|
|
55
|
+
'X-MokoSuite-Key-Version': String(conn.keyVersion ?? 1),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** True when a connection carries the credentials needed for the remote-control tier. */
|
|
59
|
+
export function hasRemoteControl(conn) {
|
|
60
|
+
return Boolean(conn.healthApiToken && conn.rsaPrivateKeyPath);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=signing.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** A connection is either a MokoSuite client site or the MokoSuite HQ control plane. */
|
|
2
|
+
export type ConnectionRole = 'client' | 'hq';
|
|
3
|
+
/**
|
|
4
|
+
* Connection configuration for a single MokoSuite instance.
|
|
5
|
+
*
|
|
6
|
+
* MokoSuiteClient exposes two auth tiers on the same base URL:
|
|
7
|
+
* - **Reads / CRUD** — the site's Joomla Web Services Bearer token (`apiToken`),
|
|
8
|
+
* authorised against `core.manage`. This is all most tools need.
|
|
9
|
+
* - **Remote-control** (provision-reset, remote-login, mass user ops) — the routes are
|
|
10
|
+
* `public` and instead validate the site's `healthApiToken` plus an RSA signature that
|
|
11
|
+
* only MokoSuiteHQ can produce. Configure `healthApiToken` + `rsaPrivateKeyPath` to let
|
|
12
|
+
* this MCP act as HQ for those operations.
|
|
13
|
+
*/
|
|
14
|
+
export interface ApiConnection {
|
|
15
|
+
/** Base URL of the site root (no trailing slash); "/api/index.php" is appended. */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** Joomla Web Services Bearer token — used for read/CRUD tools. */
|
|
18
|
+
apiToken: string;
|
|
19
|
+
/** Whether this connection is a client site (default) or the HQ control plane. */
|
|
20
|
+
role?: ConnectionRole;
|
|
21
|
+
/** Skip TLS certificate verification (self-signed certs). */
|
|
22
|
+
insecure?: boolean;
|
|
23
|
+
/** Per-site 64-hex `health_api_token` required by the remote-control routes. */
|
|
24
|
+
healthApiToken?: string;
|
|
25
|
+
/** Path to HQ's RSA-2048 private key (PEM) used to sign remote-control requests. */
|
|
26
|
+
rsaPrivateKeyPath?: string;
|
|
27
|
+
/** Signing key version advertised to the client's public keyring (default 1). */
|
|
28
|
+
keyVersion?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Top-level configuration supporting multiple named connections.
|
|
32
|
+
*/
|
|
33
|
+
export interface ApiConfig {
|
|
34
|
+
connections: Record<string, ApiConnection>;
|
|
35
|
+
defaultConnection: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Normalized API response returned by the HTTP client.
|
|
39
|
+
*/
|
|
40
|
+
export interface ApiResponse {
|
|
41
|
+
status: number;
|
|
42
|
+
data: unknown;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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.Types
|
|
9
|
+
* INGROUP: mcp-mokosuite
|
|
10
|
+
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite
|
|
11
|
+
* PATH: /src/types.ts
|
|
12
|
+
* VERSION: 01.00.00
|
|
13
|
+
* BRIEF: TypeScript type definitions for MokoSuite MCP server
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
package/docs/API.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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: /docs/API.md
|
|
8
|
+
VERSION: 01.00.00
|
|
9
|
+
BRIEF: MCP tool reference documentation
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
# API Reference
|
|
13
|
+
|
|
14
|
+
All tools accept an optional `connection` parameter to target a specific named connection. If omitted, the default connection is used.
|
|
15
|
+
|
|
16
|
+
## Example Resources
|
|
17
|
+
|
|
18
|
+
> Replace these example tools with your actual API tools.
|
|
19
|
+
|
|
20
|
+
### `example_resources_list`
|
|
21
|
+
List resources with optional search.
|
|
22
|
+
|
|
23
|
+
| Parameter | Type | Required | Description |
|
|
24
|
+
|-----------|------|----------|-------------|
|
|
25
|
+
| `search` | string | No | Search query |
|
|
26
|
+
| `limit` | number | No | Max results |
|
|
27
|
+
| `page` | number | No | Page number (0-based) |
|
|
28
|
+
|
|
29
|
+
### `example_resource_get`
|
|
30
|
+
Get a single resource by ID.
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Required | Description |
|
|
33
|
+
|-----------|------|----------|-------------|
|
|
34
|
+
| `id` | number | Yes | Resource ID |
|
|
35
|
+
|
|
36
|
+
### `example_resource_create`
|
|
37
|
+
Create a new resource.
|
|
38
|
+
|
|
39
|
+
| Parameter | Type | Required | Description |
|
|
40
|
+
|-----------|------|----------|-------------|
|
|
41
|
+
| `name` | string | Yes | Resource name |
|
|
42
|
+
| `description` | string | No | Resource description |
|
|
43
|
+
|
|
44
|
+
## Generic
|
|
45
|
+
|
|
46
|
+
### `api_request`
|
|
47
|
+
Make a raw API request to any endpoint.
|
|
48
|
+
|
|
49
|
+
| Parameter | Type | Required | Description |
|
|
50
|
+
|-----------|------|----------|-------------|
|
|
51
|
+
| `method` | `"GET"` / `"POST"` / `"PUT"` / `"PATCH"` / `"DELETE"` | Yes | HTTP method |
|
|
52
|
+
| `endpoint` | string | Yes | API path |
|
|
53
|
+
| `body` | object | No | Request body |
|
|
54
|
+
| `params` | object | No | Query parameters |
|
|
55
|
+
|
|
56
|
+
### `list_connections`
|
|
57
|
+
List all configured connections. No parameters.
|
|
58
|
+
|
|
59
|
+
## Revision History
|
|
60
|
+
|
|
61
|
+
| Date | Version | Author | Notes |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| 2026-05-07 | 0.0.1 | jmiller | Initial template API reference |
|
|
@@ -0,0 +1,73 @@
|
|
|
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: /docs/ARCHITECTURE.md
|
|
8
|
+
VERSION: 01.00.00
|
|
9
|
+
BRIEF: Architecture overview and design decisions
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
# Architecture
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
mcp-mokosuite is a Model Context Protocol (MCP) server that bridges AI assistants with a REST API.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
AI Assistant <--> MCP (stdio) <--> ApiClient <--> REST API
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Components
|
|
23
|
+
|
|
24
|
+
### `src/index.ts` — Server Entry Point
|
|
25
|
+
|
|
26
|
+
Registers all MCP tools with `McpServer` from `@modelcontextprotocol/sdk`. Each tool maps to one or more API endpoints. Uses Zod schemas for input validation.
|
|
27
|
+
|
|
28
|
+
Includes shared helpers:
|
|
29
|
+
- `formatResponse()` — normalizes error/success responses into MCP text content
|
|
30
|
+
- `paginationQuery()` — builds pagination query params
|
|
31
|
+
- `ConnectionParam` / `PaginationParams` — reusable Zod parameter spreads
|
|
32
|
+
|
|
33
|
+
### `src/client.ts` — HTTP Client
|
|
34
|
+
|
|
35
|
+
The `ApiClient` class handles all HTTP communication:
|
|
36
|
+
- Uses `node:https` / `node:http` (not `fetch`) for reliable self-signed cert support
|
|
37
|
+
- Supports GET, POST, PUT, PATCH, DELETE
|
|
38
|
+
- JSON serialization/deserialization with error handling
|
|
39
|
+
|
|
40
|
+
### `src/config.ts` — Configuration Loader
|
|
41
|
+
|
|
42
|
+
Loads connection details from `~/.<project>.json`. Supports multiple named connections with a configurable default.
|
|
43
|
+
|
|
44
|
+
### `src/types.ts` — Type Definitions
|
|
45
|
+
|
|
46
|
+
TypeScript interfaces for `ApiConnection`, `ApiConfig`, and `ApiResponse`.
|
|
47
|
+
|
|
48
|
+
### `scripts/setup.mjs` — Interactive Setup
|
|
49
|
+
|
|
50
|
+
Node.js script using `readline/promises` for interactive config creation.
|
|
51
|
+
|
|
52
|
+
## Design Decisions
|
|
53
|
+
|
|
54
|
+
### Why `node:https` instead of `fetch`?
|
|
55
|
+
|
|
56
|
+
Node.js 24's built-in `fetch` does not honor self-signed certificate bypass. The classic `node:https` module with `rejectUnauthorized: false` works reliably across all Node.js versions.
|
|
57
|
+
|
|
58
|
+
### Why multiple named connections?
|
|
59
|
+
|
|
60
|
+
Multi-instance support is a core use case — managing staging, production, and dev environments from a single MCP server.
|
|
61
|
+
|
|
62
|
+
## Data Flow
|
|
63
|
+
|
|
64
|
+
1. AI assistant sends a tool call via MCP stdio transport
|
|
65
|
+
2. `index.ts` validates parameters with Zod and resolves the connection
|
|
66
|
+
3. `ApiClient` constructs the API URL, attaches auth headers, and makes the HTTP request
|
|
67
|
+
4. Response is parsed as JSON and returned as MCP tool output
|
|
68
|
+
|
|
69
|
+
## Revision History
|
|
70
|
+
|
|
71
|
+
| Date | Version | Author | Notes |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| 2026-05-07 | 0.0.1 | jmiller | Initial architecture document |
|
|
@@ -0,0 +1,102 @@
|
|
|
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: /docs/INSTALLATION.md
|
|
8
|
+
VERSION: 01.00.00
|
|
9
|
+
BRIEF: Installation and setup instructions
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
# Installation
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- **Node.js** 20.0.0 or later
|
|
17
|
+
- **npm** (included with Node.js)
|
|
18
|
+
- Access to the target API with valid credentials
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
git clone https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite.git
|
|
24
|
+
cd mcp-mokosuite
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
npm run setup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The setup wizard will prompt for:
|
|
31
|
+
|
|
32
|
+
1. **Connection name** — a label for this instance (e.g. `production`, `staging`)
|
|
33
|
+
2. **API URL** — the base URL of the instance
|
|
34
|
+
3. **API key/token** — authentication credentials
|
|
35
|
+
4. **TLS verification** — whether to skip certificate verification (for self-signed certs)
|
|
36
|
+
|
|
37
|
+
Run `npm run setup` again to add more connections.
|
|
38
|
+
|
|
39
|
+
## Register with Claude Code
|
|
40
|
+
|
|
41
|
+
Add to your Claude Code MCP settings (`~/.claude.json` or project `.mcp.json`):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"mcp-mokosuite": {
|
|
47
|
+
"type": "stdio",
|
|
48
|
+
"command": "node",
|
|
49
|
+
"args": ["/path/to/mcp-mokosuite/dist/index.js"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Restart Claude Code after adding the server.
|
|
56
|
+
|
|
57
|
+
## Configuration File
|
|
58
|
+
|
|
59
|
+
The config is stored at `~/.mcp-mokosuite.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"defaultConnection": "production",
|
|
64
|
+
"connections": {
|
|
65
|
+
"production": {
|
|
66
|
+
"baseUrl": "https://api.example.com",
|
|
67
|
+
"apiKey": "your-api-key"
|
|
68
|
+
},
|
|
69
|
+
"staging": {
|
|
70
|
+
"baseUrl": "https://api-staging.example.com",
|
|
71
|
+
"apiKey": "your-staging-key",
|
|
72
|
+
"insecure": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
You can also set the `MOKOSUITE_CONFIG` environment variable to use a config file at a custom path.
|
|
79
|
+
|
|
80
|
+
## Verification
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm start
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If configured correctly, the server will start listening on stdio.
|
|
87
|
+
|
|
88
|
+
## Troubleshooting
|
|
89
|
+
|
|
90
|
+
### "Failed to load config" error
|
|
91
|
+
Run `npm run setup` to create the config file.
|
|
92
|
+
|
|
93
|
+
### Connection errors
|
|
94
|
+
- Verify the API is reachable from your machine
|
|
95
|
+
- For self-signed certs, set `"insecure": true`
|
|
96
|
+
- Ensure the API key/token is valid
|
|
97
|
+
|
|
98
|
+
## Revision History
|
|
99
|
+
|
|
100
|
+
| Date | Version | Author | Notes |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| 2026-05-07 | 0.0.1 | jmiller | Initial installation guide |
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
SPDX-License-Identifier: GPL-3.0-or-later -->
|
|
3
|
+
|
|
4
|
+
# mcp-mokosuite Documentation
|
|
5
|
+
|
|
6
|
+
See the [README](../README.md) for quick start, customization guide, and tool patterns.
|
|
7
|
+
|
|
8
|
+
## Documents
|
|
9
|
+
|
|
10
|
+
- [INSTALLATION.md](./INSTALLATION.md) — Prerequisites, install, setup, troubleshooting
|
|
11
|
+
- [ARCHITECTURE.md](./ARCHITECTURE.md) — Component overview, design decisions, data flow
|
|
12
|
+
- [API.md](./API.md) — Full MCP tool reference with parameter tables
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mokoconsulting/mcp-mokosuite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for MokoSuite (Joomla) REST API v1 operations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mokosuite-api-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"lint": "eslint src/",
|
|
15
|
+
"setup": "node scripts/setup.mjs",
|
|
16
|
+
"clean": "rm -rf dist/"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
20
|
+
"zod": "^3.24.4"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.15.3",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "GPL-3.0-or-later",
|
|
30
|
+
"author": "Moko Consulting <hello@mokoconsulting.tech>",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://git.mokoconsulting.tech/MokoConsulting/mcp-mokosuite.git"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
|
|
16
|
+
import * as https from 'node:https';
|
|
17
|
+
import * as http from 'node:http';
|
|
18
|
+
import type { ApiConnection, ApiResponse } from './types.js';
|
|
19
|
+
import { buildRemoteControlHeaders } from './signing.js';
|
|
20
|
+
|
|
21
|
+
// ── Endpoint conventions ────────────────────────────────────────────────
|
|
22
|
+
// Joomla Web Services live under /api/index.php; MokoSuiteClient routes are
|
|
23
|
+
// registered at /v1/mokosuiteclient/* (see MSC_BASE in index.ts).
|
|
24
|
+
const API_PREFIX = '/api/index.php';
|
|
25
|
+
const TIMEOUT_MS = 30_000;
|
|
26
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export class ApiClient {
|
|
29
|
+
private readonly conn: ApiConnection;
|
|
30
|
+
private readonly base_url: string;
|
|
31
|
+
private readonly headers: Record<string, string>;
|
|
32
|
+
private readonly insecure: boolean;
|
|
33
|
+
|
|
34
|
+
constructor(conn: ApiConnection) {
|
|
35
|
+
this.conn = conn;
|
|
36
|
+
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
|
37
|
+
|
|
38
|
+
// Read/CRUD tier: Joomla Web Services authenticate via a Bearer API token.
|
|
39
|
+
this.headers = {
|
|
40
|
+
'Authorization': `Bearer ${conn.apiToken}`,
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Accept': 'application/vnd.api+json',
|
|
43
|
+
};
|
|
44
|
+
this.insecure = conn.insecure ?? false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
|
|
48
|
+
return this.request(this.buildUrl(endpoint, params), 'GET');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
|
|
52
|
+
return this.request(this.buildUrl(endpoint), 'POST', body);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
|
|
56
|
+
return this.request(this.buildUrl(endpoint), 'PUT', body);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
|
|
60
|
+
return this.request(this.buildUrl(endpoint), 'PATCH', body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async delete(endpoint: string): Promise<ApiResponse> {
|
|
64
|
+
return this.request(this.buildUrl(endpoint), 'DELETE');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remote-control tier: swap the auth to the site's `health_api_token` and attach the
|
|
69
|
+
* RSA signature headers HQ uses, then issue a GET or POST. Requires the connection to
|
|
70
|
+
* carry `healthApiToken` + `rsaPrivateKeyPath`.
|
|
71
|
+
*/
|
|
72
|
+
async remoteControl(method: 'GET' | 'POST', endpoint: string, body?: unknown, params?: Record<string, string>): Promise<ApiResponse> {
|
|
73
|
+
const signed: Record<string, string> = {
|
|
74
|
+
...buildRemoteControlHeaders(this.conn),
|
|
75
|
+
'Authorization': `Bearer ${this.conn.healthApiToken}`,
|
|
76
|
+
};
|
|
77
|
+
return this.request(this.buildUrl(endpoint, params), method, body, signed);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private buildUrl(endpoint: string, params?: Record<string, string>): string {
|
|
81
|
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
82
|
+
const url = new URL(`${this.base_url}${path}`);
|
|
83
|
+
if (params) {
|
|
84
|
+
for (const [key, value] of Object.entries(params)) {
|
|
85
|
+
url.searchParams.set(key, value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return url.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private request(url: string, method: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<ApiResponse> {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const parsed = new URL(url);
|
|
94
|
+
const is_https = parsed.protocol === 'https:';
|
|
95
|
+
const transport = is_https ? https : http;
|
|
96
|
+
|
|
97
|
+
const options: https.RequestOptions = {
|
|
98
|
+
hostname: parsed.hostname,
|
|
99
|
+
port: parsed.port || (is_https ? 443 : 80),
|
|
100
|
+
path: parsed.pathname + parsed.search,
|
|
101
|
+
method,
|
|
102
|
+
headers: { ...this.headers, ...(extraHeaders ?? {}) },
|
|
103
|
+
timeout: TIMEOUT_MS,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (this.insecure && is_https) {
|
|
107
|
+
options.rejectUnauthorized = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
|
111
|
+
if (payload) {
|
|
112
|
+
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const req = transport.request(options, (res) => {
|
|
116
|
+
const chunks: Buffer[] = [];
|
|
117
|
+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
118
|
+
res.on('end', () => {
|
|
119
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
120
|
+
let data: unknown;
|
|
121
|
+
try {
|
|
122
|
+
data = JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
data = raw;
|
|
125
|
+
}
|
|
126
|
+
resolve({ status: res.statusCode ?? 0, data });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
req.on('error', (err) => reject(err));
|
|
131
|
+
req.on('timeout', () => {
|
|
132
|
+
req.destroy();
|
|
133
|
+
reject(new Error('Request timed out'));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (payload) {
|
|
137
|
+
req.write(payload);
|
|
138
|
+
}
|
|
139
|
+
req.end();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import type { ApiConfig, ApiConnection } from './types.js';
|
|
20
|
+
|
|
21
|
+
// ── Customize this ──────────────────────────────────────────────────────
|
|
22
|
+
// Change the filename to match your project (e.g. ".dolibarr-api-mcp.json")
|
|
23
|
+
const CONFIG_FILENAME = '.mcp_mokosuite.json';
|
|
24
|
+
// Change the env var name to match your project
|
|
25
|
+
const CONFIG_ENV_VAR = 'MOKOSUITE_CONFIG';
|
|
26
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export async function loadConfig(): Promise<ApiConfig> {
|
|
29
|
+
const config_path = process.env[CONFIG_ENV_VAR]
|
|
30
|
+
? resolve(process.env[CONFIG_ENV_VAR]!)
|
|
31
|
+
: resolve(homedir(), CONFIG_FILENAME);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(config_path, 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(raw) as Partial<ApiConfig>;
|
|
36
|
+
|
|
37
|
+
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
|
38
|
+
throw new Error('No connections defined in config');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
connections: parsed.connections,
|
|
43
|
+
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Failed to load config from ${config_path}: ${message}\n` +
|
|
49
|
+
`Create ${config_path} — see config.example.json for format.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getConnection(config: ApiConfig, name?: string): ApiConnection {
|
|
55
|
+
const key = name ?? config.defaultConnection;
|
|
56
|
+
const conn = config.connections[key];
|
|
57
|
+
if (!conn) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return conn;
|
|
63
|
+
}
|