Skip to content

Commit e07cc11

Browse files
draft mcp
1 parent 27726d3 commit e07cc11

8 files changed

Lines changed: 1239 additions & 15 deletions

File tree

mp_api/client/core/utils.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
import re
45
from typing import TYPE_CHECKING, Literal
56

@@ -13,6 +14,7 @@
1314
if TYPE_CHECKING:
1415
from monty.json import MSONable
1516

17+
_MAPI_SETTINGS = MAPIClientSettings()
1618

1719
def _compare_emmet_ver(
1820
ref_version: str, op: Literal["==", ">", ">=", "<", "<="]
@@ -70,6 +72,31 @@ def _legacy_id_validation(id_list: list[str]) -> list[str]:
7072
return id_list
7173

7274

75+
def validate_api_key(api_key: str | None = None) -> str:
76+
"""Utility to find and pre-check validity of an API key."""
77+
# SETTINGS tries to read API key from ~/.config/.pmgrc.yaml
78+
api_key = api_key or os.getenv("MP_API_KEY")
79+
if not api_key:
80+
from pymatgen.core import SETTINGS
81+
82+
api_key = SETTINGS.get("PMG_MAPI_KEY")
83+
84+
if not api_key:
85+
raise ValueError(
86+
"Please obtain an API key from https://materialsproject.org/api "
87+
"and export it as an environment variable `MP_API_KEY`."
88+
)
89+
90+
if api_key and len(api_key) != 32:
91+
raise ValueError(
92+
"Please use a new API key from https://materialsproject.org/api "
93+
"Keys for the new API are 32 characters, whereas keys for the legacy "
94+
"API are 16 characters."
95+
)
96+
97+
return api_key
98+
99+
73100
def validate_ids(id_list: list[str]):
74101
"""Function to validate material and task IDs.
75102
@@ -82,7 +109,7 @@ def validate_ids(id_list: list[str]):
82109
Returns:
83110
id_list: Returns original ID list if everything is formatted correctly.
84111
"""
85-
if len(id_list) > MAPIClientSettings().MAX_LIST_LENGTH:
112+
if len(id_list) > _MAPI_SETTINGS.MAX_LIST_LENGTH:
86113
raise ValueError(
87114
"List of material/molecule IDs provided is too long. Consider removing the ID filter to automatically pull"
88115
" data for all IDs and filter locally."

mp_api/client/mprester.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from packaging import version
1515
from pymatgen.analysis.phase_diagram import PhaseDiagram
1616
from pymatgen.analysis.pourbaix_diagram import IonEntry
17-
from pymatgen.core import SETTINGS, Composition, Element, Structure
17+
from pymatgen.core import Composition, Element, Structure
1818
from pymatgen.core.ion import Ion
1919
from pymatgen.entries.computed_entries import ComputedStructureEntry
2020
from pymatgen.io.vasp import Chgcar
@@ -24,7 +24,7 @@
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_ids
27+
from mp_api.client.core.utils import _compare_emmet_ver, load_json, validate_api_key, validate_ids
2828
from mp_api.client.routes import GeneralStoreRester, MessagesRester, UserSettingsRester
2929
from mp_api.client.routes.materials import (
3030
AbsorptionRester,
@@ -169,17 +169,7 @@ def __init__(
169169
mute_progress_bars: Whether to mute progress bars.
170170
171171
"""
172-
# SETTINGS tries to read API key from ~/.config/.pmgrc.yaml
173-
api_key = api_key or os.getenv("MP_API_KEY") or SETTINGS.get("PMG_MAPI_KEY")
174-
175-
if api_key and len(api_key) != 32:
176-
raise ValueError(
177-
"Please use a new API key from https://materialsproject.org/api "
178-
"Keys for the new API are 32 characters, whereas keys for the legacy "
179-
"API are 16 characters."
180-
)
181-
182-
self.api_key = api_key
172+
self.api_key = validate_api_key(api_key)
183173
self.endpoint = endpoint or os.getenv(
184174
"MP_API_ENDPOINT", "https://api.materialsproject.org/"
185175
)

mp_api/mcp/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Get default MCP for Materials Project."""
2+
from __future__ import annotations
3+
4+
from mp_api.mcp.mp_mcp import MPMcp
5+
6+
__all__ = ["MPMcp"]

mp_api/mcp/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Run MCP."""
2+
from __future__ import annotations
3+
4+
from mp_api.mcp.mp_mcp import MPMcp
5+
6+
MP_MCP = MPMcp().mcp()
7+
MP_MCP.run()

mp_api/mcp/mp_mcp.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Define custom MCP tools for the Materials Project API."""
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
from fastmcp import FastMCP
7+
from pydantic import BaseModel, Field, PrivateAttr
8+
9+
from mp_api.client import MPRester
10+
from mp_api.mcp import tools as mcp_tools
11+
12+
13+
class MPMcp(BaseModel):
14+
name: str = Field("Materials Project MCP")
15+
client_kwargs: dict[str, Any] | None = Field(None)
16+
_client: MPRester | None = PrivateAttr(None)
17+
18+
@property
19+
def client(self) -> MPRester:
20+
# Always return JSON compliant output for MCP
21+
kwargs = {
22+
**(self.client_kwargs or {}),
23+
"use_document_model": False,
24+
"monty_decode": False,
25+
}
26+
if not self._client:
27+
self._client = MPRester(**kwargs)
28+
return self._client
29+
30+
def mcp(self, **kwargs) -> FastMCP:
31+
mcp = FastMCP(self.name, **kwargs)
32+
33+
for attr in {x for x in dir(mcp_tools) if x.startswith("get")}:
34+
mcp.tool(getattr(mcp_tools, attr))
35+
36+
return mcp

0 commit comments

Comments
 (0)