gauge-mcp 0.1.0__tar.gz

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,29 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ permissions:
4
+ contents: read
5
+ jobs:
6
+ test:
7
+ strategy:
8
+ matrix:
9
+ os: [ubuntu-latest, macos-latest]
10
+ python: ["3.11", "3.12", "3.13"]
11
+ runs-on: ${{ matrix.os }}
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: uv venv -p ${{ matrix.python }} && uv pip install -e '.[dev]'
16
+ - run: .venv/bin/pytest -q
17
+ scan:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+ - uses: gitleaks/gitleaks-action@v2
24
+ env:
25
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26
+ - name: Identifier scan
27
+ env:
28
+ IDENTIFIER_PATTERNS: ${{ secrets.IDENTIFIER_PATTERNS }}
29
+ run: python3 scripts/scan_identifiers.py .
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .env
10
+ .env.*
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ Gauge follows [semantic versioning](https://semver.org). Modules added after 0.1 ship switched off.
4
+
5
+ ## 0.1.0 — unreleased
6
+
7
+ - First release as one bundle with two modules: `umami` (web analytics) and `resend` (email).
8
+ - `gauge_modules` discovery tool: which modules are on, which settings are missing, what each tool does.
9
+ - Every tool returns structured data with a published output schema.
10
+ - `umami_metrics` gives real ranked breakdowns (top pages, referrers, countries, browsers and more), with `limit`, `offset` and `search`.
11
+ - Umami Cloud supported through an API key, as well as a self-hosted login.
12
+ - `resend_send` previews by default. Sending needs the person's approval: asked through MCP elicitation, or given ahead of time with `GAUGE_RESEND_ALLOWED_RECIPIENTS` or `GAUGE_RESEND_REQUIRE_CONFIRM=false`. The assistant's own `confirm=true` is never enough. The sender comes from settings, never from code.
13
+ - Runs over stdio or streamable HTTP. Starts with no settings and reports what is missing when a tool is called.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seaynic Labs
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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.5
2
+ Name: gauge-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP bundle for people running a small website: web analytics (Umami) and email (Resend) in one server.
5
+ Project-URL: Repository, https://github.com/seayniclabs/gauge
6
+ Author: Seaynic Labs
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: analytics,email,mcp,model-context-protocol,resend,umami
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: mcp[cli]<2,>=1.27
15
+ Requires-Dist: pydantic>=2.7
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Gauge
21
+
22
+ <!-- mcp-name: io.github.seayniclabs/gauge -->
23
+
24
+ Gauge is an MCP server for people running a small website. It lets an AI assistant read your site's analytics and send email, from one install.
25
+
26
+ It is a **bundle**: one server made of modules. You switch on the modules you use. New modules arrive switched off, so an update never changes what your assistant can do until you choose to.
27
+
28
+ | Module | What it does | Status |
29
+ |---|---|---|
30
+ | `umami` | Web analytics from [Umami](https://umami.is), self-hosted or Umami Cloud: sites, totals, time series, top pages, referrers, countries, browsers | 0.1 |
31
+ | `resend` | Email through [Resend](https://resend.com): send (preview first), list sent mail, list sending domains | 0.1 |
32
+
33
+ Gauge works with any MCP client: Claude Desktop, Claude Code, Cursor, VS Code, Sonique, Hermes, OpenClaw and others. It runs on your computer and stores nothing; your data stays in Umami and Resend.
34
+
35
+ ## Install
36
+
37
+ Gauge needs Python 3.11 or newer. With [uv](https://docs.astral.sh/uv/) you don't install anything ahead of time:
38
+
39
+ ```bash
40
+ uvx gauge-mcp --version
41
+ ```
42
+
43
+ Or with pip:
44
+
45
+ ```bash
46
+ pip install gauge-mcp
47
+ ```
48
+
49
+ ## Settings
50
+
51
+ Set these as environment variables in your MCP client's config, or put them in a config file (see below). You only need the settings for the modules you use. Gauge starts without any of them and tells you what's missing when you call a tool.
52
+
53
+ | Variable | Module | What it is |
54
+ |---|---|---|
55
+ | `GAUGE_UMAMI_URL` | umami | Your Umami address, e.g. `https://analytics.example.com` |
56
+ | `GAUGE_UMAMI_USERNAME` / `GAUGE_UMAMI_PASSWORD` | umami | A self-hosted Umami login. A view-only user is enough. |
57
+ | `GAUGE_UMAMI_API_KEY` | umami | Umami Cloud API key (instead of a login). `GAUGE_UMAMI_URL` defaults to Umami Cloud when this is set. |
58
+ | `GAUGE_RESEND_API_KEY` | resend | Resend API key |
59
+ | `GAUGE_RESEND_FROM` | resend | Sender address on a domain you verified in Resend |
60
+ | `GAUGE_RESEND_FROM_NAME` | resend | Optional sender display name |
61
+ | `GAUGE_RESEND_ALLOWED_RECIPIENTS` | resend | Optional. Comma-separated addresses or `@domains` the assistant may send to without asking you each time, e.g. `me@example.com,@example.com` |
62
+ | `GAUGE_RESEND_REQUIRE_CONFIRM` | resend | `true` by default: you approve each send. `false` lets the assistant send on its own. |
63
+ | `GAUGE_<MODULE>_ENABLED` | any | `false` turns a module off, e.g. `GAUGE_RESEND_ENABLED=false` |
64
+
65
+ **Config file (optional).** Gauge also reads `~/.config/gauge/config.toml`, or the file named by `GAUGE_CONFIG`. Environment variables win over the file.
66
+
67
+ ```toml
68
+ [umami]
69
+ url = "https://analytics.example.com"
70
+ username = "viewer"
71
+ password = "change-me"
72
+
73
+ [resend]
74
+ enabled = false
75
+ ```
76
+
77
+ ## Add it to your MCP client
78
+
79
+ Most clients use the same JSON shape. Put it where your client keeps MCP servers:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "gauge": {
85
+ "command": "uvx",
86
+ "args": ["gauge-mcp"],
87
+ "env": {
88
+ "GAUGE_UMAMI_URL": "https://analytics.example.com",
89
+ "GAUGE_UMAMI_USERNAME": "viewer",
90
+ "GAUGE_UMAMI_PASSWORD": "change-me",
91
+ "GAUGE_RESEND_API_KEY": "re_your_key",
92
+ "GAUGE_RESEND_FROM": "hello@example.com"
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ | Client | Where the config goes |
100
+ |---|---|
101
+ | Claude Desktop | `claude_desktop_config.json` (Settings → Developer → Edit Config) |
102
+ | Claude Code | `claude mcp add gauge -- uvx gauge-mcp`, then add the `env` values, or a project `.mcp.json` |
103
+ | Cursor | `~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project |
104
+ | VS Code | `.vscode/mcp.json`, using `"servers"` instead of `"mcpServers"` |
105
+ | Sonique, Hermes, OpenClaw, others | Any client that launches a stdio MCP server: command `uvx`, args `["gauge-mcp"]`, plus the env values |
106
+
107
+ **Over HTTP.** For clients that connect to a URL instead of launching a process:
108
+
109
+ ```bash
110
+ gauge-mcp --transport http --port 8765
111
+ ```
112
+
113
+ It serves streamable HTTP at `http://127.0.0.1:8765/mcp`. It listens only on your own machine by default. Put authentication in front of it before exposing it anywhere else.
114
+
115
+ ## Tools
116
+
117
+ | Tool | What it returns |
118
+ |---|---|
119
+ | `gauge_modules` | Which modules are on, which settings are missing, and what each tool does. Call this first. |
120
+ | `umami_websites` | Tracked websites, paged |
121
+ | `umami_stats` | Pageviews, visitors, visits, bounces and bounce rate for a date range |
122
+ | `umami_pageviews` | Pageviews and sessions over time |
123
+ | `umami_metrics` | A ranked breakdown: `type` = `path` (top pages), `referrer`, `country`, `browser`, `os`, `device` and more |
124
+ | `resend_send` | Sends an email after you approve it. Without `confirm=true` it only returns a preview. |
125
+ | `resend_emails` | Sent emails, paged with a cursor |
126
+ | `resend_domains` | Sending domains and whether each is verified |
127
+
128
+ Every tool returns structured data with a published schema, so an assistant can write code against the results instead of reading text. Date ranges take `days` (default 7) or ISO 8601 `start` and `end`. Errors always look like `gauge.<module>.<code>: <message> Fix: <hint>`.
129
+
130
+ ## Safety
131
+
132
+ - Gauge only calls the services you configure. It sends nothing anywhere else and keeps no data.
133
+ - `resend_send` sends real email, so **you** approve each one, not the assistant. When the assistant asks to send, Gauge asks you directly through your MCP client (MCP elicitation) and shows the sender, recipients and subject. The assistant can't answer that question for you.
134
+ - Some MCP clients can't ask you questions yet. In those, Gauge refuses to send unless you've pre-approved the recipients with `GAUGE_RESEND_ALLOWED_RECIPIENTS`, or turned approval off with `GAUGE_RESEND_REQUIRE_CONFIRM=false`.
135
+ - Give Gauge the least access it needs: a view-only Umami user, and a Resend key limited to sending if you don't need the other email tools.
136
+
137
+ ## Develop
138
+
139
+ ```bash
140
+ uv venv && uv pip install -e '.[dev]'
141
+ .venv/bin/pytest
142
+ ```
143
+
144
+ ## License
145
+
146
+ MIT. Published by Seaynic Labs.
@@ -0,0 +1,127 @@
1
+ # Gauge
2
+
3
+ <!-- mcp-name: io.github.seayniclabs/gauge -->
4
+
5
+ Gauge is an MCP server for people running a small website. It lets an AI assistant read your site's analytics and send email, from one install.
6
+
7
+ It is a **bundle**: one server made of modules. You switch on the modules you use. New modules arrive switched off, so an update never changes what your assistant can do until you choose to.
8
+
9
+ | Module | What it does | Status |
10
+ |---|---|---|
11
+ | `umami` | Web analytics from [Umami](https://umami.is), self-hosted or Umami Cloud: sites, totals, time series, top pages, referrers, countries, browsers | 0.1 |
12
+ | `resend` | Email through [Resend](https://resend.com): send (preview first), list sent mail, list sending domains | 0.1 |
13
+
14
+ Gauge works with any MCP client: Claude Desktop, Claude Code, Cursor, VS Code, Sonique, Hermes, OpenClaw and others. It runs on your computer and stores nothing; your data stays in Umami and Resend.
15
+
16
+ ## Install
17
+
18
+ Gauge needs Python 3.11 or newer. With [uv](https://docs.astral.sh/uv/) you don't install anything ahead of time:
19
+
20
+ ```bash
21
+ uvx gauge-mcp --version
22
+ ```
23
+
24
+ Or with pip:
25
+
26
+ ```bash
27
+ pip install gauge-mcp
28
+ ```
29
+
30
+ ## Settings
31
+
32
+ Set these as environment variables in your MCP client's config, or put them in a config file (see below). You only need the settings for the modules you use. Gauge starts without any of them and tells you what's missing when you call a tool.
33
+
34
+ | Variable | Module | What it is |
35
+ |---|---|---|
36
+ | `GAUGE_UMAMI_URL` | umami | Your Umami address, e.g. `https://analytics.example.com` |
37
+ | `GAUGE_UMAMI_USERNAME` / `GAUGE_UMAMI_PASSWORD` | umami | A self-hosted Umami login. A view-only user is enough. |
38
+ | `GAUGE_UMAMI_API_KEY` | umami | Umami Cloud API key (instead of a login). `GAUGE_UMAMI_URL` defaults to Umami Cloud when this is set. |
39
+ | `GAUGE_RESEND_API_KEY` | resend | Resend API key |
40
+ | `GAUGE_RESEND_FROM` | resend | Sender address on a domain you verified in Resend |
41
+ | `GAUGE_RESEND_FROM_NAME` | resend | Optional sender display name |
42
+ | `GAUGE_RESEND_ALLOWED_RECIPIENTS` | resend | Optional. Comma-separated addresses or `@domains` the assistant may send to without asking you each time, e.g. `me@example.com,@example.com` |
43
+ | `GAUGE_RESEND_REQUIRE_CONFIRM` | resend | `true` by default: you approve each send. `false` lets the assistant send on its own. |
44
+ | `GAUGE_<MODULE>_ENABLED` | any | `false` turns a module off, e.g. `GAUGE_RESEND_ENABLED=false` |
45
+
46
+ **Config file (optional).** Gauge also reads `~/.config/gauge/config.toml`, or the file named by `GAUGE_CONFIG`. Environment variables win over the file.
47
+
48
+ ```toml
49
+ [umami]
50
+ url = "https://analytics.example.com"
51
+ username = "viewer"
52
+ password = "change-me"
53
+
54
+ [resend]
55
+ enabled = false
56
+ ```
57
+
58
+ ## Add it to your MCP client
59
+
60
+ Most clients use the same JSON shape. Put it where your client keeps MCP servers:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "gauge": {
66
+ "command": "uvx",
67
+ "args": ["gauge-mcp"],
68
+ "env": {
69
+ "GAUGE_UMAMI_URL": "https://analytics.example.com",
70
+ "GAUGE_UMAMI_USERNAME": "viewer",
71
+ "GAUGE_UMAMI_PASSWORD": "change-me",
72
+ "GAUGE_RESEND_API_KEY": "re_your_key",
73
+ "GAUGE_RESEND_FROM": "hello@example.com"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ | Client | Where the config goes |
81
+ |---|---|
82
+ | Claude Desktop | `claude_desktop_config.json` (Settings → Developer → Edit Config) |
83
+ | Claude Code | `claude mcp add gauge -- uvx gauge-mcp`, then add the `env` values, or a project `.mcp.json` |
84
+ | Cursor | `~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project |
85
+ | VS Code | `.vscode/mcp.json`, using `"servers"` instead of `"mcpServers"` |
86
+ | Sonique, Hermes, OpenClaw, others | Any client that launches a stdio MCP server: command `uvx`, args `["gauge-mcp"]`, plus the env values |
87
+
88
+ **Over HTTP.** For clients that connect to a URL instead of launching a process:
89
+
90
+ ```bash
91
+ gauge-mcp --transport http --port 8765
92
+ ```
93
+
94
+ It serves streamable HTTP at `http://127.0.0.1:8765/mcp`. It listens only on your own machine by default. Put authentication in front of it before exposing it anywhere else.
95
+
96
+ ## Tools
97
+
98
+ | Tool | What it returns |
99
+ |---|---|
100
+ | `gauge_modules` | Which modules are on, which settings are missing, and what each tool does. Call this first. |
101
+ | `umami_websites` | Tracked websites, paged |
102
+ | `umami_stats` | Pageviews, visitors, visits, bounces and bounce rate for a date range |
103
+ | `umami_pageviews` | Pageviews and sessions over time |
104
+ | `umami_metrics` | A ranked breakdown: `type` = `path` (top pages), `referrer`, `country`, `browser`, `os`, `device` and more |
105
+ | `resend_send` | Sends an email after you approve it. Without `confirm=true` it only returns a preview. |
106
+ | `resend_emails` | Sent emails, paged with a cursor |
107
+ | `resend_domains` | Sending domains and whether each is verified |
108
+
109
+ Every tool returns structured data with a published schema, so an assistant can write code against the results instead of reading text. Date ranges take `days` (default 7) or ISO 8601 `start` and `end`. Errors always look like `gauge.<module>.<code>: <message> Fix: <hint>`.
110
+
111
+ ## Safety
112
+
113
+ - Gauge only calls the services you configure. It sends nothing anywhere else and keeps no data.
114
+ - `resend_send` sends real email, so **you** approve each one, not the assistant. When the assistant asks to send, Gauge asks you directly through your MCP client (MCP elicitation) and shows the sender, recipients and subject. The assistant can't answer that question for you.
115
+ - Some MCP clients can't ask you questions yet. In those, Gauge refuses to send unless you've pre-approved the recipients with `GAUGE_RESEND_ALLOWED_RECIPIENTS`, or turned approval off with `GAUGE_RESEND_REQUIRE_CONFIRM=false`.
116
+ - Give Gauge the least access it needs: a view-only Umami user, and a Resend key limited to sending if you don't need the other email tools.
117
+
118
+ ## Develop
119
+
120
+ ```bash
121
+ uv venv && uv pip install -e '.[dev]'
122
+ .venv/bin/pytest
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT. Published by Seaynic Labs.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "gauge-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP bundle for people running a small website: web analytics (Umami) and email (Resend) in one server."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Seaynic Labs" }]
10
+ # mcp is capped below 2: 2.x removed FastMCP, and unpinned installs crash.
11
+ dependencies = [
12
+ "mcp[cli]>=1.27,<2",
13
+ "httpx>=0.27",
14
+ "pydantic>=2.7",
15
+ ]
16
+ keywords = ["mcp", "model-context-protocol", "umami", "resend", "analytics", "email"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+
22
+ [project.urls]
23
+ Repository = "https://github.com/seayniclabs/gauge"
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=8"]
27
+
28
+ [project.scripts]
29
+ gauge-mcp = "gauge_mcp.server:main"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/gauge_mcp"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """Fail if any tracked file contains a forbidden string.
3
+
4
+ The patterns are not stored in this repository. Pass them in with
5
+ ``--patterns FILE`` or the ``IDENTIFIER_PATTERNS`` environment variable
6
+ (one case-insensitive regular expression per line; ``#`` starts a comment).
7
+ A line starting with ``!`` is an allowed exception: a match that also matches
8
+ an exception is ignored.
9
+
10
+ Usage: python scripts/scan_identifiers.py --patterns ~/patterns.txt [path ...]
11
+ Exit status 0 = clean, 1 = hits found, 2 = no patterns given.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import os
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ def load(text: str) -> tuple[list[re.Pattern], list[re.Pattern]]:
25
+ block, allow = [], []
26
+ for line in text.splitlines():
27
+ line = line.split(" #", 1)[0].strip()
28
+ if not line or line.startswith("#"):
29
+ continue
30
+ (allow if line.startswith("!") else block).append(re.compile(line.lstrip("!"), re.I))
31
+ return block, allow
32
+
33
+
34
+ def tracked_files(root: Path) -> list[Path]:
35
+ try:
36
+ out = subprocess.run(["git", "ls-files", "-co", "--exclude-standard"], cwd=root,
37
+ capture_output=True, text=True, check=True).stdout
38
+ return [root / p for p in out.splitlines()]
39
+ except (subprocess.CalledProcessError, FileNotFoundError):
40
+ skip = {".git", ".venv", "venv", "node_modules", "__pycache__", "dist", "build"}
41
+ return [p for p in root.rglob("*") if p.is_file() and not skip & set(p.parts)]
42
+
43
+
44
+ def main() -> int:
45
+ ap = argparse.ArgumentParser()
46
+ ap.add_argument("--patterns", help="file of patterns (default: $IDENTIFIER_PATTERNS)")
47
+ ap.add_argument("paths", nargs="*", default=["."])
48
+ args = ap.parse_args()
49
+
50
+ text = Path(args.patterns).expanduser().read_text() if args.patterns else os.environ.get("IDENTIFIER_PATTERNS", "")
51
+ block, allow = load(text)
52
+ if not block:
53
+ print("scan_identifiers: no patterns given (use --patterns or IDENTIFIER_PATTERNS)", file=sys.stderr)
54
+ return 2
55
+
56
+ hits = 0
57
+ for root in map(Path, args.paths):
58
+ for f in tracked_files(root) if root.is_dir() else [root]:
59
+ try:
60
+ lines = f.read_text(errors="ignore").splitlines()
61
+ except OSError:
62
+ continue
63
+ for n, line in enumerate(lines, 1):
64
+ for pat in block:
65
+ for m in pat.finditer(line):
66
+ if any(a.search(line) for a in allow):
67
+ continue
68
+ hits += 1
69
+ # Print where, not the line itself: the line may hold the thing we're hiding.
70
+ print(f"{f}:{n}: matches pattern #{block.index(pat) + 1}")
71
+ print(f"scan_identifiers: {hits} hit(s)", file=sys.stderr)
72
+ return 1 if hits else 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ sys.exit(main())
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.seayniclabs/gauge",
4
+ "description": "Website analytics (Umami) and email (Resend) for an AI assistant, as one MCP bundle.",
5
+ "repository": {
6
+ "url": "https://github.com/seayniclabs/gauge",
7
+ "source": "github"
8
+ },
9
+ "version": "0.1.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "pypi",
13
+ "identifier": "gauge-mcp",
14
+ "version": "0.1.0",
15
+ "transport": { "type": "stdio" },
16
+ "environmentVariables": [
17
+ { "name": "GAUGE_UMAMI_URL", "description": "Your Umami address", "isRequired": false },
18
+ { "name": "GAUGE_UMAMI_USERNAME", "description": "Self-hosted Umami login", "isRequired": false },
19
+ { "name": "GAUGE_UMAMI_PASSWORD", "description": "Password for that login", "isRequired": false, "isSecret": true },
20
+ { "name": "GAUGE_UMAMI_API_KEY", "description": "Umami Cloud API key", "isRequired": false, "isSecret": true },
21
+ { "name": "GAUGE_RESEND_API_KEY", "description": "Resend API key", "isRequired": false, "isSecret": true },
22
+ { "name": "GAUGE_RESEND_FROM", "description": "Sender address on a verified domain", "isRequired": false }
23
+ ]
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,3 @@
1
+ """Gauge: an MCP bundle for people running a small website."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,56 @@
1
+ """Settings come from environment variables, then an optional TOML file.
2
+
3
+ Environment variables win. The file is ``$GAUGE_CONFIG`` if set, otherwise
4
+ ``$XDG_CONFIG_HOME/gauge/config.toml`` (``~/.config/gauge/config.toml``).
5
+ Each module reads its own ``[<module>]`` table; ``GAUGE_<MODULE>_<KEY>`` overrides
6
+ any key in it. Nothing here assumes a particular MCP client.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import tomllib
13
+ from pathlib import Path
14
+ from typing import Mapping
15
+
16
+ TRUE = {"1", "true", "yes", "on"}
17
+ FALSE = {"0", "false", "no", "off"}
18
+
19
+
20
+ def config_path(env: Mapping[str, str]) -> Path:
21
+ if env.get("GAUGE_CONFIG"):
22
+ return Path(env["GAUGE_CONFIG"]).expanduser()
23
+ base = env.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
24
+ return Path(base) / "gauge" / "config.toml"
25
+
26
+
27
+ class Config:
28
+ def __init__(self, env: Mapping[str, str] | None = None, file_data: dict | None = None):
29
+ self.env = dict(os.environ if env is None else env)
30
+ if file_data is None:
31
+ path = config_path(self.env)
32
+ file_data = tomllib.loads(path.read_text()) if path.is_file() else {}
33
+ self.file = file_data
34
+
35
+ def get(self, module: str, key: str, default: str | None = None) -> str | None:
36
+ value = self.env.get(f"GAUGE_{module.upper()}_{key.upper()}")
37
+ if value not in (None, ""):
38
+ return value
39
+ value = self.file.get(module, {}).get(key)
40
+ if value not in (None, ""):
41
+ return str(value)
42
+ return default
43
+
44
+ def flag(self, module: str, key: str, default: bool) -> bool:
45
+ value = self.get(module, key)
46
+ if value is None:
47
+ return default
48
+ value = value.strip().lower()
49
+ if value in TRUE:
50
+ return True
51
+ if value in FALSE:
52
+ return False
53
+ return default
54
+
55
+ def enabled(self, module: str, default: bool) -> bool:
56
+ return self.flag(module, "enabled", default)
@@ -0,0 +1,26 @@
1
+ """One error shape for every module, so scripts can rely on it.
2
+
3
+ Every failure a tool reports reads: ``gauge.<module>.<code>: <message> Fix: <hint>``.
4
+ """
5
+
6
+ from mcp.server.fastmcp.exceptions import ToolError
7
+
8
+
9
+ class GaugeError(ToolError):
10
+ def __init__(self, module: str, code: str, message: str, hint: str = ""):
11
+ self.module = module
12
+ self.code = code
13
+ text = f"gauge.{module}.{code}: {message}"
14
+ if hint:
15
+ text += f" Fix: {hint}"
16
+ super().__init__(text)
17
+
18
+
19
+ def not_configured(module: str, missing: list[str]) -> GaugeError:
20
+ return GaugeError(
21
+ module,
22
+ "not_configured",
23
+ f"missing setting(s) {', '.join(missing)}.",
24
+ "set them as environment variables or in the [" + module + "] section of the "
25
+ "config file, then restart the server. Call gauge_modules to see every setting.",
26
+ )
@@ -0,0 +1,71 @@
1
+ """Module contract and registry.
2
+
3
+ A module is one integration (Umami, Resend, ...). It declares its settings and
4
+ registers its tools on the shared server. Adding a module = adding a file here and
5
+ listing it in ``MODULES``. Modules added after 0.1 must set ``default_enabled = False``
6
+ so an update never turns on something the user did not choose.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Callable
13
+
14
+ import httpx
15
+ from mcp.server.fastmcp import FastMCP
16
+
17
+ from ..config import Config
18
+ from ..errors import not_configured
19
+
20
+ ClientFactory = Callable[[], httpx.AsyncClient]
21
+
22
+
23
+ def default_client_factory() -> httpx.AsyncClient:
24
+ return httpx.AsyncClient(timeout=10.0)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Setting:
29
+ key: str
30
+ help: str
31
+ secret: bool = False
32
+ required: bool = True
33
+
34
+ def env_var(self, module: str) -> str:
35
+ return f"GAUGE_{module.upper()}_{self.key.upper()}"
36
+
37
+
38
+ @dataclass
39
+ class Module:
40
+ name: str
41
+ summary: str
42
+ settings: list[Setting]
43
+ default_enabled: bool = True
44
+ tools: dict[str, str] = field(default_factory=dict) # tool name -> one-line purpose
45
+
46
+ def missing(self, config: Config) -> list[str]:
47
+ return [
48
+ s.env_var(self.name)
49
+ for s in self.settings
50
+ if s.required and not config.get(self.name, s.key)
51
+ ]
52
+
53
+ def require(self, config: Config, *keys: str) -> dict[str, str]:
54
+ """Return the named settings, or raise a not_configured error naming the env vars."""
55
+ values = {k: config.get(self.name, k) for k in keys}
56
+ absent = [
57
+ s.env_var(self.name) for s in self.settings if s.key in keys and not values[s.key]
58
+ ]
59
+ if absent:
60
+ raise not_configured(self.name, absent)
61
+ return values # type: ignore[return-value]
62
+
63
+ def register(self, mcp: FastMCP, config: Config, client_factory: ClientFactory) -> None:
64
+ raise NotImplementedError
65
+
66
+
67
+ def all_modules() -> list[Module]:
68
+ from .resend import ResendModule
69
+ from .umami import UmamiModule
70
+
71
+ return [UmamiModule(), ResendModule()]