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 +21 -0
- writ_mcp-0.1.0/PKG-INFO +108 -0
- writ_mcp-0.1.0/README.md +92 -0
- writ_mcp-0.1.0/pyproject.toml +27 -0
- writ_mcp-0.1.0/setup.cfg +4 -0
- writ_mcp-0.1.0/src/writ_mcp/__init__.py +3 -0
- writ_mcp-0.1.0/src/writ_mcp/server.py +229 -0
- writ_mcp-0.1.0/src/writ_mcp.egg-info/PKG-INFO +108 -0
- writ_mcp-0.1.0/src/writ_mcp.egg-info/SOURCES.txt +12 -0
- writ_mcp-0.1.0/src/writ_mcp.egg-info/dependency_links.txt +1 -0
- writ_mcp-0.1.0/src/writ_mcp.egg-info/entry_points.txt +2 -0
- writ_mcp-0.1.0/src/writ_mcp.egg-info/requires.txt +2 -0
- writ_mcp-0.1.0/src/writ_mcp.egg-info/top_level.txt +1 -0
- writ_mcp-0.1.0/tests/test_server.py +36 -0
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.
|
writ_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
writ_mcp-0.1.0/README.md
ADDED
|
@@ -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"]
|
writ_mcp-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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)
|