Skip to content
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,4 @@ _autosummary

uv.lock
JANAF_*_data.json
.gemini
24 changes: 23 additions & 1 deletion mp_api/client/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import re
from typing import TYPE_CHECKING, Literal

Expand All @@ -13,6 +14,8 @@
if TYPE_CHECKING:
from monty.json import MSONable

_MAPI_SETTINGS = MAPIClientSettings()


def _compare_emmet_ver(
ref_version: str, op: Literal["==", ">", ">=", "<", "<="]
Expand Down Expand Up @@ -70,6 +73,25 @@ def _legacy_id_validation(id_list: list[str]) -> list[str]:
return id_list


def validate_api_key(api_key: str | None = None) -> str:
"""Utility to find and pre-check validity of an API key."""
# SETTINGS tries to read API key from ~/.config/.pmgrc.yaml
api_key = api_key or os.getenv("MP_API_KEY")
if not api_key:
from pymatgen.core import SETTINGS

api_key = SETTINGS.get("PMG_MAPI_KEY")

if not api_key or len(api_key) != 32:
addendum = " Valid API keys are 32 characters." if api_key else ""
raise ValueError(
"Please obtain a valid API key from https://materialsproject.org/api "
f"and export it as an environment variable `MP_API_KEY`.{addendum}"
)

return api_key


def validate_ids(id_list: list[str]):
"""Function to validate material and task IDs.

Expand All @@ -82,7 +104,7 @@ def validate_ids(id_list: list[str]):
Returns:
id_list: Returns original ID list if everything is formatted correctly.
"""
if len(id_list) > MAPIClientSettings().MAX_LIST_LENGTH:
if len(id_list) > _MAPI_SETTINGS.MAX_LIST_LENGTH:
raise ValueError(
"List of material/molecule IDs provided is too long. Consider removing the ID filter to automatically pull"
" data for all IDs and filter locally."
Expand Down
21 changes: 8 additions & 13 deletions mp_api/client/mprester.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from packaging import version
from pymatgen.analysis.phase_diagram import PhaseDiagram
from pymatgen.analysis.pourbaix_diagram import IonEntry
from pymatgen.core import SETTINGS, Composition, Element, Structure
from pymatgen.core import Composition, Element, Structure
from pymatgen.core.ion import Ion
from pymatgen.entries.computed_entries import ComputedStructureEntry
from pymatgen.io.vasp import Chgcar
Expand All @@ -24,7 +24,12 @@
from mp_api.client.core import BaseRester, MPRestError
from mp_api.client.core._oxygen_evolution import OxygenEvolution
from mp_api.client.core.settings import MAPIClientSettings
from mp_api.client.core.utils import _compare_emmet_ver, load_json, validate_ids
from mp_api.client.core.utils import (
_compare_emmet_ver,
load_json,
validate_api_key,
validate_ids,
)
from mp_api.client.routes import GeneralStoreRester, MessagesRester, UserSettingsRester
from mp_api.client.routes.materials import (
AbsorptionRester,
Expand Down Expand Up @@ -169,17 +174,7 @@ def __init__(
mute_progress_bars: Whether to mute progress bars.

"""
# SETTINGS tries to read API key from ~/.config/.pmgrc.yaml
api_key = api_key or os.getenv("MP_API_KEY") or SETTINGS.get("PMG_MAPI_KEY")

if api_key and len(api_key) != 32:
raise ValueError(
"Please use a new API key from https://materialsproject.org/api "
"Keys for the new API are 32 characters, whereas keys for the legacy "
"API are 16 characters."
)

self.api_key = api_key
self.api_key = validate_api_key(api_key)
self.endpoint = endpoint or os.getenv(
"MP_API_ENDPOINT", "https://api.materialsproject.org/"
)
Expand Down
1 change: 1 addition & 0 deletions mp_api/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Get default MCP for Materials Project."""
36 changes: 36 additions & 0 deletions mp_api/mcp/mp_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Define custom MCP tools for the Materials Project API."""
from __future__ import annotations

from urllib.parse import urljoin

import httpx
from fastmcp import FastMCP

from mp_api.mcp.tools import MPMcpTools
from mp_api.mcp.utils import _NeedsMPClient


def get_mcp() -> FastMCP:
"""MCP with finer depth of control over tool names."""
mp_mcp = FastMCP("Materials_Project_MCP")
mcp_tools = MPMcpTools()
for attr in {x for x in dir(mcp_tools) if x.startswith("get_")}:
mp_mcp.tool(getattr(mcp_tools, attr))

# Register tool to set the user's API key
mp_mcp.tool(mcp_tools.update_user_api_key)
return mp_mcp


def get_bootstrap_mcp() -> FastMCP:
"""Bootstrap an MP API MCP only from the OpenAPI spec."""
client = _NeedsMPClient().client

return FastMCP.from_openapi(
openapi_spec=httpx.get(urljoin(client.endpoint, "openapi.json")).json(),
client=httpx.AsyncClient(
base_url=client.endpoint,
headers={"x-api-key": client.api_key},
),
name="MP_OpenAPI_MCP",
)
9 changes: 9 additions & 0 deletions mp_api/mcp/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Run MCP."""
from __future__ import annotations

from mp_api.mcp.mp_mcp import get_mcp

mcp = get_mcp()

if __name__ == "__main__":
mcp.run()
Loading
Loading