1+ from __future__ import annotations
2+
3+ import abc
4+ import time
5+ from dataclasses import dataclass
6+ from typing import Protocol , runtime_checkable
7+
8+ from mcp .server .session import ServerSession
9+ from mcp .types import (
10+ CheckpointCreateParams ,
11+ CheckpointCreateResult ,
12+ CheckpointValidateParams ,
13+ CheckpointValidateResult ,
14+ CheckpointResumeParams ,
15+ CheckpointResumeResult ,
16+ CheckpointDeleteParams ,
17+ CheckpointDeleteResult ,
18+ )
19+
20+
21+ @runtime_checkable
22+ class CheckpointBackend (Protocol ):
23+ """Backend that actually stores and restores state behind handles."""
24+
25+ async def create_checkpoint (
26+ self ,
27+ session : ServerSession ,
28+ params : CheckpointCreateParams ,
29+ ) -> CheckpointCreateResult : ...
30+
31+ async def validate_checkpoint (
32+ self ,
33+ session : ServerSession ,
34+ params : CheckpointValidateParams ,
35+ ) -> CheckpointValidateResult : ...
36+
37+ async def resume_checkpoint (
38+ self ,
39+ session : ServerSession ,
40+ params : CheckpointResumeParams ,
41+ ) -> CheckpointResumeResult : ...
42+
43+ async def delete_checkpoint (
44+ self ,
45+ session : ServerSession ,
46+ params : CheckpointDeleteParams ,
47+ ) -> CheckpointDeleteResult : ...
48+
49+
50+ @dataclass
51+ class InMemoryHandleEntry :
52+ value : object
53+ digest : str
54+ expires_at : float
55+
56+
57+ class InMemoryCheckpointBackend (CheckpointBackend ):
58+ """Simple in-memory backend you can use for tests/POC.
59+
60+ This is intentionally generic; concrete servers (data, browser, etc.)
61+ decide *what* `value` is and how to interpret it.
62+ """
63+
64+ def __init__ (self , ttl_seconds : int = 1800 ) -> None :
65+ self ._ttl = ttl_seconds
66+ self ._handles : dict [str , InMemoryHandleEntry ] = {}
67+
68+ def _now (self ) -> float :
69+ return time .time ()
70+
71+ async def create_checkpoint (
72+ self ,
73+ session : ServerSession ,
74+ params : CheckpointCreateParams ,
75+ ) -> CheckpointCreateResult :
76+ # session.fastmcp or session.server can expose some "current state"
77+ # For now you can override this backend in your server and implement
78+ # your own snapshot logic.
79+ raise NotImplementedError (
80+ "Subclass InMemoryCheckpointBackend and override create_checkpoint "
81+ "to capture concrete state (e.g. data tables, browser session)."
82+ )
83+
84+ async def validate_checkpoint (
85+ self ,
86+ session : ServerSession ,
87+ params : CheckpointValidateParams ,
88+ ) -> CheckpointValidateResult :
89+ entry = self ._handles .get (params .handle )
90+ if not entry :
91+ return CheckpointValidateResult (
92+ valid = False ,
93+ remainingTtlSeconds = 0 ,
94+ digestMatch = False ,
95+ )
96+
97+ now = self ._now ()
98+ if now >= entry .expires_at :
99+ return CheckpointValidateResult (
100+ valid = False ,
101+ remainingTtlSeconds = 0 ,
102+ digestMatch = params .expectedDigest == entry .digest ,
103+ )
104+
105+ remaining = int (entry .expires_at - now )
106+ return CheckpointValidateResult (
107+ valid = True ,
108+ remainingTtlSeconds = remaining ,
109+ digestMatch = (
110+ params .expectedDigest is None
111+ or params .expectedDigest == entry .digest
112+ ),
113+ )
114+
115+ async def resume_checkpoint (
116+ self ,
117+ session : ServerSession ,
118+ params : CheckpointResumeParams ,
119+ ) -> CheckpointResumeResult :
120+ entry = self ._handles .get (params .handle )
121+ if not entry :
122+ # You’ll map this to HANDLE_NOT_FOUND at JSON-RPC level
123+ return CheckpointResumeResult (resumed = False , handle = params .handle )
124+
125+ if self ._now () >= entry .expires_at :
126+ # Map to EXPIRED
127+ return CheckpointResumeResult (resumed = False , handle = params .handle )
128+
129+ # Subclasses should take `entry.value` and rehydrate into session state.
130+ raise NotImplementedError (
131+ "Subclass InMemoryCheckpointBackend.resume_checkpoint to rehydrate "
132+ "concrete session state from stored value."
133+ )
134+
135+ async def delete_checkpoint (
136+ self ,
137+ session : ServerSession ,
138+ params : CheckpointDeleteParams ,
139+ ) -> CheckpointDeleteResult :
140+ deleted = params .handle in self ._handles
141+ self ._handles .pop (params .handle , None )
142+ return CheckpointDeleteResult (deleted = deleted )
0 commit comments