@zgeoff/umami-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +51 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +189 -0
- package/dist/index.js.map +1 -0
- package/dist/settings.d.ts +20 -0
- package/dist/settings.js +35 -0
- package/dist/settings.js.map +1 -0
- package/dist/umami-client.d.ts +28 -0
- package/dist/umami-client.js +125 -0
- package/dist/umami-client.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Geoff Whatley
|
|
4
|
+
Portions derived from umami-mcp-server, Copyright (c) 2026 Alessandro Ferrini
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @zgeoff/umami-mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io/) server exposing [Umami](https://umami.is/) analytics — Cloud and self-hosted — as read-only tools.
|
|
4
|
+
|
|
5
|
+
> A TypeScript port of [Alurith/umami-mcp-server](https://github.com/Alurith/umami-mcp-server) (Python, MIT) by Alessandro Ferrini — tool surface and behaviour mirror the original; all credit for the design to its author.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
All tools are read-only.
|
|
10
|
+
|
|
11
|
+
- `get_websites` — list websites, with pagination and search
|
|
12
|
+
- `get_stats` — summary statistics for a time range
|
|
13
|
+
- `get_pageviews` — pageview/session series bucketed by hour/day/month/year
|
|
14
|
+
- `get_metrics` — ranked breakdowns (url, referrer, browser, country, event, …)
|
|
15
|
+
- `get_active` — visitors active in the last 5 minutes
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Auth is one of two modes:
|
|
20
|
+
|
|
21
|
+
| Variable | Mode | Notes |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `UMAMI_API_KEY` | Umami Cloud | required for Cloud; wins when both modes are set |
|
|
24
|
+
| `UMAMI_USERNAME` + `UMAMI_PASSWORD` | self-hosted | rejected against Umami Cloud |
|
|
25
|
+
| `UMAMI_API_BASE` | both | API root; defaults to `https://api.umami.is/v1`. Self-hosted: `https://your-umami.example/api` |
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"umami": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "@zgeoff/umami-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"UMAMI_API_BASE": "https://your-umami.example/api",
|
|
37
|
+
"UMAMI_USERNAME": "admin",
|
|
38
|
+
"UMAMI_PASSWORD": "…"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install
|
|
49
|
+
npm test
|
|
50
|
+
npm run build
|
|
51
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { loadSettings } from './settings.js';
|
|
6
|
+
import { UmamiClient } from './umami-client.js';
|
|
7
|
+
/**
|
|
8
|
+
* Query filters shared by the stats, pageviews, and metrics tools. Mirrors the fields the Umami
|
|
9
|
+
* API accepts; absent fields are simply omitted from the request.
|
|
10
|
+
*/
|
|
11
|
+
const filtersShape = {
|
|
12
|
+
path: z.string().optional().describe('Filter by URL path'),
|
|
13
|
+
referrer: z.string().optional().describe('Filter by referrer'),
|
|
14
|
+
title: z.string().optional().describe('Filter by page title'),
|
|
15
|
+
query: z.string().optional().describe('Filter by query string'),
|
|
16
|
+
browser: z.string().optional().describe('Filter by browser'),
|
|
17
|
+
os: z.string().optional().describe('Filter by operating system'),
|
|
18
|
+
device: z.string().optional().describe('Filter by device type'),
|
|
19
|
+
country: z.string().optional().describe('Filter by country code'),
|
|
20
|
+
region: z.string().optional().describe('Filter by region'),
|
|
21
|
+
city: z.string().optional().describe('Filter by city'),
|
|
22
|
+
hostname: z.string().optional().describe('Filter by hostname'),
|
|
23
|
+
tag: z.string().optional().describe('Filter by tag'),
|
|
24
|
+
segment: z.string().optional().describe('Filter by segment id'),
|
|
25
|
+
cohort: z.string().optional().describe('Filter by cohort id'),
|
|
26
|
+
};
|
|
27
|
+
const filtersSchema = z.object(filtersShape).optional().describe('Optional result filters');
|
|
28
|
+
const startAtSchema = z
|
|
29
|
+
.string()
|
|
30
|
+
.datetime({ offset: true })
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Range start as an ISO 8601 timestamp; defaults to 7 days before the range end');
|
|
33
|
+
const endAtSchema = z
|
|
34
|
+
.string()
|
|
35
|
+
.datetime({ offset: true })
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Range end as an ISO 8601 timestamp; defaults to now');
|
|
38
|
+
/**
|
|
39
|
+
* Umami expects epoch-millisecond `startAt`/`endAt` query params; tools accept ISO strings and
|
|
40
|
+
* default to the trailing 7 days.
|
|
41
|
+
*/
|
|
42
|
+
function toTimerangeQuery(startAt, endAt) {
|
|
43
|
+
const end = endAt !== undefined ? new Date(endAt).getTime() : Date.now();
|
|
44
|
+
const start = startAt !== undefined ? new Date(startAt).getTime() : end - 7 * 24 * 3600 * 1000;
|
|
45
|
+
if (end <= start) {
|
|
46
|
+
throw new Error('end_at must be after start_at');
|
|
47
|
+
}
|
|
48
|
+
return { startAt: String(start), endAt: String(end) };
|
|
49
|
+
}
|
|
50
|
+
function toFiltersQuery(filters) {
|
|
51
|
+
return Object.fromEntries(Object.entries(filters ?? {}).filter(([, value]) => value !== undefined));
|
|
52
|
+
}
|
|
53
|
+
function toToolResult(payload) {
|
|
54
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
55
|
+
}
|
|
56
|
+
function toErrorResult(error) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
59
|
+
isError: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
|
|
63
|
+
async function main() {
|
|
64
|
+
const settings = loadSettings();
|
|
65
|
+
const client = new UmamiClient(settings);
|
|
66
|
+
const server = new McpServer({ name: 'umami-mcp', version: '0.1.0' });
|
|
67
|
+
server.registerTool('get_websites', {
|
|
68
|
+
title: 'List websites',
|
|
69
|
+
description: 'List the websites the account can access, with pagination and search.',
|
|
70
|
+
annotations: READ_ONLY,
|
|
71
|
+
inputSchema: {
|
|
72
|
+
include_teams: z.boolean().default(false).describe('Include team websites'),
|
|
73
|
+
search: z.string().optional().describe('Search term to filter websites'),
|
|
74
|
+
page: z.number().int().min(1).default(1).describe('Page number'),
|
|
75
|
+
page_size: z.number().int().min(1).max(200).default(10).describe('Results per page'),
|
|
76
|
+
},
|
|
77
|
+
}, async ({ include_teams, search, page, page_size }) => {
|
|
78
|
+
try {
|
|
79
|
+
return toToolResult(await client.getWebsites({
|
|
80
|
+
includeTeams: include_teams,
|
|
81
|
+
search,
|
|
82
|
+
page,
|
|
83
|
+
pageSize: page_size,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return toErrorResult(error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
server.registerTool('get_stats', {
|
|
91
|
+
title: 'Get visitor statistics',
|
|
92
|
+
description: 'Summary statistics for a website over a time range: pageviews, visitors, visits, bounces, total time.',
|
|
93
|
+
annotations: READ_ONLY,
|
|
94
|
+
inputSchema: {
|
|
95
|
+
website_id: z.string().describe('Website id (from get_websites)'),
|
|
96
|
+
start_at: startAtSchema,
|
|
97
|
+
end_at: endAtSchema,
|
|
98
|
+
filters: filtersSchema,
|
|
99
|
+
},
|
|
100
|
+
}, async ({ website_id, start_at, end_at, filters }) => {
|
|
101
|
+
try {
|
|
102
|
+
return toToolResult(await client.getStats(website_id, {
|
|
103
|
+
...toTimerangeQuery(start_at, end_at),
|
|
104
|
+
...toFiltersQuery(filters),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return toErrorResult(error);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.registerTool('get_pageviews', {
|
|
112
|
+
title: 'Get pageviews over time',
|
|
113
|
+
description: 'Pageview and session series for a website, bucketed by hour, day, month, or year.',
|
|
114
|
+
annotations: READ_ONLY,
|
|
115
|
+
inputSchema: {
|
|
116
|
+
website_id: z.string().describe('Website id (from get_websites)'),
|
|
117
|
+
start_at: startAtSchema,
|
|
118
|
+
end_at: endAtSchema,
|
|
119
|
+
unit: z.enum(['hour', 'day', 'month', 'year']).default('day').describe('Bucket size'),
|
|
120
|
+
timezone: z.string().default('UTC').describe('IANA timezone for bucketing'),
|
|
121
|
+
compare: z.enum(['prev', 'yoy']).optional().describe('Comparison window: previous period or year-over-year'),
|
|
122
|
+
filters: filtersSchema,
|
|
123
|
+
},
|
|
124
|
+
}, async ({ website_id, start_at, end_at, unit, timezone, compare, filters }) => {
|
|
125
|
+
try {
|
|
126
|
+
return toToolResult(await client.getPageviews(website_id, {
|
|
127
|
+
...toTimerangeQuery(start_at, end_at),
|
|
128
|
+
unit,
|
|
129
|
+
timezone,
|
|
130
|
+
...(compare !== undefined ? { compare } : {}),
|
|
131
|
+
...toFiltersQuery(filters),
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return toErrorResult(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.registerTool('get_metrics', {
|
|
139
|
+
title: 'Get metrics breakdown',
|
|
140
|
+
description: 'Ranked metrics for a website over a time range: type is one of url, referrer, browser, os, device, country, region, city, language, screen, event, hostname, tag.',
|
|
141
|
+
annotations: READ_ONLY,
|
|
142
|
+
inputSchema: {
|
|
143
|
+
website_id: z.string().describe('Website id (from get_websites)'),
|
|
144
|
+
type: z.string().describe('Metric type, e.g. url, referrer, browser, country, event'),
|
|
145
|
+
start_at: startAtSchema,
|
|
146
|
+
end_at: endAtSchema,
|
|
147
|
+
limit: z.number().int().min(1).max(500).default(500).describe('Maximum results'),
|
|
148
|
+
offset: z.number().int().min(0).default(0).describe('Result offset'),
|
|
149
|
+
expanded: z.boolean().default(false).describe('Use the expanded metrics endpoint'),
|
|
150
|
+
filters: filtersSchema,
|
|
151
|
+
},
|
|
152
|
+
}, async ({ website_id, type, start_at, end_at, limit, offset, expanded, filters }) => {
|
|
153
|
+
try {
|
|
154
|
+
return toToolResult(await client.getMetrics(website_id, expanded, {
|
|
155
|
+
...toTimerangeQuery(start_at, end_at),
|
|
156
|
+
type,
|
|
157
|
+
limit: String(limit),
|
|
158
|
+
offset: String(offset),
|
|
159
|
+
...toFiltersQuery(filters),
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return toErrorResult(error);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
server.registerTool('get_active', {
|
|
167
|
+
title: 'Get active visitors',
|
|
168
|
+
description: 'Number of visitors active on the website within the last 5 minutes.',
|
|
169
|
+
annotations: READ_ONLY,
|
|
170
|
+
inputSchema: {
|
|
171
|
+
website_id: z.string().describe('Website id (from get_websites)'),
|
|
172
|
+
},
|
|
173
|
+
}, async ({ website_id }) => {
|
|
174
|
+
try {
|
|
175
|
+
return toToolResult(await client.getActive(website_id));
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return toErrorResult(error);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const transport = new StdioServerTransport();
|
|
182
|
+
await server.connect(transport);
|
|
183
|
+
console.error('umami-mcp connected over stdio');
|
|
184
|
+
}
|
|
185
|
+
main().catch((error) => {
|
|
186
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
});
|
|
189
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;GAGG;AACH,MAAM,YAAY,GAAG;IACnB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IAC1D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IAC9D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;IAC7D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IAC/D,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC;IAC5D,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4BAA4B,CAAC;IAChE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;IAC/D,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IACjE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IACtD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IAC9D,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;IACpD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;IAC/D,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qBAAqB,CAAC;CAC9D,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;AAE5F,MAAM,aAAa,GAAG,CAAC;KACpB,MAAM,EAAE;KACR,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;KAC1B,QAAQ,EAAE;KACV,QAAQ,CAAC,+EAA+E,CAAC,CAAC;AAE7F,MAAM,WAAW,GAAG,CAAC;KAClB,MAAM,EAAE;KACR,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;KAC1B,QAAQ,EAAE;KACV,QAAQ,CAAC,qDAAqD,CAAC,CAAC;AAInE;;;GAGG;AACH,SAAS,gBAAgB,CAAC,OAAgB,EAAE,KAAc;IACxD,MAAM,GAAG,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACzE,MAAM,KAAK,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;IAE/F,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,SAAS,cAAc,CAAC,OAAgB;IACtC,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAC/C,CAAC;AAC9B,CAAC;AAED,SAAS,YAAY,CAAC,OAAgB;IACpC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1F,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClG,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;AAEtF,KAAK,UAAU,IAAI;IACjB,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAEtE,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,uEAAuE;QACpF,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE;YACX,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;YAC3E,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;YACxE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC;YAChE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,kBAAkB,CAAC;SACrF;KACF,EACD,KAAK,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;QACnD,IAAI,CAAC;YACH,OAAO,YAAY,CACjB,MAAM,MAAM,CAAC,WAAW,CAAC;gBACvB,YAAY,EAAE,aAAa;gBAC3B,MAAM;gBACN,IAAI;gBACJ,QAAQ,EAAE,SAAS;aACpB,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,WAAW,EACX;QACE,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EACT,uGAAuG;QACzG,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE;YACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;YACjE,QAAQ,EAAE,aAAa;YACvB,MAAM,EAAE,WAAW;YACnB,OAAO,EAAE,aAAa;SACvB;KACF,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;QAClD,IAAI,CAAC;YACH,OAAO,YAAY,CACjB,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE;gBAChC,GAAG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACrC,GAAG,cAAc,CAAC,OAAO,CAAC;aAC3B,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,mFAAmF;QAChG,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE;YACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;YACjE,QAAQ,EAAE,aAAa;YACvB,MAAM,EAAE,WAAW;YACnB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC;YACrF,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,6BAA6B,CAAC;YAC3E,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sDAAsD,CAAC;YAC5G,OAAO,EAAE,aAAa;SACvB;KACF,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3E,IAAI,CAAC;YACH,OAAO,YAAY,CACjB,MAAM,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE;gBACpC,GAAG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACrC,IAAI;gBACJ,QAAQ;gBACR,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7C,GAAG,cAAc,CAAC,OAAO,CAAC;aAC3B,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EACT,mKAAmK;QACrK,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE;YACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;YACjE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0DAA0D,CAAC;YACrF,QAAQ,EAAE,aAAa;YACvB,MAAM,EAAE,WAAW;YACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,iBAAiB,CAAC;YAChF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC;YACpE,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YAClF,OAAO,EAAE,aAAa;SACvB;KACF,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE;QACjF,IAAI,CAAC;YACH,OAAO,YAAY,CACjB,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,EAAE,QAAQ,EAAE;gBAC5C,GAAG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACrC,IAAI;gBACJ,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;gBACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;gBACtB,GAAG,cAAc,CAAC,OAAO,CAAC;aAC3B,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,qEAAqE;QAClF,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE;YACX,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;SAClE;KACF,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;QACvB,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;AAClD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Umami connection settings resolved from the environment.
|
|
3
|
+
*
|
|
4
|
+
* Two auth modes exist and are mutually exclusive:
|
|
5
|
+
* - `api_key` — Umami Cloud (or any deployment issuing API keys); `UMAMI_API_KEY`.
|
|
6
|
+
* - `login` — self-hosted username/password; `UMAMI_USERNAME` + `UMAMI_PASSWORD`, with
|
|
7
|
+
* `UMAMI_API_BASE` pointing at the instance API root (e.g. `https://umami.example.com/api`).
|
|
8
|
+
*/
|
|
9
|
+
export interface Settings {
|
|
10
|
+
apiBase: string;
|
|
11
|
+
auth: {
|
|
12
|
+
mode: 'api_key';
|
|
13
|
+
apiKey: string;
|
|
14
|
+
} | {
|
|
15
|
+
mode: 'login';
|
|
16
|
+
username: string;
|
|
17
|
+
password: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function loadSettings(env?: NodeJS.ProcessEnv): Settings;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const CLOUD_DEFAULT_API_BASE = 'https://api.umami.is/v1';
|
|
2
|
+
export function loadSettings(env = process.env) {
|
|
3
|
+
const apiKey = env.UMAMI_API_KEY?.trim() || undefined;
|
|
4
|
+
const username = env.UMAMI_USERNAME?.trim() || undefined;
|
|
5
|
+
const password = env.UMAMI_PASSWORD?.trim() || undefined;
|
|
6
|
+
const apiBase = (env.UMAMI_API_BASE?.trim() || CLOUD_DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
7
|
+
if (apiKey) {
|
|
8
|
+
return { apiBase, auth: { mode: 'api_key', apiKey } };
|
|
9
|
+
}
|
|
10
|
+
if (Boolean(username) !== Boolean(password)) {
|
|
11
|
+
throw new Error('Invalid Umami auth configuration. Provide either UMAMI_API_KEY (Umami Cloud) ' +
|
|
12
|
+
'or both UMAMI_USERNAME and UMAMI_PASSWORD (self-hosted).');
|
|
13
|
+
}
|
|
14
|
+
if (!username || !password) {
|
|
15
|
+
throw new Error('Missing Umami credentials. Provide UMAMI_API_KEY (Umami Cloud) or both ' +
|
|
16
|
+
'UMAMI_USERNAME and UMAMI_PASSWORD (self-hosted). Note: Umami Cloud only ' +
|
|
17
|
+
'supports API keys.');
|
|
18
|
+
}
|
|
19
|
+
const host = safeHostname(apiBase);
|
|
20
|
+
if (host === 'api.umami.is') {
|
|
21
|
+
throw new Error('UMAMI_USERNAME/UMAMI_PASSWORD are only supported for self-hosted Umami. ' +
|
|
22
|
+
'Umami Cloud requires UMAMI_API_KEY. For self-hosted, set UMAMI_API_BASE to ' +
|
|
23
|
+
'your instance API root (e.g. https://your-umami.example/api).');
|
|
24
|
+
}
|
|
25
|
+
return { apiBase, auth: { mode: 'login', username, password } };
|
|
26
|
+
}
|
|
27
|
+
function safeHostname(url) {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(url).hostname.toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error(`UMAMI_API_BASE is not a valid URL: ${url}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAaA,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAEzD,MAAM,UAAU,YAAY,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC/D,MAAM,MAAM,GAAG,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACzD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,sBAAsB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE3F,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACxD,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,+EAA+E;YAC7E,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,yEAAyE;YACvE,0EAA0E;YAC1E,oBAAoB,CACvB,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,0EAA0E;YACxE,6EAA6E;YAC7E,+DAA+D,CAClE,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,sCAAsC,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Settings } from './settings.js';
|
|
2
|
+
export declare class UmamiError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Thin fetch client over the Umami HTTP API.
|
|
6
|
+
*
|
|
7
|
+
* Auth: `api_key` mode sends `x-umami-api-key` on every request; `login` mode lazily exchanges
|
|
8
|
+
* username/password for a bearer token at `/auth/login` and caches it for the process. Transport
|
|
9
|
+
* failures and 429s retry up to three times with exponential backoff; auth failures and other
|
|
10
|
+
* HTTP errors surface as `UmamiError` with a message that points at the misconfigured setting.
|
|
11
|
+
*/
|
|
12
|
+
export declare class UmamiClient {
|
|
13
|
+
private readonly settings;
|
|
14
|
+
private token;
|
|
15
|
+
constructor(settings: Settings);
|
|
16
|
+
getWebsites(params: {
|
|
17
|
+
includeTeams: boolean;
|
|
18
|
+
search?: string | undefined;
|
|
19
|
+
page: number;
|
|
20
|
+
pageSize: number;
|
|
21
|
+
}): Promise<unknown>;
|
|
22
|
+
getStats(websiteID: string, query: Record<string, string>): Promise<unknown>;
|
|
23
|
+
getPageviews(websiteID: string, query: Record<string, string>): Promise<unknown>;
|
|
24
|
+
getMetrics(websiteID: string, expanded: boolean, query: Record<string, string>): Promise<unknown>;
|
|
25
|
+
getActive(websiteID: string): Promise<unknown>;
|
|
26
|
+
private ensureToken;
|
|
27
|
+
private request;
|
|
28
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export class UmamiError extends Error {
|
|
2
|
+
}
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
/**
|
|
5
|
+
* Thin fetch client over the Umami HTTP API.
|
|
6
|
+
*
|
|
7
|
+
* Auth: `api_key` mode sends `x-umami-api-key` on every request; `login` mode lazily exchanges
|
|
8
|
+
* username/password for a bearer token at `/auth/login` and caches it for the process. Transport
|
|
9
|
+
* failures and 429s retry up to three times with exponential backoff; auth failures and other
|
|
10
|
+
* HTTP errors surface as `UmamiError` with a message that points at the misconfigured setting.
|
|
11
|
+
*/
|
|
12
|
+
export class UmamiClient {
|
|
13
|
+
settings;
|
|
14
|
+
token;
|
|
15
|
+
constructor(settings) {
|
|
16
|
+
this.settings = settings;
|
|
17
|
+
}
|
|
18
|
+
async getWebsites(params) {
|
|
19
|
+
return this.request('/websites', {
|
|
20
|
+
includeTeams: String(params.includeTeams),
|
|
21
|
+
page: String(params.page),
|
|
22
|
+
pageSize: String(params.pageSize),
|
|
23
|
+
...(params.search ? { search: params.search } : {}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async getStats(websiteID, query) {
|
|
27
|
+
return this.request(`/websites/${encodeURIComponent(websiteID)}/stats`, query);
|
|
28
|
+
}
|
|
29
|
+
async getPageviews(websiteID, query) {
|
|
30
|
+
return this.request(`/websites/${encodeURIComponent(websiteID)}/pageviews`, query);
|
|
31
|
+
}
|
|
32
|
+
async getMetrics(websiteID, expanded, query) {
|
|
33
|
+
const path = expanded
|
|
34
|
+
? `/websites/${encodeURIComponent(websiteID)}/metrics/expanded`
|
|
35
|
+
: `/websites/${encodeURIComponent(websiteID)}/metrics`;
|
|
36
|
+
return this.request(path, query);
|
|
37
|
+
}
|
|
38
|
+
async getActive(websiteID) {
|
|
39
|
+
return this.request(`/websites/${encodeURIComponent(websiteID)}/active`, {});
|
|
40
|
+
}
|
|
41
|
+
async ensureToken() {
|
|
42
|
+
if (this.token !== undefined) {
|
|
43
|
+
return this.token;
|
|
44
|
+
}
|
|
45
|
+
if (this.settings.auth.mode !== 'login') {
|
|
46
|
+
throw new UmamiError('ensureToken called outside login auth mode');
|
|
47
|
+
}
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await fetch(`${this.settings.apiBase}/auth/login`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
username: this.settings.auth.username,
|
|
55
|
+
password: this.settings.auth.password,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new UmamiError(`Login request failed: ${String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 401 || response.status === 403) {
|
|
63
|
+
throw new UmamiError('Authentication failed (check UMAMI_USERNAME/UMAMI_PASSWORD). Note: Umami Cloud only supports UMAMI_API_KEY.');
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new UmamiError(`Login failed (${response.status}): ${await response.text()}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
const token = typeof data === 'object' && data !== null && 'token' in data ? data.token : undefined;
|
|
70
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
71
|
+
throw new UmamiError('Login succeeded but no token was returned by Umami');
|
|
72
|
+
}
|
|
73
|
+
this.token = token;
|
|
74
|
+
return token;
|
|
75
|
+
}
|
|
76
|
+
async request(path, query) {
|
|
77
|
+
let lastError;
|
|
78
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
79
|
+
const headers = { Accept: 'application/json' };
|
|
80
|
+
if (this.settings.auth.mode === 'api_key') {
|
|
81
|
+
headers['x-umami-api-key'] = this.settings.auth.apiKey;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
headers.Authorization = `Bearer ${await this.ensureToken()}`;
|
|
85
|
+
}
|
|
86
|
+
const url = new URL(`${this.settings.apiBase}${path}`);
|
|
87
|
+
for (const [key, value] of Object.entries(query)) {
|
|
88
|
+
url.searchParams.set(key, value);
|
|
89
|
+
}
|
|
90
|
+
let response;
|
|
91
|
+
try {
|
|
92
|
+
response = await fetch(url, { headers });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
lastError = error;
|
|
96
|
+
await sleep(250 * 2 ** attempt);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 401 || response.status === 403) {
|
|
100
|
+
throw new UmamiError(this.settings.auth.mode === 'api_key'
|
|
101
|
+
? 'Authentication failed (check UMAMI_API_KEY)'
|
|
102
|
+
: 'Authentication failed (check UMAMI_USERNAME/UMAMI_PASSWORD). Note: Umami Cloud only supports UMAMI_API_KEY.');
|
|
103
|
+
}
|
|
104
|
+
if (response.status === 429) {
|
|
105
|
+
await sleep(500 * 2 ** attempt);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new UmamiError(`Umami API error ${response.status}: ${await response.text()}`);
|
|
110
|
+
}
|
|
111
|
+
if (response.status === 204) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return response.json();
|
|
115
|
+
}
|
|
116
|
+
if (lastError !== undefined) {
|
|
117
|
+
throw new UmamiError(`Request failed after retries: ${String(lastError)}`);
|
|
118
|
+
}
|
|
119
|
+
throw new UmamiError('Request rate-limited (429) after retries');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function sleep(ms) {
|
|
123
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=umami-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"umami-client.js","sourceRoot":"","sources":["../src/umami-client.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG;AAExC,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,OAAO,WAAW;IACL,QAAQ,CAAW;IAC5B,KAAK,CAAqB;IAElC,YAAY,QAAkB;QAC5B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAKjB;QACC,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;YAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;YACzC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;YACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;YACjC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,KAA6B;QAC7D,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,SAAiB,EAAE,KAA6B;QACjE,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,kBAAkB,CAAC,SAAS,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,UAAU,CACd,SAAiB,EACjB,QAAiB,EACjB,KAA6B;QAE7B,MAAM,IAAI,GAAG,QAAQ;YACnB,CAAC,CAAC,aAAa,kBAAkB,CAAC,SAAS,CAAC,mBAAmB;YAC/D,CAAC,CAAC,aAAa,kBAAkB,CAAC,SAAS,CAAC,UAAU,CAAC;QACzD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,SAAiB;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,kBAAkB,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC/E,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,KAAK,CAAC;QACpB,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACxC,MAAM,IAAI,UAAU,CAAC,4CAA4C,CAAC,CAAC;QACrE,CAAC;QAED,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,aAAa,EAAE;gBAC5D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE;gBAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ;oBACrC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ;iBACtC,CAAC;aACH,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,UAAU,CAAC,yBAAyB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjE,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvD,MAAM,IAAI,UAAU,CAClB,6GAA6G,CAC9G,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,UAAU,CAAC,iBAAiB,QAAQ,CAAC,MAAM,MAAM,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACtF,CAAC;QAED,MAAM,IAAI,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,KAAK,GACT,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAExF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,UAAU,CAAC,oDAAoD,CAAC,CAAC;QAC7E,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,KAA6B;QAC/D,IAAI,SAAkB,CAAC;QAEvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;YACvD,MAAM,OAAO,GAA2B,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;YAEvE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC1C,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YACzD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,aAAa,GAAG,UAAU,MAAM,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAC/D,CAAC;YAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC,CAAC;YACvD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACnC,CAAC;YAED,IAAI,QAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG,KAAK,CAAC;gBAClB,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACvD,MAAM,IAAI,UAAU,CAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS;oBACnC,CAAC,CAAC,6CAA6C;oBAC/C,CAAC,CAAC,6GAA6G,CAClH,CAAC;YACJ,CAAC;YAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,UAAU,CAAC,mBAAmB,QAAQ,CAAC,MAAM,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACvF,CAAC;YAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC;QAED,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,IAAI,UAAU,CAAC,iCAAiC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,IAAI,UAAU,CAAC,0CAA0C,CAAC,CAAC;IACnE,CAAC;CACF;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zgeoff/umami-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing Umami analytics (Cloud + self-hosted) via typed read-only tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"umami-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"watch": "tsc --watch",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"format": "prettier --write src/",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"umami",
|
|
34
|
+
"analytics"
|
|
35
|
+
],
|
|
36
|
+
"author": "Geoff Whatley <me@geoffwhatley.com>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/zgeoff/umami-mcp.git"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
44
|
+
"zod": "3.25.76"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "22.18.13",
|
|
48
|
+
"prettier": "3.6.2",
|
|
49
|
+
"tsx": "4.20.6",
|
|
50
|
+
"typescript": "5.9.3",
|
|
51
|
+
"vitest": "3.2.4"
|
|
52
|
+
}
|
|
53
|
+
}
|