mcp-event-subscriptions 0.1.0a1__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.
- mcp_event_subscriptions-0.1.0a1/.github/workflows/ci.yml +18 -0
- mcp_event_subscriptions-0.1.0a1/.github/workflows/publish.yml +76 -0
- mcp_event_subscriptions-0.1.0a1/.gitignore +8 -0
- mcp_event_subscriptions-0.1.0a1/LICENSE +21 -0
- mcp_event_subscriptions-0.1.0a1/PKG-INFO +114 -0
- mcp_event_subscriptions-0.1.0a1/README.md +95 -0
- mcp_event_subscriptions-0.1.0a1/RELEASING.md +49 -0
- mcp_event_subscriptions-0.1.0a1/SECURITY.md +26 -0
- mcp_event_subscriptions-0.1.0a1/SPEC.md +89 -0
- mcp_event_subscriptions-0.1.0a1/examples/appwrite-webhook.json +14 -0
- mcp_event_subscriptions-0.1.0a1/examples/encrypted-subscription.txt +1 -0
- mcp_event_subscriptions-0.1.0a1/examples/roundtrip.py +16 -0
- mcp_event_subscriptions-0.1.0a1/examples/subscription.json +22 -0
- mcp_event_subscriptions-0.1.0a1/packages/python/README.md +20 -0
- mcp_event_subscriptions-0.1.0a1/packages/python/src/mcp_event_subscriptions/__init__.py +241 -0
- mcp_event_subscriptions-0.1.0a1/packages/python/src/mcp_event_subscriptions/subscription-v1.schema.json +101 -0
- mcp_event_subscriptions-0.1.0a1/profiles/appwrite-webhook.md +47 -0
- mcp_event_subscriptions-0.1.0a1/profiles/encrypted-envelope.md +83 -0
- mcp_event_subscriptions-0.1.0a1/proposals/upstream-mcp.md +45 -0
- mcp_event_subscriptions-0.1.0a1/pyproject.toml +38 -0
- mcp_event_subscriptions-0.1.0a1/schemas/subscription-v1.schema.json +101 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/README.md +25 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/encryption/jwe-v1.json +48 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/credentialed-callback.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/http-callback.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/lease-order.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/mixed-null-lease.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/non-utc-time.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/short-secret.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/unknown-field.json +23 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/unknown-version.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/valid/finite-lease.json +22 -0
- mcp_event_subscriptions-0.1.0a1/test-vectors/valid/no-expiry.json +22 -0
- mcp_event_subscriptions-0.1.0a1/tests/test_subscriptions.py +225 -0
- mcp_event_subscriptions-0.1.0a1/uv.lock +916 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
jobs:
|
|
6
|
+
check:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: astral-sh/setup-uv@v6
|
|
11
|
+
with:
|
|
12
|
+
version: "0.11.22"
|
|
13
|
+
- run: uv python install 3.12
|
|
14
|
+
- run: uv sync --frozen --group dev --python 3.12
|
|
15
|
+
- run: uv run --group dev ruff check .
|
|
16
|
+
- run: uv run --group dev black --check .
|
|
17
|
+
- run: uv run python -m unittest discover -s tests -v
|
|
18
|
+
- run: uv build
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
tag:
|
|
9
|
+
description: Existing version tag to publish (for example v0.1.0a1)
|
|
10
|
+
required: true
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: pypi-${{ github.event.release.tag_name || inputs.tag }}
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
build:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
25
|
+
with:
|
|
26
|
+
ref: ${{ github.event.release.tag_name || inputs.tag }}
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
29
|
+
with:
|
|
30
|
+
version: "0.11.22"
|
|
31
|
+
- run: uv python install 3.12
|
|
32
|
+
- run: uv sync --frozen --group dev --python 3.12
|
|
33
|
+
- name: Check release tag matches package version
|
|
34
|
+
env:
|
|
35
|
+
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
|
36
|
+
run: |
|
|
37
|
+
uv run python - <<'PY'
|
|
38
|
+
import os
|
|
39
|
+
import tomllib
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
42
|
+
if os.environ["RELEASE_TAG"] != f"v{version}":
|
|
43
|
+
raise SystemExit("Release tag does not match package version")
|
|
44
|
+
PY
|
|
45
|
+
- run: uv run --group dev ruff check .
|
|
46
|
+
- run: uv run --group dev black --check .
|
|
47
|
+
- run: uv run python -m unittest discover -s tests -v
|
|
48
|
+
- run: uv build
|
|
49
|
+
- run: uv run --group dev twine check --strict dist/*
|
|
50
|
+
- name: Test installed wheel in an isolated environment
|
|
51
|
+
run: |
|
|
52
|
+
wheel="$(find "$GITHUB_WORKSPACE/dist" -name '*.whl' -print -quit)"
|
|
53
|
+
cd "$RUNNER_TEMP"
|
|
54
|
+
uv run --isolated --no-project --python 3.12 --with "$wheel" \
|
|
55
|
+
python -m unittest discover -s "$GITHUB_WORKSPACE/tests" -v
|
|
56
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
57
|
+
with:
|
|
58
|
+
name: distributions
|
|
59
|
+
path: dist/*
|
|
60
|
+
if-no-files-found: error
|
|
61
|
+
|
|
62
|
+
publish:
|
|
63
|
+
needs: build
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
environment:
|
|
66
|
+
name: pypi
|
|
67
|
+
url: https://pypi.org/project/mcp-event-subscriptions/
|
|
68
|
+
permissions:
|
|
69
|
+
id-token: write
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
72
|
+
with:
|
|
73
|
+
name: distributions
|
|
74
|
+
path: dist/
|
|
75
|
+
- name: Publish distributions with Trusted Publishing
|
|
76
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
@@ -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.
|
|
@@ -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,95 @@
|
|
|
1
|
+
# MCP Event Subscriptions
|
|
2
|
+
|
|
3
|
+
An experimental, portable subscription record for MCP Events, with an optional
|
|
4
|
+
[JWE](https://www.rfc-editor.org/rfc/rfc7516) encrypted envelope.
|
|
5
|
+
|
|
6
|
+
Store the same record in a database, a key-value store, or an upstream webhook
|
|
7
|
+
configuration. This repository defines its fields, encrypted encoding, and
|
|
8
|
+
validation rules, with a Python reference implementation and conformance vectors.
|
|
9
|
+
|
|
10
|
+
**Independent proposal. This is not an adopted MCP specification or an IETF RFC.**
|
|
11
|
+
The format may change before a stable release. It does not change MCP event
|
|
12
|
+
requests or notification payloads.
|
|
13
|
+
|
|
14
|
+
## What is here
|
|
15
|
+
|
|
16
|
+
- [Record specification](SPEC.md) and [JSON Schema](schemas/subscription-v1.schema.json).
|
|
17
|
+
- [Encrypted envelope profile](profiles/encrypted-envelope.md): JWE Compact,
|
|
18
|
+
`dir` + `A256GCM`, with explicit server/tenant/resource binding.
|
|
19
|
+
- [Appwrite webhook profile](profiles/appwrite-webhook.md): proposed storage
|
|
20
|
+
mapping and the limits of write-only upstream state.
|
|
21
|
+
- [Examples](examples/) and [conformance vectors](test-vectors/README.md).
|
|
22
|
+
- [Python reference implementation](packages/python/README.md).
|
|
23
|
+
- [Upstream discussion draft](proposals/upstream-mcp.md).
|
|
24
|
+
|
|
25
|
+
## Install the alpha release
|
|
26
|
+
|
|
27
|
+
Once the first release is published to PyPI:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install mcp-event-subscriptions==0.1.0a1
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See [RELEASING.md](RELEASING.md) for the Trusted Publishing setup and release process.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Python 3.12 or newer and [uv](https://docs.astral.sh/uv/):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv sync --group dev
|
|
41
|
+
uv run python examples/roundtrip.py
|
|
42
|
+
uv run python -m unittest discover -s tests -v
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import json
|
|
47
|
+
import secrets
|
|
48
|
+
from mcp_event_subscriptions import seal, open_envelope
|
|
49
|
+
|
|
50
|
+
record = json.load(open("examples/subscription.json"))
|
|
51
|
+
context = {
|
|
52
|
+
"server": "https://mcp.example.com",
|
|
53
|
+
"tenant": "project_demo",
|
|
54
|
+
"resource": record["id"],
|
|
55
|
+
}
|
|
56
|
+
keys = {"k1": secrets.token_bytes(32)}
|
|
57
|
+
token = seal(record, keys, active_key="k1", context=context)
|
|
58
|
+
restored = open_envelope(token, keys, expected_context=context)
|
|
59
|
+
assert restored == record
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The sample subscription expires in 2030. Examples and vectors use synthetic
|
|
63
|
+
principals and secrets. Fixed test-vector keys are public fixtures, never
|
|
64
|
+
production keys.
|
|
65
|
+
|
|
66
|
+
## Scope
|
|
67
|
+
|
|
68
|
+
Version 1 describes webhook subscription state and optional encrypted storage.
|
|
69
|
+
It does not specify a SQL layout, implement a delivery queue, establish current
|
|
70
|
+
caller authorization, or make refresh/delete operations atomic. Adapters must
|
|
71
|
+
provide their own coordination and revocation checks. Encryption cannot detect
|
|
72
|
+
replay of an older, still-valid envelope without current state.
|
|
73
|
+
|
|
74
|
+
The Appwrite profile is documentation, not a working Appwrite adapter. It is not
|
|
75
|
+
wire-compatible with the existing `v1.<kid>.<ciphertext>` envelope in
|
|
76
|
+
[appwrite/mcp PR #132](https://github.com/appwrite/mcp/pull/132).
|
|
77
|
+
|
|
78
|
+
## Discussing the proposal
|
|
79
|
+
|
|
80
|
+
Start with the [open questions](proposals/upstream-mcp.md#open-questions): which
|
|
81
|
+
fields should be common, whether an encrypted profile belongs alongside the
|
|
82
|
+
schema, and what lifecycle guarantees adapters must expose. No proposal has
|
|
83
|
+
been submitted upstream by this repository.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
uv run --group dev ruff check .
|
|
89
|
+
uv run --group dev black --check .
|
|
90
|
+
uv run python -m unittest discover -s tests -v
|
|
91
|
+
uv build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Licensed under the MIT License. See [SECURITY.md](SECURITY.md) before using the
|
|
95
|
+
reference implementation with real secrets.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Publishing to PyPI
|
|
2
|
+
|
|
3
|
+
The first release is `0.1.0a1`, an alpha. The package and specification remain
|
|
4
|
+
experimental. Publishing a version is permanent: PyPI does not allow replacing
|
|
5
|
+
its uploaded distribution files. Use a new version for subsequent fixes.
|
|
6
|
+
|
|
7
|
+
## One-time account setup
|
|
8
|
+
|
|
9
|
+
Sign in to PyPI, then open [account publishing](https://pypi.org/manage/account/publishing/).
|
|
10
|
+
Under **Add a new pending publisher**, select GitHub and enter:
|
|
11
|
+
|
|
12
|
+
| Field | Value |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| PyPI project name | `mcp-event-subscriptions` |
|
|
15
|
+
| Repository owner | `ChiragAgg5k` |
|
|
16
|
+
| Repository name | `mcp-event-subscriptions` |
|
|
17
|
+
| Workflow name | `publish.yml` |
|
|
18
|
+
| Environment name | `pypi` |
|
|
19
|
+
|
|
20
|
+
Click **Add**. This authorizes only the configured GitHub workflow/environment.
|
|
21
|
+
No API token or GitHub secret is required. A pending publisher creates the PyPI
|
|
22
|
+
project on its first successful upload; it does not reserve the name beforehand.
|
|
23
|
+
The repository has a `pypi` GitHub environment matching the workflow.
|
|
24
|
+
|
|
25
|
+
References: [PyPI pending publishers](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
26
|
+
and [publishing with a trusted publisher](https://docs.pypi.org/trusted-publishers/using-a-publisher/).
|
|
27
|
+
|
|
28
|
+
## Release
|
|
29
|
+
|
|
30
|
+
1. Update `project.version` in `pyproject.toml` for later releases, run `uv lock`,
|
|
31
|
+
and commit the result. For this first alpha the version is already correct.
|
|
32
|
+
2. Wait for CI to pass for the intended release commit.
|
|
33
|
+
3. Create a GitHub release with a tag matching the version, e.g. `v0.1.0a1`,
|
|
34
|
+
targeting that commit. Mark alpha releases as prereleases.
|
|
35
|
+
4. Publish the GitHub release. `.github/workflows/publish.yml` validates the
|
|
36
|
+
tag/version match, runs checks, builds distributions, checks metadata and
|
|
37
|
+
tests the installed wheel before publishing through GitHub OIDC.
|
|
38
|
+
5. Verify the project/version on PyPI and install from PyPI in a clean environment.
|
|
39
|
+
|
|
40
|
+
The publishing job downloads only the built artifacts; it does not check out or
|
|
41
|
+
execute repository code. OIDC permissions are limited to that job. Actions are
|
|
42
|
+
pinned to commit hashes. Default PyPA upload attestations are enabled.
|
|
43
|
+
|
|
44
|
+
## Retry a failed publishing run
|
|
45
|
+
|
|
46
|
+
If the initial upload fails because the pending publisher was not configured,
|
|
47
|
+
add it on PyPI, then rerun the failed job. Alternatively manually dispatch
|
|
48
|
+
**Publish to PyPI** with the existing version tag. Do not rerun an already
|
|
49
|
+
successful upload; published files cannot be overwritten.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
This repository is experimental and has not received an independent security
|
|
4
|
+
audit. Report security issues privately through GitHub's private vulnerability
|
|
5
|
+
reporting when available; otherwise use Appwrite's published security reporting
|
|
6
|
+
channel. Do not include production keys or subscription secrets in public issues.
|
|
7
|
+
|
|
8
|
+
## Trust boundaries
|
|
9
|
+
|
|
10
|
+
- Records contain sensitive callback signing secrets. Do not log plaintext,
|
|
11
|
+
envelopes, Authorization headers, or decrypted errors with payloads.
|
|
12
|
+
- Use dedicated, random 256-bit encryption keys from a secret manager. Public
|
|
13
|
+
fixture keys are intentionally insecure and must never be deployed.
|
|
14
|
+
- Expected envelope context must come from trusted server configuration and
|
|
15
|
+
authenticated adapter routing. A token cannot establish its own tenant scope.
|
|
16
|
+
- A decrypted subscription does not prove present authorization or revocation
|
|
17
|
+
status. Adapters must implement those checks and callback verification.
|
|
18
|
+
- Validate callback addresses at connection time, block internal destinations,
|
|
19
|
+
preserve TLS hostname verification, and do not follow redirects. This library
|
|
20
|
+
validates URL syntax only; it does not resolve DNS or perform outbound HTTP.
|
|
21
|
+
- Expiry limits validity; it does not prevent replay before expiry. Concurrent
|
|
22
|
+
writes and stale cleanup require storage-level coordination.
|
|
23
|
+
- No-expiry subscriptions need durable revocation and key migration mechanisms.
|
|
24
|
+
|
|
25
|
+
The decoder limits token size, rejects duplicate keys/non-finite numbers, uses
|
|
26
|
+
an algorithm allowlist, and never fetches keys referenced by an envelope.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Subscription Record v1 — experimental draft
|
|
2
|
+
|
|
3
|
+
This document defines an optional persistence representation for webhook
|
|
4
|
+
subscriptions. It is independent of the MCP wire protocol. MUST, SHOULD, and
|
|
5
|
+
MAY express requirements for implementations claiming this draft profile; they
|
|
6
|
+
are not additional requirements imposed on all MCP servers.
|
|
7
|
+
|
|
8
|
+
## Record
|
|
9
|
+
|
|
10
|
+
A record MUST be a JSON object conforming to
|
|
11
|
+
[`subscription-v1.schema.json`](schemas/subscription-v1.schema.json) and the
|
|
12
|
+
semantic rules below. Unknown fields MUST be rejected outside `extensions`.
|
|
13
|
+
JSON parsers MUST reject duplicate object keys and non-finite numbers.
|
|
14
|
+
|
|
15
|
+
| Field | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `version` | Integer `1`; unsupported versions MUST be rejected. |
|
|
18
|
+
| `id` | Nonempty, opaque subscription identifier scoped to its issuing server. |
|
|
19
|
+
| `principal` | Nonempty, opaque canonical owner identifier. It MUST NOT contain bearer or refresh tokens. |
|
|
20
|
+
| `event.name` | Event type advertised by the issuing server. |
|
|
21
|
+
| `event.arguments` | JSON object; the adapter MUST validate it against that event's published input schema. |
|
|
22
|
+
| `delivery.mode` | `webhook` in v1. |
|
|
23
|
+
| `delivery.url` | Absolute HTTPS callback URL without user information or fragment. |
|
|
24
|
+
| `delivery.secret` | `whsec_` followed by canonical padded standard base64 for 24–64 decoded bytes. |
|
|
25
|
+
| `lease.expiresAt` | Actual expiry, or `null` for an explicitly granted no-expiry subscription. |
|
|
26
|
+
| `lease.refreshBefore` | Suggested refresh deadline, or `null` together with `expiresAt`. |
|
|
27
|
+
| `extensions` | Optional object with namespaced keys such as `io.appwrite.source`. |
|
|
28
|
+
|
|
29
|
+
Timestamps MUST be UTC RFC 3339 strings using `Z`, with seconds and optionally
|
|
30
|
+
1–6 fractional digits. A finite `refreshBefore` MUST be at or before `expiresAt`.
|
|
31
|
+
At the expiry instant the subscription is expired. No-expiry records MUST have
|
|
32
|
+
both lease fields set to `null`; granting them requires durable storage and an
|
|
33
|
+
explicit no-expiry request at the protocol layer. The schema permits this
|
|
34
|
+
representation; it does not grant such a lease by itself.
|
|
35
|
+
|
|
36
|
+
Record validation is separate from activation. A stored record may be expired;
|
|
37
|
+
a consumer MUST check expiry before using it for delivery. The reference
|
|
38
|
+
`open_envelope` checks expiry by default.
|
|
39
|
+
|
|
40
|
+
The record MUST contain JSON values only. The reference implementation limits
|
|
41
|
+
encoded records and envelopes to 64 KiB each; adapters MAY impose lower limits.
|
|
42
|
+
This draft has no compression profile.
|
|
43
|
+
|
|
44
|
+
## Identity and authorization
|
|
45
|
+
|
|
46
|
+
The adapter MUST preserve MCP's logical identity based on principal, callback,
|
|
47
|
+
event name, and arguments. This draft does not prescribe a hash algorithm or
|
|
48
|
+
identifier syntax. Importing a record MUST NOT grant access: the receiving
|
|
49
|
+
server MUST establish tenant/resource ownership and current authorization.
|
|
50
|
+
Principal identifiers are server-scoped; cross-server migration requires an
|
|
51
|
+
explicit identity mapping and reauthorization, not merely copying JSON.
|
|
52
|
+
|
|
53
|
+
## Extensions
|
|
54
|
+
|
|
55
|
+
An extension key MUST have a reverse-domain namespace followed by a name,
|
|
56
|
+
for example `io.appwrite.source`. Core consumers MAY preserve extensions they
|
|
57
|
+
do not understand, but MUST NOT depend on them to establish security. A future
|
|
58
|
+
required extension needs a new profile or version with explicit negotiation.
|
|
59
|
+
|
|
60
|
+
## Storage and lifecycle
|
|
61
|
+
|
|
62
|
+
This record is a snapshot. It is not proof that a subscription remains live.
|
|
63
|
+
The storage adapter MUST define:
|
|
64
|
+
|
|
65
|
+
1. An atomic upsert or coordination mechanism for concurrent refreshes.
|
|
66
|
+
2. Conditional expiry cleanup, so a stale snapshot cannot delete a renewed lease.
|
|
67
|
+
3. Revocation semantics and the treatment of deliveries already in flight.
|
|
68
|
+
4. How callback secret changes coexist with retries signed using an old secret.
|
|
69
|
+
5. Lease durability and recovery guarantees.
|
|
70
|
+
|
|
71
|
+
A generation number alone provides no protection without compare-and-swap or
|
|
72
|
+
equivalent enforcement. Version 1 deliberately has no unenforced revision field.
|
|
73
|
+
The encrypted profile authenticates a snapshot but cannot prove its freshness.
|
|
74
|
+
|
|
75
|
+
## Versioning
|
|
76
|
+
|
|
77
|
+
Adding required fields, changing field meaning, or changing validation rules
|
|
78
|
+
requires a new record version. Readers MUST reject unsupported versions rather
|
|
79
|
+
than guessing. This experimental draft may change before a stable release.
|
|
80
|
+
|
|
81
|
+
## Related work
|
|
82
|
+
|
|
83
|
+
- [MCP Events design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md)
|
|
84
|
+
- [SEP-3415 discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3415)
|
|
85
|
+
- [JSON Schema](https://json-schema.org/draft/2020-12)
|
|
86
|
+
|
|
87
|
+
The proposal's value is a reusable persistence representation and test suite;
|
|
88
|
+
it does not claim that internal database schemas must be standardized for MCP
|
|
89
|
+
clients and servers to interoperate.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"webhookId": "sub_example",
|
|
3
|
+
"name": "MCP events | functions.execution.failed | until 2030-01-01T01:00:00Z | owner demo",
|
|
4
|
+
"url": "https://mcp.example.com/appwrite/webhooks/sub_example",
|
|
5
|
+
"events": [
|
|
6
|
+
"functions.fn_demo.executions.*.create",
|
|
7
|
+
"functions.fn_demo.executions.*.update"
|
|
8
|
+
],
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"tls": true,
|
|
11
|
+
"authUsername": "mcp-events",
|
|
12
|
+
"authPassword": "eyJhbGciOiJkaXIiLCJjdHkiOiJhcHBsaWNhdGlvbi9qc29uIiwiZW5jIjoiQTI1NkdDTSIsImtpZCI6ImZpeHR1cmUtazEiLCJ0eXAiOiJtY3Atc3Vic2NyaXB0aW9uK2p3ZSJ9..AAECAwQFBgcICQoL.PCC1dKuRp2P5Y63wk5sdHuyj9VeVWWVeSxKH2ngRYd9xfMveg-Nh_QbSGp-qvQpQmi0Q_mD5jLdc5wR8YIKYnpxZ6Bm8vARNPiDPAI7hesoCqLkQGQk2C4mi-qkVn5vw59oplMsnoXb7PgSQJRZRUskSANiAt0zqOZY4o6-DAHjePm-20fq9YirNatJIuD-hRJ2TQVx-_Wi2JMoTn76AmPbl5zfThO4TyAe42OWi8d7_pqVN4Ilfnha6o7UUsZXJMTviiWi-iYh2E5CnzRCfxoYdsTVsb_a1MKHVgzkJ3b8rboHptHlbYhAs2U_NZhgZwKuEV7ES_Nd7SV1R4-g28Wfr3b_vvytbHqkXdZlIQmu3xKaorujuwl5KGzRkgMlpfC_8LcygoGOwBkF4a-fHpvWvNSo6tvVsuldoePRJLIanyhZOqVpaj82_H0UwvD9OsWql6_Zx5nwWyhxEt5M4jTZFnLrxCxauTcCGXruXDEnpmD1DzJd1r_yRXbOyJHdX3eajrPNuvLKPFp8WUiV2pZ8nTt7nVDe6TnmxKD7EgS1kVnUCp_NrUNPNvg2agNHILQchMDChTICGyobPvNA9HQIKx6_LJP4JySDCZcGn5XxZu0Zi8s4uDXn1ioohz9yWNc1bZPHhur056GHrh96IoqqO46oOnswQhi-hbJPYYyaUWNDIj2v-Km2ZkQ9TfALR99NXGFRZosbmCyNexzVy7whqW-jOa5J9VaYkz6UBedd30uo.11BkPstaF5zWAYhg92Q1gA",
|
|
13
|
+
"secret": "PUBLIC-EXAMPLE-INDEPENDENT-APPWRITE-SIGNING-SECRET"
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eyJhbGciOiJkaXIiLCJjdHkiOiJhcHBsaWNhdGlvbi9qc29uIiwiZW5jIjoiQTI1NkdDTSIsImtpZCI6ImZpeHR1cmUtazEiLCJ0eXAiOiJtY3Atc3Vic2NyaXB0aW9uK2p3ZSJ9..AAECAwQFBgcICQoL.PCC1dKuRp2P5Y63wk5sdHuyj9VeVWWVeSxKH2ngRYd9xfMveg-Nh_QbSGp-qvQpQmi0Q_mD5jLdc5wR8YIKYnpxZ6Bm8vARNPiDPAI7hesoCqLkQGQk2C4mi-qkVn5vw59oplMsnoXb7PgSQJRZRUskSANiAt0zqOZY4o6-DAHjePm-20fq9YirNatJIuD-hRJ2TQVx-_Wi2JMoTn76AmPbl5zfThO4TyAe42OWi8d7_pqVN4Ilfnha6o7UUsZXJMTviiWi-iYh2E5CnzRCfxoYdsTVsb_a1MKHVgzkJ3b8rboHptHlbYhAs2U_NZhgZwKuEV7ES_Nd7SV1R4-g28Wfr3b_vvytbHqkXdZlIQmu3xKaorujuwl5KGzRkgMlpfC_8LcygoGOwBkF4a-fHpvWvNSo6tvVsuldoePRJLIanyhZOqVpaj82_H0UwvD9OsWql6_Zx5nwWyhxEt5M4jTZFnLrxCxauTcCGXruXDEnpmD1DzJd1r_yRXbOyJHdX3eajrPNuvLKPFp8WUiV2pZ8nTt7nVDe6TnmxKD7EgS1kVnUCp_NrUNPNvg2agNHILQchMDChTICGyobPvNA9HQIKx6_LJP4JySDCZcGn5XxZu0Zi8s4uDXn1ioohz9yWNc1bZPHhur056GHrh96IoqqO46oOnswQhi-hbJPYYyaUWNDIj2v-Km2ZkQ9TfALR99NXGFRZosbmCyNexzVy7whqW-jOa5J9VaYkz6UBedd30uo.11BkPstaF5zWAYhg92Q1gA
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import secrets
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from mcp_event_subscriptions import open_envelope, seal
|
|
6
|
+
|
|
7
|
+
record = json.loads(Path(__file__).with_name("subscription.json").read_text())
|
|
8
|
+
keys = {"k1": secrets.token_bytes(32)}
|
|
9
|
+
context = {
|
|
10
|
+
"server": "https://mcp.example.com",
|
|
11
|
+
"tenant": "project_demo",
|
|
12
|
+
"resource": record["id"],
|
|
13
|
+
}
|
|
14
|
+
token = seal(record, keys, active_key="k1", context=context)
|
|
15
|
+
assert open_envelope(token, keys, expected_context=context) == record
|
|
16
|
+
print("Subscription sealed and recovered successfully (secrets omitted).")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"id": "sub_example",
|
|
4
|
+
"principal": "principal_demo",
|
|
5
|
+
"event": {
|
|
6
|
+
"name": "functions.execution.failed",
|
|
7
|
+
"arguments": {
|
|
8
|
+
"project_id": "project_demo",
|
|
9
|
+
"function_id": "fn_demo"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"delivery": {
|
|
13
|
+
"mode": "webhook",
|
|
14
|
+
"url": "https://receiver.example.com/events",
|
|
15
|
+
"secret": "whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
|
16
|
+
},
|
|
17
|
+
"lease": {
|
|
18
|
+
"expiresAt": "2030-01-01T01:00:00Z",
|
|
19
|
+
"refreshBefore": "2030-01-01T00:54:00Z"
|
|
20
|
+
},
|
|
21
|
+
"extensions": {}
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Python reference implementation
|
|
2
|
+
|
|
3
|
+
The root `pyproject.toml` packages `packages/python/src/mcp_event_subscriptions`.
|
|
4
|
+
Install locally with `uv sync` or `pip install .` from the repository root.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
validate_subscription(record) # structural and semantic checks; returns a copy
|
|
8
|
+
seal(record, keys, active_key="k1", context=context)
|
|
9
|
+
open_envelope(token, keys, expected_context=context) # also rejects expiry
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`keys` maps key IDs to dedicated 32-byte keys. `context` contains exactly
|
|
13
|
+
`server`, `tenant`, and `resource`. Opening uses the `kid` named in the protected
|
|
14
|
+
header. Missing keys, tampering, invalid records and context mismatches raise
|
|
15
|
+
`EnvelopeError`; expired records raise `ExpiredSubscription`. Invalid direct
|
|
16
|
+
record input raises `RecordError`. Keys must be generated randomly by the caller.
|
|
17
|
+
|
|
18
|
+
`open_envelope(..., now=datetime(..., tzinfo=UTC))` permits a supplied clock for
|
|
19
|
+
fixtures; normal consumers should use the default current clock. The function
|
|
20
|
+
does not contact the callback or check current upstream revocation state.
|