dense-evolution 8.3.0__py3-none-win_amd64.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 (165) hide show
  1. dashboard_core/__init__.py +115 -0
  2. dashboard_core/_gate_tables.py +30 -0
  3. dashboard_core/band_structure.py +71 -0
  4. dashboard_core/circuit_builder_component.py +232 -0
  5. dashboard_core/circuit_diagram.py +216 -0
  6. dashboard_core/crypto_protocols.py +77 -0
  7. dashboard_core/engine.py +326 -0
  8. dashboard_core/graphical_builder.py +114 -0
  9. dashboard_core/hamiltonians.py +593 -0
  10. dashboard_core/mass_decomposition_tool.py +47 -0
  11. dashboard_core/mitigation.py +343 -0
  12. dashboard_core/native_hf_diagnostics.py +62 -0
  13. dashboard_core/noise_tools.py +125 -0
  14. dashboard_core/qasm_library.py +233 -0
  15. dashboard_core/qmmm.py +16 -0
  16. dashboard_core/rag_tool.py +45 -0
  17. dashboard_core/state_visuals.py +288 -0
  18. dashboard_core/system_limits.py +60 -0
  19. dashboard_core/vector_healing.py +102 -0
  20. dashboard_core/visuals.py +158 -0
  21. dashboard_core/vqe.py +533 -0
  22. dashboard_core/wormhole.py +580 -0
  23. dense_evolution/__init__.py +114 -0
  24. dense_evolution/autodiff.py +10 -0
  25. dense_evolution/backends/__init__.py +5 -0
  26. dense_evolution/backends/chunk/__init__.py +37 -0
  27. dense_evolution/backends/chunk/_engine_imports.py +57 -0
  28. dense_evolution/backends/chunk/circuit_chunker.py +55 -0
  29. dense_evolution/backends/chunk/core.py +432 -0
  30. dense_evolution/backends/chunk/disk_overflow.py +232 -0
  31. dense_evolution/backends/chunk/geometry.py +95 -0
  32. dense_evolution/backends/chunk/guard.py +190 -0
  33. dense_evolution/backends/chunk/kernels.py +531 -0
  34. dense_evolution/backends/mps.py +1569 -0
  35. dense_evolution/backends/statevector.py +616 -0
  36. dense_evolution/chunk.py +25 -0
  37. dense_evolution/circuits/__init__.py +20 -0
  38. dense_evolution/circuits/compiler.py +488 -0
  39. dense_evolution/circuits/diagram.py +94 -0
  40. dense_evolution/circuits/gates.py +91 -0
  41. dense_evolution/circuits/parser.py +632 -0
  42. dense_evolution/circuits/qft.py +66 -0
  43. dense_evolution/circuits/random_circuit.py +85 -0
  44. dense_evolution/circuits/registry.py +74 -0
  45. dense_evolution/circuits/topology.py +79 -0
  46. dense_evolution/circuits/trotter.py +265 -0
  47. dense_evolution/circuits/uccsd.py +275 -0
  48. dense_evolution/cli.py +199 -0
  49. dense_evolution/compiler.py +9 -0
  50. dense_evolution/config.py +49 -0
  51. dense_evolution/drawing.py +10 -0
  52. dense_evolution/entropy.py +9 -0
  53. dense_evolution/fermions.py +9 -0
  54. dense_evolution/gates.py +9 -0
  55. dense_evolution/harrison_tb.py +16 -0
  56. dense_evolution/healing.py +18 -0
  57. dense_evolution/interop/__init__.py +18 -0
  58. dense_evolution/interop/qiskit_pennylane.py +406 -0
  59. dense_evolution/measurement.py +10 -0
  60. dense_evolution/mitigation/__init__.py +54 -0
  61. dense_evolution/mitigation/healing.py +215 -0
  62. dense_evolution/mitigation/kl_divergence.py +93 -0
  63. dense_evolution/mitigation/magic_entropy.py +163 -0
  64. dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
  65. dense_evolution/mitigation/renyi.py +168 -0
  66. dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
  67. dense_evolution/mitigation/zne.py +990 -0
  68. dense_evolution/mps.py +9 -0
  69. dense_evolution/native_hf/__init__.py +26 -0
  70. dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
  71. dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
  72. dense_evolution/native_hf/assembly.py +304 -0
  73. dense_evolution/native_hf/basis.py +117 -0
  74. dense_evolution/native_hf/boys.py +35 -0
  75. dense_evolution/native_hf/bridge.py +112 -0
  76. dense_evolution/native_hf/cartesian.py +64 -0
  77. dense_evolution/native_hf/coulomb.py +196 -0
  78. dense_evolution/native_hf/differentiable.py +53 -0
  79. dense_evolution/native_hf/gaussians.py +79 -0
  80. dense_evolution/native_hf/kinetic.py +52 -0
  81. dense_evolution/native_hf/libcint_bridge.py +167 -0
  82. dense_evolution/native_hf/overlap.py +91 -0
  83. dense_evolution/native_hf/scf.py +404 -0
  84. dense_evolution/noise/__init__.py +79 -0
  85. dense_evolution/noise/coherent_attack.py +264 -0
  86. dense_evolution/noise/cosmic_ray.py +61 -0
  87. dense_evolution/noise/density_matrix_channels.py +78 -0
  88. dense_evolution/noise/differentiable.py +66 -0
  89. dense_evolution/noise/kraus/__init__.py +6 -0
  90. dense_evolution/noise/kraus/amplitude_damping.py +47 -0
  91. dense_evolution/noise/kraus/bitflip.py +22 -0
  92. dense_evolution/noise/kraus/combined.py +16 -0
  93. dense_evolution/noise/kraus/depolarizing.py +47 -0
  94. dense_evolution/noise/kraus/ideal.py +10 -0
  95. dense_evolution/noise/kraus/phaseflip.py +21 -0
  96. dense_evolution/noise/kraus_channels.py +285 -0
  97. dense_evolution/noise/oscillating.py +32 -0
  98. dense_evolution/noise/pink.py +80 -0
  99. dense_evolution/observables.py +11 -0
  100. dense_evolution/parser.py +9 -0
  101. dense_evolution/physics/__init__.py +27 -0
  102. dense_evolution/physics/entropy.py +161 -0
  103. dense_evolution/physics/fermions.py +322 -0
  104. dense_evolution/physics/observables.py +523 -0
  105. dense_evolution/physics/qec.py +1113 -0
  106. dense_evolution/physics/spectral.py +143 -0
  107. dense_evolution/physics/states.py +43 -0
  108. dense_evolution/protocols/__init__.py +27 -0
  109. dense_evolution/protocols/bb84.py +133 -0
  110. dense_evolution/protocols/di_qkd_ghz.py +199 -0
  111. dense_evolution/protocols/dicka_protocol2.py +124 -0
  112. dense_evolution/qec.py +20 -0
  113. dense_evolution/qft.py +9 -0
  114. dense_evolution/qmmm/__init__.py +13 -0
  115. dense_evolution/qmmm/ase_bridge.py +97 -0
  116. dense_evolution/qmmm/forces.py +388 -0
  117. dense_evolution/qmmm/propagation.py +80 -0
  118. dense_evolution/qmmm/region.py +137 -0
  119. dense_evolution/random_circuit.py +15 -0
  120. dense_evolution/registry.py +9 -0
  121. dense_evolution/simulator.py +10 -0
  122. dense_evolution/solvers/__init__.py +19 -0
  123. dense_evolution/solvers/autodiff.py +169 -0
  124. dense_evolution/solvers/harrison_tb.py +189 -0
  125. dense_evolution/solvers/vhd_tb.py +187 -0
  126. dense_evolution/states.py +9 -0
  127. dense_evolution/topology.py +9 -0
  128. dense_evolution/trotter.py +9 -0
  129. dense_evolution/utils/__init__.py +13 -0
  130. dense_evolution/utils/drawing.py +101 -0
  131. dense_evolution/utils/mass_decomposition.py +246 -0
  132. dense_evolution/utils/measurement.py +94 -0
  133. dense_evolution/vhd_tb.py +16 -0
  134. dense_evolution-8.3.0.dist-info/METADATA +366 -0
  135. dense_evolution-8.3.0.dist-info/RECORD +165 -0
  136. dense_evolution-8.3.0.dist-info/WHEEL +5 -0
  137. dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
  138. dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
  139. dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
  140. ia_utils/__init__.py +0 -0
  141. ia_utils/adversarial_vector_attack.py +196 -0
  142. ia_utils/rag.py +288 -0
  143. ia_utils/vector_healing.py +399 -0
  144. local_site/__init__.py +0 -0
  145. local_site/app/__init__.py +0 -0
  146. local_site/app/server.py +1009 -0
  147. mcp_server/__init__.py +0 -0
  148. mcp_server/client.py +324 -0
  149. mcp_server/config.py +32 -0
  150. mcp_server/models.py +347 -0
  151. mcp_server/molecules.py +71 -0
  152. mcp_server/server.py +119 -0
  153. mcp_server/tools/__init__.py +0 -0
  154. mcp_server/tools/chemistry_tools.py +225 -0
  155. mcp_server/tools/circuit_tools.py +83 -0
  156. mcp_server/tools/crypto_tools.py +66 -0
  157. mcp_server/tools/mitigation_tools.py +81 -0
  158. mcp_server/tools/noise_tools.py +60 -0
  159. mcp_server/tools/retrieval_tools.py +44 -0
  160. mcp_server/tools/system_tools.py +149 -0
  161. mcp_server/tools/wormhole_tools.py +142 -0
  162. mcp_server/utils/__init__.py +0 -0
  163. mcp_server/utils/cache.py +55 -0
  164. mcp_server/utils/images.py +67 -0
  165. mcp_server/utils/truncation.py +38 -0
