Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f84f816
draft mcp
esoteric-ephemera Aug 28, 2025
0b7173e
add bootstrap mcp
esoteric-ephemera Aug 28, 2025
e3fd333
review comments
esoteric-ephemera Sep 4, 2025
0359833
modify for better compliance
esoteric-ephemera Oct 1, 2025
668cd05
modify for better compliance
esoteric-ephemera Oct 1, 2025
6e54935
working mcp + phase diag tool
esoteric-ephemera Oct 1, 2025
431a670
remove uv file
esoteric-ephemera Oct 1, 2025
ae21fad
remove uv file
esoteric-ephemera Oct 1, 2025
b5c8287
try handling badly json-decoded llm queries
esoteric-ephemera Oct 7, 2025
2075c2d
update mcp with slightly better docstr + some examples
esoteric-ephemera Oct 21, 2025
384c2a9
remove llm-generated python script
esoteric-ephemera Oct 21, 2025
17d66f0
Merge remote-tracking branch 'upstream/main' into mcp
esoteric-ephemera Nov 12, 2025
ffb8fbc
resolve merge conf
esoteric-ephemera Jan 12, 2026
a8584e4
draft openai compatible mcp
esoteric-ephemera Jan 12, 2026
2d3b0ba
precommit
esoteric-ephemera Jan 12, 2026
7dcf96b
add mcp inspector tool
esoteric-ephemera Jan 12, 2026
4217daf
resolve merge conf
esoteric-ephemera Jan 12, 2026
a1b469c
schematize metadata + field annotations
esoteric-ephemera Jan 13, 2026
dfab523
agnostic naming
esoteric-ephemera Jan 13, 2026
7664954
add tests, clean out old mcps
esoteric-ephemera Jan 16, 2026
d1d8646
update deps
esoteric-ephemera Jan 16, 2026
035e92a
tweak wf order
esoteric-ephemera Jan 16, 2026
6f4c564
pin scipy temporarily pending matminer fix
esoteric-ephemera Jan 16, 2026
46da65f
pin scipy temporarily pending matminer fix
esoteric-ephemera Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade "pip<25.3"
pip install -r requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}.txt
pip install -r requirements/requirements-${{ matrix.os }}_py${{ matrix.python-version }}_extras.txt
pip install -e . --no-deps
- name: Set SSL_CERT_FILE (Linux)
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest'
Expand All @@ -59,7 +59,6 @@ jobs:
MP_API_KEY: ${{ secrets[env.API_KEY_NAME] }}
#MP_API_ENDPOINT: https://api-preview.materialsproject.org/
run: |
pip install -e .
pytest -n auto -x --cov=mp_api --cov-report=xml
- uses: codecov/codecov-action@v1
with:
Expand Down
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
164 changes: 164 additions & 0 deletions dev/generate_mcp_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Define utilities for (mostly) auto-generating MCP tools.
This file will autogenerate a (Fast)MCP set of tools with
type annotations.
The resultant tools are perhaps too general for use in an MCP.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from mp_api.client import MPRester

if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path


def get_annotation_signature(
obj: Callable, tablen: int = 4
) -> tuple[str | None, str | None]:
"""Reconstruct the type annotations associated with a Callable.
Returns the type annotations on input, and the output
kwargs as str if type annotations can be inferred.
"""
kwargs = None
out_kwargs = None
if (annos := obj.__annotations__) and (defaults := obj.__defaults__):
non_ret_type = [k for k in annos if k != "return"]
defaults = [f" = {val}" for val in defaults]
if len(defaults) < len(non_ret_type):
defaults = [""] * (len(non_ret_type) - len(defaults)) + defaults
kwargs = ",\n".join(
f"{' '*tablen}{k} : {v}{defaults[i]}"
for i, (k, v) in enumerate(annos.items())
if k != "return"
)
out_kwargs = ",\n".join(
f"{' '*2*tablen}{k} = {k}" for k in annos if k != "return"
)
return kwargs, out_kwargs


