forgingstack-watchtower 0.1.0__py3-none-any.whl

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.
app/__init__.py ADDED
File without changes
app/mcp_server.py ADDED
@@ -0,0 +1,200 @@
1
+ """MCP wrapper client for ForgingStack Watchtower, v0.1.0.
2
+
3
+ An agent-native service must be usable by agents, not just by humans
4
+ with curl. MCP is the standard interface agents already speak, so the
5
+ wrapper exposes the same live REST API as MCP tools, with no duplicated
6
+ logic: every tool call goes through the REST API, so metering and
7
+ receipts stay in one place. The service itself is private; this file
8
+ is only a client.
9
+
10
+ Protocol: JSON-RPC 2.0 over stdio, no new dependencies (stdlib only).
11
+ Supported methods: initialize, ping, tools/list, tools/call.
12
+ Notifications (no id, e.g. notifications/initialized) get no response.
13
+
14
+ Environment:
15
+ FORGINGSTACK_API_BASE default https://forgingstack-watchtower.jkbattles.workers.dev
16
+ FORGINGSTACK_API_KEY required for all tools except check_in;
17
+ sign up via POST /v1/signup (key shown once)
18
+
19
+ Run: python -m app.mcp_server
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import sys
25
+ import urllib.error
26
+ import urllib.request
27
+
28
+ BASE = os.environ.get(
29
+ "FORGINGSTACK_API_BASE",
30
+ "https://forgingstack-watchtower.jkbattles.workers.dev",
31
+ )
32
+ API_KEY = os.environ.get("FORGINGSTACK_API_KEY", "")
33
+
34
+ TOOLS = [
35
+ {
36
+ "name": "create_watch",
37
+ "description": "Register a liveness watch on an agent. The agent "
38
+ "must check in at least every interval_seconds (plus grace); if "
39
+ "it goes quiet past that, Watchtower emails an alert and issues "
40
+ "a hash-chained receipt. Costs 1 credit. Returns the watch plus "
41
+ "a check-in key, shown once — save it: the agent checks in with "
42
+ "it, and it cannot be shown again.",
43
+ "inputSchema": {
44
+ "type": "object",
45
+ "properties": {
46
+ "agent_id": {"type": "string"},
47
+ "interval_seconds": {"type": "integer"},
48
+ "grace_seconds": {"type": "integer"},
49
+ "notify_email": {"type": "string"},
50
+ },
51
+ "required": [
52
+ "agent_id", "interval_seconds", "grace_seconds",
53
+ "notify_email",
54
+ ],
55
+ },
56
+ },
57
+ {
58
+ "name": "check_in",
59
+ "description": "Check in for a watch. Free. Needs only watch_id "
60
+ "and the check-in key from watch registration — no API key. "
61
+ "A check-in after an alert or overdue period returns a "
62
+ "recovery receipt.",
63
+ "inputSchema": {
64
+ "type": "object",
65
+ "properties": {
66
+ "watch_id": {"type": "integer"},
67
+ "checkin_key": {"type": "string"},
68
+ },
69
+ "required": ["watch_id", "checkin_key"],
70
+ },
71
+ },
72
+ {
73
+ "name": "get_watch",
74
+ "description": "Fetch a watch by id, with its current status "
75
+ "(healthy, overdue, alerted, snoozed) and its last 20 events.",
76
+ "inputSchema": {
77
+ "type": "object",
78
+ "properties": {"watch_id": {"type": "integer"}},
79
+ "required": ["watch_id"],
80
+ },
81
+ },
82
+ {
83
+ "name": "get_credits",
84
+ "description": "Remaining credit balance for the configured API key.",
85
+ "inputSchema": {"type": "object", "properties": {}},
86
+ },
87
+ {
88
+ "name": "get_receipt",
89
+ "description": "Fetch a past Watchtower receipt by id. Receipts "
90
+ "are hash-chained: each receipt's hash covers the previous "
91
+ "receipt's hash, so tampering breaks the chain.",
92
+ "inputSchema": {
93
+ "type": "object",
94
+ "properties": {"receipt_id": {"type": "integer"}},
95
+ "required": ["receipt_id"],
96
+ },
97
+ },
98
+ ]
99
+
100
+
101
+ def _http(method, path, body=None):
102
+ req = urllib.request.Request(
103
+ BASE + path,
104
+ data=json.dumps(body).encode() if body is not None else None,
105
+ method=method,
106
+ )
107
+ # The check-in endpoint needs no API key; the rest do. Send the key
108
+ # only when one is configured, so check_in works keyless.
109
+ if API_KEY:
110
+ req.add_header("X-API-Key", API_KEY)
111
+ req.add_header("Content-Type", "application/json")
112
+ # A User-Agent is required: the default Python urllib agent is
113
+ # rejected (Cloudflare 1010) through some egress paths.
114
+ req.add_header("User-Agent", "ForgingStack-Watchtower/0.1.0")
115
+ try:
116
+ with urllib.request.urlopen(req, timeout=30) as resp:
117
+ return resp.status, json.loads(resp.read().decode() or "{}")
118
+ except urllib.error.HTTPError as e:
119
+ try:
120
+ return e.code, json.loads(e.read().decode() or "{}")
121
+ except Exception:
122
+ return e.code, {"detail": "api error"}
123
+
124
+
125
+ def _text(payload, is_error=False):
126
+ return {
127
+ "content": [{"type": "text", "text": json.dumps(payload)}],
128
+ "isError": is_error,
129
+ }
130
+
131
+
132
+ def handle(msg):
133
+ method = msg.get("method")
134
+ params = msg.get("params") or {}
135
+ if method == "initialize":
136
+ return {
137
+ "protocolVersion": "2024-11-05",
138
+ "capabilities": {"tools": {}},
139
+ "serverInfo": {"name": "forgingstack-watchtower", "version": "0.1.0"},
140
+ }
141
+ if method == "ping":
142
+ return {}
143
+ if method == "tools/list":
144
+ return {"tools": TOOLS}
145
+ if method == "tools/call":
146
+ name = params.get("name")
147
+ args = params.get("arguments") or {}
148
+ if name == "create_watch":
149
+ body = {k: args[k] for k in
150
+ ("agent_id", "interval_seconds", "grace_seconds",
151
+ "notify_email")}
152
+ status, data = _http("POST", "/v1/watches", body)
153
+ return _text(data, is_error=status >= 400)
154
+ if name == "check_in":
155
+ body = {k: args[k] for k in ("watch_id", "checkin_key")}
156
+ status, data = _http("POST", "/v1/checkin", body)
157
+ return _text(data, is_error=status >= 400)
158
+ if name == "get_watch":
159
+ status, data = _http(
160
+ "GET", f"/v1/watches/{args['watch_id']}"
161
+ )
162
+ return _text(data, is_error=status >= 400)
163
+ if name == "get_credits":
164
+ status, data = _http("GET", "/v1/credits")
165
+ return _text(data, is_error=status >= 400)
166
+ if name == "get_receipt":
167
+ status, data = _http(
168
+ "GET", f"/v1/receipts/{args['receipt_id']}"
169
+ )
170
+ return _text(data, is_error=status >= 400)
171
+ raise ValueError(f"unknown tool: {name}")
172
+ raise ValueError(f"unknown method: {method}")
173
+
174
+
175
+ def main():
176
+ for line in sys.stdin:
177
+ line = line.strip()
178
+ if not line:
179
+ continue
180
+ try:
181
+ msg = json.loads(line)
182
+ except json.JSONDecodeError:
183
+ continue
184
+ # Notifications carry no id and get no response.
185
+ if "id" not in msg:
186
+ continue
187
+ try:
188
+ result = handle(msg)
189
+ out = {"jsonrpc": "2.0", "id": msg["id"], "result": result}
190
+ except Exception as exc: # noqa: BLE001 - MCP error surface
191
+ out = {
192
+ "jsonrpc": "2.0", "id": msg["id"],
193
+ "error": {"code": -32603, "message": str(exc)},
194
+ }
195
+ sys.stdout.write(json.dumps(out) + "\n")
196
+ sys.stdout.flush()
197
+
198
+
199
+ if __name__ == "__main__":
200
+ main()
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: forgingstack-watchtower
3
+ Version: 0.1.0
4
+ Summary: An agent that stops checking in is an agent you hear about.
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # ForgingStack Watchtower
12
+
13
+ An agent that stops checking in is an agent you hear about.
14
+
15
+ mcp-name: io.github.jdkabattles/forgingstack-watchtower
16
+
17
+ This is the **public client** for Watchtower. The service itself —
18
+ the liveness checks, the alert emails, the receipt chain, the key
19
+ store, and billing — runs on ForgingStack's private service and is
20
+ **not** in this repo. Copying this repo does not give you the service:
21
+ you still need an API key and credits, from signup below.
22
+
23
+ Live API: https://forgingstack-watchtower.jkbattles.workers.dev
24
+
25
+ ## How it works
26
+
27
+ 1. You register a watch on an agent: how often it should check in
28
+ (`interval_seconds`, 30 to 2,592,000) plus extra quiet time allowed
29
+ (`grace_seconds`, 0 to 86,400), and where the alert email goes.
30
+ One watch per agent per owner. Registering a watch costs 1 credit
31
+ and returns a check-in key, shown once.
32
+ 2. The agent checks in with its watch id and check-in key. Check-ins
33
+ are free and need no API key.
34
+ 3. If the agent goes quiet past its interval plus grace, Watchtower
35
+ emails an alert and issues a hash-chained receipt. Each alert
36
+ costs the watch owner 1 credit; an owner at 0 credits is skipped
37
+ and the watch stays overdue.
38
+ 4. When the agent checks in again after an alert or overdue period,
39
+ a recovery receipt is issued. You can also snooze a watch
40
+ (1 to 1,440 minutes) to silence alerts for a while.
41
+
42
+ ## Signup (v1)
43
+
44
+ ```bash
45
+ curl -X POST https://forgingstack-watchtower.jkbattles.workers.dev/v1/signup \
46
+ -H "Content-Type: application/json" \
47
+ -d '{"email":"you@example.com"}'
48
+ ```
49
+
50
+ Returns an API key **once** and 100 starter credits. Save the key
51
+ immediately — it is stored only as a hash and cannot be shown again.
52
+ Click the verification link in the signup email before the key can
53
+ register watches; until then, watch registration returns 403.
54
+
55
+ ## Use it (MCP, stdio)
56
+
57
+ Set the key, then run the MCP server in this repo:
58
+
59
+ ```bash
60
+ export FORGINGSTACK_API_KEY="your-key-here"
61
+ python -m app.mcp_server
62
+ ```
63
+
64
+ Tools: `create_watch`, `check_in`, `get_watch`, `get_credits`,
65
+ `get_receipt`. See `SKILL.md` for when to use it and when not to.
66
+
67
+ ## Price
68
+
69
+ 1 credit to register a watch, 1 credit per alert, check-ins free.
70
+ 100 starter credits on signup. Credit packs: $10 for 1,000 credits,
71
+ bought with `POST /v1/billing/checkout` — credits are added once the
72
+ payment is verified (usually within the hour).
73
+
74
+ ## Files in this repo
75
+
76
+ - `app/mcp_server.py` — MCP wrapper client (stdio, stdlib only)
77
+ - `SKILL.md` — when to use it, how to read watch status
78
+ - `agent-card.json` — A2A-style discovery card
79
+ - `server.json` — MCP Registry metadata
80
+ - `pyproject.toml` — package metadata for the MCP client
@@ -0,0 +1,8 @@
1
+ app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ app/mcp_server.py,sha256=GpTkpEInv0ff5tAI5_eMG8iCLNCffkDoqR2ahhF8Fnk,7154
3
+ forgingstack_watchtower-0.1.0.dist-info/licenses/LICENSE,sha256=kDoUXgBDydvygXjgDe1nE4S2ZeXHI2C_4tc_-THZ2S4,1073
4
+ forgingstack_watchtower-0.1.0.dist-info/METADATA,sha256=ekafgI8MnCN4WqbuR9l-xPYdzzU-tTsLbsaRrZbfTBw,2953
5
+ forgingstack_watchtower-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ forgingstack_watchtower-0.1.0.dist-info/entry_points.txt,sha256=pjg7AI7nUc_DVEg68et1i9-nDLPS7GwurePCFqdSV84,64
7
+ forgingstack_watchtower-0.1.0.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
8
+ forgingstack_watchtower-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ forgingstack-watchtower = app.mcp_server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Battles
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.