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.
Files changed (35) hide show
  1. mcp_event_subscriptions-0.1.0a1/.github/workflows/ci.yml +18 -0
  2. mcp_event_subscriptions-0.1.0a1/.github/workflows/publish.yml +76 -0
  3. mcp_event_subscriptions-0.1.0a1/.gitignore +8 -0
  4. mcp_event_subscriptions-0.1.0a1/LICENSE +21 -0
  5. mcp_event_subscriptions-0.1.0a1/PKG-INFO +114 -0
  6. mcp_event_subscriptions-0.1.0a1/README.md +95 -0
  7. mcp_event_subscriptions-0.1.0a1/RELEASING.md +49 -0
  8. mcp_event_subscriptions-0.1.0a1/SECURITY.md +26 -0
  9. mcp_event_subscriptions-0.1.0a1/SPEC.md +89 -0
  10. mcp_event_subscriptions-0.1.0a1/examples/appwrite-webhook.json +14 -0
  11. mcp_event_subscriptions-0.1.0a1/examples/encrypted-subscription.txt +1 -0
  12. mcp_event_subscriptions-0.1.0a1/examples/roundtrip.py +16 -0
  13. mcp_event_subscriptions-0.1.0a1/examples/subscription.json +22 -0
  14. mcp_event_subscriptions-0.1.0a1/packages/python/README.md +20 -0
  15. mcp_event_subscriptions-0.1.0a1/packages/python/src/mcp_event_subscriptions/__init__.py +241 -0
  16. mcp_event_subscriptions-0.1.0a1/packages/python/src/mcp_event_subscriptions/subscription-v1.schema.json +101 -0
  17. mcp_event_subscriptions-0.1.0a1/profiles/appwrite-webhook.md +47 -0
  18. mcp_event_subscriptions-0.1.0a1/profiles/encrypted-envelope.md +83 -0
  19. mcp_event_subscriptions-0.1.0a1/proposals/upstream-mcp.md +45 -0
  20. mcp_event_subscriptions-0.1.0a1/pyproject.toml +38 -0
  21. mcp_event_subscriptions-0.1.0a1/schemas/subscription-v1.schema.json +101 -0
  22. mcp_event_subscriptions-0.1.0a1/test-vectors/README.md +25 -0
  23. mcp_event_subscriptions-0.1.0a1/test-vectors/encryption/jwe-v1.json +48 -0
  24. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/credentialed-callback.json +22 -0
  25. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/http-callback.json +22 -0
  26. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/lease-order.json +22 -0
  27. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/mixed-null-lease.json +22 -0
  28. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/non-utc-time.json +22 -0
  29. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/short-secret.json +22 -0
  30. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/unknown-field.json +23 -0
  31. mcp_event_subscriptions-0.1.0a1/test-vectors/invalid/unknown-version.json +22 -0
  32. mcp_event_subscriptions-0.1.0a1/test-vectors/valid/finite-lease.json +22 -0
  33. mcp_event_subscriptions-0.1.0a1/test-vectors/valid/no-expiry.json +22 -0
  34. mcp_event_subscriptions-0.1.0a1/tests/test_subscriptions.py +225 -0
  35. 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,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -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.