1818from pydantic import BaseModel
1919
2020from mcp .experimental .events ._callbacks import CallbackFailure , Callbacks , NonPublicAddress
21+ from mcp .experimental .events ._locks import KeyedLocks
2122from mcp .experimental .events .event_type import EventBatch , EventQuery , EventType , Subscriber
2223from mcp .experimental .events .signing import decode_secret
2324from mcp .experimental .events .types import (
@@ -103,7 +104,8 @@ def __init__(
103104 self ._callbacks = Callbacks (client , config .trusted_origins )
104105 self ._task_group = task_group
105106 self ._subscriptions : dict [str , _Subscription ] = {}
106- self ._verified : set [tuple [str , str ]] = set ()
107+ self ._verified : set [tuple [str | None , str ]] = set ()
108+ self ._setup = KeyedLocks ()
107109
108110 async def subscribe (
109111 self , event_type : EventType [Any , Any ], arguments : BaseModel , principal : str , params : SubscribeEventsParams
@@ -117,8 +119,26 @@ async def subscribe(
117119 key = json .dumps ([principal , url , event_type .name , params .arguments or {}], sort_keys = True )
118120 subscription_id = f"sub_{ hashlib .sha256 (key .encode ()).hexdigest ()[:16 ]} "
119121 ttl = self ._grant (params )
120- if (existing := self ._subscriptions .get (key )) is not None :
121- return self ._refresh (existing , secret , ttl )
122+ async with self ._setup (key ):
123+ if (existing := self ._subscriptions .get (key )) is not None :
124+ return self ._refresh (existing , secret , ttl )
125+ try :
126+ return await self ._create (event_type , arguments , principal , params , key , subscription_id , ttl )
127+ except BaseException :
128+ self ._forget_unused_verification (principal , url )
129+ raise
130+
131+ async def _create (
132+ self ,
133+ event_type : EventType [Any , Any ],
134+ arguments : BaseModel ,
135+ principal : str ,
136+ params : SubscribeEventsParams ,
137+ key : str ,
138+ subscription_id : str ,
139+ ttl : timedelta ,
140+ ) -> SubscribeEventsResult :
141+ url , secret = params .delivery .url , params .delivery .secret
122142 if (principal , url ) not in self ._verified :
123143 try :
124144 await self ._callbacks .verify (url , subscription_id , secret )
@@ -138,8 +158,8 @@ async def subscribe(
138158 url = url ,
139159 secret = secret ,
140160 expires_at = datetime .now (UTC ) + ttl ,
141- # Not yet delivered, so only the supplied cursor is safe to persist; after a gap it is stale .
142- cursor = params .cursor if batch .events and not batch . truncated else batch .cursor ,
161+ # The batch is not delivered yet , so only the supplied cursor is safe to persist.
162+ cursor = params .cursor if batch .events else batch .cursor ,
143163 )
144164 subscription .scope .deadline = anyio .current_time () + ttl .total_seconds ()
145165 for hook in event_type .subscribe_hooks :
@@ -160,6 +180,11 @@ def unsubscribe(self, event_type: EventType[Any, Any], principal: str, params: U
160180 raise events_error (NOT_FOUND , kind = "subscription" )
161181 subscription .scope .cancel ()
162182
183+ def _forget_unused_verification (self , principal : str | None , url : str ) -> None :
184+ # The spec scopes verification to the subscriptions it covers, so the cache cannot outgrow them.
185+ if not any (s .subscriber .principal == principal and s .url == url for s in self ._subscriptions .values ()):
186+ self ._verified .discard ((principal , url ))
187+
163188 def _grant (self , params : SubscribeEventsParams ) -> timedelta :
164189 # In-memory subscriptions cannot honour "no expiry", so an omitted or null ttlMs gets the cap.
165190 if params .ttl_ms is None :
@@ -184,6 +209,8 @@ async def _run(self, subscription: _Subscription, batch: EventBatch[Any]) -> Non
184209 try :
185210 with subscription .scope :
186211 while True :
212+ # shortcut: events go out one at a time, so a failing event delays later ones by its
213+ # retries, where the spec retries each independently; needs per-event acks behind the cursor.
187214 for index , occurrence in enumerate (batch .events ):
188215 last = index == len (batch .events ) - 1
189216 body = event_type .wire (occurrence , batch .cursor if last else subscription .cursor )
@@ -195,6 +222,7 @@ async def _run(self, subscription: _Subscription, batch: EventBatch[Any]) -> Non
195222 finally :
196223 if self ._subscriptions .get (subscription .key ) is subscription :
197224 del self ._subscriptions [subscription .key ]
225+ self ._forget_unused_verification (subscriber .principal , subscription .url )
198226 for hook in event_type .unsubscribe_hooks :
199227 await hook (subscriber )
200228
0 commit comments