Skip to content

Commit 23109b8

Browse files
Initial plumbing for sesison management
1 parent d3a1841 commit 23109b8

File tree

3 files changed

+333
-493
lines changed

3 files changed

+333
-493
lines changed

src/mcp/server/checkpoint.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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

Comments
 (0)