@martin4455/matomo-mcp-ro 0.4.1
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/CONFIGURATION.md +396 -0
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/api.mjs +150 -0
- package/cli.mjs +114 -0
- package/client.mjs +28 -0
- package/lib/client-config.mjs +14 -0
- package/lib/config-errors.mjs +8 -0
- package/lib/config-file.mjs +102 -0
- package/lib/config.mjs +102 -0
- package/lib/credential-encryption.mjs +67 -0
- package/lib/credential-store.mjs +182 -0
- package/lib/locks.mjs +60 -0
- package/lib/permissions.mjs +76 -0
- package/lib/prompt.mjs +24 -0
- package/lib/redaction.mjs +109 -0
- package/lib/reporting.mjs +104 -0
- package/lib/session-env.mjs +26 -0
- package/lib/version.mjs +1 -0
- package/package.json +51 -0
- package/server.mjs +84 -0
package/CONFIGURATION.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Matomo MCP configuration guide
|
|
2
|
+
|
|
3
|
+
For the quick setup, see the [README](README.md). This guide describes version 0.4.0 and newer.
|
|
4
|
+
|
|
5
|
+
## Source setup
|
|
6
|
+
|
|
7
|
+
From the source checkout, install its pinned dependencies (retain optional
|
|
8
|
+
dependencies: they include the native module for your OS and CPU):
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm ci --ignore-scripts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then configure from the project where analytics will be used:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
node "/absolute/path/to/matomo-mcp-ro/cli.mjs" configure
|
|
18
|
+
node "/absolute/path/to/matomo-mcp-ro/cli.mjs" check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
When working from source, replace `matomo-mcp` in the remaining examples with
|
|
22
|
+
`node "/absolute/path/to/matomo-mcp-ro/cli.mjs"`. Use that direct command in your
|
|
23
|
+
client settings until this revision is available as an installed package.
|
|
24
|
+
|
|
25
|
+
Use `npm.cmd`/`npx.cmd` in PowerShell if script execution policy blocks `.ps1`.
|
|
26
|
+
Configuration runs in your own terminal: enter the HTTPS Matomo URL, choose whether
|
|
27
|
+
Basic Auth is required, and enter the API token and optional username/password.
|
|
28
|
+
The wizard has four numbered steps. Type after the colon and press Enter. The
|
|
29
|
+
Basic Auth question accepts `tak`/`nie` or `yes`/`no` and explicitly shows the
|
|
30
|
+
Enter default; an invalid answer repeats the question. Credential input is hidden
|
|
31
|
+
(no characters or asterisks); each field confirms that a new or saved value was
|
|
32
|
+
accepted without displaying it. Ctrl+C cancels. Configuration is saved only after a successful
|
|
33
|
+
read-only site-list request. Old-format records are invalid; enter the complete
|
|
34
|
+
connection again with `configure`. Enter retains valid saved credentials;
|
|
35
|
+
changing the URL requires new credentials.
|
|
36
|
+
|
|
37
|
+
The file `.matomo-mcp.json` lives in the exact current directory, or at an explicit
|
|
38
|
+
`--config /project/.matomo-mcp.json` path. No home/parent/environment fallback is
|
|
39
|
+
used. It contains only a reference, protected by mode 0600 on Unix or a private
|
|
40
|
+
Windows ACL:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"schemaVersion": 2,
|
|
45
|
+
"credentialId": "c26a0845-51b2-40bf-80d7-e504501fa56b"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The UUID is generated randomly for each new connection. The Matomo HTTPS URL,
|
|
50
|
+
API token, and optional Basic Auth username/password are stored together in the
|
|
51
|
+
keyring, under service `matomo-mcp-ro`. Neither URLs nor usernames are used in
|
|
52
|
+
entry labels. Local metadata cannot override the stored connection's URL.
|
|
53
|
+
The full record is encrypted before it reaches keyring entries, including the
|
|
54
|
+
token and both Basic Auth fields. There is no additional passphrase at session
|
|
55
|
+
startup, signed executable, master-key file or environment key.
|
|
56
|
+
`configure` adds Git/search exclusions. Never paste credentials into AI chats,
|
|
57
|
+
CLI arguments, URLs or MCP client settings. `status`, `check` output
|
|
58
|
+
and summaries omit the connection URL as well as credentials. The URL is visible
|
|
59
|
+
in the interactive editing prompt. Analytics reports can still contain site URLs.
|
|
60
|
+
|
|
61
|
+
## Operating system requirements
|
|
62
|
+
|
|
63
|
+
- **Windows:** Windows Credential Manager of the current OS account. Normal use
|
|
64
|
+
requires no administrator account.
|
|
65
|
+
- **macOS:** User Keychain. The OS may ask to unlock the keychain or authorize
|
|
66
|
+
access by Node.
|
|
67
|
+
- **Ubuntu/Linux desktop:** Secret Service, such as GNOME Keyring, with the user's
|
|
68
|
+
D-Bus session and an unlocked persistent collection.
|
|
69
|
+
- **Linux server, SSH, container or WSL:** Prepare a user D-Bus session and
|
|
70
|
+
persistent Secret Service collection first. Node alone is insufficient. WSL
|
|
71
|
+
uses Linux storage, not Windows Credential Manager.
|
|
72
|
+
|
|
73
|
+
On Ubuntu, if these components are missing, the user may need to install
|
|
74
|
+
`gnome-keyring` and the appropriate D-Bus session integration using the system
|
|
75
|
+
package manager. This one-time system setup may require sudo. A graphical session
|
|
76
|
+
typically supplies both. Installing packages alone does not establish an unlocked
|
|
77
|
+
user session. Do not run the MCP server or configurator through sudo.
|
|
78
|
+
|
|
79
|
+
The pinned `@napi-rs/keyring@2.1.0` dependency has prebuilt modules for Windows
|
|
80
|
+
x64/ARM64, macOS Intel/Apple Silicon, and Linux glibc x64/ARM64. Do not copy
|
|
81
|
+
`node_modules` between platforms. Missing native modules produce an error; normal
|
|
82
|
+
installation does not compile Rust or require Python/Tkinter.
|
|
83
|
+
|
|
84
|
+
Linux explicitly requires Secret Service and never falls back to the kernel
|
|
85
|
+
keyring. A missing, inaccessible or unsuccessfully unlocked store stops the
|
|
86
|
+
operation. An OS unlock dialog may appear, including during `status` or server
|
|
87
|
+
startup. There is no plaintext, environment-variable or alternate-file fallback.
|
|
88
|
+
|
|
89
|
+
The local script client forwards only relevant Linux desktop/session variables
|
|
90
|
+
to its MCP child, including `DBUS_SESSION_BUS_ADDRESS`. For hosts that filter this
|
|
91
|
+
variable, the server can discover an existing user-owned socket at
|
|
92
|
+
`/run/user/<uid>/bus` in its protected runtime directory. It does not start a
|
|
93
|
+
daemon or override an explicitly supplied bus address. Custom/headless sessions
|
|
94
|
+
must make their session bus available to the client process.
|
|
95
|
+
|
|
96
|
+
## Old records and token rotation
|
|
97
|
+
|
|
98
|
+
Old plaintext configuration files and unencrypted keyring entries are treated as
|
|
99
|
+
invalid credentials. `serve` and `check` stop before any HTTP request; `status`
|
|
100
|
+
reports `credentials: "invalid"`. Run `configure` and enter a fresh URL, token and
|
|
101
|
+
any Basic Auth credentials. Old values are never accepted as defaults. The new
|
|
102
|
+
connection is encrypted and saved only after its read-only connection test
|
|
103
|
+
succeeds. There is no migration command.
|
|
104
|
+
|
|
105
|
+
Run `configure` again to rotate a token, change Basic Auth or repair a missing or
|
|
106
|
+
corrupt entry. New input is saved only after a successful connection check. Enter
|
|
107
|
+
retains existing values; selecting no Basic Auth removes it. A normalized URL
|
|
108
|
+
change requires fresh credentials. An unavailable store is not treated as a
|
|
109
|
+
missing entry. Restart MCP after changes: an existing process keeps its resolved
|
|
110
|
+
connection in memory.
|
|
111
|
+
|
|
112
|
+
Copies of the same v2 file on one OS account share a keyring entry and its updates.
|
|
113
|
+
Copying metadata alone to another computer/account does not transfer credentials;
|
|
114
|
+
run `configure` there. Provider/system policies may synchronize credentials; this
|
|
115
|
+
package does not promise otherwise. Keyring storage is not isolation from every
|
|
116
|
+
process running as the same user or from an administrator. Reconfiguration does not
|
|
117
|
+
remove historical plaintext copies from backups or version control.
|
|
118
|
+
|
|
119
|
+
## Large credentials, consistency and recovery
|
|
120
|
+
|
|
121
|
+
All supported systems use one manifest entry and one or more fragment entries.
|
|
122
|
+
The complete connection JSON is encrypted with AES-256-GCM using a fresh 32-byte
|
|
123
|
+
salt, a fresh 12-byte IV and a 16-byte authentication tag. A 32-byte key is derived
|
|
124
|
+
with HKDF-SHA-256 from the public credential UUID. The authenticated context is
|
|
125
|
+
`JSON.stringify([SERVICE, 2, 'aes-256-gcm', 'hkdf-sha256', credentialId])`, used
|
|
126
|
+
both as HKDF info and GCM additional authenticated data. The derived key buffer
|
|
127
|
+
is cleared after each operation; JavaScript strings and process memory cannot
|
|
128
|
+
be reliably wiped by this design.
|
|
129
|
+
|
|
130
|
+
The envelope uses version 2 with algorithm `aes-256-gcm` and KDF `hkdf-sha256`.
|
|
131
|
+
The UUID is public: a program with the UUID and envelope can derive the same key
|
|
132
|
+
and decrypt it.
|
|
133
|
+
This layer masks directly readable credentials and detects corruption/context
|
|
134
|
+
mismatch; the OS credential store remains the access boundary. It does not
|
|
135
|
+
provide per-script isolation from other programs under the same OS account.
|
|
136
|
+
|
|
137
|
+
The versioned encrypted envelope is encoded as UTF-8/base64 and divided into at most
|
|
138
|
+
1200 ASCII characters per fragment (2400 UTF-16LE bytes). Each fits Windows'
|
|
139
|
+
2560-byte CredentialBlob limit, including the encryption overhead. Base64 is an
|
|
140
|
+
encoding of the encrypted envelope. Manifest, fragments, salt, IV, authentication
|
|
141
|
+
tag and integrity hash all stay in keyring.
|
|
142
|
+
The application file never receives a fragment, URL or encryption key.
|
|
143
|
+
|
|
144
|
+
The format supports all accepted connection fields: up to 8192 UTF-16 code units
|
|
145
|
+
per credential field, a normalized URL up to 2048 characters, and a defensive
|
|
146
|
+
128 KiB total plaintext payload bound; the stored size bound includes ciphertext
|
|
147
|
+
base64 expansion and envelope metadata. Fragment counts, lengths, canonical
|
|
148
|
+
base64, SHA-256, envelope fields and GCM authentication are checked. Missing or
|
|
149
|
+
modified fragments fail closed. Runtime reads never fall back to plaintext.
|
|
150
|
+
|
|
151
|
+
Rotation writes a new random generation, verifies every fragment and the full
|
|
152
|
+
payload, then switches the small manifest. Previous fragments stay available until
|
|
153
|
+
the metadata file commit succeeds. On an ordinary failure the adapter restores
|
|
154
|
+
the previous manifest and removes new fragments. Failed recovery is reported
|
|
155
|
+
explicitly. Cleanup failure after success may leave old fragments in keyring and
|
|
156
|
+
is reported separately; it does not undo the valid new connection.
|
|
157
|
+
|
|
158
|
+
Cooperating processes use locks by canonical configuration path and keyring UUID,
|
|
159
|
+
including readers and references in other directories. An editor also detects a
|
|
160
|
+
changed credential generation even if the metadata file still has the same UUID.
|
|
161
|
+
Lock files under `~/.matomo-mcp-locks/` contain only process ownership metadata.
|
|
162
|
+
They have restricted write access, and no URL or credential is written there.
|
|
163
|
+
On Windows, read/traverse grants added by a host sandbox are accepted for lock
|
|
164
|
+
metadata only; grants allowing other accounts to modify it are rejected. The
|
|
165
|
+
configuration file keeps its stricter private-ACL requirement. Operations wait up
|
|
166
|
+
to five seconds for a lock, then fail with a useful message. After a killed/crashed
|
|
167
|
+
process, stop all related MCP/configuration processes and confirm no operation is
|
|
168
|
+
active before removing abandoned lock directories. Locks are deliberately not
|
|
169
|
+
stolen automatically. Do not clear the system credential store to release a lock.
|
|
170
|
+
|
|
171
|
+
Keyring and filesystem do not form one crash-atomic transaction. A sudden exit
|
|
172
|
+
can leave orphaned fragments or an abandoned lock; a committed connection is
|
|
173
|
+
always read and validated as a complete generation. External keyring editors do
|
|
174
|
+
not participate in the locking protocol. Do not manually delete shared entries
|
|
175
|
+
based on finding one local reference.
|
|
176
|
+
|
|
177
|
+
## Connect a client
|
|
178
|
+
|
|
179
|
+
The [README](README.md#3-connect-your-client) shows the CLI registration commands.
|
|
180
|
+
For a stable installation, `client-config codex`, `client-config claude-code` and
|
|
181
|
+
`client-config claude-desktop` print settings with absolute paths and no secrets.
|
|
182
|
+
These settings launch the installed Node.js executable and package directly.
|
|
183
|
+
Use `--config` to select the project you configured:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
matomo-mcp client-config codex --config "/absolute/path/to/project/.matomo-mcp.json"
|
|
187
|
+
matomo-mcp client-config claude-code --config "/absolute/path/to/project/.matomo-mcp.json"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Merge the Codex TOML into `~/.codex/config.toml`, or the trusted project's
|
|
191
|
+
`.codex/config.toml` for project scope. Codex CLI and Desktop share these settings.
|
|
192
|
+
The generated server name is `matomo`; README registration examples use
|
|
193
|
+
`matomo-local`. Use the name you registered when addressing the assistant.
|
|
194
|
+
For client behavior, see [Codex MCP](https://learn.chatgpt.com/docs/extend/mcp)
|
|
195
|
+
and [Claude Code MCP](https://code.claude.com/docs/en/mcp).
|
|
196
|
+
|
|
197
|
+
### Claude Desktop setup
|
|
198
|
+
|
|
199
|
+
Generate the desktop configuration:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
matomo-mcp client-config claude-desktop --config "/absolute/path/to/project/.matomo-mcp.json"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Open Claude Desktop's **Settings → Developer → Edit Config** and merge the printed
|
|
206
|
+
`matomo` entry into `mcpServers` in `claude_desktop_config.json`. Preserve existing
|
|
207
|
+
servers, save the file, and fully restart Claude Desktop. The generated command
|
|
208
|
+
uses absolute paths, so keep Node.js and the package at those locations; regenerate
|
|
209
|
+
it after moving an installation. If you want the README's `matomo-local` name,
|
|
210
|
+
rename the generated JSON key from `matomo` to `matomo-local`.
|
|
211
|
+
See the [official local-server setup](https://modelcontextprotocol.io/docs/develop/connect-local-servers)
|
|
212
|
+
for the client configuration workflow.
|
|
213
|
+
|
|
214
|
+
The configuration is application-wide. Its explicit `--config` selects the
|
|
215
|
+
connection; it does not restrict that connection to a particular desktop chat.
|
|
216
|
+
Keep credentials in the interactive configurator, never in the client JSON.
|
|
217
|
+
|
|
218
|
+
With no command the CLI starts `serve`. Stdout is reserved for MCP messages.
|
|
219
|
+
The process exits when the client closes stdin. Windows configuration and all
|
|
220
|
+
configuration reads, including server startup, check ACLs using hidden system
|
|
221
|
+
utilities. Keyring access uses native APIs inside Node; it does not launch shells.
|
|
222
|
+
|
|
223
|
+
## Troubleshooting
|
|
224
|
+
|
|
225
|
+
- Run `matomo-mcp status --config "/absolute/path/to/project/.matomo-mcp.json"` to
|
|
226
|
+
check the local file and stored credentials without an HTTP request.
|
|
227
|
+
- Run `matomo-mcp check --config "/absolute/path/to/project/.matomo-mcp.json"` to
|
|
228
|
+
test access to Matomo. A token needs access to at least one site.
|
|
229
|
+
- If credentials are invalid or missing, run `configure` again with the same
|
|
230
|
+
`--config`. An unavailable credential store must be unlocked or repaired first.
|
|
231
|
+
- On Linux, make the user's D-Bus session and Secret Service available to the
|
|
232
|
+
client process. See [operating system requirements](#operating-system-requirements).
|
|
233
|
+
- On Windows, use `npm.cmd` and `npx.cmd` if PowerShell blocks the corresponding
|
|
234
|
+
`.ps1` scripts. If a client cannot spawn `npx`, use the generated direct-Node
|
|
235
|
+
settings above.
|
|
236
|
+
- Restart the MCP client after changing credentials. If using `npx @latest`, it
|
|
237
|
+
can download a newer package on a later launch. For controlled upgrades, use
|
|
238
|
+
a reviewed installed version and regenerate direct-Node settings after updates.
|
|
239
|
+
|
|
240
|
+
## Tools
|
|
241
|
+
|
|
242
|
+
- **`matomo_list_sites`:** Available sites and time zones.
|
|
243
|
+
- **`matomo_site_info`:** Site settings.
|
|
244
|
+
- **`matomo_report_catalog`:** Report metadata. Filter by `query` or `module`,
|
|
245
|
+
paginate, and use `detailed=false` for a compact index.
|
|
246
|
+
- **`matomo_goals`:** Configured goals.
|
|
247
|
+
- **`matomo_segments`:** Saved segments.
|
|
248
|
+
- **`matomo_segments_metadata`:** Available segment fields.
|
|
249
|
+
- **`matomo_dimensions`:** Configured custom dimensions, active state and
|
|
250
|
+
visit/action scope.
|
|
251
|
+
- **`matomo_report`:** 53 explicitly allowed reporting methods, including
|
|
252
|
+
`UserId.getUsers`.
|
|
253
|
+
- **`matomo_visits`:** `Live.getLastVisitsDetails`, with action details, paging
|
|
254
|
+
and a maximum 31-day date window.
|
|
255
|
+
- **`matomo_report_batch`:** 1–10 reports with ordered per-item results and errors.
|
|
256
|
+
Concurrency is 1 by default, at most 2.
|
|
257
|
+
|
|
258
|
+
Example tool arguments (synthetic identifiers):
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"method": "UserId.getUsers",
|
|
263
|
+
"idSite": 1,
|
|
264
|
+
"period": "day",
|
|
265
|
+
"date": "2026-09-01,2026-09-10",
|
|
266
|
+
"segment": "dimension2==trial",
|
|
267
|
+
"filter_limit": 1000,
|
|
268
|
+
"filter_offset": 0
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Confirm the actual dimension ID/value using metadata before querying. A daily
|
|
273
|
+
series has one pagination entry per date. `period=range` instead returns the
|
|
274
|
+
aggregate for the window. All dates are explicit `YYYY-MM-DD` values; metrics are
|
|
275
|
+
requested as numbers (`format_metrics=0`).
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"idSite": 1,
|
|
280
|
+
"period": "day",
|
|
281
|
+
"date": "2026-09-10",
|
|
282
|
+
"segment": "userId==example-user",
|
|
283
|
+
"includeActions": true,
|
|
284
|
+
"filter_limit": 20,
|
|
285
|
+
"filter_offset": 0
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Visits support at most 100 rows per page and `filter_sort_order`, not arbitrary
|
|
290
|
+
sort columns. `includeActions=false` reduces payload. Dedupe visits by site and
|
|
291
|
+
visit ID when collecting pages. A full page means another page may exist; inspect
|
|
292
|
+
`pagination.mayHaveMore`/`nextOffset`. Choose closed dates where possible.
|
|
293
|
+
|
|
294
|
+
Batch takes `{ "requests": [REPORT_ARGUMENTS, ...], "concurrency": 1 }`. Every
|
|
295
|
+
request is validated before any network access. Results preserve request indexes;
|
|
296
|
+
errors never become zero counts. Batch response budgets are 2 MiB per result and
|
|
297
|
+
8 MiB total; fetch oversized results individually with a smaller page size.
|
|
298
|
+
|
|
299
|
+
## Read-only boundaries and completeness
|
|
300
|
+
|
|
301
|
+
Write methods, arbitrary API URLs, `API.getBulkRequest`, credential overrides and
|
|
302
|
+
unlisted parameters are blocked before network requests. Redirects are rejected;
|
|
303
|
+
TLS validation stays enabled. API credentials are sent as a POST body token and
|
|
304
|
+
optional Basic Authorization header. Boolean API flags use PHP-safe 0/1 values.
|
|
305
|
+
Requests have a 60-second timeout and 10 MiB response limit. Report page limit is
|
|
306
|
+
1000. Cancellation stops queued batch work.
|
|
307
|
+
|
|
308
|
+
API results, MCP responses and error messages replace recognized credentials with
|
|
309
|
+
`[UKRYTO]`. Filtering covers configured credentials and encoded forms, sensitive
|
|
310
|
+
JSON fields such as `token_auth`/`api_key`/`password`, name/value pairs, credentials
|
|
311
|
+
in URLs, authorization strings and common provider token formats. It also covers
|
|
312
|
+
parameters echoed in report and batch results. Token-management APIs are outside
|
|
313
|
+
the allowlist; upstream HTTP bodies and headers are never included in HTTP errors.
|
|
314
|
+
|
|
315
|
+
This is credential filtering, not anonymization: User IDs, visitor IDs, IP
|
|
316
|
+
addresses and other analytics data can remain in reports. An unknown opaque
|
|
317
|
+
secret inside an ordinary title or custom dimension cannot always be recognized.
|
|
318
|
+
Do not track credentials in the first place. Filtering is local and does not
|
|
319
|
+
remove data already stored in Matomo or in older exported reports.
|
|
320
|
+
|
|
321
|
+
Metadata does not automatically allow new executable methods. The server does
|
|
322
|
+
not create saved segments, configure archiving, or change retention. Reading a
|
|
323
|
+
report can trigger Matomo's normal archive/cache generation.
|
|
324
|
+
|
|
325
|
+
Responses preserve `method`, `parameters`, `data` and add `fetchedAt`, `pagination`
|
|
326
|
+
and `completeness`. `Others` summary rows and unfetched subtables produce warnings.
|
|
327
|
+
Finishing pagination does not prove complete telemetry: archive row limits can
|
|
328
|
+
remove identities, raw data can expire, and tracking can be absent. Visitor-log
|
|
329
|
+
access may also be disabled. Action lists are server-returned, not guaranteed
|
|
330
|
+
complete. Visit-scoped dimensions do not establish a state for every individual
|
|
331
|
+
historical action in that visit.
|
|
332
|
+
|
|
333
|
+
## Use from a local Node script
|
|
334
|
+
|
|
335
|
+
The generic client export uses the same MCP transport and policy as an AI client.
|
|
336
|
+
It never reads the credential file itself:
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
import { connectMatomo } from '@martin4455/matomo-mcp-ro/client';
|
|
340
|
+
const client = await connectMatomo({ configPath: '/project/.matomo-mcp.json' });
|
|
341
|
+
try {
|
|
342
|
+
const sites = await client.call('matomo_list_sites', {});
|
|
343
|
+
console.log(sites.data.map(site => site.name));
|
|
344
|
+
} finally {
|
|
345
|
+
await client.close();
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Domain identity conversions, license/business rules, input lists, caching and
|
|
350
|
+
analysis outputs belong in separate private project skills/scripts. They are not
|
|
351
|
+
part of this public package. No persistent job service or output-file tool is
|
|
352
|
+
exposed over MCP.
|
|
353
|
+
|
|
354
|
+
## Development
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
npm ci --ignore-scripts
|
|
358
|
+
npm test
|
|
359
|
+
npm run test:native
|
|
360
|
+
npm run release:check
|
|
361
|
+
npm pack --ignore-scripts
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Unit tests use synthetic credentials, an in-memory keyring and mocked HTTP with
|
|
365
|
+
real MCP transports. They do not access real user entries. The explicit native
|
|
366
|
+
integration test uses a random test service, large synthetic credentials,
|
|
367
|
+
cross-process reads and rotation, then removes its own entries. It also checks
|
|
368
|
+
Windows' actual per-entry boundary and Linux's refusal to fall back without D-Bus.
|
|
369
|
+
It requires a working OS keyring and may display a system dialog.
|
|
370
|
+
|
|
371
|
+
CI runs both suites on Windows, Ubuntu and macOS; it prepares isolated test
|
|
372
|
+
keychains/Secret Service sessions. The CI setup password is public synthetic test
|
|
373
|
+
data, not an application credential. Session/reboot persistence, denial of unlock
|
|
374
|
+
dialogs and desktop-client authorization should also be checked manually on the
|
|
375
|
+
target OS; a passing mock test or an available binary is not that verification.
|
|
376
|
+
`release:check` verifies an explicit source
|
|
377
|
+
and npm file list. Keep this list current when adding code; the npm package
|
|
378
|
+
contains runtime files, README, this guide and LICENSE, not tests or private data.
|
|
379
|
+
|
|
380
|
+
Maintainers publish a reviewed release from the source checkout after the checks:
|
|
381
|
+
|
|
382
|
+
```sh
|
|
383
|
+
npm run release:publish
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
This publishes the current directory. Do not publish a `.tgz` by its local path:
|
|
387
|
+
npm can copy that path into public registry metadata. `release:check` also rejects
|
|
388
|
+
private npm fields, local paths and credential URLs in the package manifest.
|
|
389
|
+
After publishing, inspect registry metadata and the downloaded package. The npm
|
|
390
|
+
account's publisher email remains public; changing the package does not hide it.
|
|
391
|
+
|
|
392
|
+
- [Matomo Reporting API](https://developer.matomo.org/guides/reporting-api)
|
|
393
|
+
- [Matomo API tokens](https://matomo.org/faq/general/faq_114/)
|
|
394
|
+
- [Codex MCP](https://learn.chatgpt.com/docs/extend/mcp)
|
|
395
|
+
- [Claude Code MCP](https://code.claude.com/docs/en/mcp)
|
|
396
|
+
- Package license: MIT.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 luskan
|
|
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,89 @@
|
|
|
1
|
+
# Matomo MCP Server (Read-Only)
|
|
2
|
+
|
|
3
|
+
A local MCP server for Claude Code, Claude Desktop and Codex. Use it to read Matomo
|
|
4
|
+
analytics, compare traffic and conversions, and investigate visits and User ID reports.
|
|
5
|
+
|
|
6
|
+
The server only reads from Matomo: it cannot change sites, goals, segments or users.
|
|
7
|
+
|
|
8
|
+
## 1. Install
|
|
9
|
+
|
|
10
|
+
You need Node.js 24+ and a Matomo account with an API token and access to at least one site.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
node -v
|
|
14
|
+
npm install -g @martin4455/matomo-mcp-ro@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The encrypted storage described here requires version **0.4.0+**. For an unreleased
|
|
18
|
+
checkout, use the [source setup](CONFIGURATION.md#source-setup).
|
|
19
|
+
|
|
20
|
+
## 2. Configure
|
|
21
|
+
|
|
22
|
+
Run the configurator in your project directory:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
cd "/absolute/path/to/project"
|
|
26
|
+
matomo-mcp configure
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Enter your HTTPS Matomo URL (for example, `https://matomo.example.com`) and API token.
|
|
30
|
+
Create a token under **Administration → Personal → Security → Create new token**;
|
|
31
|
+
see the [Matomo token guide](https://matomo.org/faq/general/faq_114/).
|
|
32
|
+
If your server also requires HTTP Basic Auth, enter its login and password.
|
|
33
|
+
The configurator tests the connection before saving.
|
|
34
|
+
|
|
35
|
+
The URL, token and optional Basic Auth credentials are encrypted together and stored
|
|
36
|
+
in macOS Keychain, Windows Credential Manager or Linux Secret Service (such as GNOME
|
|
37
|
+
Keyring). The project's `.matomo-mcp.json` stores a UUID reference, not credentials.
|
|
38
|
+
There is no additional passphrase when starting a session.
|
|
39
|
+
Access protection comes from the OS credential store; see the
|
|
40
|
+
[encryption details and limits](CONFIGURATION.md#large-credentials-consistency-and-recovery).
|
|
41
|
+
|
|
42
|
+
Existing entries in the old format are treated as invalid credentials. Run
|
|
43
|
+
`matomo-mcp configure` again and enter the complete connection.
|
|
44
|
+
|
|
45
|
+
## 3. Connect your client
|
|
46
|
+
|
|
47
|
+
From the same project directory, run the command for your client. Replace the quoted
|
|
48
|
+
path with the absolute path to the configuration file created above.
|
|
49
|
+
|
|
50
|
+
**Claude Code:**
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
claude mcp add matomo-local -- npx -y \
|
|
54
|
+
@martin4455/matomo-mcp-ro@latest \
|
|
55
|
+
--config "/absolute/path/to/project/.matomo-mcp.json"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Codex CLI:**
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
codex mcp add matomo-local -- npx -y \
|
|
62
|
+
@martin4455/matomo-mcp-ro@latest \
|
|
63
|
+
--config "/absolute/path/to/project/.matomo-mcp.json"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Claude Desktop:** follow the [Desktop setup](CONFIGURATION.md#claude-desktop-setup).
|
|
67
|
+
|
|
68
|
+
Start or restart your client. In Claude Code or Codex CLI, use `/mcp` to check the
|
|
69
|
+
server's status. The first launch may take a moment to download the package.
|
|
70
|
+
|
|
71
|
+
## 4. Try it
|
|
72
|
+
|
|
73
|
+
Ask your assistant:
|
|
74
|
+
|
|
75
|
+
> Use matomo-local to check which Matomo sites I can access.
|
|
76
|
+
|
|
77
|
+
Then give it an analytics question:
|
|
78
|
+
|
|
79
|
+
> Compare visits, traffic sources and goal conversions for site 1 in September
|
|
80
|
+
> 2026 with August 2026. Explain the largest changes and flag incomplete data.
|
|
81
|
+
|
|
82
|
+
You can also ask about a particular User ID, visit details or custom dimensions.
|
|
83
|
+
Use the server name you registered; the generated Desktop settings use `matomo`.
|
|
84
|
+
|
|
85
|
+
For token changes, platform requirements, storage details, report tools and
|
|
86
|
+
troubleshooting, see the [configuration guide](CONFIGURATION.md).
|
|
87
|
+
|
|
88
|
+
MIT license. [Source](https://github.com/luskan/matomo-mcp-ro) |
|
|
89
|
+
[Report an issue](https://github.com/luskan/matomo-mcp-ro/issues).
|
package/api.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { normalizeBaseUrl } from './lib/config.mjs';
|
|
2
|
+
import { VERSION } from './lib/version.mjs';
|
|
3
|
+
import { createRedactor } from './lib/redaction.mjs';
|
|
4
|
+
|
|
5
|
+
export const REPORT_METHODS = [
|
|
6
|
+
'VisitsSummary.get', 'VisitFrequency.get', 'VisitTime.getVisitInformationPerServerTime',
|
|
7
|
+
'VisitTime.getByDayOfWeek', 'Actions.get', 'Actions.getPageUrls', 'Actions.getPageTitles',
|
|
8
|
+
'Actions.getEntryPageUrls', 'Actions.getExitPageUrls', 'Actions.getDownloads',
|
|
9
|
+
'Actions.getOutlinks', 'Actions.getSiteSearchKeywords', 'Actions.getSiteSearchNoResultKeywords',
|
|
10
|
+
'Referrers.get', 'Referrers.getReferrerType', 'Referrers.getAll', 'Referrers.getWebsites',
|
|
11
|
+
'Referrers.getSearchEngines', 'Referrers.getKeywords', 'Referrers.getSocials',
|
|
12
|
+
'Referrers.getCampaigns', 'UserCountry.getCountry', 'UserCountry.getContinent',
|
|
13
|
+
'UserCountry.getRegion', 'UserCountry.getCity', 'UserLanguage.getLanguage',
|
|
14
|
+
'DevicesDetection.getType', 'DevicesDetection.getBrand', 'DevicesDetection.getModel',
|
|
15
|
+
'DevicesDetection.getBrowsers', 'DevicesDetection.getBrowserVersions',
|
|
16
|
+
'DevicesDetection.getOsFamilies', 'DevicesDetection.getOsVersions',
|
|
17
|
+
'Resolution.getResolution', 'VisitorInterest.getNumberOfVisitsPerPage',
|
|
18
|
+
'VisitorInterest.getNumberOfVisitsPerVisitDuration', 'VisitorInterest.getNumberOfVisitsByVisitCount',
|
|
19
|
+
'VisitorInterest.getNumberOfVisitsByDaysSinceLast', 'Events.getCategory',
|
|
20
|
+
'Events.getAction', 'Events.getName', 'Goals.get', 'Goals.getItemsName',
|
|
21
|
+
'Goals.getItemsSku', 'Goals.getItemsCategory', 'Contents.getContentNames',
|
|
22
|
+
'Contents.getContentPieces', 'PagePerformance.get', 'CustomDimensions.getCustomDimension',
|
|
23
|
+
'CustomReports.getCustomReport', 'MultiSites.getAll', 'MultiSites.getOne', 'UserId.getUsers',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const READ_METHODS = new Set([
|
|
27
|
+
...REPORT_METHODS, 'SitesManager.getSitesWithAtLeastViewAccess',
|
|
28
|
+
'SitesManager.getSiteFromId', 'API.getReportMetadata', 'API.getSegmentsMetadata',
|
|
29
|
+
'Goals.getGoals', 'SegmentEditor.getAll',
|
|
30
|
+
'CustomDimensions.getConfiguredCustomDimensions', 'Live.getLastVisitsDetails',
|
|
31
|
+
]);
|
|
32
|
+
const PARAMETERS = new Set([
|
|
33
|
+
'idSite', 'idSites', 'period', 'date', 'segment', 'idGoal', 'idDimension',
|
|
34
|
+
'idCustomReport', 'idSubtable', 'expanded', 'flat', 'columns', 'filter_limit',
|
|
35
|
+
'filter_offset', 'filter_sort_column', 'filter_sort_order', 'filter_pattern',
|
|
36
|
+
'filter_column', 'format_metrics', 'language', 'showSubtableReports', 'hideMetricsDoc',
|
|
37
|
+
]);
|
|
38
|
+
const LIVE_PARAMETERS = new Set(['idSite', 'period', 'date', 'segment', 'filter_limit',
|
|
39
|
+
'filter_offset', 'filter_sort_order', 'doNotFetchActions', 'language']);
|
|
40
|
+
|
|
41
|
+
export class MatomoError extends Error {}
|
|
42
|
+
|
|
43
|
+
export class MatomoApi {
|
|
44
|
+
#token;
|
|
45
|
+
#authorization;
|
|
46
|
+
#redactor;
|
|
47
|
+
#fetch;
|
|
48
|
+
#endpoint;
|
|
49
|
+
|
|
50
|
+
constructor({ token, username, password, baseUrl, fetchImpl = globalThis.fetch }) {
|
|
51
|
+
const hasBasic = username !== undefined || password !== undefined;
|
|
52
|
+
if (typeof token !== 'string' || !token.length || hasBasic && ![username, password].every(value => typeof value === 'string' && value.length > 0)) {
|
|
53
|
+
throw new MatomoError('Brak danych logowania. Uruchom matomo-mcp configure w katalogu projektu.');
|
|
54
|
+
}
|
|
55
|
+
if (username?.includes(':') || [token, username, password].filter(value => value !== undefined).some(value => /[\x00-\x1f\x7f]/.test(value))) {
|
|
56
|
+
throw new MatomoError('Login Basic Auth nie może zawierać dwukropka ani końca wiersza.');
|
|
57
|
+
}
|
|
58
|
+
this.#endpoint = new URL('index.php', normalizeBaseUrl(baseUrl)).href;
|
|
59
|
+
this.#token = token;
|
|
60
|
+
this.#authorization = hasBasic ? `Basic ${Buffer.from(`${username}:${password}`, 'utf8').toString('base64')}` : undefined;
|
|
61
|
+
this.#redactor = createRedactor([token, username, password, this.#authorization, this.#authorization?.slice(6)]);
|
|
62
|
+
this.#fetch = fetchImpl;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
redact(value) {
|
|
66
|
+
return this.#redactor.text(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sanitize(value) { return this.#redactor.sanitize(value); }
|
|
70
|
+
|
|
71
|
+
async call(method, parameters = {}, signal) {
|
|
72
|
+
if (!READ_METHODS.has(method)) throw new MatomoError('Ta metoda nie jest dostępna w adapterze raportowym.');
|
|
73
|
+
const body = new URLSearchParams();
|
|
74
|
+
const allowed = method === 'Live.getLastVisitsDetails' ? LIVE_PARAMETERS : PARAMETERS;
|
|
75
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
76
|
+
if (!allowed.has(key)) throw new MatomoError('Niedozwolony parametr API.');
|
|
77
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)) throw new MatomoError('Parametry API muszą być wartościami prostymi.');
|
|
78
|
+
if (typeof value === 'number' && !Number.isFinite(value)) throw new MatomoError('Parametr liczbowy musi być skończony.');
|
|
79
|
+
if (key === 'filter_limit' && (!Number.isInteger(Number(value)) || Number(value) < 1 || Number(value) > 1000)) throw new MatomoError('Limit wierszy musi mieścić się w zakresie 1–1000.');
|
|
80
|
+
if (method === 'Live.getLastVisitsDetails' && key === 'filter_limit' && Number(value) > 100) throw new MatomoError('Limit wizyt wynosi 100.');
|
|
81
|
+
// PHP APIs must receive 0/1: the non-empty string "false" can be truthy.
|
|
82
|
+
body.set(key, typeof value === 'boolean' ? (value ? '1' : '0') : String(value));
|
|
83
|
+
}
|
|
84
|
+
body.set('module', 'API');
|
|
85
|
+
body.set('method', method);
|
|
86
|
+
body.set('format', 'JSON');
|
|
87
|
+
body.set('token_auth', this.#token);
|
|
88
|
+
const requestSignal = signal
|
|
89
|
+
? AbortSignal.any([signal, AbortSignal.timeout(60000)])
|
|
90
|
+
: AbortSignal.timeout(60000);
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await this.#fetch(this.#endpoint, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
...(this.#authorization ? { Authorization: this.#authorization } : {}),
|
|
97
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
98
|
+
Accept: 'application/json',
|
|
99
|
+
'User-Agent': `matomo-mcp-ro/${VERSION}`,
|
|
100
|
+
},
|
|
101
|
+
body,
|
|
102
|
+
redirect: 'manual',
|
|
103
|
+
signal: requestSignal,
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
throw new MatomoError(requestSignal.aborted
|
|
107
|
+
? 'Zapytanie zostało przerwane lub przekroczyło limit 60 sekund.'
|
|
108
|
+
: 'Nie udało się połączyć z Matomo. Sprawdź sieć i certyfikat HTTPS.');
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
await response.body?.cancel();
|
|
112
|
+
if (response.status === 401) throw new MatomoError(this.#authorization ? 'HTTP 401: serwer odrzucił uwierzytelnienie; sprawdź token API oraz login i hasło Basic Auth.' : 'HTTP 401: serwer odrzucił dostęp; sprawdź token lub wymaganie HTTP Basic Auth.');
|
|
113
|
+
if (response.status === 403) throw new MatomoError('HTTP 403: serwer odrzucił dostęp do API.');
|
|
114
|
+
if (response.status >= 300 && response.status < 400) throw new MatomoError('Serwer zwrócił przekierowanie. Dane logowania nie zostały przekazane pod inny adres.');
|
|
115
|
+
throw new MatomoError(`Matomo zwróciło HTTP ${response.status}.`);
|
|
116
|
+
}
|
|
117
|
+
if (!response.body) throw new MatomoError('Matomo zwróciło pustą odpowiedź.');
|
|
118
|
+
const reader = response.body.getReader();
|
|
119
|
+
const chunks = [];
|
|
120
|
+
let bytes = 0;
|
|
121
|
+
try {
|
|
122
|
+
for (;;) {
|
|
123
|
+
const { done, value } = await reader.read();
|
|
124
|
+
if (done) break;
|
|
125
|
+
bytes += value.length;
|
|
126
|
+
if (bytes > 10 * 1024 * 1024) {
|
|
127
|
+
await reader.cancel();
|
|
128
|
+
throw new MatomoError('Raport przekracza 10 MB. Zawęź okres lub liczbę wierszy.');
|
|
129
|
+
}
|
|
130
|
+
chunks.push(value);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof MatomoError) throw error;
|
|
134
|
+
throw new MatomoError('Przerwano pobieranie odpowiedzi Matomo.');
|
|
135
|
+
} finally {
|
|
136
|
+
reader.releaseLock();
|
|
137
|
+
}
|
|
138
|
+
let data;
|
|
139
|
+
try {
|
|
140
|
+
data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
141
|
+
} catch {
|
|
142
|
+
throw new MatomoError('Odpowiedź nie jest danymi JSON z API Matomo; możliwa strona logowania lub blokada serwera.');
|
|
143
|
+
}
|
|
144
|
+
data = this.sanitize(data);
|
|
145
|
+
if (data?.result === 'error') {
|
|
146
|
+
throw new MatomoError(`Matomo API: ${this.redact(data.message ?? 'Błąd zapytania')}`);
|
|
147
|
+
}
|
|
148
|
+
return data;
|
|
149
|
+
}
|
|
150
|
+
}
|