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/models.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Pydantic input schemas for every dense_evolution_mcp tool, extracted
|
|
2
|
+
from server.py so tool logic and schema definitions can change
|
|
3
|
+
independently. Field descriptions stay here (not moved into tool
|
|
4
|
+
docstrings) since MCP clients read them directly off the schema to build
|
|
5
|
+
their own UI/validation -- the docstrings still carry the longer
|
|
6
|
+
explanation for a human or agent reading the tool's full documentation.
|
|
7
|
+
"""
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ListMoleculesInput(BaseModel):
|
|
14
|
+
model_config = ConfigDict(extra="forbid")
|
|
15
|
+
mapping: str = Field(
|
|
16
|
+
default="jordan_wigner",
|
|
17
|
+
description="Fermion-to-qubit mapping: 'jordan_wigner' or 'bravyi_kitaev'. Both represent "
|
|
18
|
+
"the identical physical Hamiltonian (same spectrum) in a different qubit basis.",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BuildCircuitInput(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="forbid")
|
|
24
|
+
n_qubits: int = Field(..., ge=1, description="Number of qubits in the circuit.")
|
|
25
|
+
ops: list = Field(
|
|
26
|
+
..., description="List of graphical-builder gate operations to convert into OpenQASM. "
|
|
27
|
+
"Get valid gate names from dense_evolution_list_gates first."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RunCircuitInput(BaseModel):
|
|
32
|
+
model_config = ConfigDict(extra="forbid")
|
|
33
|
+
qasm: str = Field(..., min_length=1, description="OpenQASM 2.0 circuit source to simulate.")
|
|
34
|
+
shots: int = Field(default=1000, ge=1, le=1_000_000, description="Number of measurement shots for the counts histogram.")
|
|
35
|
+
seed: int = Field(default=42, description="Random seed for sampling/noise reproducibility.")
|
|
36
|
+
noise_model: str = Field(default="ideal", description="Noise model name from dense_evolution_list_noise_models, or 'ideal'.")
|
|
37
|
+
noise_p: float = Field(default=0.0, ge=0.0, le=1.0, description="Noise channel error probability (ignored if noise_model='ideal').")
|
|
38
|
+
backend: str = Field(default="dense", description="'dense' (exact statevector, up to the safe qubit ceiling) or 'mps' "
|
|
39
|
+
"(matrix-product-state, approximate top-k states, for larger circuits).")
|
|
40
|
+
top_k: int = Field(
|
|
41
|
+
default=25, ge=1, le=500,
|
|
42
|
+
description="How many of the largest-magnitude statevector amplitudes / highest-probability "
|
|
43
|
+
"basis states to include in the response (the rest are summarized by 'total_*'/'shown' "
|
|
44
|
+
"counts, not dropped from the kernel's own computation). Raise this if you need to see "
|
|
45
|
+
"further down the tail; lower it to save context on a circuit with many comparably-sized "
|
|
46
|
+
"amplitudes.",
|
|
47
|
+
)
|
|
48
|
+
include_visualizations: bool = Field(
|
|
49
|
+
default=False,
|
|
50
|
+
description="If true, also render and save circuit/histogram/Q-sphere/Bloch PNGs to disk and "
|
|
51
|
+
"return their file paths. Leave false unless you actually need to view an image -- "
|
|
52
|
+
"rendering costs extra time and the paths are not useful without viewing them.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@model_validator(mode="after")
|
|
56
|
+
def _noise_p_requires_a_noise_model(self):
|
|
57
|
+
# prog.txt Sezione 3, punto 8: noise_p > 0 with noise_model='ideal'
|
|
58
|
+
# was silently ignored by the kernel (ideal = no channel applied at
|
|
59
|
+
# all) -- catch it here so the caller sees why their noise had no
|
|
60
|
+
# effect, instead of a passing-but-surprising ideal-circuit result.
|
|
61
|
+
# (The other invariant prog.txt names for this model -- backend='mps'
|
|
62
|
+
# + include_visualizations + n_qubits>24 -- needs the parsed qubit
|
|
63
|
+
# count, which only the kernel has for a raw QASM string; not
|
|
64
|
+
# checkable here without parsing QASM locally too.)
|
|
65
|
+
if self.noise_model == "ideal" and self.noise_p != 0.0:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"noise_p={self.noise_p} has no effect with noise_model='ideal' (ideal means no "
|
|
68
|
+
"noise channel is applied at all) -- set noise_model to a real model name from "
|
|
69
|
+
"dense_evolution_list_noise_models, or leave noise_p at its default 0.0."
|
|
70
|
+
)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MoleculeEnergyInput(BaseModel):
|
|
75
|
+
model_config = ConfigDict(extra="forbid")
|
|
76
|
+
name: str = Field(
|
|
77
|
+
..., description="Catalog molecule -- short id (e.g. 'H2', 'LiH', 'HeH+') or the full catalog "
|
|
78
|
+
"name. See dense_evolution_list_molecules."
|
|
79
|
+
)
|
|
80
|
+
mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MixMoleculesInput(BaseModel):
|
|
84
|
+
model_config = ConfigDict(extra="forbid")
|
|
85
|
+
name_a: str = Field(..., description="First catalog molecule -- short id or full catalog name.")
|
|
86
|
+
name_b: str = Field(..., description="Second catalog molecule -- short id or full catalog name. "
|
|
87
|
+
"Must have the same qubit count as name_a.")
|
|
88
|
+
weight_a: float = Field(default=0.5, description="Weight of the first Hamiltonian in the mix.")
|
|
89
|
+
weight_b: float = Field(default=0.5, description="Weight of the second Hamiltonian in the mix.")
|
|
90
|
+
mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
|
|
91
|
+
|
|
92
|
+
# prog.txt's third invariant for this model (same qubit count between
|
|
93
|
+
# name_a and name_b) is NOT added here: it needs a molecule-catalog
|
|
94
|
+
# lookup, which is an async kernel request (molecules.py's cache) --
|
|
95
|
+
# Pydantic's own validators are synchronous. Today that check happens
|
|
96
|
+
# only server-side, inside the kernel's /api/hamiltonian/mix endpoint
|
|
97
|
+
# (see chemistry_tools.py::dense_evolution_mix_molecules's own
|
|
98
|
+
# docstring) -- a mismatched pair is still rejected, just one round
|
|
99
|
+
# trip later than a local schema check could catch it.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CustomMoleculeInput(BaseModel):
|
|
103
|
+
model_config = ConfigDict(extra="forbid")
|
|
104
|
+
symbols: list = Field(..., min_length=1, description="Atomic symbols, e.g. ['H', 'H', 'O'].")
|
|
105
|
+
geometry: list = Field(..., min_length=1, description="[[x, y, z], ...] coordinates in Angstrom, one row per symbol.")
|
|
106
|
+
charge: int = Field(default=0, description="Total molecular charge.")
|
|
107
|
+
mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class EnergyScanInput(BaseModel):
|
|
111
|
+
model_config = ConfigDict(extra="forbid")
|
|
112
|
+
symbols: list = Field(..., min_length=1, description="Atomic symbols shared by every point in the scan, e.g. ['H', 'H'].")
|
|
113
|
+
geometries: list = Field(
|
|
114
|
+
..., min_length=1, max_length=50,
|
|
115
|
+
description="List of [[x,y,z], ...] geometries (Angstrom) to evaluate, one per scan point -- "
|
|
116
|
+
"e.g. a bond-length or angle sweep. Each geometry must have the same number of rows as `symbols`. "
|
|
117
|
+
"Capped at 50 points per call to keep one request's kernel load bounded.",
|
|
118
|
+
)
|
|
119
|
+
charge: int = Field(default=0, description="Molecular charge, shared by every point.")
|
|
120
|
+
mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
|
|
121
|
+
labels: Optional[list] = Field(
|
|
122
|
+
default=None,
|
|
123
|
+
description="Optional label per point (e.g. bond lengths in Angstrom: [0.4, 0.5, ...]) shown "
|
|
124
|
+
"alongside each result. Defaults to the point's index (0, 1, 2, ...) if omitted. Must be the "
|
|
125
|
+
"same length as `geometries` if given.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RunVqeInput(BaseModel):
|
|
130
|
+
model_config = ConfigDict(extra="forbid")
|
|
131
|
+
name: Optional[str] = Field(default=None, description="Catalog molecule -- short id or full catalog name. "
|
|
132
|
+
"Provide this OR symbols+geometry, not both.")
|
|
133
|
+
symbols: Optional[list] = Field(default=None, description="Atomic symbols for a custom molecule.")
|
|
134
|
+
geometry: Optional[list] = Field(default=None, description="[[x, y, z], ...] in Angstrom for a custom molecule.")
|
|
135
|
+
charge: int = Field(default=0, description="Molecular charge (custom molecule only).")
|
|
136
|
+
ansatz_type: str = Field(
|
|
137
|
+
default="hardware_efficient",
|
|
138
|
+
description="'hardware_efficient' (generic n_layers-deep RY+CNOT template) or 'uccsd' "
|
|
139
|
+
"(chemically-motivated, fewer parameters but much deeper circuits per iteration).",
|
|
140
|
+
)
|
|
141
|
+
n_layers: int = Field(default=8, ge=1, description="Ansatz depth (hardware_efficient only).")
|
|
142
|
+
maxiter: int = Field(default=200, ge=1, le=5000, description="Maximum Adam optimizer iterations.")
|
|
143
|
+
step_size: float = Field(default=0.1, gt=0, description="Adam optimizer learning rate.")
|
|
144
|
+
beta1: float = Field(default=0.9, description="Adam optimizer beta1.")
|
|
145
|
+
beta2: float = Field(default=0.999, description="Adam optimizer beta2.")
|
|
146
|
+
seed: int = Field(default=0, description="Random seed for the initial variational parameters.")
|
|
147
|
+
|
|
148
|
+
@model_validator(mode="after")
|
|
149
|
+
def _name_xor_custom_molecule(self):
|
|
150
|
+
has_name = self.name is not None
|
|
151
|
+
has_custom = self.symbols is not None or self.geometry is not None
|
|
152
|
+
if has_name and has_custom:
|
|
153
|
+
raise ValueError("provide `name` OR `symbols`+`geometry`, not both.")
|
|
154
|
+
if not has_name and not has_custom:
|
|
155
|
+
raise ValueError("provide either `name` (a catalog molecule) or `symbols`+`geometry` (a custom one).")
|
|
156
|
+
if has_custom and (self.symbols is None or self.geometry is None):
|
|
157
|
+
raise ValueError("a custom molecule needs both `symbols` and `geometry`, not just one.")
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class QmmmForcesInput(BaseModel):
|
|
162
|
+
model_config = ConfigDict(extra="forbid")
|
|
163
|
+
name: str = Field(..., description="Catalog molecule -- short id or full catalog name. See dense_evolution_list_molecules.")
|
|
164
|
+
mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class MdTrajectoryInput(BaseModel):
|
|
168
|
+
model_config = ConfigDict(extra="forbid")
|
|
169
|
+
name: str = Field(..., description="Catalog molecule -- short id or full catalog name. See dense_evolution_list_molecules.")
|
|
170
|
+
n_steps: int = Field(default=20, ge=1, le=200, description="Number of MD steps (capped at 200 to bound request cost).")
|
|
171
|
+
dt_fs: float = Field(default=0.5, gt=0, description="Timestep in femtoseconds.")
|
|
172
|
+
mapping: str = Field(default="jordan_wigner", description="'jordan_wigner' or 'bravyi_kitaev'.")
|
|
173
|
+
recompute_electronic_state: bool = Field(
|
|
174
|
+
default=False,
|
|
175
|
+
description="If true, re-solves real Hartree-Fock at every step (true ab-initio MD, much more "
|
|
176
|
+
"expensive; n_steps capped at 30 in that case). If false, holds the initial "
|
|
177
|
+
"electronic state fixed throughout (accurate only close to the starting geometry).",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class MitigateZneInput(BaseModel):
|
|
182
|
+
model_config = ConfigDict(extra="forbid")
|
|
183
|
+
qasm: str = Field(..., min_length=1, description="OpenQASM circuit to run under noise and mitigate.")
|
|
184
|
+
pauli_string: str = Field(..., description="Pauli observable to measure and mitigate, e.g. 'ZZI'.")
|
|
185
|
+
noise_model: str = Field(..., description="Noise model name -- see dense_evolution_list_noise_models.")
|
|
186
|
+
noise_p: float = Field(..., ge=0.0, le=1.0, description="Base noise channel error probability.")
|
|
187
|
+
seed: int = Field(default=42, description="Random seed for the stochastic Kraus draws.")
|
|
188
|
+
extrapolation_method: str = Field(
|
|
189
|
+
default="richardson",
|
|
190
|
+
description="'richardson' (exact, through 1x/2x/3x noise_p) or 'polynomial' "
|
|
191
|
+
"(degree-2 least-squares fit through 5 noise scales).",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class MitigateDensityMatrixInput(BaseModel):
|
|
196
|
+
model_config = ConfigDict(extra="forbid")
|
|
197
|
+
qasm: str = Field(..., min_length=1, description="OpenQASM circuit to run under noise and mitigate.")
|
|
198
|
+
noise_model: str = Field(..., description="Noise model name -- see dense_evolution_list_noise_models.")
|
|
199
|
+
noise_p: float = Field(..., ge=0.0, le=1.0, description="Base noise channel error probability.")
|
|
200
|
+
seed: int = Field(default=42, description="Random seed for the Monte-Carlo density-matrix estimate.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class CosmicRayBurstInput(BaseModel):
|
|
204
|
+
model_config = ConfigDict(extra="forbid")
|
|
205
|
+
baseline_gamma: float = Field(..., ge=0.0, le=1.0, description="Undisturbed per-slice decay probability.")
|
|
206
|
+
times_us: Optional[List[float]] = Field(
|
|
207
|
+
default=None,
|
|
208
|
+
description="Time points since impact, in microseconds. Defaults to the impact instant, the "
|
|
209
|
+
"~10us and ~1ms rise checkpoints, and a point well into the ~25ms recovery.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class OscillatingNoiseInput(BaseModel):
|
|
214
|
+
model_config = ConfigDict(extra="forbid")
|
|
215
|
+
base_p: float = Field(..., ge=0.0, le=1.0, description="Noise probability around which p_eff oscillates.")
|
|
216
|
+
freq: float = Field(..., gt=0.0, description="Oscillation frequency parameter (see dense_evolution.oscillating_p_eff).")
|
|
217
|
+
amp: float = Field(..., description="Oscillation amplitude, as a fraction of base_p.")
|
|
218
|
+
factors: Optional[List[float]] = Field(
|
|
219
|
+
default=None, description="Scale-factor points to evaluate p_eff at. Defaults to one full period.",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class DensityMatrixChannelInput(BaseModel):
|
|
224
|
+
model_config = ConfigDict(extra="forbid")
|
|
225
|
+
qasm: str = Field(..., min_length=1, description="OpenQASM circuit whose ideal density matrix the channel is applied to.")
|
|
226
|
+
channel: str = Field(..., description="'global_depolarizing' (whole-register, SPAM-style) or "
|
|
227
|
+
"'amplitude_damping' (single-qubit only, asymmetric energy relaxation).")
|
|
228
|
+
param: float = Field(..., ge=0.0, le=1.0, description="Channel strength: p for global_depolarizing, gamma for amplitude_damping.")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class VectorHealingInput(BaseModel):
|
|
232
|
+
model_config = ConfigDict(extra="forbid")
|
|
233
|
+
vectors: list = Field(..., description="(n_steps, dim) sequence of equal-length numeric rows to heal -- "
|
|
234
|
+
"e.g. a VQE parameter/energy trajectory, MD telemetry, or any other noisy vector "
|
|
235
|
+
"sequence. NaN/Inf entries are sanitized automatically.")
|
|
236
|
+
radius_baseline: int | None = Field(
|
|
237
|
+
default=None,
|
|
238
|
+
description="Fixed radius for the local baseline window used to judge each step. If omitted, "
|
|
239
|
+
"computed adaptively as min(20, max(3, n_steps // 3)).",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class WormholeSelectInstanceInput(BaseModel):
|
|
244
|
+
model_config = ConfigDict(extra="forbid")
|
|
245
|
+
n_majorana: int = Field(default=8, ge=4, description="Number of Majorana modes per side (must be even). "
|
|
246
|
+
"8 matches arXiv:2604.10090's own experiment (4 qubits/side).")
|
|
247
|
+
k_terms: int = Field(default=10, ge=1, description="Number of sparse 4-Majorana coupling terms to keep, "
|
|
248
|
+
"out of C(n_majorana,4) possible -- 10 matches the paper's K=10.")
|
|
249
|
+
J: float = Field(default=1.4142135623730951, description="SYK coupling strength (paper's J=sqrt(2)).")
|
|
250
|
+
n_candidates: int = Field(default=200, ge=1, le=2000, description="Number of random seeds to screen.")
|
|
251
|
+
target_commuting: int = Field(default=34, description="Target commuting-pair count out of C(k_terms,2) pairs "
|
|
252
|
+
"-- 34 (out of 45) is the paper's own selected instance's ratio for K=10.")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class WormholeTeleportationInput(BaseModel):
|
|
256
|
+
model_config = ConfigDict(extra="forbid")
|
|
257
|
+
n_majorana: int = Field(default=8, ge=4, description="Number of Majorana modes per side (must be even).")
|
|
258
|
+
k_terms: int = Field(default=10, ge=1, description="Number of sparse 4-Majorana coupling terms per side.")
|
|
259
|
+
J: float = Field(default=1.4142135623730951, description="SYK coupling strength (paper's J=sqrt(2)).")
|
|
260
|
+
mu: float = Field(default=12.0, description="L-R coupling strength exp(i*mu*V). Sign matters -- the "
|
|
261
|
+
"protocol's signature is the difference in teleported mutual information between "
|
|
262
|
+
"+mu and -mu (call twice, or use dense_evolution_wormhole_scan for a batch sweep).")
|
|
263
|
+
t0: float = Field(default=0.3, description="Pre-coupling evolution time under H_L+H_R.")
|
|
264
|
+
t1: float = Field(default=0.6, description="Post-coupling evolution time under H_L+H_R. The known signal "
|
|
265
|
+
"peak for seed=61/n_majorana=8/k_terms=10 is at t1=0.60.")
|
|
266
|
+
seed: int = Field(default=61, description="SYK instance seed -- use dense_evolution_wormhole_select_instance "
|
|
267
|
+
"to find a good one for other (n_majorana, k_terms) combinations; 61 is the verified "
|
|
268
|
+
"match for the defaults.")
|
|
269
|
+
with_message: bool = Field(default=True, description="Inject the reference qubit's message into the L "
|
|
270
|
+
"register (swap). False = no message injected, a baseline/control run.")
|
|
271
|
+
backend: str = Field(default="exact", description="'exact' (matrix exponentiation, cheap, exact) or "
|
|
272
|
+
"'trotter' (real Trotterized gate circuit, closer to actual hardware execution, "
|
|
273
|
+
"verified to reproduce the exact backend's result closely).")
|
|
274
|
+
n_steps_evolution: int = Field(default=8, ge=1, description="Trotter steps for the t0/t1 evolution (trotter backend only).")
|
|
275
|
+
n_steps_coupling: int = Field(default=16, ge=1, description="Trotter steps for the mu coupling (trotter backend only).")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class WormholeScanInput(BaseModel):
|
|
279
|
+
model_config = ConfigDict(extra="forbid")
|
|
280
|
+
n_majorana: int = Field(default=8, ge=4, description="Number of Majorana modes per side (must be even).")
|
|
281
|
+
k_terms: int = Field(default=10, ge=1, description="Number of sparse 4-Majorana coupling terms per side.")
|
|
282
|
+
J: float = Field(default=1.4142135623730951, description="SYK coupling strength (paper's J=sqrt(2)).")
|
|
283
|
+
mu_magnitude: float = Field(default=12.0, ge=0, description="Both +mu_magnitude and -mu_magnitude are "
|
|
284
|
+
"run at every t1 point -- this is the wormhole signal's defining comparison.")
|
|
285
|
+
t0: float = Field(default=0.3, description="Pre-coupling evolution time under H_L+H_R, shared by every point.")
|
|
286
|
+
t1_values: list = Field(..., min_length=1, max_length=20, description="List of post-coupling evolution "
|
|
287
|
+
"times to sweep. Capped at 20 points (each point runs backend twice, +mu and "
|
|
288
|
+
"-mu, sequentially -- unlike dense_evolution_energy_scan this is NOT cheap: "
|
|
289
|
+
"each single teleportation call takes several seconds, so a full sweep can "
|
|
290
|
+
"take a few minutes; see the tool's own docstring).")
|
|
291
|
+
seed: int = Field(default=61, description="SYK instance seed -- use dense_evolution_wormhole_select_instance first.")
|
|
292
|
+
with_message: bool = Field(default=True, description="Inject the reference qubit's message into L.")
|
|
293
|
+
backend: str = Field(default="exact", description="'exact' or 'trotter' -- see dense_evolution_wormhole_teleportation.")
|
|
294
|
+
n_steps_evolution: int = Field(default=8, ge=1, description="Trotter steps for t0/t1 evolution (trotter backend only).")
|
|
295
|
+
n_steps_coupling: int = Field(default=16, ge=1, description="Trotter steps for the mu coupling (trotter backend only).")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class NativeHfDiagnosticsInput(BaseModel):
|
|
299
|
+
model_config = ConfigDict(extra="forbid")
|
|
300
|
+
symbols: list = Field(..., min_length=1, description="Atomic symbols, e.g. ['H', 'H', 'O'].")
|
|
301
|
+
geometry: list = Field(..., min_length=1, description="[[x, y, z], ...] coordinates in Angstrom, one row per symbol.")
|
|
302
|
+
charge: int = Field(default=0, description="Total molecular charge.")
|
|
303
|
+
active_electrons: Optional[int] = Field(default=None, description="Active-space electron count, if restricting the active space.")
|
|
304
|
+
active_orbitals: Optional[int] = Field(default=None, description="Active-space orbital count, if restricting the active space.")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class RagSearchInput(BaseModel):
|
|
308
|
+
model_config = ConfigDict(extra="forbid")
|
|
309
|
+
documents: list = Field(..., min_length=1, description="[[text, source], ...] documents to index -- each a 2-element list of the document text and a source label (e.g. filename).")
|
|
310
|
+
query: str = Field(..., min_length=1, description="Search query -- natural-language for semantic search, or the literal/regex pattern when exact=True.")
|
|
311
|
+
top: int = Field(default=5, ge=1, description="Number of top results to return (semantic search only, ignored when exact=True).")
|
|
312
|
+
rerank: bool = Field(default=True, description="If true, pools TF-IDF and dense-embedding candidates then re-scores with a cross-encoder. If false, plain TF-IDF ranking only. Ignored when exact=True.")
|
|
313
|
+
exact: bool = Field(default=False, description="If true, substring/regex search over raw chunk text instead of semantic search -- for finding a specific known phrase or value semantic ranking buries.")
|
|
314
|
+
regex: bool = Field(default=False, description="Only with exact=True: treat query as a regex (case-sensitive) instead of a literal substring (case-insensitive).")
|
|
315
|
+
max_hits: int = Field(default=10, ge=1, description="Only with exact=True: stop after this many matches.")
|
|
316
|
+
context: int = Field(default=300, ge=0, description="Only with exact=True: characters of context to show around each match.")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class MassDecompositionInput(BaseModel):
|
|
320
|
+
model_config = ConfigDict(extra="forbid")
|
|
321
|
+
formula: str = Field(..., min_length=1, description="Molecular formula string, e.g. 'C6H12O6'.")
|
|
322
|
+
target_mass: float = Field(..., description="Mass difference to check for chemical reachability, e.g. a peak-pair difference from a real spectrum.")
|
|
323
|
+
max_mass: Optional[float] = Field(default=None, description="Upper bound for the search space; defaults to target_mass itself if omitted.")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class Bb84Input(BaseModel):
|
|
327
|
+
model_config = ConfigDict(extra="forbid")
|
|
328
|
+
n_rounds: int = Field(..., ge=1, description="Number of BB84 rounds to run.")
|
|
329
|
+
p_channel: float = Field(default=0.0, ge=0.0, le=1.0, description="Isotropic depolarizing channel error probability.")
|
|
330
|
+
eve: bool = Field(default=False, description="If true, simulates an intercept-resend eavesdropping attack instead of a clean channel.")
|
|
331
|
+
seed: Optional[int] = Field(default=None, description="Random seed for reproducibility.")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class DiQkdGhzInput(BaseModel):
|
|
335
|
+
model_config = ConfigDict(extra="forbid")
|
|
336
|
+
n_rounds: int = Field(..., ge=1, description="Number of test rounds to run.")
|
|
337
|
+
p_dep: float = Field(default=0.0, ge=0.0, le=1.0, description="Isotropic depolarizing error probability applied to the GHZ(3) state.")
|
|
338
|
+
seed: Optional[int] = Field(default=None, description="Random seed for reproducibility.")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class DickaInput(BaseModel):
|
|
342
|
+
model_config = ConfigDict(extra="forbid")
|
|
343
|
+
n_rounds: int = Field(..., ge=1, description="Number of protocol rounds to run.")
|
|
344
|
+
gamma: float = Field(..., description="Test-round selection probability.")
|
|
345
|
+
beta: float = Field(..., gt=0.75, lt=0.8535533905932737, description="Abort threshold: must sit strictly between the classical bound (0.75) and the quantum max (~0.8536) to meaningfully distinguish a device-independent channel from a classically-bound one.")
|
|
346
|
+
p_dep: float = Field(default=0.0, ge=0.0, le=1.0, description="Isotropic depolarizing error probability.")
|
|
347
|
+
seed: Optional[int] = Field(default=None, description="Random seed for reproducibility.")
|
mcp_server/molecules.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Molecule name resolution: short ids <-> the kernel's full catalog keys.
|
|
2
|
+
|
|
3
|
+
The kernel's own catalog keys are long, human-readable strings, e.g.
|
|
4
|
+
"H2 (Idrogeno) - R = 0.7414 A [equilibrio reale]" -- fine for a web page
|
|
5
|
+
label, error-prone for an agent to reproduce verbatim across several tool
|
|
6
|
+
calls (exact punctuation, accented characters, etc.). Rather than change
|
|
7
|
+
the kernel's catalog (that key is also what the published Composer page
|
|
8
|
+
uses), this adapter derives a short id from each key's leading token
|
|
9
|
+
(e.g. "H2", "LiH", "HeH+") and accepts either form everywhere a molecule
|
|
10
|
+
`name` is expected. Derived from the live catalog, not hardcoded, so it
|
|
11
|
+
stays correct if the catalog grows.
|
|
12
|
+
|
|
13
|
+
Cached per mapping via TTLCache (utils/cache.py) instead of a plain dict
|
|
14
|
+
that only ever got reset by test code -- BUG FIX: the pre-Phase-2 cache
|
|
15
|
+
never expired on its own, so a long-running MCP server process would
|
|
16
|
+
never pick up a real catalog change (e.g. after a kernel restart with a
|
|
17
|
+
different build). A failed fetch is also cached briefly, so a tight loop
|
|
18
|
+
of tool calls made while the kernel is down doesn't each wait out their
|
|
19
|
+
own connection/timeout error.
|
|
20
|
+
|
|
21
|
+
Shared by tools/system_tools.py (dense_evolution_list_molecules) and
|
|
22
|
+
tools/chemistry_tools.py (every tool taking a molecule `name`) -- lives
|
|
23
|
+
in its own module rather than either of those, since Phase 3 (prog.txt
|
|
24
|
+
Sezione 3) splits tools by topic and this is domain glue, not a tool
|
|
25
|
+
itself.
|
|
26
|
+
"""
|
|
27
|
+
from .client import _request
|
|
28
|
+
from .utils.cache import TTLCache
|
|
29
|
+
|
|
30
|
+
_molecule_catalog_cache = TTLCache(ttl_seconds=300.0, failure_ttl_seconds=10.0)
|
|
31
|
+
# cache value per mapping: (annotated_catalog_list, {short_id_lower: full_key})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _short_id(full_key: str) -> str:
|
|
35
|
+
return full_key.split(" (")[0].split(" -")[0].strip()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def _get_annotated_molecule_catalog(mapping: str) -> list:
|
|
39
|
+
"""Catalog entries with a short `id` field added. See the cache note
|
|
40
|
+
in this module's docstring."""
|
|
41
|
+
cached = _molecule_catalog_cache.get(mapping)
|
|
42
|
+
if cached is not None:
|
|
43
|
+
return cached[0]
|
|
44
|
+
try:
|
|
45
|
+
catalog = await _request("GET", "/api/hamiltonians", timeout=10.0, params={"mapping": mapping})
|
|
46
|
+
except Exception as e:
|
|
47
|
+
_molecule_catalog_cache.set_failure(mapping, e)
|
|
48
|
+
raise
|
|
49
|
+
aliases = {}
|
|
50
|
+
annotated = []
|
|
51
|
+
for full_key, spec in catalog.items():
|
|
52
|
+
short = _short_id(full_key)
|
|
53
|
+
aliases[short.lower()] = full_key
|
|
54
|
+
annotated.append({"id": short, "full_name": full_key, **spec})
|
|
55
|
+
_molecule_catalog_cache.set(mapping, (annotated, aliases))
|
|
56
|
+
return annotated
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _resolve_molecule_name(name: str) -> str:
|
|
60
|
+
"""Accept either a short id ('H2') or the full catalog key and return
|
|
61
|
+
the full catalog key the kernel expects. Falls back to returning the
|
|
62
|
+
input unchanged if it's neither -- the kernel's own 404 (with the name
|
|
63
|
+
as given) is a clearer error than silently guessing."""
|
|
64
|
+
cached = _molecule_catalog_cache.get("jordan_wigner")
|
|
65
|
+
if cached is None:
|
|
66
|
+
await _get_annotated_molecule_catalog("jordan_wigner")
|
|
67
|
+
cached = _molecule_catalog_cache.get("jordan_wigner")
|
|
68
|
+
_, aliases = cached
|
|
69
|
+
if name in aliases.values():
|
|
70
|
+
return name
|
|
71
|
+
return aliases.get(name.lower(), name)
|
mcp_server/server.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP server for Dense-Evolution's Composer API (dense_evolution_mcp).
|
|
4
|
+
|
|
5
|
+
This is a thin adapter, not a reimplementation: every tool here calls the
|
|
6
|
+
same local FastAPI kernel the published Composer web page talks to
|
|
7
|
+
(local_site/app/server.py, started with `dense-evolution serve`, listening
|
|
8
|
+
on http://127.0.0.1:8800 by default). All computation -- circuit
|
|
9
|
+
simulation, VQE, molecular Hamiltonians, QM/MM forces, ZNE mitigation --
|
|
10
|
+
happens inside dense_evolution/dashboard_core exactly as it does for the
|
|
11
|
+
web UI; this file only exposes those same endpoints as MCP tools so an
|
|
12
|
+
agent can drive them directly instead of a browser.
|
|
13
|
+
|
|
14
|
+
main() auto-starts the kernel if it isn't already running (see
|
|
15
|
+
client.ensure_kernel_running) -- best-effort, and skipped if
|
|
16
|
+
DENSE_EVOLUTION_KERNEL_URL points somewhere non-default. If that fails
|
|
17
|
+
(e.g. the composer extra isn't installed) or you'd rather manage it
|
|
18
|
+
yourself, start it separately:
|
|
19
|
+
pip install dense-evolution[composer]
|
|
20
|
+
dense-evolution serve
|
|
21
|
+
(or `python -m local_site.app.server` from the repo root)
|
|
22
|
+
|
|
23
|
+
Override the kernel URL with the DENSE_EVOLUTION_KERNEL_URL env var if it's
|
|
24
|
+
not on the default host/port.
|
|
25
|
+
|
|
26
|
+
Images (circuit diagrams, histograms, Q-sphere, Bloch vectors) come back
|
|
27
|
+
from the kernel as base64 PNGs meant for a browser <img> tag -- inlining
|
|
28
|
+
that into a tool's text response would flood an agent's context with a
|
|
29
|
+
wall of base64 for a picture it can't even see inline. Instead this
|
|
30
|
+
adapter decodes and writes each one to DENSE_EVOLUTION_MCP_IMAGE_DIR
|
|
31
|
+
(default ~/.dense_evolution_mcp/images) and returns the file path, which
|
|
32
|
+
Claude Code (or any agent with file access) can open directly. Large
|
|
33
|
+
numeric arrays (statevector, probabilities) are similarly truncated to
|
|
34
|
+
their most significant entries rather than dumped in full -- the kernel's
|
|
35
|
+
own response can be tens of thousands of floats for a 20+ qubit circuit.
|
|
36
|
+
|
|
37
|
+
Structure (prog.txt Sezione 3, now complete): settings live in config.py,
|
|
38
|
+
the HTTP client + error handling in client.py, the Pydantic input schemas
|
|
39
|
+
in models.py (Phase 1); image saving/truncation/molecule-catalog caching
|
|
40
|
+
in utils/ and molecules.py (Phase 2); the 32 tools themselves (7 system +
|
|
41
|
+
2 circuit + 9 chemistry + 4 mitigation + 3 wormhole + 3 noise + 3 crypto +
|
|
42
|
+
1 retrieval -- counted directly from the imports below, not assumed: a
|
|
43
|
+
`len(...) == N` regression test guards against drift, see
|
|
44
|
+
test_mcp_server.py), split by topic, in tools/ (Phase 3,
|
|
45
|
+
this file). This file's only job is to create
|
|
46
|
+
the MCPServer instance, import each tools/*.py module so its `@mcp.tool`
|
|
47
|
+
decorators register against it, re-export every tool function (so
|
|
48
|
+
`from mcp_server.server import dense_evolution_health` keeps working for
|
|
49
|
+
existing callers, including this repo's own test suite), and provide the
|
|
50
|
+
`main()` console-script entry point.
|
|
51
|
+
|
|
52
|
+
`mcp` is created here BEFORE the `from .tools....` imports below, and
|
|
53
|
+
every tools/*.py module does `from ..server import mcp` -- Python resolves
|
|
54
|
+
this correctly despite looking circular: by the time those imports run,
|
|
55
|
+
`mcp_server.server` is already in `sys.modules` (registered before this
|
|
56
|
+
file's body starts executing) with `mcp` already assigned, so each
|
|
57
|
+
submodule's `from ..server import mcp` finds it immediately. Reordering
|
|
58
|
+
the `mcp = MCPServer(...)` line to after the tool imports would break this.
|
|
59
|
+
"""
|
|
60
|
+
from mcp.server import MCPServer
|
|
61
|
+
|
|
62
|
+
mcp = MCPServer("dense_evolution_mcp")
|
|
63
|
+
|
|
64
|
+
from .tools.system_tools import ( # noqa: E402
|
|
65
|
+
dense_evolution_health, dense_evolution_kernel_status, dense_evolution_list_gates,
|
|
66
|
+
dense_evolution_list_molecules, dense_evolution_list_noise_models, dense_evolution_list_presets,
|
|
67
|
+
dense_evolution_system_limits,
|
|
68
|
+
)
|
|
69
|
+
from .tools.circuit_tools import dense_evolution_build_circuit, dense_evolution_run_circuit # noqa: E402
|
|
70
|
+
from .tools.chemistry_tools import ( # noqa: E402
|
|
71
|
+
dense_evolution_custom_molecule_energy, dense_evolution_energy_scan, dense_evolution_mass_decomposition,
|
|
72
|
+
dense_evolution_md_trajectory, dense_evolution_mix_molecules, dense_evolution_molecule_energy,
|
|
73
|
+
dense_evolution_native_hf_diagnostics, dense_evolution_qmmm_forces, dense_evolution_run_vqe,
|
|
74
|
+
)
|
|
75
|
+
from .tools.mitigation_tools import ( # noqa: E402
|
|
76
|
+
dense_evolution_mitigate_coherence, dense_evolution_mitigate_density_matrix,
|
|
77
|
+
dense_evolution_mitigate_zne, dense_evolution_vector_healing,
|
|
78
|
+
)
|
|
79
|
+
from .tools.wormhole_tools import ( # noqa: E402
|
|
80
|
+
dense_evolution_wormhole_scan, dense_evolution_wormhole_select_instance, dense_evolution_wormhole_teleportation,
|
|
81
|
+
)
|
|
82
|
+
from .tools.noise_tools import ( # noqa: E402
|
|
83
|
+
dense_evolution_cosmic_ray_burst, dense_evolution_oscillating_noise, dense_evolution_density_matrix_channel,
|
|
84
|
+
)
|
|
85
|
+
from .tools.crypto_tools import ( # noqa: E402
|
|
86
|
+
dense_evolution_crypto_bb84, dense_evolution_crypto_di_qkd_ghz, dense_evolution_crypto_dicka,
|
|
87
|
+
)
|
|
88
|
+
from .tools.retrieval_tools import dense_evolution_rag_search # noqa: E402
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
"""Console-script entry point (`dense-evolution mcp`, see
|
|
93
|
+
dense_evolution/cli.py) -- identical to running this file directly.
|
|
94
|
+
stdio transport: this process is meant to be launched by an MCP
|
|
95
|
+
client (Claude Code, Claude Desktop, ...) as a subprocess, not run
|
|
96
|
+
standalone in a terminal."""
|
|
97
|
+
import asyncio
|
|
98
|
+
from .client import close_shared_client, ensure_kernel_running
|
|
99
|
+
try:
|
|
100
|
+
asyncio.run(ensure_kernel_running())
|
|
101
|
+
except RuntimeError:
|
|
102
|
+
pass # best-effort only -- an event-loop hiccup here must never block startup
|
|
103
|
+
try:
|
|
104
|
+
mcp.run() # pragma: no cover -- blocks on the real stdio transport loop
|
|
105
|
+
finally:
|
|
106
|
+
# Best-effort: mcp.run() has already torn down its own event loop
|
|
107
|
+
# by the time this runs, so close_shared_client() executes under a
|
|
108
|
+
# fresh one here -- fine for aclose() (it just closes the pool),
|
|
109
|
+
# but swallow a RuntimeError anyway rather than let cleanup crash
|
|
110
|
+
# shutdown for what's ultimately a leaked-socket warning, not a
|
|
111
|
+
# correctness issue.
|
|
112
|
+
try:
|
|
113
|
+
asyncio.run(close_shared_client())
|
|
114
|
+
except RuntimeError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
|
File without changes
|