writ-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.
writ_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avenue D
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,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: writ-mcp
3
+ Version: 0.1.0
4
+ Summary: Writ MCP server — commit-time policy checks for AI agents (ALLOW/DENY/STEP_UP + audit receipts)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/AvenueDAdmin/pywrit
7
+ Project-URL: Repository, https://github.com/AvenueDAdmin/pywrit/tree/main/mcp
8
+ Project-URL: Documentation, https://withwrit.com/docs
9
+ Keywords: ai-agents,mcp,policy,audit-log,guardrails
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: mcp<2,>=1.0
14
+ Requires-Dist: httpx>=0.27
15
+ Dynamic: license-file
16
+
17
+ # writ-mcp
18
+
19
+ The [Writ](https://withwrit.com) gate as an MCP server. Give any MCP-compatible
20
+ agent commit-time control: the agent calls Writ **before** a consequential
21
+ write, gets back `ALLOW`, `DENY`, or `STEP_UP`, and every outcome creates an
22
+ audit receipt.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install writ-mcp
28
+ ```
29
+
30
+ Requires Python 3.9+. Installs the `writ-mcp` command (stdio transport).
31
+
32
+ ## Configure
33
+
34
+ ```bash
35
+ export WRIT_API_KEY="writ_..." # required; get one free: POST /v1/keys with an email
36
+ export WRIT_SPONSOR_TOKEN="..." # only for sponsor tools (grant, revoke, reinstate)
37
+ # export WRIT_BASE_URL="..." # default: https://api.withwrit.com
38
+ ```
39
+
40
+ ## Add to your agent
41
+
42
+ Claude Code:
43
+
44
+ ```bash
45
+ claude mcp add writ --env WRIT_API_KEY="$WRIT_API_KEY" -- writ-mcp
46
+ ```
47
+
48
+ Claude Desktop (`claude_desktop_config.json`):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "writ": {
54
+ "command": "writ-mcp",
55
+ "env": { "WRIT_API_KEY": "writ_..." }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ Any MCP client (generic stdio config):
62
+
63
+ ```json
64
+ {
65
+ "command": "writ-mcp",
66
+ "env": {
67
+ "WRIT_API_KEY": "writ_...",
68
+ "WRIT_SPONSOR_TOKEN": "writ_sp_..."
69
+ }
70
+ }
71
+ ```
72
+
73
+ Hermes (NousResearch/hermes-agent) catalog entry — once published, this
74
+ package is installable from the `optional-mcps` catalog:
75
+
76
+ ```bash
77
+ hermes mcp install writ
78
+ ```
79
+
80
+ ## The check-before-write loop
81
+
82
+ The tool descriptions teach the agent this flow, but the short version:
83
+
84
+ 1. `writ_check(sponsor_id, agent_id, verb, target, purpose)` — before the write.
85
+ 2. `ALLOW` → you get a 90-second `authToken` bound to that exact write.
86
+ 3. `writ_verify_token(auth_token, verb, target, purpose)` — immediately before
87
+ executing, to prove the authorization still matches what you're doing.
88
+ 4. `DENY` → do not proceed. `STEP_UP` → a human sponsor approves
89
+ (via `writ_grant` or the dashboard), then check again.
90
+
91
+ ## Tools
92
+
93
+ | Tool | Who | What |
94
+ |---|---|---|
95
+ | `writ_check` | agent | Decision + 90s purpose-bound token |
96
+ | `writ_verify_token` | agent | Commit-time token verification |
97
+ | `writ_grant` | sponsor | Approve a STEP_UP (one-time grant) |
98
+ | `writ_revoke` / `writ_reinstate` | sponsor | Kill switch on/off for a principal |
99
+ | `writ_receipts` | agent | Audit trail of decisions |
100
+ | `writ_policy` | agent | Tenant verb policy |
101
+ | `writ_sandbox` | anyone | Free 90-second demo grant, no key needed |
102
+
103
+ Try it with no key: `writ_sandbox` → `writ_check` with `verb="demo_write"`.
104
+
105
+ ## Source
106
+
107
+ Public repo: [AvenueDAdmin/pywrit](https://github.com/AvenueDAdmin/pywrit) (`mcp/`
108
+ directory). License: MIT.
@@ -0,0 +1,92 @@
1
+ # writ-mcp
2
+
3
+ The [Writ](https://withwrit.com) gate as an MCP server. Give any MCP-compatible
4
+ agent commit-time control: the agent calls Writ **before** a consequential
5
+ write, gets back `ALLOW`, `DENY`, or `STEP_UP`, and every outcome creates an
6
+ audit receipt.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install writ-mcp
12
+ ```
13
+
14
+ Requires Python 3.9+. Installs the `writ-mcp` command (stdio transport).
15
+
16
+ ## Configure
17
+
18
+ ```bash
19
+ export WRIT_API_KEY="writ_..." # required; get one free: POST /v1/keys with an email
20
+ export WRIT_SPONSOR_TOKEN="..." # only for sponsor tools (grant, revoke, reinstate)
21
+ # export WRIT_BASE_URL="..." # default: https://api.withwrit.com
22
+ ```
23
+
24
+ ## Add to your agent
25
+
26
+ Claude Code:
27
+
28
+ ```bash
29
+ claude mcp add writ --env WRIT_API_KEY="$WRIT_API_KEY" -- writ-mcp
30
+ ```
31
+
32
+ Claude Desktop (`claude_desktop_config.json`):
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "writ": {
38
+ "command": "writ-mcp",
39
+ "env": { "WRIT_API_KEY": "writ_..." }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Any MCP client (generic stdio config):
46
+
47
+ ```json
48
+ {
49
+ "command": "writ-mcp",
50
+ "env": {
51
+ "WRIT_API_KEY": "writ_...",
52
+ "WRIT_SPONSOR_TOKEN": "writ_sp_..."
53
+ }
54
+ }
55
+ ```
56
+
57
+ Hermes (NousResearch/hermes-agent) catalog entry — once published, this
58
+ package is installable from the `optional-mcps` catalog:
59
+
60
+ ```bash
61
+ hermes mcp install writ
62
+ ```
63
+
64
+ ## The check-before-write loop
65
+
66
+ The tool descriptions teach the agent this flow, but the short version:
67
+
68
+ 1. `writ_check(sponsor_id, agent_id, verb, target, purpose)` — before the write.
69
+ 2. `ALLOW` → you get a 90-second `authToken` bound to that exact write.
70
+ 3. `writ_verify_token(auth_token, verb, target, purpose)` — immediately before
71
+ executing, to prove the authorization still matches what you're doing.
72
+ 4. `DENY` → do not proceed. `STEP_UP` → a human sponsor approves
73
+ (via `writ_grant` or the dashboard), then check again.
74
+
75
+ ## Tools
76
+
77
+ | Tool | Who | What |
78
+ |---|---|---|
79
+ | `writ_check` | agent | Decision + 90s purpose-bound token |
80
+ | `writ_verify_token` | agent | Commit-time token verification |
81
+ | `writ_grant` | sponsor | Approve a STEP_UP (one-time grant) |
82
+ | `writ_revoke` / `writ_reinstate` | sponsor | Kill switch on/off for a principal |
83
+ | `writ_receipts` | agent | Audit trail of decisions |
84
+ | `writ_policy` | agent | Tenant verb policy |
85
+ | `writ_sandbox` | anyone | Free 90-second demo grant, no key needed |
86
+
87
+ Try it with no key: `writ_sandbox` → `writ_check` with `verb="demo_write"`.
88
+
89
+ ## Source
90
+
91
+ Public repo: [AvenueDAdmin/pywrit](https://github.com/AvenueDAdmin/pywrit) (`mcp/`
92
+ directory). License: MIT.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "writ-mcp"
7
+ version = "0.1.0"
8
+ description = "Writ MCP server — commit-time policy checks for AI agents (ALLOW/DENY/STEP_UP + audit receipts)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ dependencies = [
13
+ "mcp>=1.0,<2",
14
+ "httpx>=0.27",
15
+ ]
16
+ keywords = ["ai-agents", "mcp", "policy", "audit-log", "guardrails"]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/AvenueDAdmin/pywrit"
20
+ Repository = "https://github.com/AvenueDAdmin/pywrit/tree/main/mcp"
21
+ Documentation = "https://withwrit.com/docs"
22
+
23
+ [project.scripts]
24
+ writ-mcp = "writ_mcp.server:main"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """writ-mcp — the Writ gate as an MCP server for AI agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,229 @@
1
+ """Writ MCP server — commit-time policy checks for AI agents, as MCP tools.
2
+
3
+ An agent (or any MCP client) calls `writ_check` BEFORE performing a
4
+ consequential write. Writ answers ALLOW, DENY, or STEP_UP, and every outcome
5
+ creates an audit receipt. On ALLOW the agent also receives a 90-second
6
+ purpose-bound auth token; it must pass `writ_verify_token` against the
7
+ intended verb/target/purpose immediately before executing the write.
8
+
9
+ Configuration (environment variables):
10
+ WRIT_BASE_URL Gate base URL. Defaults to the hosted Writ gate
11
+ (https://api.withwrit.com).
12
+ WRIT_API_KEY API key (starts with writ_). Get one free: POST /v1/keys
13
+ with an email (see the Writ docs quickstart).
14
+ WRIT_SPONSOR_TOKEN Tenant sponsor token (writ_sp_..., shown once at key
15
+ issuance). Only needed for sponsor tools
16
+ (writ_grant, writ_revoke, writ_reinstate).
17
+
18
+ Run: writ-mcp (stdio transport, installed via pip)
19
+ python -m writ_mcp.server (same, without the console script)
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import urllib.parse
25
+
26
+ import httpx
27
+ from mcp.server.fastmcp import FastMCP
28
+
29
+ BASE_URL = os.environ.get(
30
+ "WRIT_BASE_URL",
31
+ "https://api.withwrit.com",
32
+ ).rstrip("/")
33
+ API_KEY = os.environ.get("WRIT_API_KEY", "").strip()
34
+ SPONSOR_TOKEN = os.environ.get("WRIT_SPONSOR_TOKEN", "").strip()
35
+
36
+ mcp = FastMCP("writ")
37
+
38
+
39
+ class WritError(Exception):
40
+ def __init__(self, status, payload):
41
+ self.status = status
42
+ self.payload = payload
43
+ super().__init__(f"Writ API error {status}: {payload}")
44
+
45
+
46
+ def _call(method, path, body=None, credential="", timeout=20.0):
47
+ """Low-level gate call. Returns parsed JSON, raises WritError on failure."""
48
+ url = BASE_URL + path
49
+ headers = {"Content-Type": "application/json"}
50
+ if credential:
51
+ headers["Authorization"] = f"Bearer {credential}"
52
+ with httpx.Client(timeout=timeout) as client:
53
+ resp = client.request(method, url, headers=headers,
54
+ content=json.dumps(body) if body is not None else None)
55
+ try:
56
+ payload = resp.json()
57
+ except ValueError:
58
+ payload = {"raw": resp.text[:500]}
59
+ if resp.status_code >= 400:
60
+ raise WritError(resp.status_code, payload)
61
+ return payload
62
+
63
+
64
+ def _key_call(method, path, body=None):
65
+ if not API_KEY:
66
+ raise WritError(0, {"error": "WRIT_API_KEY is not set. Get a free key: POST /v1/keys with an email."})
67
+ return _call(method, path, body, credential=API_KEY)
68
+
69
+
70
+ def _sponsor_call(method, path, body=None):
71
+ if not SPONSOR_TOKEN:
72
+ raise WritError(0, {"error": "WRIT_SPONSOR_TOKEN is not set. Sponsor tools need the tenant's sponsor token."})
73
+ return _call(method, path, body, credential=SPONSOR_TOKEN)
74
+
75
+
76
+ def _fmt(result):
77
+ return json.dumps(result, indent=2)
78
+
79
+
80
+ @mcp.tool()
81
+ def writ_check(sponsor_id: str, agent_id: str, verb: str, target: str, purpose: str) -> str:
82
+ """Ask Writ whether an action may proceed. CALL THIS BEFORE any consequential write.
83
+
84
+ A consequential write is anything hard to undo: sending money or messages,
85
+ changing access or identity records, deleting data, calling an external API
86
+ that acts in the world.
87
+
88
+ Args:
89
+ sponsor_id: The human sponsor accountable for this action (e.g. a user id or email).
90
+ agent_id: The agent or workflow performing the action.
91
+ verb: What is being done (e.g. "verify_human", "send_payment").
92
+ target: What it acts on (e.g. "benefit-case-123").
93
+ purpose: Why, in plain words. Be specific — the approval is bound to this purpose.
94
+
95
+ Returns a decision:
96
+ ALLOW — proceed. You get an authToken valid 90 seconds, bound to this
97
+ exact sponsor/agent/verb/target/purpose. Call writ_verify_token
98
+ against the intended write immediately before executing it.
99
+ DENY — do NOT perform the action. Explain the receipt reason to the user.
100
+ STEP_UP — a human sponsor must approve first. Tell the user what needs
101
+ approval; they can approve via writ_grant (or the dashboard),
102
+ then call writ_check again.
103
+ Every outcome creates an audit receipt with a receiptId.
104
+ """
105
+ try:
106
+ return _fmt(_key_call("POST", "/v1/check", {
107
+ "sponsorId": sponsor_id, "agentId": agent_id,
108
+ "verb": verb, "target": target, "purpose": purpose,
109
+ }))
110
+ except WritError as e:
111
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
112
+
113
+
114
+ @mcp.tool()
115
+ def writ_verify_token(auth_token: str, verb: str, target: str, purpose: str) -> str:
116
+ """Verify a Writ auth token against the write you are about to execute.
117
+
118
+ Call this IMMEDIATELY before executing an ALLOWed write, with the exact
119
+ verb/target/purpose you intend. If valid is false — expired, tampered, or
120
+ bound to a different purpose — do NOT execute the write; call writ_check again.
121
+
122
+ This is the commit-time enforcement: the check authorizes, the token proves
123
+ the authorization still matches what you are actually doing.
124
+ """
125
+ try:
126
+ return _fmt(_key_call("POST", "/v1/tokens/verify", {
127
+ "token": auth_token, "verb": verb, "target": target, "purpose": purpose,
128
+ }))
129
+ except WritError as e:
130
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
131
+
132
+
133
+ @mcp.tool()
134
+ def writ_grant(sponsor_id: str, agent_id: str, verb: str, target: str, purpose: str,
135
+ ttl_seconds: int = 90) -> str:
136
+ """SPONSOR ONLY. Mint a one-time grant approving a specific action.
137
+
138
+ Use this when writ_check returned STEP_UP and the human sponsor approves.
139
+ The grant is single-use: the next writ_check consumes it and returns ALLOW.
140
+ The grant is scoped to your tenant. Requires WRIT_SPONSOR_TOKEN.
141
+ """
142
+ try:
143
+ return _fmt(_sponsor_call("POST", "/v1/grants", {
144
+ "sponsorId": sponsor_id, "agentId": agent_id,
145
+ "verb": verb, "target": target, "purpose": purpose,
146
+ "ttlSeconds": ttl_seconds,
147
+ }))
148
+ except WritError as e:
149
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
150
+
151
+
152
+ @mcp.tool()
153
+ def writ_revoke(sponsor_id: str, agent_id: str) -> str:
154
+ """SPONSOR ONLY. Kill switch: immediately deny all future checks for this principal.
155
+
156
+ Revocation overrides live grants and tenant policy. Use when a principal is
157
+ compromised, misbehaving, or its task is done. Scoped to your tenant.
158
+ Requires WRIT_SPONSOR_TOKEN.
159
+ """
160
+ try:
161
+ return _fmt(_sponsor_call("POST", "/v1/revoke", {
162
+ "sponsorId": sponsor_id, "agentId": agent_id,
163
+ }))
164
+ except WritError as e:
165
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
166
+
167
+
168
+ @mcp.tool()
169
+ def writ_reinstate(sponsor_id: str, agent_id: str) -> str:
170
+ """SPONSOR ONLY. Undo writ_revoke: the principal is evaluated by policy again.
171
+
172
+ Scoped to your tenant. Requires WRIT_SPONSOR_TOKEN.
173
+ """
174
+ try:
175
+ return _fmt(_sponsor_call("POST", "/v1/reinstate", {
176
+ "sponsorId": sponsor_id, "agentId": agent_id,
177
+ }))
178
+ except WritError as e:
179
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
180
+
181
+
182
+ @mcp.tool()
183
+ def writ_receipts(limit: int = 10) -> str:
184
+ """List recent decision receipts (audit trail). Newest first."""
185
+ try:
186
+ data = _key_call("GET", "/v1/receipts")
187
+ receipts = data.get("receipts", data)
188
+ if isinstance(receipts, list):
189
+ receipts = receipts[: max(1, limit)]
190
+ return _fmt(receipts)
191
+ except WritError as e:
192
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
193
+
194
+
195
+ @mcp.tool()
196
+ def writ_policy() -> str:
197
+ """Read the tenant's verb policy: which verbs allow, deny, require a grant, or step up."""
198
+ try:
199
+ return _fmt(_key_call("GET", "/v1/policy"))
200
+ except WritError as e:
201
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
202
+
203
+
204
+ @mcp.tool()
205
+ def writ_sandbox(sponsor_id: str, agent_id: str, target: str, purpose: str) -> str:
206
+ """Get a free 90-second sandbox grant for a demo write. No API key needed.
207
+
208
+ Issues a live grant for verb "demo_write" on a target starting with "demo-",
209
+ then runs writ_check so you get back an ALLOW decision plus auth token to
210
+ practice the verify-before-write flow end to end.
211
+ """
212
+ try:
213
+ grant = _call("POST", "/v1/sandbox", {
214
+ "sponsorId": sponsor_id, "agentId": agent_id,
215
+ "verb": "demo_write", "target": target, "purpose": purpose,
216
+ })
217
+ return _fmt({"sandboxGrant": grant,
218
+ "note": 'Now call writ_check with verb "demo_write" to get your ALLOW + authToken.'})
219
+ except WritError as e:
220
+ return _fmt({"error": str(e), "status": e.status, "detail": e.payload})
221
+
222
+
223
+ def main():
224
+ """Console-script entry point: run the MCP server over stdio."""
225
+ mcp.run()
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: writ-mcp
3
+ Version: 0.1.0
4
+ Summary: Writ MCP server — commit-time policy checks for AI agents (ALLOW/DENY/STEP_UP + audit receipts)
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/AvenueDAdmin/pywrit
7
+ Project-URL: Repository, https://github.com/AvenueDAdmin/pywrit/tree/main/mcp
8
+ Project-URL: Documentation, https://withwrit.com/docs
9
+ Keywords: ai-agents,mcp,policy,audit-log,guardrails
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: mcp<2,>=1.0
14
+ Requires-Dist: httpx>=0.27
15
+ Dynamic: license-file
16
+
17
+ # writ-mcp
18
+
19
+ The [Writ](https://withwrit.com) gate as an MCP server. Give any MCP-compatible
20
+ agent commit-time control: the agent calls Writ **before** a consequential
21
+ write, gets back `ALLOW`, `DENY`, or `STEP_UP`, and every outcome creates an
22
+ audit receipt.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install writ-mcp
28
+ ```
29
+
30
+ Requires Python 3.9+. Installs the `writ-mcp` command (stdio transport).
31
+
32
+ ## Configure
33
+
34
+ ```bash
35
+ export WRIT_API_KEY="writ_..." # required; get one free: POST /v1/keys with an email
36
+ export WRIT_SPONSOR_TOKEN="..." # only for sponsor tools (grant, revoke, reinstate)
37
+ # export WRIT_BASE_URL="..." # default: https://api.withwrit.com
38
+ ```
39
+
40
+ ## Add to your agent
41
+
42
+ Claude Code:
43
+
44
+ ```bash
45
+ claude mcp add writ --env WRIT_API_KEY="$WRIT_API_KEY" -- writ-mcp
46
+ ```
47
+
48
+ Claude Desktop (`claude_desktop_config.json`):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "writ": {
54
+ "command": "writ-mcp",
55
+ "env": { "WRIT_API_KEY": "writ_..." }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ Any MCP client (generic stdio config):
62
+
63
+ ```json
64
+ {
65
+ "command": "writ-mcp",
66
+ "env": {
67
+ "WRIT_API_KEY": "writ_...",
68
+ "WRIT_SPONSOR_TOKEN": "writ_sp_..."
69
+ }
70
+ }
71
+ ```
72
+
73
+ Hermes (NousResearch/hermes-agent) catalog entry — once published, this
74
+ package is installable from the `optional-mcps` catalog:
75
+
76
+ ```bash
77
+ hermes mcp install writ
78
+ ```
79
+
80
+ ## The check-before-write loop
81
+
82
+ The tool descriptions teach the agent this flow, but the short version:
83
+
84
+ 1. `writ_check(sponsor_id, agent_id, verb, target, purpose)` — before the write.
85
+ 2. `ALLOW` → you get a 90-second `authToken` bound to that exact write.
86
+ 3. `writ_verify_token(auth_token, verb, target, purpose)` — immediately before
87
+ executing, to prove the authorization still matches what you're doing.
88
+ 4. `DENY` → do not proceed. `STEP_UP` → a human sponsor approves
89
+ (via `writ_grant` or the dashboard), then check again.
90
+
91
+ ## Tools
92
+
93
+ | Tool | Who | What |
94
+ |---|---|---|
95
+ | `writ_check` | agent | Decision + 90s purpose-bound token |
96
+ | `writ_verify_token` | agent | Commit-time token verification |
97
+ | `writ_grant` | sponsor | Approve a STEP_UP (one-time grant) |
98
+ | `writ_revoke` / `writ_reinstate` | sponsor | Kill switch on/off for a principal |
99
+ | `writ_receipts` | agent | Audit trail of decisions |
100
+ | `writ_policy` | agent | Tenant verb policy |
101
+ | `writ_sandbox` | anyone | Free 90-second demo grant, no key needed |
102
+
103
+ Try it with no key: `writ_sandbox` → `writ_check` with `verb="demo_write"`.
104
+
105
+ ## Source
106
+
107
+ Public repo: [AvenueDAdmin/pywrit](https://github.com/AvenueDAdmin/pywrit) (`mcp/`
108
+ directory). License: MIT.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/writ_mcp/__init__.py
5
+ src/writ_mcp/server.py
6
+ src/writ_mcp.egg-info/PKG-INFO
7
+ src/writ_mcp.egg-info/SOURCES.txt
8
+ src/writ_mcp.egg-info/dependency_links.txt
9
+ src/writ_mcp.egg-info/entry_points.txt
10
+ src/writ_mcp.egg-info/requires.txt
11
+ src/writ_mcp.egg-info/top_level.txt
12
+ tests/test_server.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ writ-mcp = writ_mcp.server:main
@@ -0,0 +1,2 @@
1
+ mcp<2,>=1.0
2
+ httpx>=0.27
@@ -0,0 +1 @@
1
+ writ_mcp
@@ -0,0 +1,36 @@
1
+ """writ-mcp package tests — import-time checks only, no network."""
2
+
3
+ import asyncio
4
+
5
+ import writ_mcp
6
+ from writ_mcp import server
7
+
8
+ EXPECTED_TOOLS = {
9
+ "writ_check",
10
+ "writ_verify_token",
11
+ "writ_grant",
12
+ "writ_revoke",
13
+ "writ_reinstate",
14
+ "writ_receipts",
15
+ "writ_policy",
16
+ "writ_sandbox",
17
+ }
18
+
19
+
20
+ def test_version():
21
+ assert writ_mcp.__version__ == "0.1.0"
22
+
23
+
24
+ def test_public_base_url_default():
25
+ # The public package must never default to an internal endpoint.
26
+ assert server.BASE_URL == "https://api.withwrit.com"
27
+ assert "lambda-url" not in server.BASE_URL
28
+
29
+
30
+ def test_all_tools_registered():
31
+ tools = asyncio.run(server.mcp.list_tools())
32
+ assert {t.name for t in tools} == EXPECTED_TOOLS
33
+
34
+
35
+ def test_main_entrypoint_exists():
36
+ assert callable(server.main)