mcp-event-subscriptions 0.1.0a1__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.
- mcp_event_subscriptions/__init__.py +241 -0
- mcp_event_subscriptions/subscription-v1.schema.json +101 -0
- mcp_event_subscriptions-0.1.0a1.dist-info/METADATA +114 -0
- mcp_event_subscriptions-0.1.0a1.dist-info/RECORD +6 -0
- mcp_event_subscriptions-0.1.0a1.dist-info/WHEEL +4 -0
- mcp_event_subscriptions-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Experimental webhook subscription records and a restricted JWE profile."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import urlsplit
|
|
14
|
+
|
|
15
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
16
|
+
from jwcrypto import jwe, jwk
|
|
17
|
+
|
|
18
|
+
MAX_BYTES = 64 * 1024
|
|
19
|
+
PROFILE = "mcp-subscription-envelope-v1"
|
|
20
|
+
HEADER = {
|
|
21
|
+
"alg": "dir",
|
|
22
|
+
"enc": "A256GCM",
|
|
23
|
+
"typ": "mcp-subscription+jwe",
|
|
24
|
+
"cty": "application/json",
|
|
25
|
+
}
|
|
26
|
+
SCHEMA = json.loads(
|
|
27
|
+
files(__package__).joinpath("subscription-v1.schema.json").read_text()
|
|
28
|
+
)
|
|
29
|
+
VALIDATOR = Draft202012Validator(SCHEMA, format_checker=FormatChecker())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RecordError(ValueError):
|
|
33
|
+
"""Invalid subscription data. Messages do not include the record."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EnvelopeError(ValueError):
|
|
37
|
+
"""Invalid, unauthenticated, or incorrectly bound envelope."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExpiredSubscription(EnvelopeError):
|
|
41
|
+
"""The recovered subscription lease has expired."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
45
|
+
result: dict[str, Any] = {}
|
|
46
|
+
for key, value in pairs:
|
|
47
|
+
if key in result:
|
|
48
|
+
raise ValueError("duplicate JSON key")
|
|
49
|
+
result[key] = value
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _constant(value: str) -> Any:
|
|
54
|
+
raise ValueError("non-finite JSON value")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _loads(value: str | bytes) -> Any:
|
|
58
|
+
return json.loads(value, object_pairs_hook=_pairs, parse_constant=_constant)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _encode(value: Any) -> bytes:
|
|
62
|
+
return json.dumps(
|
|
63
|
+
value, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
64
|
+
).encode()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _json_value(value: Any) -> None:
|
|
68
|
+
if value is None or type(value) in (str, bool, int, float):
|
|
69
|
+
return
|
|
70
|
+
if type(value) is list:
|
|
71
|
+
for item in value:
|
|
72
|
+
_json_value(item)
|
|
73
|
+
return
|
|
74
|
+
if type(value) is dict and all(type(k) is str for k in value):
|
|
75
|
+
for item in value.values():
|
|
76
|
+
_json_value(item)
|
|
77
|
+
return
|
|
78
|
+
raise ValueError("non-JSON value")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _stamp(value: str) -> datetime:
|
|
82
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_subscription(record: Any) -> dict[str, Any]:
|
|
86
|
+
"""Validate structure and semantics; return an independent JSON copy."""
|
|
87
|
+
try:
|
|
88
|
+
_json_value(record)
|
|
89
|
+
encoded = _encode(record)
|
|
90
|
+
if len(encoded) > MAX_BYTES or not VALIDATOR.is_valid(record):
|
|
91
|
+
raise ValueError("invalid structure or size")
|
|
92
|
+
url = urlsplit(record["delivery"]["url"])
|
|
93
|
+
# DNS, SSRF policy, and endpoint verification remain adapter responsibilities.
|
|
94
|
+
if (
|
|
95
|
+
url.scheme != "https"
|
|
96
|
+
or not url.hostname
|
|
97
|
+
or "@" in url.netloc
|
|
98
|
+
or "#" in record["delivery"]["url"]
|
|
99
|
+
or any(c.isspace() or ord(c) < 32 for c in record["delivery"]["url"])
|
|
100
|
+
):
|
|
101
|
+
raise ValueError("invalid callback URL")
|
|
102
|
+
if url.port is not None and not 1 <= url.port <= 65535:
|
|
103
|
+
raise ValueError("invalid callback port")
|
|
104
|
+
secret = record["delivery"]["secret"][6:]
|
|
105
|
+
raw = base64.b64decode(secret, validate=True)
|
|
106
|
+
if not 24 <= len(raw) <= 64 or base64.b64encode(raw).decode() != secret:
|
|
107
|
+
raise ValueError("invalid secret")
|
|
108
|
+
expiry, refresh = (
|
|
109
|
+
record["lease"]["expiresAt"],
|
|
110
|
+
record["lease"]["refreshBefore"],
|
|
111
|
+
)
|
|
112
|
+
if (expiry is None) != (refresh is None):
|
|
113
|
+
raise ValueError("inconsistent lease")
|
|
114
|
+
if expiry is not None and _stamp(refresh) > _stamp(expiry):
|
|
115
|
+
raise ValueError("invalid lease order")
|
|
116
|
+
return _loads(encoded)
|
|
117
|
+
except (ValueError, TypeError, KeyError, OverflowError, RecursionError) as error:
|
|
118
|
+
raise RecordError("invalid subscription record") from error
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _context(context: Any, resource: str) -> dict[str, str]:
|
|
122
|
+
if (
|
|
123
|
+
type(context) is not dict
|
|
124
|
+
or set(context) != {"server", "tenant", "resource"}
|
|
125
|
+
or any(type(v) is not str or not v for v in context.values())
|
|
126
|
+
or context["resource"] != resource
|
|
127
|
+
):
|
|
128
|
+
raise EnvelopeError("invalid envelope context")
|
|
129
|
+
return dict(context)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _b64(value: bytes) -> str:
|
|
133
|
+
return base64.urlsafe_b64encode(value).rstrip(b"=").decode()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _unb64(value: str) -> bytes:
|
|
137
|
+
if re.fullmatch(r"[A-Za-z0-9_-]*", value) is None:
|
|
138
|
+
raise EnvelopeError("invalid base64url")
|
|
139
|
+
try:
|
|
140
|
+
raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
141
|
+
except (ValueError, binascii.Error) as error:
|
|
142
|
+
raise EnvelopeError("invalid base64url") from error
|
|
143
|
+
if _b64(raw) != value:
|
|
144
|
+
raise EnvelopeError("noncanonical base64url")
|
|
145
|
+
return raw
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _key(keys: Mapping[str, bytes], kid: str) -> jwk.JWK:
|
|
149
|
+
if type(kid) is not str or re.fullmatch(r"[A-Za-z0-9_-]{1,128}", kid) is None:
|
|
150
|
+
raise EnvelopeError("invalid key identifier")
|
|
151
|
+
material = keys.get(kid)
|
|
152
|
+
if type(material) is not bytes or len(material) != 32:
|
|
153
|
+
raise EnvelopeError("unknown or invalid encryption key")
|
|
154
|
+
return jwk.JWK(kty="oct", k=_b64(material))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def seal(
|
|
158
|
+
record: Any,
|
|
159
|
+
keys: Mapping[str, bytes],
|
|
160
|
+
*,
|
|
161
|
+
active_key: str,
|
|
162
|
+
context: dict[str, str],
|
|
163
|
+
) -> str:
|
|
164
|
+
"""Encrypt a validated record with a random IV and trusted storage context."""
|
|
165
|
+
record = validate_subscription(record)
|
|
166
|
+
context = _context(context, record["id"])
|
|
167
|
+
key = _key(keys, active_key)
|
|
168
|
+
payload = _encode({"profile": PROFILE, "context": context, "subscription": record})
|
|
169
|
+
token = jwe.JWE(
|
|
170
|
+
payload,
|
|
171
|
+
protected=_encode({**HEADER, "kid": active_key}).decode(),
|
|
172
|
+
algs=["dir", "A256GCM"],
|
|
173
|
+
)
|
|
174
|
+
token.add_recipient(key)
|
|
175
|
+
compact = token.serialize(compact=True)
|
|
176
|
+
if len(compact.encode()) > MAX_BYTES:
|
|
177
|
+
raise EnvelopeError("envelope exceeds size limit")
|
|
178
|
+
return compact
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def open_envelope(
|
|
182
|
+
token: str,
|
|
183
|
+
keys: Mapping[str, bytes],
|
|
184
|
+
*,
|
|
185
|
+
expected_context: dict[str, str],
|
|
186
|
+
now: datetime | None = None,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
"""Authenticate, validate, bind and check expiry before returning a record."""
|
|
189
|
+
try:
|
|
190
|
+
if type(token) is not str or len(token) > MAX_BYTES or not token.isascii():
|
|
191
|
+
raise EnvelopeError("invalid envelope size or encoding")
|
|
192
|
+
parts = token.split(".")
|
|
193
|
+
if len(parts) != 5 or parts[1]:
|
|
194
|
+
raise EnvelopeError("invalid compact JWE")
|
|
195
|
+
header_bytes, _, iv, ciphertext, tag = map(_unb64, parts)
|
|
196
|
+
header = _loads(header_bytes)
|
|
197
|
+
if (
|
|
198
|
+
type(header) is not dict
|
|
199
|
+
or set(header) != set(HEADER) | {"kid"}
|
|
200
|
+
or any(header[k] != v for k, v in HEADER.items())
|
|
201
|
+
or len(iv) != 12
|
|
202
|
+
or len(tag) != 16
|
|
203
|
+
or not ciphertext
|
|
204
|
+
):
|
|
205
|
+
raise EnvelopeError("unsupported envelope header or framing")
|
|
206
|
+
key = _key(keys, header["kid"])
|
|
207
|
+
decoded = jwe.JWE(algs=["dir", "A256GCM"])
|
|
208
|
+
decoded.deserialize(token, key=key)
|
|
209
|
+
payload = _loads(decoded.payload)
|
|
210
|
+
if (
|
|
211
|
+
type(payload) is not dict
|
|
212
|
+
or set(payload) != {"profile", "context", "subscription"}
|
|
213
|
+
or payload["profile"] != PROFILE
|
|
214
|
+
):
|
|
215
|
+
raise EnvelopeError("unsupported envelope payload")
|
|
216
|
+
record = validate_subscription(payload["subscription"])
|
|
217
|
+
expected = _context(expected_context, record["id"])
|
|
218
|
+
if _context(payload["context"], record["id"]) != expected:
|
|
219
|
+
raise EnvelopeError("envelope context mismatch")
|
|
220
|
+
current = now if now is not None else datetime.now(UTC)
|
|
221
|
+
if current.tzinfo is None or current.utcoffset() is None:
|
|
222
|
+
raise EnvelopeError("clock must be timezone-aware")
|
|
223
|
+
expiry = record["lease"]["expiresAt"]
|
|
224
|
+
if expiry is not None and current >= _stamp(expiry):
|
|
225
|
+
raise ExpiredSubscription("subscription expired")
|
|
226
|
+
return record
|
|
227
|
+
except EnvelopeError:
|
|
228
|
+
raise
|
|
229
|
+
except Exception as error:
|
|
230
|
+
# No raw token, secret, or decrypted record in the public error message.
|
|
231
|
+
raise EnvelopeError("invalid encrypted subscription") from error
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
__all__ = [
|
|
235
|
+
"EnvelopeError",
|
|
236
|
+
"ExpiredSubscription",
|
|
237
|
+
"RecordError",
|
|
238
|
+
"open_envelope",
|
|
239
|
+
"seal",
|
|
240
|
+
"validate_subscription",
|
|
241
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://chiragagg5k.github.io/mcp-event-subscriptions/schemas/subscription-v1.schema.json",
|
|
4
|
+
"title": "Experimental MCP webhook subscription record v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"version": {
|
|
8
|
+
"type": "integer",
|
|
9
|
+
"const": 1
|
|
10
|
+
},
|
|
11
|
+
"id": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"minLength": 1
|
|
14
|
+
},
|
|
15
|
+
"principal": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1
|
|
18
|
+
},
|
|
19
|
+
"event": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"name": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"arguments": {
|
|
27
|
+
"type": "object"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": [
|
|
31
|
+
"name",
|
|
32
|
+
"arguments"
|
|
33
|
+
],
|
|
34
|
+
"additionalProperties": false
|
|
35
|
+
},
|
|
36
|
+
"delivery": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"mode": {
|
|
40
|
+
"const": "webhook"
|
|
41
|
+
},
|
|
42
|
+
"url": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"format": "uri",
|
|
45
|
+
"pattern": "^https://"
|
|
46
|
+
},
|
|
47
|
+
"secret": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^whsec_[A-Za-z0-9+/]+={0,2}$"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"required": [
|
|
53
|
+
"mode",
|
|
54
|
+
"url",
|
|
55
|
+
"secret"
|
|
56
|
+
],
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
},
|
|
59
|
+
"lease": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"expiresAt": {
|
|
63
|
+
"type": [
|
|
64
|
+
"string",
|
|
65
|
+
"null"
|
|
66
|
+
],
|
|
67
|
+
"format": "date-time",
|
|
68
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?Z$"
|
|
69
|
+
},
|
|
70
|
+
"refreshBefore": {
|
|
71
|
+
"type": [
|
|
72
|
+
"string",
|
|
73
|
+
"null"
|
|
74
|
+
],
|
|
75
|
+
"format": "date-time",
|
|
76
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?Z$"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"required": [
|
|
80
|
+
"expiresAt",
|
|
81
|
+
"refreshBefore"
|
|
82
|
+
],
|
|
83
|
+
"additionalProperties": false
|
|
84
|
+
},
|
|
85
|
+
"extensions": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"propertyNames": {
|
|
88
|
+
"pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*){2,}$"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"required": [
|
|
93
|
+
"version",
|
|
94
|
+
"id",
|
|
95
|
+
"principal",
|
|
96
|
+
"event",
|
|
97
|
+
"delivery",
|
|
98
|
+
"lease"
|
|
99
|
+
],
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-event-subscriptions
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Experimental portable MCP webhook subscription records and JWE envelopes
|
|
5
|
+
Project-URL: Repository, https://github.com/ChiragAgg5k/mcp-event-subscriptions
|
|
6
|
+
Project-URL: Documentation, https://github.com/ChiragAgg5k/mcp-event-subscriptions#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/ChiragAgg5k/mcp-event-subscriptions/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: jsonschema<5,>=4.23
|
|
17
|
+
Requires-Dist: jwcrypto<2,>=1.5.6
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# MCP Event Subscriptions
|
|
21
|
+
|
|
22
|
+
An experimental, portable subscription record for MCP Events, with an optional
|
|
23
|
+
[JWE](https://www.rfc-editor.org/rfc/rfc7516) encrypted envelope.
|
|
24
|
+
|
|
25
|
+
Store the same record in a database, a key-value store, or an upstream webhook
|
|
26
|
+
configuration. This repository defines its fields, encrypted encoding, and
|
|
27
|
+
validation rules, with a Python reference implementation and conformance vectors.
|
|
28
|
+
|
|
29
|
+
**Independent proposal. This is not an adopted MCP specification or an IETF RFC.**
|
|
30
|
+
The format may change before a stable release. It does not change MCP event
|
|
31
|
+
requests or notification payloads.
|
|
32
|
+
|
|
33
|
+
## What is here
|
|
34
|
+
|
|
35
|
+
- [Record specification](SPEC.md) and [JSON Schema](schemas/subscription-v1.schema.json).
|
|
36
|
+
- [Encrypted envelope profile](profiles/encrypted-envelope.md): JWE Compact,
|
|
37
|
+
`dir` + `A256GCM`, with explicit server/tenant/resource binding.
|
|
38
|
+
- [Appwrite webhook profile](profiles/appwrite-webhook.md): proposed storage
|
|
39
|
+
mapping and the limits of write-only upstream state.
|
|
40
|
+
- [Examples](examples/) and [conformance vectors](test-vectors/README.md).
|
|
41
|
+
- [Python reference implementation](packages/python/README.md).
|
|
42
|
+
- [Upstream discussion draft](proposals/upstream-mcp.md).
|
|
43
|
+
|
|
44
|
+
## Install the alpha release
|
|
45
|
+
|
|
46
|
+
Once the first release is published to PyPI:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install mcp-event-subscriptions==0.1.0a1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See [RELEASING.md](RELEASING.md) for the Trusted Publishing setup and release process.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
Python 3.12 or newer and [uv](https://docs.astral.sh/uv/):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
uv sync --group dev
|
|
60
|
+
uv run python examples/roundtrip.py
|
|
61
|
+
uv run python -m unittest discover -s tests -v
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import json
|
|
66
|
+
import secrets
|
|
67
|
+
from mcp_event_subscriptions import seal, open_envelope
|
|
68
|
+
|
|
69
|
+
record = json.load(open("examples/subscription.json"))
|
|
70
|
+
context = {
|
|
71
|
+
"server": "https://mcp.example.com",
|
|
72
|
+
"tenant": "project_demo",
|
|
73
|
+
"resource": record["id"],
|
|
74
|
+
}
|
|
75
|
+
keys = {"k1": secrets.token_bytes(32)}
|
|
76
|
+
token = seal(record, keys, active_key="k1", context=context)
|
|
77
|
+
restored = open_envelope(token, keys, expected_context=context)
|
|
78
|
+
assert restored == record
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The sample subscription expires in 2030. Examples and vectors use synthetic
|
|
82
|
+
principals and secrets. Fixed test-vector keys are public fixtures, never
|
|
83
|
+
production keys.
|
|
84
|
+
|
|
85
|
+
## Scope
|
|
86
|
+
|
|
87
|
+
Version 1 describes webhook subscription state and optional encrypted storage.
|
|
88
|
+
It does not specify a SQL layout, implement a delivery queue, establish current
|
|
89
|
+
caller authorization, or make refresh/delete operations atomic. Adapters must
|
|
90
|
+
provide their own coordination and revocation checks. Encryption cannot detect
|
|
91
|
+
replay of an older, still-valid envelope without current state.
|
|
92
|
+
|
|
93
|
+
The Appwrite profile is documentation, not a working Appwrite adapter. It is not
|
|
94
|
+
wire-compatible with the existing `v1.<kid>.<ciphertext>` envelope in
|
|
95
|
+
[appwrite/mcp PR #132](https://github.com/appwrite/mcp/pull/132).
|
|
96
|
+
|
|
97
|
+
## Discussing the proposal
|
|
98
|
+
|
|
99
|
+
Start with the [open questions](proposals/upstream-mcp.md#open-questions): which
|
|
100
|
+
fields should be common, whether an encrypted profile belongs alongside the
|
|
101
|
+
schema, and what lifecycle guarantees adapters must expose. No proposal has
|
|
102
|
+
been submitted upstream by this repository.
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
uv run --group dev ruff check .
|
|
108
|
+
uv run --group dev black --check .
|
|
109
|
+
uv run python -m unittest discover -s tests -v
|
|
110
|
+
uv build
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Licensed under the MIT License. See [SECURITY.md](SECURITY.md) before using the
|
|
114
|
+
reference implementation with real secrets.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
mcp_event_subscriptions/__init__.py,sha256=CN7ecNATFiIu64p1aJ386h89aDTPVPdeKuakk-jsN1o,8301
|
|
2
|
+
mcp_event_subscriptions/subscription-v1.schema.json,sha256=8fkYQerIm6TBmj357PxqSC0cYUvEtNBtL7y9WXiX9VU,2194
|
|
3
|
+
mcp_event_subscriptions-0.1.0a1.dist-info/METADATA,sha256=oBcz0pGnouODsu9wKxdyS8eKyIJw0TdCeyVJi_9NiI8,4268
|
|
4
|
+
mcp_event_subscriptions-0.1.0a1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
5
|
+
mcp_event_subscriptions-0.1.0a1.dist-info/licenses/LICENSE,sha256=FDRCGLH-p80m3w5GPU7K3VIJ00j37PKHvbAfViPs5aY,1065
|
|
6
|
+
mcp_event_subscriptions-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Appwrite
|
|
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.
|