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 +0 -0
- app/mcp_server.py +200 -0
- forgingstack_watchtower-0.1.0.dist-info/METADATA +80 -0
- forgingstack_watchtower-0.1.0.dist-info/RECORD +8 -0
- forgingstack_watchtower-0.1.0.dist-info/WHEEL +5 -0
- forgingstack_watchtower-0.1.0.dist-info/entry_points.txt +2 -0
- forgingstack_watchtower-0.1.0.dist-info/licenses/LICENSE +21 -0
- forgingstack_watchtower-0.1.0.dist-info/top_level.txt +1 -0
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,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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
app
|