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.
- dashboard_core/__init__.py +115 -0
- dashboard_core/_gate_tables.py +30 -0
- dashboard_core/band_structure.py +71 -0
- dashboard_core/circuit_builder_component.py +232 -0
- dashboard_core/circuit_diagram.py +216 -0
- dashboard_core/crypto_protocols.py +77 -0
- dashboard_core/engine.py +326 -0
- dashboard_core/graphical_builder.py +114 -0
- dashboard_core/hamiltonians.py +593 -0
- dashboard_core/mass_decomposition_tool.py +47 -0
- dashboard_core/mitigation.py +343 -0
- dashboard_core/native_hf_diagnostics.py +62 -0
- dashboard_core/noise_tools.py +125 -0
- dashboard_core/qasm_library.py +233 -0
- dashboard_core/qmmm.py +16 -0
- dashboard_core/rag_tool.py +45 -0
- dashboard_core/state_visuals.py +288 -0
- dashboard_core/system_limits.py +60 -0
- dashboard_core/vector_healing.py +102 -0
- dashboard_core/visuals.py +158 -0
- dashboard_core/vqe.py +533 -0
- dashboard_core/wormhole.py +580 -0
- dense_evolution/__init__.py +114 -0
- dense_evolution/autodiff.py +10 -0
- dense_evolution/backends/__init__.py +5 -0
- dense_evolution/backends/chunk/__init__.py +37 -0
- dense_evolution/backends/chunk/_engine_imports.py +57 -0
- dense_evolution/backends/chunk/circuit_chunker.py +55 -0
- dense_evolution/backends/chunk/core.py +432 -0
- dense_evolution/backends/chunk/disk_overflow.py +232 -0
- dense_evolution/backends/chunk/geometry.py +95 -0
- dense_evolution/backends/chunk/guard.py +190 -0
- dense_evolution/backends/chunk/kernels.py +531 -0
- dense_evolution/backends/mps.py +1569 -0
- dense_evolution/backends/statevector.py +616 -0
- dense_evolution/chunk.py +25 -0
- dense_evolution/circuits/__init__.py +20 -0
- dense_evolution/circuits/compiler.py +488 -0
- dense_evolution/circuits/diagram.py +94 -0
- dense_evolution/circuits/gates.py +91 -0
- dense_evolution/circuits/parser.py +632 -0
- dense_evolution/circuits/qft.py +66 -0
- dense_evolution/circuits/random_circuit.py +85 -0
- dense_evolution/circuits/registry.py +74 -0
- dense_evolution/circuits/topology.py +79 -0
- dense_evolution/circuits/trotter.py +265 -0
- dense_evolution/circuits/uccsd.py +275 -0
- dense_evolution/cli.py +199 -0
- dense_evolution/compiler.py +9 -0
- dense_evolution/config.py +49 -0
- dense_evolution/drawing.py +10 -0
- dense_evolution/entropy.py +9 -0
- dense_evolution/fermions.py +9 -0
- dense_evolution/gates.py +9 -0
- dense_evolution/harrison_tb.py +16 -0
- dense_evolution/healing.py +18 -0
- dense_evolution/interop/__init__.py +18 -0
- dense_evolution/interop/qiskit_pennylane.py +406 -0
- dense_evolution/measurement.py +10 -0
- dense_evolution/mitigation/__init__.py +54 -0
- dense_evolution/mitigation/healing.py +215 -0
- dense_evolution/mitigation/kl_divergence.py +93 -0
- dense_evolution/mitigation/magic_entropy.py +163 -0
- dense_evolution/mitigation/magic_entropy_shadows.py +262 -0
- dense_evolution/mitigation/renyi.py +168 -0
- dense_evolution/mitigation/stabilizer_renyi_entropy.py +103 -0
- dense_evolution/mitigation/zne.py +990 -0
- dense_evolution/mps.py +9 -0
- dense_evolution/native_hf/__init__.py +26 -0
- dense_evolution/native_hf/_libcint/LICENSE-libcint +10 -0
- dense_evolution/native_hf/_libcint/libdecint.dll +0 -0
- dense_evolution/native_hf/assembly.py +304 -0
- dense_evolution/native_hf/basis.py +117 -0
- dense_evolution/native_hf/boys.py +35 -0
- dense_evolution/native_hf/bridge.py +112 -0
- dense_evolution/native_hf/cartesian.py +64 -0
- dense_evolution/native_hf/coulomb.py +196 -0
- dense_evolution/native_hf/differentiable.py +53 -0
- dense_evolution/native_hf/gaussians.py +79 -0
- dense_evolution/native_hf/kinetic.py +52 -0
- dense_evolution/native_hf/libcint_bridge.py +167 -0
- dense_evolution/native_hf/overlap.py +91 -0
- dense_evolution/native_hf/scf.py +404 -0
- dense_evolution/noise/__init__.py +79 -0
- dense_evolution/noise/coherent_attack.py +264 -0
- dense_evolution/noise/cosmic_ray.py +61 -0
- dense_evolution/noise/density_matrix_channels.py +78 -0
- dense_evolution/noise/differentiable.py +66 -0
- dense_evolution/noise/kraus/__init__.py +6 -0
- dense_evolution/noise/kraus/amplitude_damping.py +47 -0
- dense_evolution/noise/kraus/bitflip.py +22 -0
- dense_evolution/noise/kraus/combined.py +16 -0
- dense_evolution/noise/kraus/depolarizing.py +47 -0
- dense_evolution/noise/kraus/ideal.py +10 -0
- dense_evolution/noise/kraus/phaseflip.py +21 -0
- dense_evolution/noise/kraus_channels.py +285 -0
- dense_evolution/noise/oscillating.py +32 -0
- dense_evolution/noise/pink.py +80 -0
- dense_evolution/observables.py +11 -0
- dense_evolution/parser.py +9 -0
- dense_evolution/physics/__init__.py +27 -0
- dense_evolution/physics/entropy.py +161 -0
- dense_evolution/physics/fermions.py +322 -0
- dense_evolution/physics/observables.py +523 -0
- dense_evolution/physics/qec.py +1113 -0
- dense_evolution/physics/spectral.py +143 -0
- dense_evolution/physics/states.py +43 -0
- dense_evolution/protocols/__init__.py +27 -0
- dense_evolution/protocols/bb84.py +133 -0
- dense_evolution/protocols/di_qkd_ghz.py +199 -0
- dense_evolution/protocols/dicka_protocol2.py +124 -0
- dense_evolution/qec.py +20 -0
- dense_evolution/qft.py +9 -0
- dense_evolution/qmmm/__init__.py +13 -0
- dense_evolution/qmmm/ase_bridge.py +97 -0
- dense_evolution/qmmm/forces.py +388 -0
- dense_evolution/qmmm/propagation.py +80 -0
- dense_evolution/qmmm/region.py +137 -0
- dense_evolution/random_circuit.py +15 -0
- dense_evolution/registry.py +9 -0
- dense_evolution/simulator.py +10 -0
- dense_evolution/solvers/__init__.py +19 -0
- dense_evolution/solvers/autodiff.py +169 -0
- dense_evolution/solvers/harrison_tb.py +189 -0
- dense_evolution/solvers/vhd_tb.py +187 -0
- dense_evolution/states.py +9 -0
- dense_evolution/topology.py +9 -0
- dense_evolution/trotter.py +9 -0
- dense_evolution/utils/__init__.py +13 -0
- dense_evolution/utils/drawing.py +101 -0
- dense_evolution/utils/mass_decomposition.py +246 -0
- dense_evolution/utils/measurement.py +94 -0
- dense_evolution/vhd_tb.py +16 -0
- dense_evolution-8.3.0.dist-info/METADATA +366 -0
- dense_evolution-8.3.0.dist-info/RECORD +165 -0
- dense_evolution-8.3.0.dist-info/WHEEL +5 -0
- dense_evolution-8.3.0.dist-info/entry_points.txt +2 -0
- dense_evolution-8.3.0.dist-info/licenses/license.md +58 -0
- dense_evolution-8.3.0.dist-info/top_level.txt +5 -0
- ia_utils/__init__.py +0 -0
- ia_utils/adversarial_vector_attack.py +196 -0
- ia_utils/rag.py +288 -0
- ia_utils/vector_healing.py +399 -0
- local_site/__init__.py +0 -0
- local_site/app/__init__.py +0 -0
- local_site/app/server.py +1009 -0
- mcp_server/__init__.py +0 -0
- mcp_server/client.py +324 -0
- mcp_server/config.py +32 -0
- mcp_server/models.py +347 -0
- mcp_server/molecules.py +71 -0
- mcp_server/server.py +119 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/chemistry_tools.py +225 -0
- mcp_server/tools/circuit_tools.py +83 -0
- mcp_server/tools/crypto_tools.py +66 -0
- mcp_server/tools/mitigation_tools.py +81 -0
- mcp_server/tools/noise_tools.py +60 -0
- mcp_server/tools/retrieval_tools.py +44 -0
- mcp_server/tools/system_tools.py +149 -0
- mcp_server/tools/wormhole_tools.py +142 -0
- mcp_server/utils/__init__.py +0 -0
- mcp_server/utils/cache.py +55 -0
- mcp_server/utils/images.py +67 -0
- 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
|