Skip to content

Commit 2075c2d

Browse files
update mcp with slightly better docstr + some examples
1 parent b5c8287 commit 2075c2d

7 files changed

Lines changed: 136 additions & 35 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
uv.lock
2-
31
# Byte-compiled / optimized / DLL files
42
__pycache__/
53
*.py[cod]
@@ -123,3 +121,4 @@ _autosummary
123121

124122
uv.lock
125123
JANAF_*_data.json
124+
.gemini

find_stable_si_mcp.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
from mp_api.mcp.tools import MPMcpTools
4+
5+
tools = MPMcpTools()
6+
7+
stable_si_docs = tools.get_thermo_data(
8+
formula="Si",
9+
is_stable=True,
10+
fields=["material_id", "formula_pretty", "energy_above_hull"],
11+
)
12+
13+
if stable_si_docs:
14+
print("Found stable silicon structures:")
15+
for doc in stable_si_docs:
16+
print(
17+
f" Material ID: {doc['material_id']}, "
18+
f"Formula: {doc['pretty_formula']}, "
19+
f"Energy Above Hull: {doc['energy_above_hull']}"
20+
)
21+
else:
22+
print("No stable silicon structures found.")

mp_api/client/core/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
_MAPI_SETTINGS = MAPIClientSettings()
1818

19+
1920
def _compare_emmet_ver(
2021
ref_version: str, op: Literal["==", ">", ">=", "<", "<="]
2122
) -> bool:

mp_api/client/mprester.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
from mp_api.client.core import BaseRester, MPRestError
2525
from mp_api.client.core._oxygen_evolution import OxygenEvolution
2626
from mp_api.client.core.settings import MAPIClientSettings
27-
from mp_api.client.core.utils import _compare_emmet_ver, load_json, validate_api_key, validate_ids
27+
from mp_api.client.core.utils import (
28+
_compare_emmet_ver,
29+
load_json,
30+
validate_api_key,
31+
validate_ids,
32+
)
2833
from mp_api.client.routes import GeneralStoreRester, MessagesRester, UserSettingsRester
2934
from mp_api.client.routes.materials import (
3035
AbsorptionRester,

mp_api/mcp/mp_mcp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def get_mcp() -> FastMCP:
1616
mcp_tools = MPMcpTools()
1717
for attr in {x for x in dir(mcp_tools) if x.startswith("get_")}:
1818
mp_mcp.tool(getattr(mcp_tools, attr))
19+
20+
# Register tool to set the user's API key
21+
mp_mcp.tool(mcp_tools.update_user_api_key)
1922
return mp_mcp
2023

2124

mp_api/mcp/tools.py

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
from __future__ import annotations
33

44
from datetime import datetime
5-
import json
6-
from typing import Literal
5+
from typing import Literal, Any
76

87
import plotly.graph_objects as plotly_go
98

@@ -22,9 +21,9 @@
2221
from emmet.core.thermo import ThermoType
2322
from emmet.core.vasp.calc_types import CalcType
2423
from emmet.core.xas import Edge, Type
25-
from pymatgen.analysis.phase_diagram import PhaseDiagram
2624
from pymatgen.analysis.magnetism.analyzer import Ordering
2725
from pymatgen.core.periodic_table import Element
26+
from pymatgen.core.composition import Composition
2827
from pymatgen.core.structure import Structure
2928
from pymatgen.electronic_structure.core import OrbitalType, Spin
3029
from pymatgen.entries.computed_entries import ComputedEntry
@@ -35,30 +34,93 @@
3534
class MPMcpTools(_NeedsMPClient):
3635
"""Define tools needed for the MP MCP client."""
3736

37+
def get_structure_by_material_id(
38+
self,
39+
material_id: str,
40+
structure_format: Literal["json", "poscar", "cif"],
41+
) -> dict[str, Any] | str:
42+
"""Find a structure in the Materials Project by its identifier/ID.
43+
44+
Return type changes based on format:
45+
structure_format = "json":
46+
Returns the JSON representation of a pymatgen structure object.
47+
structure_format = "poscar":
48+
Returns a VASP POSCAR-like representation
49+
structure_format = "cif":
50+
Returns a crystallographic information file (CIF)
51+
"""
52+
struct = self.client.get_structure_by_material_id(
53+
material_id=material_id,
54+
final=True,
55+
conventional_unit_cell=False,
56+
)
57+
if structure_format == "json":
58+
return struct.as_dict() if hasattr(struct, "as_dict") else struct
59+
60+
if isinstance(struct, dict):
61+
struct = Structure.from_dict(struct)
62+
return struct.to(fmt=structure_format)
63+
3864
def get_phase_diagram_from_elements(
3965
self,
4066
elements: list[str],
4167
thermo_type: ThermoType | str = "GGA_GGA+U_R2SCAN",
4268
) -> plotly_go.Figure:
69+
"""Find a thermodynamic phase diagram in the Materials Project by specified elements.
70+
71+
### Examples:
72+
Given elements Na and Cl:
73+
```
74+
phase_diagram = MPMcpTools().get_phase_diagram_from_elements(
75+
elements = ["Na","Cl"],
76+
)
77+
```
78+
79+
Given a chemical system, "K-P-O":
80+
```
81+
phase_diagrasm = MPMcpTools().get_phase_diagram_from_elements(
82+
elements = "K-P-O".split("-"),
83+
)
84+
```
85+
86+
"""
4387
pd = self.client.materials.thermo.get_phase_diagram_from_chemsys(
4488
"-".join(elements), thermo_type
4589
)
4690
return pd.get_plot() # has to be JSON serializable
4791

48-
def get_stability(
92+
def get_stability_or_energy_above_hull(
4993
self,
50-
composition: dict[str, float],
94+
formula: str,
5195
energy: float,
52-
run_type: Literal["GGA", "GGA+U", "R2SCAN"] | None = None,
53-
thermo_type: str | ThermoType = "GGA_GGA+U",
54-
) -> list[dict]:
55-
data = None
56-
if run_type:
57-
data = {"run_type": run_type}
58-
return self.client.get_stability(
59-
entries=ComputedEntry(composition, energy, data=data),
60-
thermo_type=thermo_type,
61-
)
96+
run_type: Literal["GGA", "PBE", "GGA+U", "PBE+U", "R2SCAN"],
97+
) -> float:
98+
"""Get the stability of a particular material.
99+
100+
Given a material's formula and energy in eV, tells you the material's
101+
stability from the energy above the hull (positive for unstable, 0 for stable)
102+
in eV/atom.
103+
104+
The user must specify a particular functional:
105+
- PBE or GGA
106+
- PBE+U or GGA+U
107+
- R2SCAN
108+
this will add necessary corrections to the energy to compare with
109+
the Materials Project.
110+
111+
"""
112+
run_type = run_type.replace("PBE", "GGA")
113+
data = {"run_type": run_type}
114+
thermo_type = "GGA_GGA+U" if run_type in {"GGA", "GGA+U"} else "R2SCAN"
115+
116+
try:
117+
stability = self.client.get_stability(
118+
entries=[ComputedEntry(Composition(formula), energy, data=data)],
119+
thermo_type=thermo_type,
120+
)
121+
return stability[0]["e_above_hull"]
122+
except ValueError:
123+
return float("inf")
62124

63125
def get_absorption_data(
64126
self,
@@ -644,7 +706,7 @@ def get_material_data(
644706
efermi=efermi,
645707
elastic_anisotropy=elastic_anisotropy,
646708
elements=elements,
647-
energy_above_hull=tuple(float(x) for x in json.loads(energy_above_hull)),
709+
energy_above_hull=energy_above_hull,
648710
equilibrium_reaction_energy=equilibrium_reaction_energy,
649711
exclude_elements=exclude_elements,
650712
formation_energy=formation_energy,
@@ -964,15 +1026,6 @@ def get_entry_by_material_id(
9641026
conventional_unit_cell=conventional_unit_cell,
9651027
)
9661028

967-
def get_structure_by_material_id(
968-
self, material_id: str, final: bool = True, conventional_unit_cell: bool = False
969-
) -> list[dict]:
970-
return self.client.get_structure_by_material_id(
971-
material_id=material_id,
972-
final=final,
973-
conventional_unit_cell=conventional_unit_cell,
974-
)
975-
9761029
def get_structures(self, chemsys_formula: str | list[str] = True) -> list[dict]:
9771030
return self.client.get_structures(chemsys_formula=chemsys_formula)
9781031

mp_api/mcp/utils.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,38 @@
1010
from collections.abc import Callable
1111
from pathlib import Path
1212

13+
_REQUIRED_CLIENT_KWARGS = {
14+
"use_document_model": False,
15+
"monty_decode": False,
16+
"include_user_agent": True,
17+
}
18+
1319

1420
class _NeedsMPClient:
1521
def __init__(
1622
self,
1723
client_kwargs: dict[str, Any] | None = None,
1824
client: MPRester | None = None,
1925
):
20-
self.client = client or MPRester(
21-
**{
22-
**(client_kwargs or {}),
23-
"use_document_model": False,
24-
"monty_decode": False,
25-
}
26-
)
26+
self._client_kwargs = {
27+
**(client_kwargs or {}),
28+
**_REQUIRED_CLIENT_KWARGS,
29+
}
30+
self.reset_client()
31+
32+
def reset_client(self) -> None:
33+
"""Reset the API client."""
34+
self.client = MPRester(**self._client_kwargs)
35+
36+
def update_user_api_key(self, api_key: str) -> None:
37+
"""Change the API key used in the client.
38+
39+
Call this method to set the user's API correctly.
40+
Ask the user for their API key as plain text,
41+
and input the result to this method.
42+
"""
43+
self._client_kwargs["api_key"] = api_key
44+
self.reset_client()
2745

2846

2947
def get_annotation_signature(

0 commit comments

Comments
 (0)