def regenerate_tools(
client: MPRester | None = None, file_name: str | Path | None = None
) -> str:
"""Utility to regenerate the informative tool names with annotations."""
func_str = """# ruff: noqa
from __future__ import annotations
from datetime import datetime
from typing import Literal
from emmet.core.chemenv import (
COORDINATION_GEOMETRIES,
COORDINATION_GEOMETRIES_IUCR,
COORDINATION_GEOMETRIES_IUPAC,
COORDINATION_GEOMETRIES_NAMES,
)
from emmet.core.electronic_structure import BSPathType, DOSProjectionType
from emmet.core.grain_boundary import GBTypeEnum
from emmet.core.mpid import MPID
from emmet.core.thermo import ThermoType
from emmet.core.summary import HasProps
from emmet.core.symmetry import CrystalSystem
from emmet.core.synthesis import SynthesisTypeEnum, OperationTypeEnum
from emmet.core.vasp.calc_types import CalcType
from emmet.core.xas import Edge, Type
from pymatgen.analysis.magnetism.analyzer import Ordering
from pymatgen.core.periodic_table import Element
from pymatgen.core.structure import Structure
from pymatgen.electronic_structure.core import OrbitalType, Spin
"""

translate = {
"chemenv": "chemical_environment",
"dos": "density_of_states",
"eos": "equation_of_state",
"summary": "material",
"robocrys": "crystal_summary",
}

mp_client = client or MPRester()

def _get_rester_sub_name(name, route) -> str | None:
for y in [x for x in dir(route) if not x.startswith("_")]:
attr = getattr(route, y)
if (
(hasattr(attr, "__name__") and attr.__name__ == name)
or (hasattr(attr, "__class__"))
and attr.__class__.__name__ == name
):
return y
return None

for x in mp_client._all_resters:
if not (
sub_rest_route := _get_rester_sub_name(x.__name__, mp_client.materials)
):
continue

search_method = "search"
if "robocrys" in x.__name__.lower():
search_method = "search_docs"

informed_name = sub_rest_route
for k, v in translate.items():
if k in informed_name:
informed_name = informed_name.replace(k, v)

kwargs, out_kwargs = get_annotation_signature(getattr(x, search_method))
if not kwargs:
# FastMCP raises a ValueError if types are not provided:
# `Functions with **kwargs are not supported as tools`
continue
func_str += (
f"def get_{informed_name}_data(\n"
f" self,\n{kwargs}\n) -> list[dict]:\n"
f" return self.client.materials.{sub_rest_route}"
f".search(\n{out_kwargs}\n)\n\n"
)

helpers = [
method
for method in dir(mp_client)
if any(
method.startswith(signature)
for signature in (
"get",
"find",
)
)
]
for func_name in helpers:
func = getattr(mp_client, func_name)
# MCP doesn't work with LRU cached functions?
if hasattr(func, "cache_info"):
continue

kwargs, out_kwargs = get_annotation_signature(func)
if not kwargs:
continue

informed_name = func_name.replace("find", "get")
for k, v in translate.items():
if k in informed_name:
informed_name = informed_name.replace(k, v)

func_str += (
f"def {informed_name}(\n"
f" self,\n{kwargs}\n) -> list[dict]:\n"
f" return self.client.{func_name}(\n"
f"{out_kwargs}\n)\n\n"
)

if file_name:
with open(file_name, "w") as f:
f.write(func_str)

return func_str
12 changes: 12 additions & 0 deletions dev/inspect_mcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash -l

# Tool to run the MCP inspector:
# https://modelcontextprotocol.io/docs/tools/inspector

server_path=$(python -c 'from importlib_resources import files ; print(str((files("mp_api.client") / ".."/ "..").resolve()))')

fastmcp dev \
--python 3.12 \
--with-editable $server_path \
--with-requirements "$server_path/requirements/requirements-ubuntu-latest_py3.12_extras.txt" \
"$server_path/mp_api/mcp/server.py"
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."""
Loading
Loading