@asif2bd/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 +21 -0
- package/README.md +400 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.js +163 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +121 -0
- package/dist/config.js.map +1 -0
- package/dist/envfile.d.ts +21 -0
- package/dist/envfile.js +72 -0
- package/dist/envfile.js.map +1 -0
- package/dist/http.d.ts +7 -0
- package/dist/http.js +133 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/consent.d.ts +13 -0
- package/dist/oauth/consent.js +99 -0
- package/dist/oauth/consent.js.map +1 -0
- package/dist/oauth/router.d.ts +27 -0
- package/dist/oauth/router.js +329 -0
- package/dist/oauth/router.js.map +1 -0
- package/dist/oauth/seal.d.ts +22 -0
- package/dist/oauth/seal.js +82 -0
- package/dist/oauth/seal.js.map +1 -0
- package/dist/oauth/store.d.ts +40 -0
- package/dist/oauth/store.js +48 -0
- package/dist/oauth/store.js.map +1 -0
- package/dist/redact.d.ts +14 -0
- package/dist/redact.js +54 -0
- package/dist/redact.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +47 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/admin.d.ts +1 -0
- package/dist/tools/admin.js +99 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/analytics.d.ts +1 -0
- package/dist/tools/analytics.js +141 -0
- package/dist/tools/analytics.js.map +1 -0
- package/dist/tools/common.d.ts +46 -0
- package/dist/tools/common.js +86 -0
- package/dist/tools/common.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/reports.d.ts +1 -0
- package/dist/tools/reports.js +122 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/websites.d.ts +1 -0
- package/dist/tools/websites.js +194 -0
- package/dist/tools/websites.js.map +1 -0
- package/package.json +54 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration is read exclusively from the process environment.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here is ever transmitted anywhere except to the single Umami
|
|
5
|
+
* instance named by UMAMI_URL. There is no telemetry, no phone-home, and no
|
|
6
|
+
* remote credential broker. If you self-host this server, your credentials
|
|
7
|
+
* stay on your machine.
|
|
8
|
+
*/
|
|
9
|
+
export class ConfigError extends Error {
|
|
10
|
+
}
|
|
11
|
+
const MODES = ['read', 'write', 'admin'];
|
|
12
|
+
function bool(v, fallback) {
|
|
13
|
+
if (v === undefined || v === '')
|
|
14
|
+
return fallback;
|
|
15
|
+
return ['1', 'true', 'yes', 'on'].includes(v.trim().toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
function int(v, fallback, name) {
|
|
18
|
+
if (v === undefined || v === '')
|
|
19
|
+
return fallback;
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
22
|
+
throw new ConfigError(`${name} must be a positive integer, got ${JSON.stringify(v)}`);
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
export function loadConfig(env = process.env) {
|
|
27
|
+
// In OAuth mode this server is multi-tenant: each user supplies their own
|
|
28
|
+
// Umami and their own login at consent time, so the process itself holds no
|
|
29
|
+
// instance URL and no credentials.
|
|
30
|
+
const oauthEnabled = bool(env.UMAMI_MCP_OAUTH, false);
|
|
31
|
+
const rawUrl = (env.UMAMI_URL ?? '').trim().replace(/\/+$/, '');
|
|
32
|
+
if (!rawUrl && !oauthEnabled) {
|
|
33
|
+
throw new ConfigError('UMAMI_URL is required (e.g. https://analytics.example.com). ' +
|
|
34
|
+
'See .env.example for the full list of settings.');
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
if (rawUrl) {
|
|
38
|
+
try {
|
|
39
|
+
parsed = new URL(rawUrl);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ConfigError(`UMAMI_URL is not a valid URL: ${JSON.stringify(rawUrl)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (parsed && parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
46
|
+
throw new ConfigError(`UMAMI_URL must be http(s), got ${parsed.protocol}`);
|
|
47
|
+
}
|
|
48
|
+
// Credentials over plaintext HTTP are only tolerated on the loopback
|
|
49
|
+
// interface, where there is no network to sniff.
|
|
50
|
+
const loopback = parsed ? ['localhost', '127.0.0.1', '::1', '[::1]'].includes(parsed.hostname) : false;
|
|
51
|
+
if (parsed && parsed.protocol === 'http:' && !loopback) {
|
|
52
|
+
throw new ConfigError(`Refusing to send credentials over plaintext HTTP to ${parsed.hostname}. ` +
|
|
53
|
+
'Use https://, or point UMAMI_URL at localhost for local development.');
|
|
54
|
+
}
|
|
55
|
+
const username = env.UMAMI_USERNAME?.trim() || undefined;
|
|
56
|
+
const password = env.UMAMI_PASSWORD ?? undefined;
|
|
57
|
+
const apiKey = env.UMAMI_API_KEY?.trim() || undefined;
|
|
58
|
+
if (!oauthEnabled && !apiKey && !(username && password)) {
|
|
59
|
+
throw new ConfigError('No credentials found. Set UMAMI_USERNAME and UMAMI_PASSWORD (self-hosted), ' +
|
|
60
|
+
'or UMAMI_API_KEY (Umami Cloud). ' +
|
|
61
|
+
'Alternatively set UMAMI_MCP_OAUTH=true to run multi-tenant, where each user brings their own.');
|
|
62
|
+
}
|
|
63
|
+
let oauth;
|
|
64
|
+
if (oauthEnabled) {
|
|
65
|
+
const issuer = (env.UMAMI_MCP_ISSUER ?? '').trim().replace(/\/+$/, '');
|
|
66
|
+
if (!issuer) {
|
|
67
|
+
throw new ConfigError('UMAMI_MCP_OAUTH is on, so UMAMI_MCP_ISSUER must be the public HTTPS URL of this server ' +
|
|
68
|
+
'(e.g. https://umami-mcp.example.com). Clients build their redirect back from it.');
|
|
69
|
+
}
|
|
70
|
+
const issuerUrl = new URL(issuer);
|
|
71
|
+
if (issuerUrl.protocol !== 'https:' && !['localhost', '127.0.0.1'].includes(issuerUrl.hostname)) {
|
|
72
|
+
throw new ConfigError('UMAMI_MCP_ISSUER must be https (or localhost for development).');
|
|
73
|
+
}
|
|
74
|
+
const tokenKey = env.UMAMI_MCP_TOKEN_KEY ?? '';
|
|
75
|
+
if (tokenKey.length < 16) {
|
|
76
|
+
throw new ConfigError('UMAMI_MCP_TOKEN_KEY must be set to at least 16 characters and kept stable. ' +
|
|
77
|
+
'It encrypts user credentials inside access tokens; changing it invalidates every issued token. ' +
|
|
78
|
+
'Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'base64url\'))"');
|
|
79
|
+
}
|
|
80
|
+
oauth = {
|
|
81
|
+
enabled: true,
|
|
82
|
+
issuer,
|
|
83
|
+
tokenKey,
|
|
84
|
+
ttlSeconds: int(env.UMAMI_MCP_TOKEN_TTL, 30 * 24 * 3600, 'UMAMI_MCP_TOKEN_TTL'),
|
|
85
|
+
fixedUrl: rawUrl || undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const rawMode = (env.UMAMI_MCP_MODE ?? 'read').trim().toLowerCase();
|
|
89
|
+
if (!MODES.includes(rawMode)) {
|
|
90
|
+
throw new ConfigError(`UMAMI_MCP_MODE must be one of ${MODES.join(' | ')}, got ${JSON.stringify(rawMode)}`);
|
|
91
|
+
}
|
|
92
|
+
const rawTransport = (env.UMAMI_MCP_TRANSPORT ?? env.TRANSPORT ?? 'stdio').trim().toLowerCase();
|
|
93
|
+
if (rawTransport !== 'stdio' && rawTransport !== 'http') {
|
|
94
|
+
throw new ConfigError(`transport must be "stdio" or "http", got ${JSON.stringify(rawTransport)}`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
url: rawUrl,
|
|
98
|
+
username,
|
|
99
|
+
password,
|
|
100
|
+
apiKey,
|
|
101
|
+
teamId: env.UMAMI_TEAM_ID?.trim() || undefined,
|
|
102
|
+
mode: rawMode,
|
|
103
|
+
allowDestructive: bool(env.UMAMI_MCP_ALLOW_DESTRUCTIVE, false),
|
|
104
|
+
transport: rawTransport,
|
|
105
|
+
host: (env.UMAMI_MCP_HOST ?? '127.0.0.1').trim(),
|
|
106
|
+
port: int(env.UMAMI_MCP_PORT ?? env.PORT, 3334, 'UMAMI_MCP_PORT'),
|
|
107
|
+
timeoutMs: int(env.UMAMI_MCP_TIMEOUT_MS, 30_000, 'UMAMI_MCP_TIMEOUT_MS'),
|
|
108
|
+
oauth,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Human-readable summary for startup logs. Never includes secrets. */
|
|
112
|
+
export function describeConfig(c) {
|
|
113
|
+
if (c.oauth?.enabled) {
|
|
114
|
+
const scope = c.oauth.fixedUrl ? `pinned to ${c.oauth.fixedUrl}` : 'multi-tenant';
|
|
115
|
+
return `OAuth mode, ${scope} (issuer=${c.oauth.issuer}, transport=${c.transport})`;
|
|
116
|
+
}
|
|
117
|
+
const auth = c.apiKey ? 'api-key' : `user:${c.username}`;
|
|
118
|
+
const destructive = c.allowDestructive ? ' +destructive' : '';
|
|
119
|
+
return `${c.url} (auth=${auth}, mode=${c.mode}${destructive}, transport=${c.transport})`;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional env-file loading.
|
|
3
|
+
*
|
|
4
|
+
* MCP clients configure servers by embedding environment variables in a JSON
|
|
5
|
+
* config file -- ~/.claude.json, mcp.json, and friends. Those files are widely
|
|
6
|
+
* readable, are routinely pasted into issues and screen-shares, and several
|
|
7
|
+
* clients sync them between machines. Self-hosted Umami has no API keys, so the
|
|
8
|
+
* value being embedded is a real account password.
|
|
9
|
+
*
|
|
10
|
+
* Loading credentials from a file the operator controls means the client config
|
|
11
|
+
* can name the server and nothing else.
|
|
12
|
+
*
|
|
13
|
+
* Precedence: real environment variables always win, so an explicit
|
|
14
|
+
* `UMAMI_PASSWORD=...` in the client config still overrides the file.
|
|
15
|
+
*/
|
|
16
|
+
export interface EnvFileResult {
|
|
17
|
+
path?: string;
|
|
18
|
+
loaded: number;
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function loadEnvFile(env?: NodeJS.ProcessEnv): EnvFileResult;
|
package/dist/envfile.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
function parse(text) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (const rawLine of text.split('\n')) {
|
|
7
|
+
const line = rawLine.trim();
|
|
8
|
+
if (!line || line.startsWith('#'))
|
|
9
|
+
continue;
|
|
10
|
+
const eq = line.indexOf('=');
|
|
11
|
+
if (eq === -1)
|
|
12
|
+
continue;
|
|
13
|
+
const key = line.slice(0, eq).trim().replace(/^export\s+/, '');
|
|
14
|
+
if (!key)
|
|
15
|
+
continue;
|
|
16
|
+
let value = line.slice(eq + 1).trim();
|
|
17
|
+
// Strip one layer of matching quotes; leave inner content untouched so
|
|
18
|
+
// passwords containing '#' or spaces survive intact.
|
|
19
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
20
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
|
|
21
|
+
value = value.slice(1, -1);
|
|
22
|
+
}
|
|
23
|
+
out[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function candidatePaths(env) {
|
|
28
|
+
if (env.UMAMI_MCP_ENV_FILE)
|
|
29
|
+
return [env.UMAMI_MCP_ENV_FILE];
|
|
30
|
+
return [
|
|
31
|
+
join(env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'umami-mcp', 'env'),
|
|
32
|
+
join(process.cwd(), '.env'),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
export function loadEnvFile(env = process.env) {
|
|
36
|
+
const explicit = Boolean(env.UMAMI_MCP_ENV_FILE);
|
|
37
|
+
const warnings = [];
|
|
38
|
+
for (const path of candidatePaths(env)) {
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = readFileSync(path, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// An explicitly named file that cannot be read is an operator error worth
|
|
45
|
+
// surfacing; the implicit locations are just absent.
|
|
46
|
+
if (explicit)
|
|
47
|
+
warnings.push(`UMAMI_MCP_ENV_FILE is set to ${path} but it could not be read.`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const mode = statSync(path).mode & 0o077;
|
|
52
|
+
if (mode !== 0) {
|
|
53
|
+
warnings.push(`${path} is readable by other users (mode ${(statSync(path).mode & 0o777).toString(8)}). ` +
|
|
54
|
+
`Run: chmod 600 ${path}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* stat failing is not fatal */
|
|
59
|
+
}
|
|
60
|
+
let loaded = 0;
|
|
61
|
+
for (const [k, v] of Object.entries(parse(text))) {
|
|
62
|
+
// Real environment wins.
|
|
63
|
+
if (env[k] === undefined) {
|
|
64
|
+
env[k] = v;
|
|
65
|
+
loaded++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { path, loaded, warnings };
|
|
69
|
+
}
|
|
70
|
+
return { loaded: 0, warnings };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=envfile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envfile.js","sourceRoot":"","sources":["../src/envfile.ts"],"names":[],"mappings":"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"}
|
package/dist/http.d.ts
ADDED
package/dist/http.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { UmamiClient } from './client.js';
|
|
4
|
+
import { redactUnknown } from './redact.js';
|
|
5
|
+
import { buildServer } from './server.js';
|
|
6
|
+
import { OAuthProvider } from './oauth/router.js';
|
|
7
|
+
import { deriveKey, SealError } from './oauth/seal.js';
|
|
8
|
+
async function readJsonBody(req, limit = 4 * 1024 * 1024) {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
let size = 0;
|
|
11
|
+
for await (const chunk of req) {
|
|
12
|
+
size += chunk.length;
|
|
13
|
+
if (size > limit)
|
|
14
|
+
throw new Error('Request body too large.');
|
|
15
|
+
chunks.push(chunk);
|
|
16
|
+
}
|
|
17
|
+
if (chunks.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
20
|
+
}
|
|
21
|
+
export async function startHttp({ config, log }) {
|
|
22
|
+
const oauth = config.oauth?.enabled
|
|
23
|
+
? new OAuthProvider({
|
|
24
|
+
issuer: config.oauth.issuer,
|
|
25
|
+
key: deriveKey(config.oauth.tokenKey),
|
|
26
|
+
fixedUrl: config.oauth.fixedUrl,
|
|
27
|
+
tokenTtlSeconds: config.oauth.ttlSeconds,
|
|
28
|
+
baseConfig: config,
|
|
29
|
+
})
|
|
30
|
+
: undefined;
|
|
31
|
+
// Single-tenant mode keeps one long-lived server, as before.
|
|
32
|
+
const shared = oauth ? undefined : buildServer(config);
|
|
33
|
+
let sharedTransport;
|
|
34
|
+
if (shared) {
|
|
35
|
+
sharedTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
36
|
+
await shared.server.connect(sharedTransport);
|
|
37
|
+
log(`${shared.registered.length} tools available, ${shared.withheld.length} withheld by mode "${config.mode}"`);
|
|
38
|
+
}
|
|
39
|
+
const server = http.createServer((req, res) => {
|
|
40
|
+
void handle(req, res).catch((err) => {
|
|
41
|
+
log('request failed:', redactUnknown(err));
|
|
42
|
+
if (!res.headersSent) {
|
|
43
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
44
|
+
res.end(JSON.stringify({ error: 'internal_error' }));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
res.end();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
async function handle(req, res) {
|
|
52
|
+
const base = config.oauth?.issuer ?? `http://${req.headers.host ?? 'localhost'}`;
|
|
53
|
+
const url = new URL(req.url ?? '/', base);
|
|
54
|
+
if (url.pathname === '/health') {
|
|
55
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
56
|
+
res.end(JSON.stringify({
|
|
57
|
+
ok: true,
|
|
58
|
+
mode: oauth ? 'oauth' : config.mode,
|
|
59
|
+
multiTenant: Boolean(oauth && !config.oauth?.fixedUrl),
|
|
60
|
+
}));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (oauth && (await oauth.handle(req, res, url)))
|
|
64
|
+
return;
|
|
65
|
+
if (!url.pathname.startsWith('/mcp')) {
|
|
66
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
67
|
+
res.end(JSON.stringify({ error: 'not found; the MCP endpoint is /mcp' }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!oauth) {
|
|
71
|
+
const body = await readJsonBody(req).catch(() => undefined);
|
|
72
|
+
await sharedTransport.handleRequest(req, res, body);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// --- OAuth mode: authenticate, then serve this one caller only. ---
|
|
76
|
+
let identity;
|
|
77
|
+
try {
|
|
78
|
+
identity = oauth.authenticate(req);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// RFC 9728: point unauthenticated clients at the metadata so they can
|
|
82
|
+
// start the flow on their own.
|
|
83
|
+
res.writeHead(401, {
|
|
84
|
+
'content-type': 'application/json',
|
|
85
|
+
'www-authenticate': `Bearer resource_metadata="${config.oauth.issuer}/.well-known/oauth-protected-resource"`,
|
|
86
|
+
});
|
|
87
|
+
res.end(JSON.stringify({
|
|
88
|
+
error: 'unauthorized',
|
|
89
|
+
error_description: err instanceof SealError ? err.message : 'Authentication required.',
|
|
90
|
+
}));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// A per-request server, scoped to exactly this user's Umami. Nothing is
|
|
94
|
+
// shared between callers -- not the client, not the token, not the tools.
|
|
95
|
+
const scoped = {
|
|
96
|
+
...config,
|
|
97
|
+
url: identity.url,
|
|
98
|
+
username: identity.username,
|
|
99
|
+
password: identity.password,
|
|
100
|
+
apiKey: identity.apiKey,
|
|
101
|
+
mode: identity.mode,
|
|
102
|
+
// Irreversible operations are never reachable over a hosted connection,
|
|
103
|
+
// whatever the user consented to. A remote caller cannot be shown the
|
|
104
|
+
// typed confirmation guard the local tools rely on.
|
|
105
|
+
allowDestructive: false,
|
|
106
|
+
oauth: undefined,
|
|
107
|
+
};
|
|
108
|
+
const built = buildServer(scoped, new UmamiClient(scoped));
|
|
109
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
110
|
+
try {
|
|
111
|
+
await built.server.connect(transport);
|
|
112
|
+
const body = await readJsonBody(req).catch(() => undefined);
|
|
113
|
+
await transport.handleRequest(req, res, body);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await transport.close().catch(() => { });
|
|
117
|
+
await built.server.close().catch(() => { });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await new Promise((resolve) => server.listen(config.port, config.host, resolve));
|
|
121
|
+
log(`ready on http://${config.host}:${config.port}/mcp`);
|
|
122
|
+
if (oauth) {
|
|
123
|
+
log(`OAuth issuer: ${config.oauth.issuer}`);
|
|
124
|
+
log(config.oauth.fixedUrl ? `pinned to ${config.oauth.fixedUrl}` : 'multi-tenant: users bring their own Umami');
|
|
125
|
+
}
|
|
126
|
+
else if (config.host !== '127.0.0.1' && config.host !== 'localhost') {
|
|
127
|
+
log(`WARNING: bound to ${config.host} with no authentication. ` +
|
|
128
|
+
'Anyone who can reach this port can use your Umami credentials. ' +
|
|
129
|
+
'Bind to 127.0.0.1 and use an SSH tunnel, enable UMAMI_MCP_OAUTH, or front it with an authenticating proxy.');
|
|
130
|
+
}
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { startHttp } from './http.js';
|
|
4
|
+
import { loadConfig, describeConfig, ConfigError } from './config.js';
|
|
5
|
+
import { loadEnvFile } from './envfile.js';
|
|
6
|
+
import { UmamiClient } from './client.js';
|
|
7
|
+
import { registerSecrets, redactUnknown } from './redact.js';
|
|
8
|
+
import { buildServer } from './server.js';
|
|
9
|
+
/**
|
|
10
|
+
* On stdio, stdout carries the JSON-RPC stream and must never be polluted.
|
|
11
|
+
* All human-facing logging therefore goes to stderr.
|
|
12
|
+
*/
|
|
13
|
+
const log = (...args) => console.error('[umami-mcp]', ...args);
|
|
14
|
+
async function startStdio(config, client) {
|
|
15
|
+
const { server, registered, withheld } = buildServer(config, client);
|
|
16
|
+
log(`${registered.length} tools available, ${withheld.length} withheld by mode "${config.mode}"`);
|
|
17
|
+
await server.connect(new StdioServerTransport());
|
|
18
|
+
log('ready on stdio');
|
|
19
|
+
}
|
|
20
|
+
async function main() {
|
|
21
|
+
// Credentials may live in a file the operator controls rather than in the
|
|
22
|
+
// MCP client's config JSON. Must run before loadConfig reads the env.
|
|
23
|
+
const envFile = loadEnvFile();
|
|
24
|
+
if (envFile.path && envFile.loaded > 0) {
|
|
25
|
+
log(`loaded ${envFile.loaded} setting(s) from ${envFile.path}`);
|
|
26
|
+
}
|
|
27
|
+
for (const w of envFile.warnings)
|
|
28
|
+
log(`WARNING: ${w}`);
|
|
29
|
+
let config;
|
|
30
|
+
try {
|
|
31
|
+
config = loadConfig();
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err instanceof ConfigError) {
|
|
35
|
+
log('configuration error:', err.message);
|
|
36
|
+
process.exit(78); // EX_CONFIG
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
// Must happen before any request can produce output.
|
|
41
|
+
registerSecrets(config.password, config.apiKey);
|
|
42
|
+
log(describeConfig(config));
|
|
43
|
+
if (config.oauth?.enabled) {
|
|
44
|
+
// Multi-tenant: there is no instance to verify at boot. Each user proves
|
|
45
|
+
// their own credentials on the consent screen before a token is issued.
|
|
46
|
+
if (config.transport !== 'http') {
|
|
47
|
+
log('OAuth mode requires the http transport. Set UMAMI_MCP_TRANSPORT=http.');
|
|
48
|
+
process.exit(78);
|
|
49
|
+
}
|
|
50
|
+
await startHttp({ config, log });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const client = new UmamiClient(config);
|
|
54
|
+
try {
|
|
55
|
+
const user = await client.verify();
|
|
56
|
+
log(`connected as ${user.username} (role: ${user.role})`);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
log('could not reach Umami:', redactUnknown(err));
|
|
60
|
+
log('check UMAMI_URL and your credentials, then restart.');
|
|
61
|
+
process.exit(69); // EX_UNAVAILABLE
|
|
62
|
+
}
|
|
63
|
+
if (config.transport === 'http')
|
|
64
|
+
await startHttp({ config, log });
|
|
65
|
+
else
|
|
66
|
+
await startStdio(config, client);
|
|
67
|
+
}
|
|
68
|
+
main().catch((err) => {
|
|
69
|
+
log('fatal:', redactUnknown(err));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consent screen. This is where a user hands over their Umami credentials,
|
|
3
|
+
* so it must be honest about what happens to them.
|
|
4
|
+
*/
|
|
5
|
+
export interface ConsentPageOptions {
|
|
6
|
+
action: string;
|
|
7
|
+
hiddenFields: Record<string, string>;
|
|
8
|
+
clientName?: string;
|
|
9
|
+
fixedUrl?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function consentPage(o: ConsentPageOptions): string;
|
|
13
|
+
export declare function errorPage(title: string, detail: string): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consent screen. This is where a user hands over their Umami credentials,
|
|
3
|
+
* so it must be honest about what happens to them.
|
|
4
|
+
*/
|
|
5
|
+
function escapeHtml(s) {
|
|
6
|
+
return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
7
|
+
}
|
|
8
|
+
export function consentPage(o) {
|
|
9
|
+
const hidden = Object.entries(o.hiddenFields)
|
|
10
|
+
.map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`)
|
|
11
|
+
.join('\n ');
|
|
12
|
+
const client = o.clientName ? escapeHtml(o.clientName) : 'An MCP client';
|
|
13
|
+
const urlField = o.fixedUrl
|
|
14
|
+
? `<input type="hidden" name="umami_url" value="${escapeHtml(o.fixedUrl)}">
|
|
15
|
+
<div class="fixed">Connecting to <strong>${escapeHtml(o.fixedUrl)}</strong></div>`
|
|
16
|
+
: `<label>Your Umami URL
|
|
17
|
+
<input type="url" name="umami_url" required placeholder="https://analytics.example.com"
|
|
18
|
+
autocomplete="url" spellcheck="false">
|
|
19
|
+
</label>`;
|
|
20
|
+
return `<!doctype html>
|
|
21
|
+
<html lang="en"><head>
|
|
22
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
23
|
+
<title>Connect your Umami</title>
|
|
24
|
+
<style>
|
|
25
|
+
:root { color-scheme: light dark; --bg:#fff; --fg:#111; --mut:#666; --line:#ddd; --acc:#2563eb; --warn:#8a5a00; --warnbg:#fff8e6; }
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root { --bg:#15171a; --fg:#e8e8e8; --mut:#9aa0a6; --line:#33363b; --acc:#5b8cff; --warn:#ffcf70; --warnbg:#2a2416; }
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
body { margin:0; padding:2rem 1rem; background:var(--bg); color:var(--fg);
|
|
31
|
+
font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
|
|
32
|
+
main { max-width: 27rem; margin: 0 auto; }
|
|
33
|
+
h1 { font-size:1.25rem; margin:0 0 .35rem; }
|
|
34
|
+
p.sub { color:var(--mut); margin:0 0 1.5rem; }
|
|
35
|
+
label { display:block; margin:0 0 1rem; font-weight:500; }
|
|
36
|
+
input[type=url],input[type=text],input[type=password],select {
|
|
37
|
+
width:100%; margin-top:.35rem; padding:.6rem .7rem; font:inherit; font-weight:400;
|
|
38
|
+
border:1px solid var(--line); border-radius:8px; background:var(--bg); color:var(--fg); }
|
|
39
|
+
input:focus,select:focus { outline:2px solid var(--acc); outline-offset:1px; border-color:transparent; }
|
|
40
|
+
button { width:100%; padding:.7rem; font:inherit; font-weight:600; color:#fff; background:var(--acc);
|
|
41
|
+
border:0; border-radius:8px; cursor:pointer; margin-top:.5rem; }
|
|
42
|
+
button:hover { filter:brightness(1.08); }
|
|
43
|
+
.note { border:1px solid var(--line); border-radius:8px; padding:.8rem .9rem; margin:1.5rem 0 0;
|
|
44
|
+
color:var(--mut); font-size:.85rem; }
|
|
45
|
+
.note strong { color:var(--fg); }
|
|
46
|
+
.warn { background:var(--warnbg); border-color:transparent; color:var(--warn); margin:0 0 1.25rem; }
|
|
47
|
+
.err { background:#fdecea; color:#a4271c; border:0; border-radius:8px; padding:.7rem .9rem; margin:0 0 1rem; font-size:.9rem; }
|
|
48
|
+
@media (prefers-color-scheme: dark) { .err { background:#3a1d1a; color:#ffb4a8; } }
|
|
49
|
+
.fixed { padding:.6rem .7rem; border:1px solid var(--line); border-radius:8px; margin-bottom:1rem; font-size:.9rem; }
|
|
50
|
+
hr { border:0; border-top:1px solid var(--line); margin:1.5rem 0; }
|
|
51
|
+
</style></head>
|
|
52
|
+
<body><main>
|
|
53
|
+
<h1>Connect your Umami</h1>
|
|
54
|
+
<p class="sub">${client} is asking to read your Umami Analytics.</p>
|
|
55
|
+
|
|
56
|
+
${o.error ? `<div class="err">${escapeHtml(o.error)}</div>` : ''}
|
|
57
|
+
|
|
58
|
+
<div class="note warn">
|
|
59
|
+
Self-hosted Umami has no API keys, so this is your <strong>account password</strong>.
|
|
60
|
+
Only continue if you trust whoever runs this server. Prefer a dedicated,
|
|
61
|
+
limited Umami account over your admin login.
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<form method="POST" autocomplete="off">
|
|
65
|
+
${hidden}
|
|
66
|
+
${urlField}
|
|
67
|
+
<label>Username
|
|
68
|
+
<input type="text" name="umami_username" required autocomplete="username" spellcheck="false">
|
|
69
|
+
</label>
|
|
70
|
+
<label>Password
|
|
71
|
+
<input type="password" name="umami_password" required autocomplete="current-password">
|
|
72
|
+
</label>
|
|
73
|
+
<label>Permission
|
|
74
|
+
<select name="mode">
|
|
75
|
+
<option value="read" selected>Read only — analytics and reports</option>
|
|
76
|
+
<option value="write">Read and write — also create and update websites</option>
|
|
77
|
+
<option value="admin">Admin — also manage users</option>
|
|
78
|
+
</select>
|
|
79
|
+
</label>
|
|
80
|
+
<button type="submit">Connect</button>
|
|
81
|
+
</form>
|
|
82
|
+
|
|
83
|
+
<div class="note">
|
|
84
|
+
<strong>What happens to your password:</strong> it is verified against the Umami URL you
|
|
85
|
+
entered, then encrypted into the access token itself. This server keeps no database and
|
|
86
|
+
stores no credentials. Irreversible operations — deleting a website, wiping its data,
|
|
87
|
+
removing a user — are never available through this connection, whichever permission
|
|
88
|
+
you pick.
|
|
89
|
+
</div>
|
|
90
|
+
</main></body></html>`;
|
|
91
|
+
}
|
|
92
|
+
export function errorPage(title, detail) {
|
|
93
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title>
|
|
95
|
+
<style>:root{color-scheme:light dark}body{margin:0;padding:3rem 1rem;font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;text-align:center}
|
|
96
|
+
h1{font-size:1.15rem;margin:0 0 .5rem}p{color:#666;max-width:30rem;margin:0 auto}</style></head>
|
|
97
|
+
<body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(detail)}</p></body></html>`;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.js","sourceRoot":"","sources":["../../src/oauth/consent.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CACjC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAE,CAC9E,CAAC;AACJ,CAAC;AAUD,MAAM,UAAU,WAAW,CAAC,CAAqB;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;SAC1C,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,8BAA8B,UAAU,CAAC,CAAC,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;SACzF,IAAI,CAAC,UAAU,CAAC,CAAC;IAEpB,MAAM,MAAM,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC;IAEzE,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ;QACzB,CAAC,CAAC,gDAAgD,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC;kDAC1B,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,iBAAiB;QACrF,CAAC,CAAC;;;gBAGU,CAAC;IAEf,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAkCU,MAAM;;IAErB,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,oBAAoB,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;;;;;;;;;QAS1D,MAAM;QACN,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;sBAwBM,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,MAAc;IACrD,OAAO;4EACmE,UAAU,CAAC,KAAK,CAAC;;;YAGjF,UAAU,CAAC,KAAK,CAAC,WAAW,UAAU,CAAC,MAAM,CAAC,oBAAoB,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { Config } from '../config.js';
|
|
3
|
+
import { MemoryStore } from './store.js';
|
|
4
|
+
import { type SealedIdentity } from './seal.js';
|
|
5
|
+
export interface OAuthOptions {
|
|
6
|
+
/** Public base URL of this server, e.g. https://umami-mcp.example.com */
|
|
7
|
+
issuer: string;
|
|
8
|
+
key: Buffer;
|
|
9
|
+
/** Pin every connection to one Umami instance; omit for multi-tenant. */
|
|
10
|
+
fixedUrl?: string;
|
|
11
|
+
tokenTtlSeconds: number;
|
|
12
|
+
baseConfig: Config;
|
|
13
|
+
}
|
|
14
|
+
export declare class OAuthProvider {
|
|
15
|
+
private readonly o;
|
|
16
|
+
readonly store: MemoryStore;
|
|
17
|
+
constructor(o: OAuthOptions);
|
|
18
|
+
/** Validate an Authorization header and return the caller's identity. */
|
|
19
|
+
authenticate(req: IncomingMessage): SealedIdentity;
|
|
20
|
+
/** Returns true when the request was an OAuth endpoint and has been handled. */
|
|
21
|
+
handle(req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean>;
|
|
22
|
+
private register;
|
|
23
|
+
private showConsent;
|
|
24
|
+
/** Consent submitted: verify the credentials for real, then mint a code. */
|
|
25
|
+
private grant;
|
|
26
|
+
private token;
|
|
27
|
+
}
|