forked from Baffelan/sdmx-mcp-gateway
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp_context.py
More file actions
223 lines (173 loc) · 6.7 KB
/
app_context.py
File metadata and controls
223 lines (173 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""
Application Context and Lifespan Management for SDMX MCP Gateway.
This module implements the lifespan pattern recommended by MCP SDK v2,
providing proper resource initialization and cleanup.
Updated to support multi-user deployments via SessionManager.
Usage:
mcp = MCPServer("SDMX Gateway", lifespan=app_lifespan)
@mcp.tool()
async def my_tool(ctx: Context) -> str:
# Get session-specific client
app_ctx = get_app_context_from_ctx(ctx)
if app_ctx:
session = app_ctx.get_session(ctx)
client = session.client
# Use client...
Note: Logging is intentionally minimal in this module to avoid
interfering with STDIO transport JSON-RPC communication.
"""
from __future__ import annotations
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from session_manager import (
DEFAULT_SESSION_ID,
SessionManager,
SessionState,
get_session_id_from_context,
)
if TYPE_CHECKING:
from mcp.server.fastmcp import Context, FastMCP
@dataclass
class AppContext:
"""
Application context holding shared resources.
This context is created during server lifespan initialization
and is accessible in all tool/resource handlers via:
ctx.request_context.lifespan_context
Multi-User Support:
The session_manager tracks per-session endpoint configuration.
Use get_session(ctx) to get the current session's state.
Attributes:
session_manager: Manager for per-session endpoint tracking
global_config: Server-wide configuration settings
cache: Shared cache for expensive operations (use sparingly)
"""
session_manager: SessionManager
global_config: dict[str, Any] = field(default_factory=dict)
cache: dict[str, Any] = field(default_factory=dict)
def get_session(self, ctx: Context[Any, Any, Any] | None = None) -> SessionState:
"""
Get the session state for the current request context.
Args:
ctx: MCP Context (if None, returns default session)
Returns:
SessionState with session-specific client and config
"""
session_id = get_session_id_from_context(ctx)
return self.session_manager.get_session(session_id)
def get_client(self, ctx: Context[Any, Any, Any] | None = None):
"""
Get the SDMX client for the current session.
Args:
ctx: MCP Context (if None, returns default session's client)
Returns:
SDMXProgressiveClient for the current session
"""
return self.get_session(ctx).client
def get_endpoint_info(self, ctx: Context[Any, Any, Any] | None = None) -> dict[str, Any]:
"""
Get current endpoint information for the session.
Args:
ctx: MCP Context
Returns:
Dictionary with endpoint details
"""
session = self.get_session(ctx)
return {
"key": session.endpoint_key,
"name": session.endpoint_name,
"base_url": session.base_url,
"agency_id": session.agency_id,
"description": session.description,
}
async def switch_endpoint(
self, endpoint_key: str, ctx: Context[Any, Any, Any] | None = None
) -> dict[str, Any]:
"""
Switch endpoint for the current session.
Args:
endpoint_key: Target endpoint key (e.g., "ECB", "UNICEF")
ctx: MCP Context
Returns:
Dictionary with switch result information
"""
session_id = get_session_id_from_context(ctx)
return await self.session_manager.switch_endpoint(endpoint_key, session_id)
def clear_cache(self, ctx: Context[Any, Any, Any] | None = None) -> None:
"""
Clear cache for the current session.
Args:
ctx: MCP Context (if None, clears default session's cache)
"""
session = self.get_session(ctx)
session.clear_cache()
@asynccontextmanager
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
"""
Manage application lifecycle with proper resource initialization and cleanup.
This is the recommended pattern for MCP SDK v2. Resources are initialized
when the server starts and cleaned up when it stops.
Multi-User Support:
The SessionManager is initialized here and handles per-session
endpoint tracking. Sessions are created on-demand when tools
access them.
Args:
server: The MCP server instance (FastMCP or MCPServer)
Yields:
AppContext: The application context with initialized resources
Example:
from mcp.server.fastmcp import FastMCP
from app_context import app_lifespan, AppContext
mcp = FastMCP("My Server", lifespan=app_lifespan)
@mcp.tool()
async def my_tool(ctx: Context) -> str:
# Get session-specific client
app_ctx = ctx.request_context.lifespan_context
client = app_ctx.get_client(ctx)
return await client.some_method()
"""
# Suppress unused variable warning - server is required by the lifespan protocol
_ = server
# Get default endpoint from environment or use SPC
default_endpoint = os.getenv("SDMX_ENDPOINT", "SPC")
# Initialize the session manager
session_manager = SessionManager(default_endpoint_key=default_endpoint)
# Create the application context
context = AppContext(
session_manager=session_manager,
global_config={
"default_endpoint": default_endpoint,
"server_name": "SDMX Data Gateway",
},
)
try:
yield context
finally:
# Cleanup all sessions on shutdown - no logging to avoid STDIO interference
await session_manager.close_all()
async def switch_endpoint_context(context: AppContext, endpoint_key: str) -> dict[str, Any]:
"""
Switch the SDMX endpoint in the application context (default session).
This is a convenience function for backward compatibility.
For multi-user support, use context.switch_endpoint(endpoint_key, ctx) instead.
Args:
context: The current application context
endpoint_key: Key of the endpoint to switch to
Returns:
Dictionary with switch result information
Raises:
ValueError: If the endpoint_key is not recognized
"""
return await context.switch_endpoint(endpoint_key, ctx=None)
# Backward compatibility: expose SessionManager types
__all__ = [
"AppContext",
"app_lifespan",
"switch_endpoint_context",
"DEFAULT_SESSION_ID",
"SessionState",
"get_session_id_from_context",
]