mcp_server/__init__.py ADDED
File without changes
mcp_server/client.py ADDED
@@ -0,0 +1,324 @@
1
+ """HTTP client layer for the dense_evolution_mcp adapter: the shared
2
+ httpx.AsyncClient, the low-level _request helper every tool calls, and the
3
+ @catch_errors decorator that replaces the try/except-return-_handle_error(e)
4
+ boilerplate every tool used to repeat individually.
5
+
6
+ KERNEL_URL and _TEST_TRANSPORT live here (not config.py) specifically
7
+ because tests/integration/test_mcp_server.py mutates them via
8
+ `monkeypatch.setattr(mcp_client, "KERNEL_URL"/"_TEST_TRANSPORT", ...)`
9
+ (autouse fixture + a couple of per-test overrides, never a raw manual
10
+ assignment) to exercise the unreachable-kernel and in-process-ASGI-
11
+ transport paths -- _get_client/_request must see those mutations as
12
+ their OWN module globals, which only works if the value's single source
13
+ of truth lives in the same module that reads it. `_TEST_TRANSPORT`
14
+ staying a real module attribute in production code (rather than
15
+ disappearing entirely behind dependency injection) is a deliberate,
16
+ narrow seam for exactly that monkeypatch, not leftover tech debt --
17
+ Phase 4's original goal (prog.txt Sezione 3.3) of routing every override
18
+ through pytest's own fixture mechanism, not a bare global, is already
19
+ what happens here.
20
+ """
21
+ import functools
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ import threading
26
+ from typing import Optional
27
+
28
+ import httpx
29
+
30
+ from .config import DEFAULT_TIMEOUT
31
+
32
+ KERNEL_URL = os.environ.get("DENSE_EVOLUTION_KERNEL_URL", "http://127.0.0.1:8800").rstrip("/")
33
+
34
+ # Only auto-start a kernel we'd actually be able to reach: skip entirely if
35
+ # the caller pointed KERNEL_URL somewhere else (a remote kernel, a
36
+ # different port) via the env var -- spawning a *local* subprocess would
37
+ # never help in that case, and could confuse a deliberately-remote setup.
38
+ _KERNEL_URL_IS_DEFAULT = "DENSE_EVOLUTION_KERNEL_URL" not in os.environ
39
+
40
+ # Set by tests to route through httpx.ASGITransport straight into the real,
41
+ # in-process local_site.app.server.app (see tests/integration/test_mcp_server.py) --
42
+ # None here means "use a real TCP connection to KERNEL_URL", unchanged from
43
+ # before this existed. This is the one seam in the whole adapter: it lets
44
+ # tests exercise every tool function against the real FastAPI kernel (real
45
+ # DenseSVSimulator, real PennyLane Hamiltonians -- no mocked physics) without
46
+ # a live subprocess bound to a real port in CI.
47
+ _TEST_TRANSPORT = None
48
+
49
+ # Lazily-created, reused across calls -- _request used to open a fresh
50
+ # httpx.AsyncClient (TCP handshake to the kernel) per tool call, real
51
+ # overhead for an MCP session that calls many tools in a row. Keyed on
52
+ # (_TEST_TRANSPORT identity, KERNEL_URL): production code never changes
53
+ # KERNEL_URL after import (it's a plain module-level constant read once
54
+ # from os.environ), but the test suite mutates both KERNEL_URL and
55
+ # _TEST_TRANSPORT directly to exercise the unreachable-kernel path, and its
56
+ # autouse fixture swaps _TEST_TRANSPORT to a fresh ASGITransport before
57
+ # every test and back to None after -- a client cached against a stale
58
+ # transport or base_url must not be reused across either kind of swap.
59
+ _shared_client: Optional[httpx.AsyncClient] = None
60
+ _shared_client_key = None # (transport identity, base_url) the cached client was built with
61
+ # Issue #258 point 6 asked whether the originally-suspected data race here
62
+ # is actually reachable: verified by reading the installed mcp package
63
+ # (mcp/shared/jsonrpc_dispatcher.py) -- tool calls are dispatched via
64
+ # `anyio.create_task_group()` / `start_soon`, i.e. concurrent tasks on ONE
65
+ # event loop, never separate OS threads. _get_client() has no `await`
66
+ # inside it, so it can't actually be preempted mid-check-then-create by
67
+ # another task today -- the race prog.txt described isn't currently live.
68
+ # This threading.Lock (not asyncio.Lock -- that was the first attempt,
69
+ # reverted for breaking 3 tests that monkeypatch _get_client as a plain
70
+ # sync callable) is defensive, not a fix for an active bug: near-zero cost
71
+ # uncontended, and it keeps this safe if a future edit ever adds an
72
+ # `await` here or a caller ever invokes it off the event-loop thread.
73
+ _client_lock = threading.Lock()
74
+
75
+
76
+ def _get_client() -> httpx.AsyncClient:
77
+ global _shared_client, _shared_client_key
78
+ current_key = (id(_TEST_TRANSPORT), KERNEL_URL)
79
+ with _client_lock:
80
+ if _shared_client is None or _shared_client_key != current_key:
81
+ # Client-level timeout is just the fallback ceiling -- every real
82
+ # call site passes its own timeout= to _request, sized to that
83
+ # endpoint's actual expected cost (a health check and a 10-minute
84
+ # VQE run have nothing in common).
85
+ _shared_client = httpx.AsyncClient(base_url=KERNEL_URL, transport=_TEST_TRANSPORT, timeout=DEFAULT_TIMEOUT)
86
+ _shared_client_key = current_key
87
+ return _shared_client
88
+
89
+
90
+ async def ensure_kernel_running(startup_wait: float = 20.0, poll_interval: float = 0.5) -> None:
91
+ """Best-effort auto-start for the Composer kernel, called once from
92
+ server.py's main() before mcp.run() starts serving tool calls -- not
93
+ from _request()'s own error handling, so the existing
94
+ kernel-unreachable tests (which force a real ConnectError against a
95
+ kernel that's deliberately never started) keep exercising that path
96
+ unmodified and un-slowed.
97
+
98
+ Skips entirely if KERNEL_URL was overridden via the env var (a remote
99
+ or custom-port kernel -- spawning a local subprocess would never help
100
+ there), or if the kernel is already reachable. Otherwise spawns
101
+ `python -m local_site.app.server` detached, with its stdout/stderr
102
+ redirected away from this process's own stdio -- this process IS an
103
+ MCP stdio server, so anything the child wrote to inherited stdout
104
+ would corrupt the JSON-RPC stream -- and polls health for up to
105
+ `startup_wait` seconds (observed real cold-start: ~10s).
106
+
107
+ Never raises: if the composer extra isn't installed, the subprocess
108
+ fails to spawn, or it doesn't come up in time, this just returns and
109
+ the first real tool call gets the normal, actionable
110
+ KernelUnreachableError -- auto-start is a convenience on top of that
111
+ existing fallback, not a replacement for it."""
112
+ if not _KERNEL_URL_IS_DEFAULT:
113
+ return
114
+ try:
115
+ client = _get_client()
116
+ resp = await client.get("/api/health", timeout=2.0)
117
+ if resp.status_code == 200:
118
+ _warn_if_version_mismatch(resp.json())
119
+ return # already up, nothing to do
120
+ except httpx.HTTPError:
121
+ pass # genuinely unreachable -- fall through to the spawn attempt
122
+
123
+ try:
124
+ creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
125
+ subprocess.Popen(
126
+ [sys.executable, "-m", "local_site.app.server"],
127
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
128
+ start_new_session=(sys.platform != "win32"),
129
+ creationflags=creationflags,
130
+ )
131
+ except Exception:
132
+ return # e.g. composer extra not installed -- let the normal error path handle it
133
+
134
+ import asyncio
135
+ elapsed = 0.0
136
+ while elapsed < startup_wait:
137
+ await asyncio.sleep(poll_interval)
138
+ elapsed += poll_interval
139
+ try:
140
+ resp = await client.get("/api/health", timeout=2.0)
141
+ if resp.status_code == 200:
142
+ _warn_if_version_mismatch(resp.json())
143
+ return
144
+ except httpx.HTTPError:
145
+ continue
146
+
147
+
148
+ def version_mismatch_message(health: dict) -> Optional[str]:
149
+ """None if the kernel's dense_evolution_version matches the one
150
+ importable in this adapter's own environment (or the kernel didn't
151
+ report one), else an actionable message describing the drift -- shared
152
+ by _warn_if_version_mismatch (stderr, at startup) and
153
+ dense_evolution_health (a `version_mismatch` JSON field, on every call)
154
+ so the two surfaces can never say something different for a bug this
155
+ trivial to describe consistently."""
156
+ kernel_version = health.get("dense_evolution_version")
157
+ if kernel_version is None:
158
+ return None
159
+ import dense_evolution
160
+ adapter_version = dense_evolution.__version__
161
+ if kernel_version == adapter_version:
162
+ return None
163
+ return (
164
+ f"Kernel is running dense_evolution {kernel_version}, but this MCP adapter's "
165
+ f"own environment has {adapter_version} installed. The kernel process was "
166
+ "likely started before the last `pip install --upgrade dense-evolution` -- "
167
+ "restart it (stop the running `dense-evolution serve` process, then start it "
168
+ "again) to pick up the current version."
169
+ )
170
+
171
+
172
+ def _warn_if_version_mismatch(health: dict) -> None:
173
+ """Print version_mismatch_message to stderr (never stdout -- this
174
+ process is an MCP stdio server) if there is one. Deliberately does NOT
175
+ kill/restart the kernel automatically: it may be a long-running process
176
+ someone is actively using through the public Composer web page, and
177
+ force-killing another user's live session to fix a version drift this
178
+ adapter merely noticed would be a worse failure mode than the drift
179
+ itself. Surfacing it loudly (here, and again on every
180
+ dense_evolution_health call -- see system_tools.py) is the safe middle
181
+ ground: visible, never destructive."""
182
+ message = version_mismatch_message(health)
183
+ if message is not None:
184
+ print(f"[dense_evolution_mcp] WARNING: {message}", file=sys.stderr)
185
+
186
+
187
+ async def close_shared_client() -> None:
188
+ """Closes the shared client's connection pool, if one was ever created,
189
+ and resets the cache so a later call rebuilds a fresh one. Call this
190
+ once at process shutdown (server.py's main(), in a try/finally around
191
+ mcp.run()) -- without it httpx logs unclosed-connection warnings when
192
+ the interpreter exits, and a dev reload can leave the client bound to
193
+ an event loop that's already gone."""
194
+ global _shared_client, _shared_client_key
195
+ with _client_lock:
196
+ client, _shared_client, _shared_client_key = _shared_client, None, None
197
+ if client is not None:
198
+ await client.aclose()
199
+
200
+
201
+ class KernelError(RuntimeError):
202
+ """Base for every error this adapter raises about reaching or talking to
203
+ the kernel. `kind` is a short, stable token -- not the message text --
204
+ that catch_errors below embeds in its "Error: <kind>: ..." output, so
205
+ an MCP client can branch on error type (prog.txt Sezione 3, punto 10:
206
+ "ConnectError -> suggerisci dense-evolution serve; TimeoutException ->
207
+ suggerisci di ridurre n_qubits; ...") without parsing prose."""
208
+ kind = "kernel_error"
209
+
210
+
211
+ class KernelUnreachableError(KernelError):
212
+ kind = "unreachable"
213
+
214
+
215
+ class KernelTimeoutError(KernelError):
216
+ kind = "timeout"
217
+
218
+
219
+ class KernelProtocolError(KernelError):
220
+ kind = "protocol_error"
221
+
222
+
223
+ class KernelConnectionClosedError(KernelError):
224
+ kind = "connection_closed"
225
+
226
+
227
+ class KernelHTTPError(KernelError):
228
+ kind = "http_error"
229
+
230
+
231
+ async def _request(method: str, path: str, timeout: float = DEFAULT_TIMEOUT, **kwargs) -> dict:
232
+ """Reusable request helper for every tool. `timeout` (seconds) should be
233
+ sized to the specific endpoint's real expected cost -- see each tool's
234
+ own call site."""
235
+ client = _get_client()
236
+ try:
237
+ resp = await client.request(method, path, timeout=timeout, **kwargs)
238
+ except httpx.ConnectError:
239
+ raise KernelUnreachableError(
240
+ f"Dense Evolution kernel not reachable at {KERNEL_URL}. Start it with "
241
+ "`dense-evolution serve` (or `python -m local_site.app.server` from the "
242
+ "repo root), then retry. Use dense_evolution_health to check connectivity."
243
+ )
244
+ except httpx.TimeoutException:
245
+ raise KernelTimeoutError(
246
+ f"Request to the Dense Evolution kernel timed out after {timeout:g}s -- the "
247
+ "simulation may be too large or slow for this request (e.g. high qubit "
248
+ "count, many VQE iterations, or a long MD trajectory). Try reducing its size."
249
+ )
250
+ except httpx.RemoteProtocolError:
251
+ # kernel process died/crashed mid-response rather than closing the
252
+ # connection cleanly -- the exact symptom documented in the README
253
+ # for the BLAS/eigh thread-safety issue under wormhole_scan's heavy
254
+ # linear algebra, not a generic network blip.
255
+ raise KernelProtocolError(
256
+ "Dense Evolution kernel terminated mid-response, likely the BLAS "
257
+ "thread-safety issue documented in the README for heavy linear "
258
+ "algebra (e.g. wormhole_scan). Retry, or reduce the request's size."
259
+ )
260
+ except httpx.ReadError:
261
+ raise KernelConnectionClosedError(
262
+ f"Connection to the Dense Evolution kernel at {KERNEL_URL} was closed "
263
+ "before the response finished -- the kernel process may have crashed. "
264
+ "Use dense_evolution_health to check it's still up, then retry."
265
+ )
266
+ except httpx.HTTPError as e:
267
+ # Catch-all for the rest of httpx's exception hierarchy (e.g.
268
+ # WriteError, PoolTimeout) that isn't specific enough to warrant its
269
+ # own message/kind above.
270
+ raise KernelError(f"Dense Evolution kernel request failed: {e}")
271
+ if resp.status_code >= 400:
272
+ try:
273
+ detail = resp.json().get("detail", resp.text)
274
+ except Exception:
275
+ detail = resp.text
276
+ raise KernelHTTPError(f"Dense Evolution kernel returned HTTP {resp.status_code}: {detail}")
277
+ return resp.json()
278
+
279
+
280
+ def _handle_error(e: Exception) -> str:
281
+ """Consistent error formatting across all tools: "Error: <kind>:
282
+ <message>". `kind` is a stable token (see the KernelError hierarchy
283
+ above for kernel-communication errors; "invalid_input" for a
284
+ ValueError a tool's own validation raised, e.g. models.py's
285
+ cross-field checks; "internal" for anything else) an MCP client can
286
+ branch on programmatically, without parsing message prose -- the
287
+ "Error: " prefix itself is unchanged so every existing
288
+ `result.startswith("Error:")` check still holds."""
289
+ if isinstance(e, KernelError):
290
+ kind = e.kind
291
+ elif isinstance(e, ValueError):
292
+ kind = "invalid_input"
293
+ else:
294
+ kind = "internal"
295
+ return f"Error: {kind}: {e}"
296
+
297
+
298
+ def catch_errors(func):
299
+ """Wraps an async tool function so any exception raised anywhere in its
300
+ body becomes the same "Error: ..." string every tool used to return
301
+ from its own try/except -- replaces ~20 repeated
302
+ `try: ... except Exception as e: return _handle_error(e)` blocks with
303
+ one decorator.
304
+
305
+ A strict superset of the old behavior for the handful of tools whose
306
+ try block only wrapped part of the function -- e.g.
307
+ dense_evolution_run_circuit's post-request image saving/truncation used
308
+ to sit OUTSIDE its try block, so an exception there would have
309
+ propagated uncaught; now it's caught too.
310
+
311
+ Tools that do their own internal per-item exception handling
312
+ (dense_evolution_energy_scan, dense_evolution_wormhole_scan -- a single
313
+ failed point shouldn't abort the whole batch) are unaffected by this:
314
+ it only adds an outer safety net around exceptions those inner
315
+ try/excepts don't already catch (e.g. a bug in the post-loop
316
+ aggregation code), it does not change their partial-failure behavior.
317
+ """
318
+ @functools.wraps(func)
319
+ async def wrapper(*args, **kwargs):
320
+ try:
321
+ return await func(*args, **kwargs)
322
+ except Exception as e:
323
+ return _handle_error(e)
324
+ return wrapper
mcp_server/config.py ADDED
@@ -0,0 +1,32 @@
1
+ """Central configuration for the dense_evolution_mcp adapter: the MCP
2
+ tool-annotation presets shared across every tool, plus DEFAULT_TIMEOUT.
3
+
4
+ Two settings that conceptually belong here are deliberately NOT here,
5
+ because the test suite mutates them directly and that mutation has to
6
+ reach the specific function that reads them as its own module global (a
7
+ `from .config import X` copy elsewhere would silently stop seeing the
8
+ mutation): KERNEL_URL/_TEST_TRANSPORT live in client.py (see its
9
+ docstring), and IMAGE_OUTPUT_DIR/IMAGE_MAX_FILES live in utils/images.py
10
+ (see its docstring) since that's where _save_png/_prune_old_images moved
11
+ in Phase 2 of the refactor (prog.txt Sezione 3).
12
+ """
13
+
14
+ READ_ONLY_IDEMPOTENT = {
15
+ "readOnlyHint": True,
16
+ "destructiveHint": False,
17
+ "idempotentHint": True,
18
+ "openWorldHint": False,
19
+ }
20
+ # Circuit/VQE/MD runs don't mutate any stored resource -- nothing server-side
21
+ # persists between calls -- so they're "read-only" in the MCP sense too, but
22
+ # re-running with the same seed can differ slightly run-to-run (floating
23
+ # point reduction order in the linear-algebra backend), so idempotentHint
24
+ # is left False for anything that actually executes a simulation.
25
+ COMPUTE = {
26
+ "readOnlyHint": True,
27
+ "destructiveHint": False,
28
+ "idempotentHint": False,
29
+ "openWorldHint": False,
30
+ }
31
+
32
+ DEFAULT_TIMEOUT = 60.0 # fallback for any call site that doesn't pass timeout= explicitly