codrspot-processor-mcp 0.1.0__py3-none-any.whl

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 (100) hide show
  1. codepreproc_client/__init__.py +14 -0
  2. codepreproc_client/__main__.py +9 -0
  3. codepreproc_client/layer1_business/__init__.py +0 -0
  4. codepreproc_client/layer1_business/allowed_scope_cache.py +62 -0
  5. codepreproc_client/layer1_business/api_client/__init__.py +0 -0
  6. codepreproc_client/layer1_business/api_client/lease_client.py +34 -0
  7. codepreproc_client/layer1_business/api_client/net_guard.py +98 -0
  8. codepreproc_client/layer1_business/api_client/promote_client.py +59 -0
  9. codepreproc_client/layer1_business/api_client/reasoning_client.py +163 -0
  10. codepreproc_client/layer1_business/api_client/registry_client.py +119 -0
  11. codepreproc_client/layer1_business/api_client/superkg_client.py +327 -0
  12. codepreproc_client/layer1_business/cli/__init__.py +0 -0
  13. codepreproc_client/layer1_business/cli/init.py +578 -0
  14. codepreproc_client/layer1_business/cli/main.py +78 -0
  15. codepreproc_client/layer1_business/config.py +104 -0
  16. codepreproc_client/layer1_business/git_utils.py +67 -0
  17. codepreproc_client/layer1_business/license/__init__.py +0 -0
  18. codepreproc_client/layer1_business/license/fingerprint.py +61 -0
  19. codepreproc_client/layer1_business/license/jwt_client.py +60 -0
  20. codepreproc_client/layer1_business/license/project_lock.py +44 -0
  21. codepreproc_client/layer1_business/local_registry.py +40 -0
  22. codepreproc_client/layer1_business/manifest/__init__.py +0 -0
  23. codepreproc_client/layer1_business/manifest/credentials.py +35 -0
  24. codepreproc_client/layer1_business/mcp/__init__.py +0 -0
  25. codepreproc_client/layer1_business/mcp/action_monitor.py +246 -0
  26. codepreproc_client/layer1_business/mcp/lifecycle.py +649 -0
  27. codepreproc_client/layer1_business/mcp/server.py +656 -0
  28. codepreproc_client/layer1_business/mcp/tools.py +1665 -0
  29. codepreproc_client/layer1_business/project_registry.py +99 -0
  30. codepreproc_client/layer2_tooling/__init__.py +0 -0
  31. codepreproc_client/layer2_tooling/condensation/__init__.py +0 -0
  32. codepreproc_client/layer2_tooling/condensation/ast_graph.py +426 -0
  33. codepreproc_client/layer2_tooling/condensation/bm25_store.py +132 -0
  34. codepreproc_client/layer2_tooling/condensation/chunking/__init__.py +0 -0
  35. codepreproc_client/layer2_tooling/condensation/chunking/ast_chunker.py +313 -0
  36. codepreproc_client/layer2_tooling/condensation/chunking/languages.py +238 -0
  37. codepreproc_client/layer2_tooling/condensation/embed_worker.py +128 -0
  38. codepreproc_client/layer2_tooling/condensation/embeddings.py +452 -0
  39. codepreproc_client/layer2_tooling/condensation/extract/__init__.py +0 -0
  40. codepreproc_client/layer2_tooling/condensation/extract/compactor.py +103 -0
  41. codepreproc_client/layer2_tooling/condensation/extract/providers/__init__.py +0 -0
  42. codepreproc_client/layer2_tooling/condensation/extract/providers/base.py +38 -0
  43. codepreproc_client/layer2_tooling/condensation/indexer.py +945 -0
  44. codepreproc_client/layer2_tooling/condensation/ppl/__init__.py +0 -0
  45. codepreproc_client/layer2_tooling/condensation/ppl/emitter.py +162 -0
  46. codepreproc_client/layer2_tooling/condensation/ppl/parser.py +352 -0
  47. codepreproc_client/layer2_tooling/condensation/qdrant_store.py +387 -0
  48. codepreproc_client/layer2_tooling/condensation/scanning/__init__.py +0 -0
  49. codepreproc_client/layer2_tooling/condensation/scanning/manifest_scanner.py +72 -0
  50. codepreproc_client/layer2_tooling/condensation/scanning/md_scanner.py +97 -0
  51. codepreproc_client/layer2_tooling/condensation/scanning/repo_mapper.py +495 -0
  52. codepreproc_client/layer2_tooling/condensation/session_vector_store.py +69 -0
  53. codepreproc_client/layer2_tooling/coupling/__init__.py +0 -0
  54. codepreproc_client/layer2_tooling/coupling/domain.py +263 -0
  55. codepreproc_client/layer2_tooling/coupling/graph.py +255 -0
  56. codepreproc_client/layer2_tooling/coupling/layer2_gate.py +189 -0
  57. codepreproc_client/layer2_tooling/coupling/projector.py +139 -0
  58. codepreproc_client/layer2_tooling/coupling/resolver.py +150 -0
  59. codepreproc_client/layer2_tooling/materialization/__init__.py +0 -0
  60. codepreproc_client/layer2_tooling/materialization/pipeline/__init__.py +0 -0
  61. codepreproc_client/layer2_tooling/materialization/pipeline/execute_semantic.py +1249 -0
  62. codepreproc_client/layer2_tooling/materialization/pipeline/filesystem_reorg.py +575 -0
  63. codepreproc_client/layer2_tooling/materialization/pipeline/patch_generator.py +127 -0
  64. codepreproc_client/layer2_tooling/materialization/pipeline/patch_validator.py +101 -0
  65. codepreproc_client/layer2_tooling/materialization/semantic/__init__.py +19 -0
  66. codepreproc_client/layer2_tooling/materialization/semantic/anchors.py +61 -0
  67. codepreproc_client/layer2_tooling/materialization/semantic/applicability.py +75 -0
  68. codepreproc_client/layer2_tooling/materialization/semantic/edit_planner.py +264 -0
  69. codepreproc_client/layer2_tooling/materialization/semantic/materializer.py +121 -0
  70. codepreproc_client/layer2_tooling/materialization/semantic/merger.py +73 -0
  71. codepreproc_client/layer2_tooling/materialization/semantic/ops/__init__.py +0 -0
  72. codepreproc_client/layer2_tooling/materialization/semantic/ops/add_import.py +36 -0
  73. codepreproc_client/layer2_tooling/materialization/semantic/ops/append_argument.py +37 -0
  74. codepreproc_client/layer2_tooling/materialization/semantic/ops/insert_literal.py +24 -0
  75. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_import.py +33 -0
  76. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_statement_unique.py +14 -0
  77. codepreproc_client/layer2_tooling/materialization/semantic/ops/rename_symbol_local.py +16 -0
  78. codepreproc_client/layer2_tooling/materialization/semantic/region_resolver.py +229 -0
  79. codepreproc_client/layer2_tooling/materialization/semantic/synthesizer.py +186 -0
  80. codepreproc_client/layer2_tooling/materialization/semantic/target_locator.py +94 -0
  81. codepreproc_client/layer2_tooling/materialization/semantic/validator.py +322 -0
  82. codepreproc_client/layer2_tooling/materialization/snippet/__init__.py +0 -0
  83. codepreproc_client/layer2_tooling/materialization/snippet/assembler.py +503 -0
  84. codepreproc_client/layer2_tooling/materialization/snippet/loader.py +187 -0
  85. codepreproc_client/layer2_tooling/retrieval/__init__.py +0 -0
  86. codepreproc_client/layer2_tooling/retrieval/graph_walk.py +47 -0
  87. codepreproc_client/layer2_tooling/retrieval/hybrid.py +95 -0
  88. codepreproc_client/layer2_tooling/retrieval/rerank_worker.py +86 -0
  89. codepreproc_client/layer2_tooling/retrieval/reranker.py +213 -0
  90. codepreproc_client/layer2_tooling/watcher/__init__.py +0 -0
  91. codepreproc_client/layer2_tooling/watcher/fs_watcher.py +3 -0
  92. codrspot_processor_mcp-0.1.0.dist-info/METADATA +396 -0
  93. codrspot_processor_mcp-0.1.0.dist-info/RECORD +100 -0
  94. codrspot_processor_mcp-0.1.0.dist-info/WHEEL +5 -0
  95. codrspot_processor_mcp-0.1.0.dist-info/entry_points.txt +3 -0
  96. codrspot_processor_mcp-0.1.0.dist-info/licenses/LICENSE +23 -0
  97. codrspot_processor_mcp-0.1.0.dist-info/top_level.txt +2 -0
  98. contracts/__init__.py +0 -0
  99. contracts/dtos.py +1239 -0
  100. contracts/ir.py +102 -0
