@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.
@@ -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
+ }