@@ -0,0 +1,649 @@
1
+ """MCP connection lifecycle logging and instrumented server/session wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import signal
8
+ import threading
9
+ from contextlib import AsyncExitStack
10
+ import atexit
11
+ import time
12
+ from itertools import count
13
+ from typing import Any
14
+
15
+ import anyio
16
+ import mcp.types as types
17
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
18
+ from mcp.server.experimental.request_context import Experimental
19
+ from mcp.server.lowlevel.server import Server
20
+ from mcp.server.lowlevel.server import logger as sdk_server_logger
21
+ from mcp.server.lowlevel.server import request_ctx
22
+ from mcp.server.models import InitializationOptions
23
+ from mcp.server.session import InitializationState, ServerSession
24
+ from mcp.shared.context import RequestContext
25
+ from mcp.shared.exceptions import McpError
26
+ from mcp.shared.message import ServerMessageMetadata, SessionMessage
27
+ from mcp.shared.session import RequestResponder
28
+ from contracts.dtos import MCPConnectionVerbosity, WorldModelLease
29
+
30
+ _world_model_lease_logger = logging.getLogger("codepreproc_client.mcp.world_model_lease")
31
+
32
+
33
+ class WorldModelLeaseGuard:
34
+ """Owns the temporary world-model copy for a session and guarantees deletion.
35
+
36
+ Architecture doc S5/S12: the leased world model is kept only in
37
+ session memory/scratch and MUST be discarded on the client's last MCP
38
+ call - not persisted to disk, not reused offline, deleted even on
39
+ abrupt termination. This registers teardown three ways so no exit
40
+ path skips it: explicit `discard()`/context-manager exit, `atexit`,
41
+ and the process signal handlers (SIGINT/SIGTERM; SIGBREAK on Windows).
42
+ """
43
+
44
+ _instances_lock = threading.Lock()
45
+ _instances: "set[WorldModelLeaseGuard]" = set()
46
+ _signal_handlers_installed = False
47
+
48
+ def __init__(self) -> None:
49
+ self._lease: WorldModelLease | None = None
50
+ with WorldModelLeaseGuard._instances_lock:
51
+ WorldModelLeaseGuard._instances.add(self)
52
+ WorldModelLeaseGuard._ensure_global_teardown_hooks()
53
+
54
+ @property
55
+ def lease(self) -> WorldModelLease | None:
56
+ return self._lease
57
+
58
+ def set_lease(self, lease: WorldModelLease) -> None:
59
+ self._lease = lease
60
+
61
+ def discard(self) -> None:
62
+ """Drop the in-memory temporary world-model copy. Idempotent."""
63
+
64
+ if self._lease is not None:
65
+ _world_model_lease_logger.debug("world_model_lease_discarded lease_id=%s", self._lease.lease_id)
66
+ self._lease = None
67
+
68
+ def __enter__(self) -> "WorldModelLeaseGuard":
69
+ return self
70
+
71
+ def __exit__(self, *exc_info: Any) -> None:
72
+ self.discard()
73
+
74
+ @classmethod
75
+ def _ensure_global_teardown_hooks(cls) -> None:
76
+ if cls._signal_handlers_installed:
77
+ return
78
+ cls._signal_handlers_installed = True
79
+ atexit.register(cls._discard_all)
80
+ for sig_name in ("SIGINT", "SIGTERM", "SIGBREAK"):
81
+ sig = getattr(signal, sig_name, None)
82
+ if sig is None:
83
+ continue
84
+ try:
85
+ previous_handler = signal.getsignal(sig)
86
+ except (ValueError, OSError):
87
+ # Not in the main thread, or unsupported on this platform - atexit still covers this.
88
+ continue
89
+
90
+ def _handler(signum: int, frame: Any, _previous: Any = previous_handler) -> None:
91
+ cls._discard_all()
92
+ if callable(_previous):
93
+ _previous(signum, frame)
94
+
95
+ try:
96
+ signal.signal(sig, _handler)
97
+ except (ValueError, OSError):
98
+ continue
99
+
100
+ @classmethod
101
+ def _discard_all(cls) -> None:
102
+ with cls._instances_lock:
103
+ for instance in cls._instances:
104
+ instance.discard()
105
+
106
+
107
+ class MCPConnectionLifecycleLogger:
108
+ """Log MCP transport/session lifecycle events to file and client notifications."""
109
+
110
+ def __init__(self, logger: logging.Logger, verbosity: MCPConnectionVerbosity = "basic") -> None:
111
+ self.logger = logger
112
+ self.verbosity = verbosity
113
+ self._session_counter = count(1)
114
+
115
+ @property
116
+ def enabled(self) -> bool:
117
+ return self.verbosity != "off"
118
+
119
+ @property
120
+ def verbose(self) -> bool:
121
+ return self.verbosity == "verbose"
122
+
123
+ def next_session_id(self) -> str:
124
+ return f"session-{next(self._session_counter)}"
125
+
126
+ async def emit(
127
+ self,
128
+ event: str,
129
+ *,
130
+ session: InstrumentedServerSession | Any | None = None,
131
+ request_id: str | int | None = None,
132
+ reason: str | None = None,
133
+ exception: BaseException | None = None,
134
+ error: str | None = None,
135
+ fields: dict[str, Any] | None = None,
136
+ verbose_fields: dict[str, Any] | None = None,
137
+ mirror_to_client: bool = True,
138
+ ) -> None:
139
+ if not self.enabled:
140
+ return
141
+
142
+ payload: dict[str, Any] = {"event": event}
143
+ if session is not None:
144
+ payload.update(_session_log_fields(session))
145
+ if request_id is not None:
146
+ payload["request_id"] = request_id
147
+ if reason:
148
+ payload["reason"] = reason
149
+ if exception is not None:
150
+ payload["exception_type"] = type(exception).__name__
151
+ if error is None and exception is not None:
152
+ error = str(exception)
153
+ if error:
154
+ payload["error"] = _clean_text(error, limit=200)
155
+ payload.update(fields or {})
156
+ if self.verbose and verbose_fields:
157
+ payload.update(verbose_fields)
158
+
159
+ message = _format_log_line(payload)
160
+ self.logger.info(message)
161
+
162
+ if not mirror_to_client or session is None or not getattr(session, "can_mirror_lifecycle_logs", False):
163
+ return
164
+ try:
165
+ await session.send_log_message(
166
+ level="info",
167
+ data=message,
168
+ logger="codepreproc.mcp.lifecycle",
169
+ related_request_id=request_id,
170
+ )
171
+ except Exception:
172
+ self.logger.debug("failed_to_send_lifecycle_log event=%s", event, exc_info=True)
173
+
174
+
175
+ class InstrumentedServerSession(ServerSession):
176
+ """ServerSession with explicit lifecycle logging hooks."""
177
+
178
+ def __init__(
179
+ self,
180
+ read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
181
+ write_stream: MemoryObjectSendStream[SessionMessage],
182
+ init_options: InitializationOptions,
183
+ *,
184
+ lifecycle_logger: MCPConnectionLifecycleLogger,
185
+ session_id: str,
186
+ stateless: bool = False,
187
+ ) -> None:
188
+ super().__init__(read_stream, write_stream, init_options, stateless=stateless)
189
+ self.lifecycle_logger = lifecycle_logger
190
+ self.session_id = session_id
191
+ self.started_at = time.monotonic()
192
+ self.initialize_started_at: float | None = None
193
+ self.initialize_completed_at: float | None = None
194
+ self.transport_close_reason: str | None = None
195
+ self.transport_close_error: str | None = None
196
+ self.transport_close_exception_type: str | None = None
197
+
198
+ @property
199
+ def uptime_ms(self) -> int:
200
+ return int((time.monotonic() - self.started_at) * 1000)
201
+
202
+ @property
203
+ def client_name(self) -> str | None:
204
+ return self._client_params.clientInfo.name if self._client_params else None
205
+
206
+ @property
207
+ def client_version(self) -> str | None:
208
+ return self._client_params.clientInfo.version if self._client_params else None
209
+
210
+ @property
211
+ def protocol_version(self) -> str | int | None:
212
+ return self._client_params.protocolVersion if self._client_params else None
213
+
214
+ @property
215
+ def can_mirror_lifecycle_logs(self) -> bool:
216
+ return self._initialization_state == InitializationState.Initialized
217
+
218
+ async def _received_request(self, responder: RequestResponder[types.ClientRequest, types.ServerResult]):
219
+ match responder.request.root:
220
+ case types.InitializeRequest(params=params):
221
+ self.initialize_started_at = time.monotonic()
222
+ await self.lifecycle_logger.emit(
223
+ "initialize_received",
224
+ session=self,
225
+ request_id=responder.request_id,
226
+ fields={
227
+ "client_name": params.clientInfo.name,
228
+ "client_version": params.clientInfo.version,
229
+ "protocol_version": params.protocolVersion,
230
+ },
231
+ verbose_fields={
232
+ "client_capabilities": _capabilities_summary(params.capabilities),
233
+ "roots_supported": bool(params.capabilities and params.capabilities.roots),
234
+ },
235
+ mirror_to_client=False,
236
+ )
237
+ try:
238
+ await super()._received_request(responder)
239
+ except Exception as exc:
240
+ await self.lifecycle_logger.emit(
241
+ "initialize_failed",
242
+ session=self,
243
+ request_id=responder.request_id,
244
+ reason="initialize_request_failed",
245
+ exception=exc,
246
+ fields={
247
+ "client_name": params.clientInfo.name,
248
+ "client_version": params.clientInfo.version,
249
+ "protocol_version": params.protocolVersion,
250
+ },
251
+ verbose_fields={
252
+ "client_capabilities": _capabilities_summary(params.capabilities),
253
+ },
254
+ mirror_to_client=False,
255
+ )
256
+ raise
257
+ self.initialize_completed_at = time.monotonic()
258
+ await self.lifecycle_logger.emit(
259
+ "initialize_completed",
260
+ session=self,
261
+ request_id=responder.request_id,
262
+ fields={
263
+ "client_name": params.clientInfo.name,
264
+ "client_version": params.clientInfo.version,
265
+ "protocol_version": params.protocolVersion,
266
+ "initialization_ms": int((self.initialize_completed_at - self.initialize_started_at) * 1000),
267
+ },
268
+ verbose_fields={
269
+ "client_capabilities": _capabilities_summary(params.capabilities),
270
+ "roots_supported": bool(params.capabilities and params.capabilities.roots),
271
+ },
272
+ )
273
+ case types.PingRequest():
274
+ await super()._received_request(responder)
275
+ case _:
276
+ if self._initialization_state != InitializationState.Initialized:
277
+ message = "Received request before initialization was complete"
278
+ await self.lifecycle_logger.emit(
279
+ "request_before_initialize",
280
+ session=self,
281
+ request_id=responder.request_id,
282
+ reason="request_before_initialize",
283
+ error=message,
284
+ fields={"request_type": type(responder.request.root).__name__},
285
+ mirror_to_client=False,
286
+ )
287
+ raise RuntimeError(message)
288
+ await super()._received_request(responder)
289
+
290
+ async def _received_notification(self, notification: types.ClientNotification) -> None:
291
+ await super()._received_notification(notification)
292
+ if isinstance(notification.root, types.InitializedNotification):
293
+ await self.lifecycle_logger.emit(
294
+ "initialized_notification_received",
295
+ session=self,
296
+ verbose_fields={
297
+ "client_capabilities": _capabilities_summary(self._client_params.capabilities)
298
+ if self._client_params
299
+ else "unknown",
300
+ },
301
+ )
302
+
303
+ async def _receive_loop(self) -> None:
304
+ transport_reason = "client_eof"
305
+ transport_exception: BaseException | None = None
306
+ async with (self._read_stream, self._write_stream):
307
+ try:
308
+ async for message in self._read_stream:
309
+ if isinstance(message, Exception): # pragma: no cover
310
+ await self._handle_incoming(message)
311
+ elif isinstance(message.message.root, types.JSONRPCRequest):
312
+ try:
313
+ validated_request = self._receive_request_type.model_validate(
314
+ message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
315
+ )
316
+ responder = RequestResponder(
317
+ request_id=message.message.root.id,
318
+ request_meta=validated_request.root.params.meta if validated_request.root.params else None,
319
+ request=validated_request,
320
+ session=self,
321
+ on_complete=lambda r: self._in_flight.pop(r.request_id, None),
322
+ message_metadata=message.metadata,
323
+ )
324
+ self._in_flight[responder.request_id] = responder
325
+ await self._received_request(responder)
326
+
327
+ if not responder._completed: # type: ignore[reportPrivateUsage]
328
+ await self._handle_incoming(responder)
329
+ except Exception as exc:
330
+ sdk_server_logger.warning("Failed to validate request: %s", exc)
331
+ sdk_server_logger.debug("Message that failed validation: %s", message.message.root)
332
+ error_response = types.JSONRPCError(
333
+ jsonrpc="2.0",
334
+ id=message.message.root.id,
335
+ error=types.ErrorData(
336
+ code=types.INVALID_PARAMS,
337
+ message="Invalid request parameters",
338
+ data="",
339
+ ),
340
+ )
341
+ session_message = SessionMessage(message=types.JSONRPCMessage(error_response))
342
+ await self._write_stream.send(session_message)
343
+
344
+ elif isinstance(message.message.root, types.JSONRPCNotification):
345
+ try:
346
+ notification = self._receive_notification_type.model_validate(
347
+ message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
348
+ )
349
+ if isinstance(notification.root, types.CancelledNotification):
350
+ cancelled_id = notification.root.params.requestId
351
+ if cancelled_id in self._in_flight:
352
+ await self._in_flight[cancelled_id].cancel()
353
+ else:
354
+ if isinstance(notification.root, types.ProgressNotification): # pragma: no cover
355
+ progress_token = notification.root.params.progressToken
356
+ if progress_token in self._progress_callbacks:
357
+ callback = self._progress_callbacks[progress_token]
358
+ try:
359
+ await callback(
360
+ notification.root.params.progress,
361
+ notification.root.params.total,
362
+ notification.root.params.message,
363
+ )
364
+ except Exception as exc: # pragma: no cover
365
+ sdk_server_logger.error("Progress callback raised an exception: %s", exc)
366
+ await self._received_notification(notification)
367
+ await self._handle_incoming(notification)
368
+ except Exception as exc: # pragma: no cover
369
+ sdk_server_logger.warning(
370
+ "Failed to validate notification: %s. Message was: %s",
371
+ exc,
372
+ message.message.root,
373
+ )
374
+ else:
375
+ await self._handle_response(message)
376
+
377
+ except anyio.ClosedResourceError as exc:
378
+ transport_reason = "read_stream_closed"
379
+ transport_exception = exc
380
+ except Exception as exc: # pragma: no cover
381
+ transport_reason = "receive_loop_exception"
382
+ transport_exception = exc
383
+ await self.lifecycle_logger.emit(
384
+ "unhandled_receive_exception",
385
+ session=self,
386
+ reason=transport_reason,
387
+ exception=exc,
388
+ mirror_to_client=False,
389
+ )
390
+ finally:
391
+ self.transport_close_reason = transport_reason
392
+ if transport_exception is not None:
393
+ self.transport_close_exception_type = type(transport_exception).__name__
394
+ self.transport_close_error = _clean_text(str(transport_exception), limit=200)
395
+ pending_response_count = len(self._response_streams)
396
+ in_flight_request_count = len(self._in_flight)
397
+
398
+ for response_id, stream in list(self._response_streams.items()):
399
+ error = types.ErrorData(code=types.CONNECTION_CLOSED, message="Connection closed")
400
+ try:
401
+ await stream.send(types.JSONRPCError(jsonrpc="2.0", id=response_id, error=error))
402
+ await stream.aclose()
403
+ except Exception: # pragma: no cover
404
+ pass
405
+ self._response_streams.clear()
406
+
407
+ await self.lifecycle_logger.emit(
408
+ "transport_closed",
409
+ session=self,
410
+ reason=transport_reason,
411
+ exception=transport_exception,
412
+ verbose_fields={
413
+ "pending_response_count": pending_response_count,
414
+ "in_flight_request_count": in_flight_request_count,
415
+ "transport_reason_detail": transport_reason,
416
+ },
417
+ )
418
+
419
+
420
+ class InstrumentedServer(Server):
421
+ """Server wrapper that instantiates InstrumentedServerSession and logs lifecycle events."""
422
+
423
+ def __init__(self, *args: Any, lifecycle_logger: MCPConnectionLifecycleLogger, **kwargs: Any) -> None:
424
+ super().__init__(*args, **kwargs)
425
+ self.lifecycle_logger = lifecycle_logger
426
+
427
+ async def run(
428
+ self,
429
+ read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
430
+ write_stream: MemoryObjectSendStream[SessionMessage],
431
+ initialization_options: InitializationOptions,
432
+ raise_exceptions: bool = False,
433
+ stateless: bool = False,
434
+ ) -> None:
435
+ async with AsyncExitStack() as stack:
436
+ lifespan_context = await stack.enter_async_context(self.lifespan(self))
437
+ session = await stack.enter_async_context(
438
+ InstrumentedServerSession(
439
+ read_stream,
440
+ write_stream,
441
+ initialization_options,
442
+ stateless=stateless,
443
+ lifecycle_logger=self.lifecycle_logger,
444
+ session_id=self.lifecycle_logger.next_session_id(),
445
+ )
446
+ )
447
+ await self.lifecycle_logger.emit(
448
+ "transport_open",
449
+ session=session,
450
+ verbose_fields={"stateless": stateless},
451
+ mirror_to_client=False,
452
+ )
453
+
454
+ task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
455
+ if task_support is not None:
456
+ task_support.configure_session(session)
457
+ await stack.enter_async_context(task_support.run())
458
+
459
+ async with anyio.create_task_group() as tg:
460
+ try:
461
+ async for message in session.incoming_messages:
462
+ sdk_server_logger.debug("Received message: %s", message)
463
+ tg.start_soon(
464
+ self._handle_message,
465
+ message,
466
+ session,
467
+ lifespan_context,
468
+ raise_exceptions,
469
+ )
470
+ finally:
471
+ tg.cancel_scope.cancel()
472
+ await self.lifecycle_logger.emit(
473
+ "session_shutdown",
474
+ session=session,
475
+ reason=session.transport_close_reason or "server_run_complete",
476
+ verbose_fields={
477
+ "in_flight_request_count": len(session._in_flight),
478
+ "pending_response_count": len(session._response_streams),
479
+ },
480
+ )
481
+
482
+ async def _handle_request(
483
+ self,
484
+ message: RequestResponder[types.ClientRequest, types.ServerResult],
485
+ req: types.ClientRequestType,
486
+ session: ServerSession,
487
+ lifespan_context: Any,
488
+ raise_exceptions: bool,
489
+ ):
490
+ sdk_server_logger.info("Processing request of type %s", type(req).__name__)
491
+
492
+ if handler := self.request_handlers.get(type(req)):
493
+ sdk_server_logger.debug("Dispatching request of type %s", type(req).__name__)
494
+
495
+ token = None
496
+ try:
497
+ request_data = None
498
+ close_sse_stream_cb = None
499
+ close_standalone_sse_stream_cb = None
500
+ if message.message_metadata is not None and isinstance(message.message_metadata, ServerMessageMetadata):
501
+ request_data = message.message_metadata.request_context
502
+ close_sse_stream_cb = message.message_metadata.close_sse_stream
503
+ close_standalone_sse_stream_cb = message.message_metadata.close_standalone_sse_stream
504
+
505
+ client_capabilities = session.client_params.capabilities if session.client_params else None
506
+ task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
507
+ task_metadata = None
508
+ if hasattr(req, "params") and req.params is not None:
509
+ task_metadata = getattr(req.params, "task", None)
510
+ token = request_ctx.set(
511
+ RequestContext(
512
+ message.request_id,
513
+ message.request_meta,
514
+ session,
515
+ lifespan_context,
516
+ Experimental(
517
+ task_metadata=task_metadata,
518
+ _client_capabilities=client_capabilities,
519
+ _session=session,
520
+ _task_support=task_support,
521
+ ),
522
+ request=request_data,
523
+ close_sse_stream=close_sse_stream_cb,
524
+ close_standalone_sse_stream=close_standalone_sse_stream_cb,
525
+ )
526
+ )
527
+ response = await handler(req)
528
+ except McpError as err: # pragma: no cover
529
+ response = err.error
530
+ except anyio.get_cancelled_exc_class():
531
+ if message.cancelled:
532
+ sdk_server_logger.info("Request %s cancelled - duplicate response suppressed", message.request_id)
533
+ return
534
+ raise
535
+ except Exception as err: # pragma: no cover
536
+ if raise_exceptions:
537
+ raise err
538
+ response = types.ErrorData(code=0, message=str(err), data=None)
539
+ finally:
540
+ if token is not None:
541
+ request_ctx.reset(token)
542
+ else: # pragma: no cover
543
+ response = types.ErrorData(
544
+ code=types.METHOD_NOT_FOUND,
545
+ message="Method not found",
546
+ )
547
+
548
+ try:
549
+ await message.respond(response)
550
+ except (anyio.BrokenResourceError, anyio.ClosedResourceError) as exc:
551
+ await self.lifecycle_logger.emit(
552
+ "response_dropped_transport_closed",
553
+ session=session,
554
+ request_id=message.request_id,
555
+ reason="transport_closed_before_response",
556
+ exception=exc,
557
+ )
558
+ sdk_server_logger.debug("Response for %s dropped - transport closed", message.request_id)
559
+ return
560
+
561
+ sdk_server_logger.debug("Response sent")
562
+
563
+
564
+ async def resolve_session_roots(
565
+ *,
566
+ session: ServerSession,
567
+ lifecycle_logger: MCPConnectionLifecycleLogger,
568
+ request_id: str | int | None,
569
+ ) -> list[str]:
570
+ """Resolve client roots and log failures without breaking tool calls."""
571
+
572
+ if not (session.client_params and session.client_params.capabilities and session.client_params.capabilities.roots):
573
+ return []
574
+ try:
575
+ roots_result = await session.list_roots()
576
+ except Exception as exc:
577
+ await lifecycle_logger.emit(
578
+ "roots_resolution_failed",
579
+ session=session,
580
+ request_id=request_id,
581
+ reason="list_roots_failed",
582
+ exception=exc,
583
+ verbose_fields={"roots_supported": True},
584
+ )
585
+ return []
586
+ return [str(root.uri) for root in roots_result.roots if str(root.uri).startswith("file://")]
587
+
588
+
589
+ def _session_log_fields(session: InstrumentedServerSession | Any) -> dict[str, Any]:
590
+ payload: dict[str, Any] = {}
591
+ session_id = getattr(session, "session_id", None)
592
+ if session_id is not None:
593
+ payload["session_id"] = session_id
594
+ client_name = getattr(session, "client_name", None)
595
+ if client_name:
596
+ payload["client_name"] = client_name
597
+ client_version = getattr(session, "client_version", None)
598
+ if client_version:
599
+ payload["client_version"] = client_version
600
+ protocol_version = getattr(session, "protocol_version", None)
601
+ if protocol_version is not None:
602
+ payload["protocol_version"] = protocol_version
603
+ uptime_ms = getattr(session, "uptime_ms", None)
604
+ if uptime_ms is not None:
605
+ payload["uptime_ms"] = uptime_ms
606
+ return payload
607
+
608
+
609
+ def _capabilities_summary(capabilities: types.ClientCapabilities | None) -> str:
610
+ if capabilities is None:
611
+ return "none"
612
+ names: list[str] = []
613
+ if capabilities.roots is not None:
614
+ names.append("roots")
615
+ if capabilities.sampling is not None:
616
+ names.append("sampling")
617
+ if capabilities.elicitation is not None:
618
+ names.append("elicitation")
619
+ if capabilities.experimental:
620
+ names.append("experimental")
621
+ if capabilities.tasks is not None:
622
+ names.append("tasks")
623
+ return ",".join(names) or "none"
624
+
625
+
626
+ def _clean_text(value: str, *, limit: int) -> str:
627
+ return " ".join(value.split())[:limit]
628
+
629
+
630
+ def _format_log_line(payload: dict[str, Any]) -> str:
631
+ parts = ["mcp_lifecycle"]
632
+ for key, value in payload.items():
633
+ if value is None:
634
+ continue
635
+ parts.append(f"{key}={_format_value(value)}")
636
+ return " ".join(parts)
637
+
638
+
639
+ def _format_value(value: Any) -> str:
640
+ if isinstance(value, bool):
641
+ return "true" if value else "false"
642
+ if isinstance(value, (int, float)):
643
+ return str(value)
644
+ if isinstance(value, (dict, list)):
645
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
646
+ text = str(value)
647
+ if not text or any(char.isspace() for char in text) or any(char in {'"', "="} for char in text):
648
+ return json.dumps(text, ensure_ascii=False)
649
+ return text