# Directory Structure
```
├── __init__.py
├── .gitignore
├── .python-version
├── docker-compose.yml
├── Dockerfile
├── LICENSE
├── pyproject.toml
├── README.md
├── requirements.txt
├── server
│ ├── __init__.py
│ ├── .ipynb
│ └── server.py
├── tests
│ └── __init__.py
├── utils
│ ├── __init__.py
│ ├── prompts_templates.py
│ └── utility_functions.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.12
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | .venv
2 | .env
3 | __pycache__
4 | .idea/*
5 | .vscode/*
6 | .vscode/mcp.json
7 | markdown_folder/
8 | .vscode/
9 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Materials Project MCP
2 |
3 | A Model Context Protocol (MCP) server for querying the Materials Project database using the mp_api client.
4 |
5 | ## Requirements
6 |
7 | - **Materials Project API Key** - [Get one here](https://materialsproject.org/) (free account required)
8 | - **Docker Desktop** (must be running)
9 | - **Python 3.12+** with [uv](https://github.com/astral-sh/uv)
10 |
11 |
12 |
13 | ### Getting Your Materials Project API Key
14 |
15 | 1. Visit [Materials Project](https://materialsproject.org/)
16 | 2. Create a free account or log in
17 | 3. Go to your dashboard
18 | 4. Navigate to API settings
19 | 5. Generate or copy your API key
20 | 6. Keep this key secure - you'll need it for setup
21 |
22 | ## Installation Options
23 |
24 | ### Step 1: Docker (Recommended)
25 |
26 | #### Using Docker Run
27 | 1. **Install Docker Desktop:**
28 | - Download from [docker.com](https://www.docker.com/products/docker-desktop/)
29 | - Install and **make sure Docker Desktop is running**
30 |
31 | 2. **Pull the Docker image:**
32 | ```bash
33 | docker pull benedict2002/materials-project-mcp
34 | ```
35 |
36 | 3. **Test the installation:**
37 | ```bash
38 | docker run --rm -i -e MP_API_KEY="your-api-key" benedict2002/materials-project-mcp
39 | ```
40 |
41 | #### Using Docker Compose (Easiest)
42 | 1. **Install Docker Desktop and make sure it's running**
43 |
44 | 2. **Clone the repository:**
45 | ```bash
46 | git clone <repository-url>
47 | cd materials-project-mcp
48 | ```
49 |
50 | 3. **Create a `.env` file:**
51 | ```bash
52 | echo "MP_API_KEY=your-materials-project-api-key" > .env
53 | ```
54 |
55 | 4. **Test the setup:**
56 | ```bash
57 | docker-compose up
58 | ```
59 |
60 | 5. **For background running:**
61 | ```bash
62 | docker-compose up -d
63 | ```
64 |
65 | 6. **Stop the service:**
66 | ```bash
67 | docker-compose down
68 | ```
69 |
70 | ### Step 1 : Local Python Installation
71 |
72 | 1. **Install uv (if not already installed):**
73 | ```bash
74 | curl -Ls https://astral.sh/uv/install.sh | sh
75 | ```
76 |
77 | 2. **Clone the repository:**
78 | ```bash
79 | git clone <repository-url>
80 | cd materials-project-mcp
81 | ```
82 |
83 | 3. **Create and activate virtual environment:**
84 | ```bash
85 | uv venv
86 | source .venv/bin/activate # Linux/macOS
87 | # or
88 | .venv\Scripts\activate # Windows
89 | ```
90 |
91 | 4. **Install dependencies:**
92 | ```bash
93 | uv pip install -r requirements.txt
94 | ```
95 |
96 | 5. **Set your API key:**
97 | ```bash
98 | export MP_API_KEY="your-api-key" # Linux/macOS
99 | # or
100 | set MP_API_KEY=your-api-key # Windows
101 | ```
102 |
103 | 6. **Test the installation:**
104 | ```bash
105 | python server.py
106 | ```
107 |
108 | ## Step 2 : Setup with Claude Desktop
109 |
110 | 1. **Locate your Claude configuration file:**
111 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
112 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
113 |
114 | 2. **Choose your configuration method:**
115 |
116 | **Using Docker Run**
117 | ```json
118 | {
119 | "mcpServers": {
120 | "Materials Project MCP": {
121 | "command": "docker",
122 | "args": [
123 | "run", "--rm", "-i",
124 | "-e", "MP_API_KEY=your-materials-project-api-key",
125 | "benedict2002/materials-project-mcp"
126 | ]
127 | }
128 | }
129 | }
130 | ```
131 |
132 | 3. **Replace `your-materials-project-api-key` with your actual API key**
133 |
134 | 4. **Ensure Docker Desktop is running**
135 |
136 | 5. **Restart Claude Desktop**
137 |
138 | 6. **Verify installation:**
139 | - Open a new chat in Claude
140 | - Ask something like "Search for silicon materials in the Materials Project database" or test any of the availabe tools.
141 | - You should see Materials Project data in the response
142 |
143 | ## Setup with VS Code Copilot
144 |
145 | 1. **Open VS Code Settings:**
146 | - Press `Ctrl+Shift+P` (Windows/Linux) or `Cmd+Shift+P` (macOS)
147 | - Type "Preferences: Open User Settings (JSON)"
148 | - Select it to open settings.json
149 |
150 | 2. **Add MCP configuration:**
151 | ```json
152 | {
153 | "mcp": {
154 | "inputs": [],
155 | "servers": {
156 | "Materials Project MCP": {
157 | "command": "docker",
158 | "args": [
159 | "run", "--rm", "-i",
160 | "-e", "MP_API_KEY=your-api-key",
161 | "benedict2002/materials-project-mcp"
162 | ]
163 | }
164 | }
165 | },
166 | "chat.mcp.discovery.enabled": true,
167 | "workbench.secondarySideBar.showLabels": false
168 | }
169 | ```
170 |
171 | 3. **Alternative: Local Python setup for VS Code:**
172 | ```json
173 | {
174 | "mcp": {
175 | "inputs": [],
176 | "servers": {
177 | "Materials Project MCP": {
178 | "command": "/usr/local/bin/uv",
179 | "args": [
180 | "run",
181 | "--with",
182 | "mcp[cli],aiohttp,pydantic,mp_api,pymatgen,emmet-core",
183 | "/path/to/your/server.py"
184 | ],
185 | "env": {
186 | "MP_API_KEY": "your-api-key"
187 | }
188 | }
189 | }
190 | },
191 | "chat.mcp.discovery.enabled": true
192 | }
193 | ```
194 |
195 | 4. **Replace placeholders:**
196 | - `your-api-key` with your Materials Project API key
197 | - `/path/to/your/server.py` with the actual path to server.py
198 |
199 | 5. **Ensure Docker Desktop is running** (for Docker configurations)
200 |
201 | 6. **Restart VS Code**
202 |
203 | 7. **Test in VS Code:**
204 | - Open VS Code chat/copilot
205 | - Ask about materials from the Materials Project
206 | - The Docker container will start automatically when VS Code makes requests
207 |
208 |
209 | ## Testing & Development (developers)
210 |
211 | ### Testing Your Installation
212 |
213 | 1. **Test MCP server locally:**
214 | ```bash
215 | mcp dev server.py
216 | ```
217 | Look for the line "🔗 Open inspector with token pre-filled:" and use that URL
218 |
219 | ### Development Workflow
220 |
221 | 1. **Create a feature branch:**
222 | ```bash
223 | git checkout -b feature-name
224 | ```
225 |
226 | 2. **Make your changes and test:**
227 | ```bash
228 | # Local testing with MCP Inspector
229 | mcp dev server.py
230 | # Use the inspector URL to test your changes interactively
231 |
232 | # Docker testing
233 | docker build -t materials-project-mcp-local .
234 | docker run --rm -i -e MP_API_KEY="your-api-key" materials-project-mcp-local
235 |
236 | # Docker Compose testing
237 | docker-compose up --build
238 | ```
239 |
240 | 3. **Commit and push:**
241 | ```bash
242 | git add .
243 | git commit -m "Add feature description"
244 | git push origin feature-name
245 | ```
246 |
247 | 4. **Open a pull request**
248 |
249 |
250 | ## Available Tools & Features
251 |
252 | - **search_materials** - Search by elements, band gap range, stability
253 | - **get_structure_by_id** - Get crystal structures and lattice parameters
254 | - **get_electronic_bandstructure** - Plot electronic band structures
255 | - **get_electronic_dos_by_id** - Get electronic density of states
256 | - **get_phonon_bandstructure** - Plot phonon band structures
257 | - **get_phonon_dos_by_id** - Get phonon density of states
258 | - **get_ion_reference_data_for_chemsys** - Download aqueous ion reference data for Pourbaix diagrams
259 | - **get_cohesive_energy** - Calculate cohesive energies
260 | - **get_atom_reference_data** - Retrieve reference energies of isolated neutral atoms
261 | - **get_magnetic_data_by_id** - Magnetic properties and ordering
262 | - **get_charge_density_by_id** - Charge density data
263 | - **get_dielectric_data_by_id** - Dielectric constants and properties
264 | - **get_diffraction_patterns** - X-ray and neutron diffraction
265 | - **get_xRay_absorption_spectra** - XAFS, XANES, EXAFS spectra
266 | - **get_elastic_constants** - Mechanical properties
267 | - **get_suggested_substrates** - Find substrates for thin films
268 | - **get_thermo_stability** - Thermodynamic stability analysis
269 | - **get_surface_properties** - Surface energies, work functions, and Wulff shapes
270 | - **get_grain_boundaries** - Computed grain boundaries for a material
271 | - **get_insertion_electrodes** - Insertion electrode and battery data
272 | - **get_oxidation_states** - Element oxidation states, formula, and structure info
273 |
274 | ## Troubleshooting
275 |
276 | ### Common Issues
277 |
278 | 1. **"Invalid API key" error:**
279 | - Verify your API key is correct
280 | - Check that you've set the environment variable properly
281 | - Ensure your Materials Project account is active
282 |
283 | 2. **"Docker not found" or "Cannot connect to Docker daemon":**
284 | - **Make sure Docker Desktop is installed and running**
285 | - You should see the Docker Desktop icon in your system tray/menu bar
286 | - Try `docker --version` to verify Docker is accessible
287 | - On Windows/Mac: Open Docker Desktop application
288 | - On Linux: Start Docker service with `sudo systemctl start docker`
289 |
290 | 3. **Container startup issues:**
291 | - Docker containers start automatically when Claude/VS Code makes requests
292 | - No need to manually start containers - they're ephemeral (start → run → stop)
293 | - Each query creates a fresh container instance
294 |
295 | 4. **Docker Compose issues:**
296 | - Make sure Docker Compose is installed: `docker-compose --version`
297 | - Check your `.env` file exists and has the correct API key
298 | - Verify the docker-compose.yml file is in the correct location
299 | - Ensure Docker Desktop is running
300 |
301 | 5. **MCP server not recognized in Claude:**
302 | - Check your configuration file path
303 | - Verify JSON syntax is correct
304 | - Restart Claude Desktop after configuration changes
305 | - Ensure Docker Desktop is running
306 |
307 |
308 | ### Getting Help
309 |
310 | - **MCP Inspector**: Use `mcp dev server.py` for interactive testing and debugging.
311 | - **GitHub Issues**: [Create an Issue](https://github.com/yourusername/materials-project-mcp/issues) for bug reports, feature requests, or questions.
312 | - **Materials Project API Docs**: [docs.materialsproject.org](https://docs.materialsproject.org/)
313 | - **MCP Documentation**: [modelcontextprotocol.io](https://modelcontextprotocol.io/)
314 | - **Docker Help**: [docs.docker.com](https://docs.docker.com/)
315 |
316 | ---
317 | ### Authors
318 |
319 | - Benedict Debrah
320 | - Peniel Fiawornu
321 |
322 | ### Reference
323 |
324 | Yin, Xiangyu. 2025. "Building an MCP Server for the Materials Project." March 23, 2025. https://xiangyu-yin.com/content/post_mp_mcp.html.
```
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
1 | services:
2 | materials-project-mcp:
3 | image: benedict2002/materials-project-mcp:latest
4 | container_name: materials-mcp
5 | environment:
6 | - MP_API_KEY=${MP_API_KEY}
7 | restart: unless-stopped
8 | stdin_open: true
9 | tty: true
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM python:3.12-slim
2 |
3 | # Install uv
4 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
5 |
6 | # Copy the application into the container
7 | COPY . /app
8 |
9 | # Install the application dependencies
10 | WORKDIR /app
11 | RUN uv sync --frozen --no-cache
12 |
13 | # Run server.py directly
14 | CMD ["/app/.venv/bin/python", "/app/server/server.py"]
```
--------------------------------------------------------------------------------
/utils/utility_functions.py:
--------------------------------------------------------------------------------
```python
1 | from pydantic import Field
2 | from dataclasses import dataclass
3 | from pathlib import Path
4 | from datetime import datetime
5 | from typing import Dict, List
6 | import os
7 |
8 |
9 |
10 | MARKDOWN_FOLDER = Path(os.path.dirname(__file__)).parent / "markdown_folder"
11 | print(MARKDOWN_FOLDER)
12 |
13 | @dataclass
14 | class MarkdownFile:
15 | name: str
16 | path: Path
17 | content: str = Field(default_factory=str)
18 |
19 |
20 | class MarkdownResourceManager:
21 | def __init__(self, folder_path: Path):
22 | self.folder_path = folder_path
23 | self.files: Dict[str, MarkdownFile] = {}
24 |
25 |
26 | def load_files(self):
27 | """"Load all markdown files from the folder path"""
28 |
29 | self.files.clear()
30 |
31 | if not self.folder_path.exists():
32 | print(f"Folder {self.folder_path} does not exist.")
33 | return "Folder does not exist."
34 |
35 | for file_path in self.folder_path.glob("*.md"):
36 | try:
37 | content = file_path.read_text(encoding="utf-8")
38 | last_modified = datetime.fromtimestamp(file_path.stat().st_mtime)
39 | markdown_file = MarkdownFile(
40 | name=file_path.stem,
41 | path=file_path,
42 | content=content,
43 | # last_modified=last_modified
44 | )
45 | self.files[file_path.stem] = markdown_file
46 | except Exception as e:
47 | print(f"Error reading file {file_path}: {e}")
48 |
49 |
50 | if __name__ == "__main__":
51 | manager = MarkdownResourceManager(MARKDOWN_FOLDER)
52 | manager.load_files()
53 | print(manager.files["docsmaterialsproject"].content)
54 |
```
--------------------------------------------------------------------------------
/utils/prompts_templates.py:
--------------------------------------------------------------------------------
```python
1 | from dataclasses import dataclass
2 |
3 |
4 |
5 |
6 | @dataclass
7 | class ElectronicBandStructurePrompt:
8 | """
9 | Dataclass to store the prompt template for electronic band structure tool usage.
10 | Contains the comprehensive template with instructions, parameters, and examples.
11 | """
12 | template: str = """
13 | # Electronic Band Structure Tool Usage Guide
14 |
15 | ## Tool Overview
16 | The get_electronic_bandstructure tool generates electronic band structure plots for materials from the Materials Project database. It returns the plot as a base64-encoded PNG image within a JSON response structure.
17 |
18 | ## Tool Parameters
19 | - material_id (required): Materials Project ID (e.g., 'mp-149', 'mp-22526')
20 | - path_type (optional): K-point path type:
21 | - 'setyawan_curtarolo' (default) - Standard path for cubic systems
22 | - 'hinuma' - Standard path for hexagonal systems
23 | - 'latimer_munro' - Alternative path for cubic systems
24 | - 'uniform' - Uniform k-point sampling (not recommended for plotting)
25 |
26 | ## Expected Output Format
27 | The tool returns a JSON object with the following structure:
28 |
29 | {
30 | "success": true,
31 | "material_id": "mp-149",
32 | "image_base64": "iVBORw0KGgoAAAANSUhEUgAAB...[~500KB-2MB base64 string]",
33 | "metadata": {
34 | "path_type": "setyawan_curtarolo",
35 | "description": "Band structure plot for material mp-149 using setyawan_curtarolo path",
36 | "width": 1200,
37 | "height": 800
38 | }
39 | }
40 |
41 |
42 | Note: The image_base64 field contains a very long base64 string (typically 500KB-2MB). For brevity, examples show truncated versions.
43 |
44 | ## How to Parse and Display the Response
45 |
46 | ### Method 1: Extract and Display Base64 Image (Python)
47 |
48 | import json
49 | import base64
50 | from PIL import Image
51 | import io
52 | import matplotlib.pyplot as plt
53 |
54 | def display_bandstructure(response):
55 | # Parse the JSON response
56 | data = json.loads(response) if isinstance(response, str) else response
57 |
58 | if not data.get("success"):
59 | print("Error: Tool execution failed")
60 | return
61 |
62 | # Extract base64 image data (this will be a very long string)
63 | image_base64 = data["image_base64"]
64 | material_id = data["material_id"]
65 | metadata = data["metadata"]
66 |
67 | # Decode base64 to image
68 | image_bytes = base64.b64decode(image_base64)
69 | image = Image.open(io.BytesIO(image_bytes))
70 |
71 | # Display the image
72 | plt.figure(figsize=(12, 8))
73 | plt.imshow(image)
74 | plt.axis('off')
75 | plt.title(f"Band Structure: {material_id} ({metadata['path_type']})")
76 | plt.tight_layout()
77 | plt.show()
78 |
79 | print(f"Material ID: {material_id}")
80 | print(f"Path Type: {metadata['path_type']}")
81 | print(f"Image Size: {metadata['width']} × {metadata['height']}")
82 |
83 | """
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | aiohappyeyeballs==2.6.1
2 | aiohttp==3.11.18
3 | aioitertools==0.12.0
4 | aiosignal==1.3.2
5 | annotated-types==0.7.0
6 | anyio==4.9.0
7 | appnope==0.1.4
8 | arrow==1.3.0
9 | ase==3.25.0
10 | asttokens==3.0.0
11 | atomate==1.1.0
12 | attrs==25.3.0
13 | bcrypt==4.3.0
14 | bibtexparser==1.4.3
15 | blinker==1.9.0
16 | boltons==25.0.0
17 | boto3==1.38.17
18 | botocore==1.38.17
19 | bravado==12.0.1
20 | bravado-core==6.1.1
21 | brewer2mpl==1.4.1
22 | cachetools==6.1.0
23 | castepxbin==0.3.0
24 | certifi==2025.4.26
25 | cffi==1.17.1
26 | charset-normalizer==3.4.2
27 | click==8.2.0
28 | colormath==3.0.0
29 | comm==0.2.2
30 | contourpy==1.3.2
31 | cryptography==44.0.3
32 | custodian==2025.5.12
33 | cycler==0.12.1
34 | debugpy==1.8.14
35 | decorator==5.2.1
36 | dnspython==2.7.0
37 | dotenv==0.9.9
38 | emmet==2018.6.7
39 | emmet-core==0.84.6
40 | executing==2.2.0
41 | fastapi==0.115.13
42 | filetype==1.2.0
43 | fireworks==2.0.4
44 | flask==3.1.1
45 | flask-paginate==2024.4.12
46 | flatten-dict==0.4.2
47 | flexcache==0.3
48 | flexparser==0.4
49 | fonttools==4.58.0
50 | fqdn==1.5.1
51 | frozenlist==1.6.0
52 | gunicorn==23.0.0
53 | h11==0.16.0
54 | h5py==3.13.0
55 | httpcore==1.0.9
56 | httpx==0.28.1
57 | httpx-sse==0.4.0
58 | idna==3.10
59 | imageio==2.37.0
60 | importlib-resources==6.5.2
61 | ipykernel==6.29.5
62 | ipython==9.2.0
63 | ipython-pygments-lexers==1.1.1
64 | isoduration==20.11.0
65 | itsdangerous==2.2.0
66 | jedi==0.19.2
67 | jinja2==3.1.6
68 | jmespath==1.0.1
69 | joblib==1.5.0
70 | json2html==1.3.0
71 | jsonlines==4.0.0
72 | jsonpointer==3.0.0
73 | jsonref==1.1.0
74 | jsonschema==4.23.0
75 | jsonschema-specifications==2025.4.1
76 | jupyter-client==8.6.3
77 | jupyter-core==5.7.2
78 | kiwisolver==1.4.8
79 | latexcodec==3.0.0
80 | lazy-loader==0.4
81 | maggma==0.71.5
82 | markdown-it-py==3.0.0
83 | markupsafe==3.0.2
84 | matminer==0.9.3
85 | matplotlib==3.10.3
86 | matplotlib-inline==0.1.7
87 | mcp==1.8.1
88 | mdurl==0.1.2
89 | mongomock==4.3.0
90 | monotonic==1.6
91 | monty==2025.3.3
92 | mp-api==0.45.5
93 | mp-pyrho==0.4.5
94 | mpcontribs-client==5.10.2
95 | mpmath==1.3.0
96 | msgpack==1.1.0
97 | multidict==6.4.3
98 | narwhals==1.39.1
99 | nest-asyncio==1.6.0
100 | networkx==3.4.2
101 | numpy==1.26.4
102 | orjson==3.10.18
103 | packaging==25.0
104 | palettable==3.3.3
105 | pandas==2.2.3
106 | paramiko==3.5.1
107 | parso==0.8.4
108 | pexpect==4.9.0
109 | phonopy==2.38.2
110 | pillow==11.2.1
111 | pint==0.24.4
112 | platformdirs==4.3.8
113 | plotly==6.1.0
114 | prettyplotlib==0.1.7
115 | prompt-toolkit==3.0.51
116 | propcache==0.3.1
117 | psutil==7.0.0
118 | ptyprocess==0.7.0
119 | pure-eval==0.2.3
120 | pybtex==0.24.0
121 | pycparser==2.22
122 | pydantic==2.11.4
123 | pydantic-core==2.33.2
124 | pydantic-settings==2.9.1
125 | pydash==8.0.5
126 | pygments==2.19.1
127 | pyisemail==2.0.1
128 | pymatgen==2025.1.9
129 | pymatgen-analysis-defects==2025.1.18
130 | pymatgen-analysis-diffusion==2024.7.15
131 | pymongo==4.10.1
132 | pynacl==1.5.0
133 | pyparsing==3.2.3
134 | python-dateutil==2.9.0.post0
135 | python-dotenv==1.1.0
136 | python-multipart==0.0.20
137 | pytz==2025.2
138 | pyyaml==6.0.2
139 | pyzmq==26.4.0
140 | referencing==0.36.2
141 | requests==2.32.3
142 | requests-futures==1.0.2
143 | rfc3339-validator==0.1.4
144 | rfc3986-validator==0.1.1
145 | rich==14.0.0
146 | rpds-py==0.25.0
147 | ruamel-yaml==0.18.10
148 | ruamel-yaml-clib==0.2.12
149 | s3transfer==0.12.0
150 | scikit-image==0.25.2
151 | scikit-learn==1.6.1
152 | scipy==1.15.3
153 | seaborn==0.13.2
154 | seekpath==2.1.0
155 | semantic-version==2.10.0
156 | sentinels==1.0.0
157 | setuptools==80.7.1
158 | shellingham==1.5.4
159 | simplejson==3.20.1
160 | six==1.17.0
161 | smart-open==7.1.0
162 | sniffio==1.3.1
163 | spglib==2.6.0
164 | sse-starlette==2.3.5
165 | sshtunnel==0.4.0
166 | stack-data==0.6.3
167 | starlette==0.46.2
168 | sumo==2.3.12
169 | swagger-spec-validator==3.0.4
170 | symfc==1.3.4
171 | sympy==1.14.0
172 | tabulate==0.9.0
173 | threadpoolctl==3.6.0
174 | tifffile==2025.5.10
175 | tornado==6.4.2
176 | tqdm==4.67.1
177 | traitlets==5.14.3
178 | typer==0.15.3
179 | types-python-dateutil==2.9.0.20250516
180 | typing-extensions==4.13.2
181 | typing-inspection==0.4.0
182 | tzdata==2025.2
183 | ujson==5.10.0
184 | uncertainties==3.2.3
185 | uri-template==1.3.0
186 | urllib3==2.4.0
187 | uvicorn==0.34.2
188 | wcwidth==0.2.13
189 | webcolors==24.11.1
190 | werkzeug==3.1.3
191 | wrapt==1.17.2
192 | yarl==1.20.0
193 | beautifulsoup4==4.13.4
194 | soupsieve==2.7
195 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "materials-project-mcp"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.12"
7 | dependencies = [
8 | "aiohappyeyeballs==2.6.1",
9 | "aiohttp==3.11.18",
10 | "aioitertools==0.12.0",
11 | "aiosignal==1.3.2",
12 | "annotated-types==0.7.0",
13 | "anyio==4.9.0",
14 | "appnope==0.1.4",
15 | "ase==3.25.0",
16 | "asttokens==3.0.0",
17 | "atomate==1.1.0",
18 | "attrs==25.3.0",
19 | "bcrypt==4.3.0",
20 | "bibtexparser==1.4.3",
21 | "blinker==1.9.0",
22 | "boto3==1.38.17",
23 | "botocore==1.38.17",
24 | "brewer2mpl==1.4.1",
25 | "castepxbin==0.3.0",
26 | "certifi==2025.4.26",
27 | "cffi==1.17.1",
28 | "charset-normalizer==3.4.2",
29 | "click==8.2.0",
30 | "colormath==3.0.0",
31 | "comm==0.2.2",
32 | "contourpy==1.3.2",
33 | "cryptography==44.0.3",
34 | "custodian==2025.5.12",
35 | "cycler==0.12.1",
36 | "debugpy==1.8.14",
37 | "decorator==5.2.1",
38 | "dnspython==2.7.0",
39 | "dotenv>=0.9.9",
40 | "emmet==2018.6.7",
41 | "emmet-core==0.84.6",
42 | "executing==2.2.0",
43 | "fastapi>=0.115.13",
44 | "fireworks==2.0.4",
45 | "flask==3.1.1",
46 | "flask-paginate==2024.4.12",
47 | "fonttools==4.58.0",
48 | "frozenlist==1.6.0",
49 | "gunicorn==23.0.0",
50 | "h11==0.16.0",
51 | "h5py==3.13.0",
52 | "httpcore==1.0.9",
53 | "httpx==0.28.1",
54 | "httpx-sse==0.4.0",
55 | "idna==3.10",
56 | "imageio==2.37.0",
57 | "importlib-resources==6.5.2",
58 | "ipykernel==6.29.5",
59 | "ipython==9.2.0",
60 | "ipython-pygments-lexers==1.1.1",
61 | "itsdangerous==2.2.0",
62 | "jedi==0.19.2",
63 | "jinja2==3.1.6",
64 | "jmespath==1.0.1",
65 | "joblib==1.5.0",
66 | "jsonlines==4.0.0",
67 | "jsonschema==4.23.0",
68 | "jsonschema-specifications==2025.4.1",
69 | "jupyter-client==8.6.3",
70 | "jupyter-core==5.7.2",
71 | "kiwisolver==1.4.8",
72 | "latexcodec==3.0.0",
73 | "lazy-loader==0.4",
74 | "maggma==0.71.5",
75 | "markdown-it-py==3.0.0",
76 | "markupsafe==3.0.2",
77 | "matminer==0.9.3",
78 | "matplotlib==3.10.3",
79 | "matplotlib-inline==0.1.7",
80 | "mcp[cli]==1.8.1",
81 | "mdurl==0.1.2",
82 | "mongomock==4.3.0",
83 | "monty==2025.3.3",
84 | "mp-api==0.45.5",
85 | "mp-pyrho==0.4.5",
86 | "mpcontribs-client>=5.10.2",
87 | "mpmath==1.3.0",
88 | "msgpack==1.1.0",
89 | "multidict==6.4.3",
90 | "narwhals==1.39.1",
91 | "nest-asyncio==1.6.0",
92 | "networkx==3.4.2",
93 | "numpy==1.26.4",
94 | "orjson==3.10.18",
95 | "packaging==25.0",
96 | "palettable==3.3.3",
97 | "pandas==2.2.3",
98 | "paramiko==3.5.1",
99 | "parso==0.8.4",
100 | "pexpect==4.9.0",
101 | "phonopy==2.38.2",
102 | "pillow==11.2.1",
103 | "platformdirs==4.3.8",
104 | "plotly==6.1.0",
105 | "prettyplotlib==0.1.7",
106 | "prompt-toolkit==3.0.51",
107 | "propcache==0.3.1",
108 | "psutil==7.0.0",
109 | "ptyprocess==0.7.0",
110 | "pure-eval==0.2.3",
111 | "pybtex==0.24.0",
112 | "pycparser==2.22",
113 | "pydantic==2.11.4",
114 | "pydantic-core==2.33.2",
115 | "pydantic-settings==2.9.1",
116 | "pydash==8.0.5",
117 | "pygments==2.19.1",
118 | "pymatgen==2025.1.9",
119 | "pymatgen-analysis-defects==2025.1.18",
120 | "pymatgen-analysis-diffusion==2024.7.15",
121 | "pymongo==4.10.1",
122 | "pynacl==1.5.0",
123 | "pyparsing==3.2.3",
124 | "python-dateutil==2.9.0.post0",
125 | "python-dotenv==1.1.0",
126 | "python-multipart==0.0.20",
127 | "pytz==2025.2",
128 | "pyyaml==6.0.2",
129 | "pyzmq==26.4.0",
130 | "referencing==0.36.2",
131 | "requests==2.32.3",
132 | "rich==14.0.0",
133 | "rpds-py==0.25.0",
134 | "ruamel-yaml==0.18.10",
135 | "ruamel-yaml-clib==0.2.12",
136 | "s3transfer==0.12.0",
137 | "scikit-image==0.25.2",
138 | "scikit-learn==1.6.1",
139 | "scipy==1.15.3",
140 | "seaborn==0.13.2",
141 | "seekpath==2.1.0",
142 | "sentinels==1.0.0",
143 | "setuptools==80.7.1",
144 | "shellingham==1.5.4",
145 | "six==1.17.0",
146 | "smart-open==7.1.0",
147 | "sniffio==1.3.1",
148 | "spglib==2.6.0",
149 | "sse-starlette==2.3.5",
150 | "sshtunnel==0.4.0",
151 | "stack-data==0.6.3",
152 | "starlette==0.46.2",
153 | "sumo==2.3.12",
154 | "symfc==1.3.4",
155 | "sympy==1.14.0",
156 | "tabulate==0.9.0",
157 | "threadpoolctl==3.6.0",
158 | "tifffile==2025.5.10",
159 | "tornado==6.4.2",
160 | "tqdm==4.67.1",
161 | "traitlets==5.14.3",
162 | "typer==0.15.3",
163 | "typing-extensions==4.13.2",
164 | "typing-inspection==0.4.0",
165 | "tzdata==2025.2",
166 | "uncertainties==3.2.3",
167 | "urllib3==2.4.0",
168 | "uvicorn==0.34.2",
169 | "wcwidth==0.2.13",
170 | "werkzeug==3.1.3",
171 | "wrapt==1.17.2",
172 | "yarl==1.20.0",
173 | ]
174 |
```
--------------------------------------------------------------------------------
/server/server.py:
--------------------------------------------------------------------------------
```python
1 | import os
2 | import logging
3 | from typing import Optional, List, Union
4 | from mcp.server.fastmcp import FastMCP
5 | from pydantic import Field, AnyUrl
6 | import matplotlib.pyplot as plt
7 | from pymatgen.electronic_structure.plotter import BSPlotter
8 | from pymatgen.electronic_structure.bandstructure import BandStructureSymmLine
9 | from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
10 | from pymatgen.analysis.diffraction.xrd import XRDCalculator
11 | from pymatgen.phonon.bandstructure import PhononBandStructureSymmLine
12 | from pymatgen.phonon.plotter import PhononBSPlotter
13 | from pymatgen.analysis.wulff import WulffShape
14 | from emmet.core.electronic_structure import BSPathType
15 | from typing import Literal
16 | from dotenv import load_dotenv
17 | from mp_api.client import MPRester
18 | import io
19 | import base64
20 | import sys
21 |
22 |
23 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
24 | # Setup logging
25 | logging.basicConfig(level=logging.INFO)
26 | logger = logging.getLogger("materials_project_mcp")
27 | load_dotenv()
28 | API_KEY = os.environ.get("MP_API_KEY")
29 |
30 | # Create the MCP server instance
31 | mcp = FastMCP()
32 |
33 |
34 | def _get_mp_rester() -> MPRester:
35 | """
36 | Initialize and return a MPRester session with the user's API key.
37 |
38 | Returns:
39 | MPRester: An authenticated MPRester instance for querying the Materials Project API.
40 |
41 | Note:
42 | If no API key is found in environment variables, attempts to initialize without key.
43 | """
44 | if not API_KEY:
45 | logger.warning(
46 | "No MP_API_KEY found in environment. Attempting MPRester() without key."
47 | )
48 | return MPRester()
49 | return MPRester(API_KEY)
50 |
51 |
52 | @mcp.tool()
53 | async def search_materials(
54 | elements: Optional[List[str]] = Field(
55 | default=None,
56 | description="List of element symbols to filter by (e.g. ['Si', 'O']). If None, searches across all elements.",
57 | ),
58 | band_gap_min: float = Field(
59 | default=0.0,
60 | description="Lower bound for band gap filtering in eV. Materials with band gaps below this value will be excluded.",
61 | ),
62 | band_gap_max: float = Field(
63 | default=10.0,
64 | description="Upper bound for band gap filtering in eV. Materials with band gaps above this value will be excluded.",
65 | ),
66 | is_stable: bool = Field(
67 | default=False,
68 | description="If True, only returns materials that are thermodynamically stable (energy above hull = 0). If False, returns all materials.",
69 | ),
70 | max_results: int = Field(
71 | default=50,
72 | ge=1,
73 | le=200,
74 | description="Maximum number of results to return. Must be between 1 and 200.",
75 | ),
76 | ) -> str:
77 | """
78 | Search for materials in the Materials Project database using various filters.
79 |
80 | This function allows searching for materials based on their elemental composition,
81 | band gap range, and thermodynamic stability. Results are returned in a formatted
82 | markdown string containing material IDs, formulas, band gaps, and energy above hull values.
83 |
84 | Args:
85 | elements: Optional list of element symbols to filter by (e.g. ['Si', 'O'])
86 | band_gap_min: Minimum band gap in eV (default: 0.0)
87 | band_gap_max: Maximum band gap in eV (default: 10.0)
88 | is_stable: Whether to only return stable materials (default: False)
89 | max_results: Maximum number of results to return (default: 50, max: 200)
90 |
91 | Returns:
92 | str: A formatted markdown string containing the search results
93 |
94 | Example:
95 | >>> search_materials(elements=['Si', 'O'], band_gap_min=1.0, band_gap_max=5.0)
96 | Returns materials containing Si and O with band gaps between 1 and 5 eV
97 | """
98 | logger.info("Starting search_materials query...")
99 | with _get_mp_rester() as mpr:
100 | docs = mpr.materials.summary.search(
101 | elements=elements,
102 | band_gap=(band_gap_min, band_gap_max),
103 | is_stable=is_stable,
104 | fields=["material_id", "formula_pretty", "band_gap", "energy_above_hull"],
105 | )
106 |
107 | # Truncate results to max_results
108 | docs = list(docs)[:max_results]
109 |
110 | if not docs:
111 | return "No materials found matching your criteria."
112 |
113 | results_md = (
114 | f"## Materials Search Results\n\n"
115 | f"- **Elements**: {elements or 'Any'}\n"
116 | f"- **Band gap range**: {band_gap_min} eV to {band_gap_max} eV\n"
117 | f"- **Stable only**: {is_stable}\n\n"
118 | f"**Showing up to {max_results} matches**\n\n"
119 | )
120 | for i, mat in enumerate(docs, 1):
121 | results_md += (
122 | f"**{i}.** ID: `{mat.material_id}` | Formula: **{mat.formula_pretty}** | "
123 | f"Band gap: {mat.band_gap:.3f} eV | E above hull: {mat.energy_above_hull:.3f} eV\n"
124 | )
125 | return results_md
126 |
127 |
128 | @mcp.tool()
129 | async def get_structure_by_id(
130 | material_id: str = Field(
131 | ...,
132 | description="Materials Project ID (e.g. 'mp-149'). Must be a valid MP ID."
133 | )
134 | ) -> str:
135 | """
136 | Retrieve and format the crystal structure for a given material from the Materials Project.
137 |
138 | This function fetches the final computed structure for a material and returns a
139 | formatted summary including the lattice parameters, number of sites, and chemical formula.
140 |
141 | Args:
142 | material_id: The Materials Project ID of the material (e.g. 'mp-149')
143 |
144 | Returns:
145 | str: A formatted markdown string containing the structure information
146 |
147 | Example:
148 | >>> get_structure_by_id('mp-149')
149 | Returns the crystal structure information for silicon (mp-149)
150 | """
151 | logger.info(f"Fetching structure for {material_id}...")
152 | with _get_mp_rester() as mpr:
153 | structure = mpr.get_structure_by_material_id(material_id)
154 |
155 | if not structure:
156 | return f"No structure found for {material_id}."
157 |
158 | formula = structure.composition.reduced_formula
159 | lattice = structure.lattice
160 | sites_count = len(structure)
161 | text_summary = (
162 | f"## Structure for {material_id}\n\n"
163 | f"- **Formula**: {formula}\n"
164 | f"- **Lattice**:\n"
165 | f" a = {lattice.a:.3f} Å, b = {lattice.b:.3f} Å, c = {lattice.c:.3f} Å\n"
166 | f" α = {lattice.alpha:.2f}°, β = {lattice.beta:.2f}°, γ = {lattice.gamma:.2f}°\n"
167 | f"- **Number of sites**: {sites_count}\n"
168 | f"- **Reduced formula**: {structure.composition.reduced_formula}\n"
169 | )
170 | return text_summary
171 |
172 |
173 | @mcp.tool()
174 | async def get_electronic_bandstructure(
175 | material_id: str = Field(
176 | ...,
177 | description="Materials Project ID (e.g. 'mp-149'). Must be a valid MP ID."
178 | ),
179 | path_type: Literal["setyawan_curtarolo", "hinuma", "latimer_munro", "uniform"] = Field(
180 | default="setyawan_curtarolo",
181 | description="Type of k-point path to use for the band structure plot. Options are:\n"
182 | "- setyawan_curtarolo: Standard path for cubic systems\n"
183 | "- hinuma: Standard path for hexagonal systems\n"
184 | "- latimer_munro: Alternative path for cubic systems\n"
185 | "- uniform: Uniform k-point sampling (not recommended for plotting)"
186 | ),
187 | ):
188 | """
189 | Generate and return a electronic band structure plot for a given material.
190 |
191 | This function fetches the band structure data from the Materials Project and creates
192 | a plot showing the electronic band structure along high-symmetry k-points. The plot
193 | is returned as a base64-encoded PNG image embedded in a markdown string.
194 |
195 | Args:
196 | material_id: The Materials Project ID of the material (e.g. 'mp-149')
197 | path_type: The type of k-point path to use for the band structure plot
198 |
199 | Returns:
200 | A plot of the electronic band structure
201 |
202 | Example:
203 | >>> get_electronic_bandstructure('mp-149', path_type='setyawan_curtarolo')
204 | Returns a band structure plot for silicon using the standard cubic path
205 | """
206 | logger.info(f"Plotting band structure for {material_id} with path_type: {path_type}")
207 |
208 | with _get_mp_rester() as mpr:
209 | if path_type == "uniform":
210 | bs = mpr.get_bandstructure_by_material_id(material_id, line_mode=False)
211 | else:
212 | bs = mpr.get_bandstructure_by_material_id(
213 | material_id, path_type=BSPathType(path_type)
214 | )
215 |
216 | if not isinstance(bs, BandStructureSymmLine):
217 | return f"Cannot plot `{path_type}` band structure. Only line-mode paths are plottable."
218 |
219 | # Generate the plot
220 | plotter = BSPlotter(bs)
221 | ax = plotter.get_plot()
222 | fig = ax.get_figure()
223 |
224 |
225 |
226 | plt.title(f"Band Structure for {material_id} using {path_type} path")
227 | plt.ylabel("Energy (eV)")
228 | plt.tight_layout()
229 | #plot the image
230 | # Save the figure to a buffer
231 | buffer = io.BytesIO()
232 | fig.savefig(buffer, format='png', dpi=150, bbox_inches='tight')
233 | buffer.seek(0)
234 | #plt.close(fig) # Close the figure to free memory
235 | # return image object
236 | return fig
237 |
238 |
239 |
240 |
241 | ## save to buffer
242 | #buffer = io.BytesIO()
243 | #fig.savefig(buffer, format='png', dpi=70, bbox_inches='tight')
244 | #plt.close(fig) # Close the figure to free memory
245 |
246 | ## figure dimensions
247 | #fig_width = fig.get_figwidth() * fig.dpi
248 | #fig_height = fig.get_figheight() * fig.dpi
249 |
250 |
251 | #band_image_data = buffer.getvalue()
252 | #image_base64 = base64.b64encode(band_image_data).decode('ascii')
253 |
254 | #return {
255 | # "success": True,
256 | # "material_id": material_id,
257 | # "image_base64": image_base64[:40000],
258 | # "metadata": {
259 | # # "material_id": material_id,
260 | # "path_type": path_type,
261 | # "description": f"Band structure plot for material {material_id} using {path_type} path",
262 | # "width": int(fig_width),
263 | # "height": int(fig_height)
264 | # }
265 | #}
266 |
267 |
268 | @mcp.tool()
269 | async def get_electronic_dos_by_id(
270 | material_id: str = Field(
271 | ...,
272 | description="Materials Project ID (e.g. 'mp-149'). Must be a valid MP ID."
273 | ),
274 | ) -> str:
275 | """
276 | Retrieve the electronic density of states (DOS) data for a given material.
277 |
278 | This function fetches the electronic density of states data from the Materials Project
279 | for the specified material. The DOS data includes information about the
280 | electronic states available to electrons in the material.
281 |
282 | Args:
283 | material_id: The Materials Project ID of the material (e.g. 'mp-149')
284 |
285 | Returns:
286 | str: A string containing the density of states information
287 |
288 | Example:
289 | >>> get_electronic_dos_by_id('mp-149')
290 | Returns the electronic density of states data for silicon
291 | """
292 | logger.info(f"Fetching electronic density of states for {material_id}...")
293 | with _get_mp_rester() as mpr:
294 | dos = mpr.get_dos_by_material_id(material_id)
295 |
296 | if not dos:
297 | return f"No density of states found for {material_id}."
298 |
299 | return f"Electronic density of states for {material_id}: {dos}"
300 |
301 | #phonons
302 | @mcp.tool()
303 | async def get_phonon_bandstructure(
304 | material_id: str = Field(
305 | ...,
306 | description="Materials Project ID (e.g. 'mp-149'). Must be a valid MP ID."
307 | ),
308 | ) -> str:
309 | """
310 | Retrieve the phonon band structure for a given material.
311 |
312 | This function fetches the phonon band structure data from the Materials Project
313 | for the specified material. The phonon band structure includes information about
314 | the vibrational modes and frequencies of the material.
315 |
316 | Args:
317 | material_id: The Materials Project ID of the material (e.g. 'mp-149')
318 |
319 | Returns:
320 | A plot of the phonon band structure
321 | """
322 |
323 | logger.info(f"Fetching phonon band structure for {material_id}...")
324 | with _get_mp_rester() as mpr:
325 | bs = mpr.get_phonon_bandstructure_by_material_id(material_id)
326 |
327 | if not isinstance(bs, PhononBandStructureSymmLine):
328 | return "Cannot plot phonon band structure. Only line-mode paths are plottable."
329 |
330 | plotter = PhononBSPlotter(bs)
331 | fig = plotter.get_plot()
332 |
333 | plt.title(f"Phonon Band Structure for {material_id}")
334 | plt.ylabel("Frequency (THz)")
335 | plt.tight_layout()
336 | # Save the figure to a buffer
337 | buffer = io.BytesIO()
338 | fig.savefig(buffer, format='png', dpi=300, bbox_inches='tight')
339 | plt.close(fig)
340 |
341 | # Convert the buffer to base64
342 | phonon_image_data = buffer.getvalue()
343 | image_base64 = base64.b64encode(phonon_image_data).decode('ascii')
344 |
345 | # figure dimensions
346 | fig_width = fig.get_figwidth() * fig.dpi
347 | fig_height = fig.get_figheight() * fig.dpi
348 |
349 |
350 | return {
351 | "success": True,
352 | "material_id": material_id,
353 | "image_base64": image_base64,
354 | "metadata": {
355 | "path_type": "phonon",
356 | "description": f"Phonon band structure plot for material {material_id}",
357 | "width": int(fig_width),
358 | "height": int(fig_height)
359 | }
360 | }
361 |
362 |
363 |
364 |
365 | @mcp.tool()
366 | async def get_phonon_dos_by_id(
367 | material_id: str = Field(
368 | ...,
369 | description="Materials Project ID (e.g. 'mp-149'). Must be a valid MP ID."
370 | ),
371 | ) -> str:
372 | """
373 | Retrieve the phonon density of states (DOS) data for a given material.
374 |
375 | This function fetches the phonon density of states data from the Materials Project
376 | for the specified material. The DOS data includes information about the
377 | vibrational modes and frequencies of the material.
378 | """
379 | logger.info(f"Fetching phonon density of states for {material_id}...")
380 | with _get_mp_rester() as mpr:
381 | dos = mpr.get_phonon_dos_by_material_id(material_id)
382 |
383 | if not dos:
384 | return f"No density of states found for {material_id}."
385 |
386 | return f"Phonon density of states for {material_id}: {dos}"
387 |
388 |
389 | @mcp.tool()
390 | async def get_ion_reference_data_for_chemsys(
391 | chemsys: Optional[Union[List, str]] = Field(
392 | ...,
393 | description="Chemical system string comprising element symbols separated by dashes, e.g., 'Li-Fe-O' or List of element symbols, e.g., ['Li', 'Fe', 'O']"
394 | )
395 | ) -> str:
396 | """
397 | Downloads aqueouse ion reference data used in the contruction Pourbaix
398 | The data returned from this method can be passed to get_ion_entries().
399 |
400 | Args:
401 | chemsys (str | list): Chemical system string comprising element
402 | symbols separated by dashes, e.g., "Li-Fe-O" or List of element
403 | symbols, e.g., ["Li", "Fe", "O"].
404 |
405 | Returns:
406 | str: markdown format of the reference data for ions
407 | """
408 |
409 | logger.info("Fetch reference data for ion by Chemsys")
410 | mpr_rester = _get_mp_rester()
411 |
412 | with mpr_rester as mpr:
413 | ion_reference_data = mpr.get_ion_reference_data_for_chemsys(chemsys=chemsys)
414 |
415 | if not ion_reference_data:
416 | logger.info(f"data not found for {chemsys}")
417 | return f"No ion reference data for {chemsys}"
418 |
419 |
420 | ion_data = f"Ion Reference Data for Chemical System: {chemsys}\n\n"
421 |
422 | for idx, ion in enumerate(ion_reference_data, 1):
423 | identifier = ion.get("identifier", "Unknown")
424 | formula = ion.get("formula", "Unknown")
425 | data = ion.get("data", {})
426 |
427 | # get the properties for idx
428 | charge_info = data.get("charge", {})
429 | charge_value = charge_info.get('value', 0)
430 | charge_display = charge_info.get('display', str(charge_value))
431 |
432 | delta_gf_info = data.get('ΔGᶠ', {})
433 | delta_gf_value = delta_gf_info.get('value', 'N/A')
434 | delta_gf_display = delta_gf_info.get('display', f'{delta_gf_value} kJ/mol' if delta_gf_value != 'N/A' else 'N/A')
435 |
436 | maj_elements = data.get('MajElements', 'Unknown')
437 | ref_solid = data.get('RefSolid', 'Unknown')
438 |
439 | ref_solid_info = data.get('ΔGᶠRefSolid', {})
440 | ref_solid_value = ref_solid_info.get('value', 'N/A')
441 | ref_solid_display = ref_solid_info.get('display', f'{ref_solid_value} kJ/mol' if ref_solid_value != 'N/A' else 'N/A')
442 |
443 | reference = data.get('reference', 'No reference provided')
444 |
445 | ion_data += f"""## {idx}. {identifier}
446 |
447 | | Property | Value |
448 | |----------|--------|
449 | | *Formula* | {formula} |
450 | | *Charge* | {charge_display} |
451 | | *Formation Energy (ΔGᶠ)* | {delta_gf_display} |
452 | | *Major Elements* | {maj_elements} |
453 | | *Reference Solid* | {ref_solid} |
454 | | *Ref. Solid ΔGᶠ* | {ref_solid_display} |
455 |
456 | *Reference:* {reference}
457 | """
458 |
459 | return ion_data
460 |
461 |
462 | @mcp.tool()
463 | async def get_cohesive_energy(
464 | material_ids: List[str] = Field(
465 | ...,
466 | description="List of Material IDs to compute cohesive energies"
467 | ),
468 | normalization: str = Field(
469 | default="atom",
470 | description="The normalization to use, whether to normalize cohesive energy by number of atoms (deflaut) or by number of formula units "
471 | )
472 | ) -> str:
473 | """
474 | Obtain the cohesive energy of the structure(s) corresponding to single or multiple material IDs
475 |
476 | Args:
477 | material_ids: List to material IDs to compute their cohesive energy
478 | normalization: Whether to normalize cohesive energy using number of atoms or number of formula
479 |
480 | Returns:
481 | str: The Markdown of cohesive energies (in eV/atom or eV/formula unit) for
482 | each material, indexed by Material IDs .
483 |
484 | """
485 | logger.info("Getting cohesive energy for material IDs")
486 |
487 | with _get_mp_rester() as mpr:
488 | cohesive_energies = mpr.get_cohesive_energy(material_ids=material_ids, normalization=normalization)
489 |
490 | if not cohesive_energies:
491 | logger.info(f"No cohesive energy was retrived for {material_ids}")
492 | return f"No cohesive energies found for these Material IDs: {material_ids}"
493 |
494 |
495 | energies = f"## Cohesive Energies \n"
496 | for identifier, energy in cohesive_energies.items():
497 | unit = "eV/atom" if normalization == "atom" else "eV/formula unit"
498 | energies += f"-- **{identifier}** : {energy} {unit}\n"
499 |
500 | return energies
501 |
502 |
503 | @mcp.tool()
504 | async def get_atom_reference_data(
505 | funcs: tuple[str, ...] = Field(
506 | default=("PBE",),
507 | description="list of functionals to retrieve data for "
508 | )
509 | ) -> str:
510 | """
511 | Retrieve reference energies of isolated neutral atoms. this energies can be used to calculate formations energies of compounds,
512 | Write the meaning of these funcs eg thier full names
513 | Args:
514 | funcs ([str] or None ) : list of functionals to retrieve data for.
515 | Returns:
516 | str : Markdown containing isolated atom energies
517 | """
518 | logger.info("Getting Atom Reference Data")
519 | with _get_mp_rester() as mpr:
520 | atom_data = mpr.get_atom_reference_data(funcs=funcs)
521 |
522 | if not atom_data:
523 | return f"No atom data retrieved for functionals {funcs}"
524 |
525 | atom_references = "| Element | Reference Energy (eV/atom) |\n"
526 |
527 | for element, energy in atom_data.items():
528 | atom_references += f"| **{element}** | {energy} | \n"
529 |
530 | return atom_references
531 |
532 |
533 |
534 | @mcp.tool()
535 | async def get_magnetic_data_by_id(
536 | material_ids: list[str] = Field(
537 | ...,
538 | description="Material ID of the material"
539 | ),
540 | ) -> str:
541 | """
542 | Get magnetic data using material ID. The materials api provides computed
543 | magnetic propertics from Density Functional Theory (DFT) calculations. This includes
544 | 1. Magnetic ordering
545 | 2. Total Magnetization
546 | 3. Site-projected Magnetic Moments
547 | 4. Spin-polarized electronic structures
548 |
549 | Args:
550 | material_id: Material ID of the material e.g., mp-20664, which is Mn2Sb
551 |
552 | Returns:
553 | (str): returns a markdown string containing the magnetic data for the material.
554 | """
555 | logger.info(f"Getting magnetic data for material{material_ids}")
556 | with _get_mp_rester() as mpr:
557 | magnetic_data = mpr.magnetism.search(material_ids=material_ids)
558 |
559 |
560 | if not magnetic_data:
561 | logger.info(f"Not data collected for {material_ids}")
562 | return f"No magnetic data found for material {material_ids}"
563 |
564 | data_md = f"|## Magnetic Data for Material IDs |\n\n"
565 | for idx, model in enumerate(magnetic_data):
566 | data_md += f"idx : {idx}"
567 | data = model.model_dump()
568 | for key, value in data.items():
569 | data_md += f"| **{key} : {value} |\n\n"
570 |
571 |
572 | return data_md
573 |
574 |
575 |
576 | @mcp.tool()
577 | async def get_charge_density_by_id(
578 | material_ids: str = Field(
579 | ...,
580 | description="Material ID of the material"
581 | )
582 | ):
583 | """
584 | Get charge density data for a given materials project ID
585 | Args:
586 | material_id: Material Project ID
587 |
588 | Returns:
589 | str :
590 |
591 | """
592 | logging.info(f"Getting charge density of material {material_ids}")
593 | with _get_mp_rester() as mpr:
594 | charge_density = mpr.get_charge_density_from_material_id(material_id=material_ids)
595 | logger.info(f"Charge density data retrieved for {material_ids}")
596 |
597 |
598 | if not charge_density:
599 | return f"No data found for material {charge_density}"
600 |
601 | density_data = f"""
602 | ## Material ID: {material_ids}
603 |
604 | ### Structure Summary:
605 | {charge_density.structure}
606 |
607 | ### Charge Density (Total):
608 | {charge_density.data["total"]}
609 |
610 | ### Is charge Density Polarized :
611 | {charge_density.is_spin_polarized}
612 |
613 | """
614 |
615 | return density_data
616 |
617 |
618 | @mcp.tool()
619 | async def get_dielectric_data_by_id(
620 | material_id: str = Field(
621 | ...,
622 | description="Material ID of the material"
623 | )
624 | ) -> str:
625 | """
626 | Gets the dielectric data for a given material. Dielectric is a
627 | material the can be polarized by an applied electric field.
628 | The mathematical description of the dielectric effect is a tensor
629 | constant of proportionality that relates an externally applied electric
630 | field to the field within the material
631 |
632 | Args:
633 | material_id (str): Material ID for the material
634 |
635 | Returns:
636 | str: markdown of the dielectric data
637 |
638 |
639 | """
640 | logger.info(f"Getting Dielectric data for material: {material_id}")
641 | with _get_mp_rester() as mpr:
642 | dielectric_data = mpr.materials.dielectric.search(material_id)
643 |
644 | if not dielectric_data:
645 | logger.info(f"No data found for material {material_id}")
646 | return f"No data for the material: {material_id}"
647 |
648 | data_md = f"|## Dielectric Data for Material IDs |\n\n"
649 | for idx, model in enumerate(dielectric_data):
650 | data_md += f"idx : {idx}"
651 | data = model.model_dump()
652 | for key, value in data.items():
653 | data_md += f"| **{key} : {value} |\n\n"
654 |
655 | return data_md
656 |
657 |
658 |
659 | @mcp.tool()
660 | async def get_diffraction_patterns(
661 | material_id : str = Field(
662 | ...,
663 | description="Material ID of the material "
664 | )
665 | ) -> str:
666 | """
667 | Gets diffraction patterns of a material given its ID.
668 | Diffraction occurs when waves (electrons, x-rays, neutrons)
669 | scattering from obstructions act as a secondary sources of propagations
670 |
671 | Args:
672 | material id (str): the material id of the material to get the diffracton pattern
673 |
674 |
675 | Return:
676 | str: markdown of the patterns
677 |
678 | """
679 | logger.info(f"Getting the Diffraction Pattern of element: {material_id}")
680 |
681 | with _get_mp_rester() as mpr:
682 | # first retrieve the relevant structure
683 | structure = mpr.get_structure_by_material_id(material_id)
684 | try:
685 | sga = SpacegroupAnalyzer(structure=structure)
686 | conventional_structure = sga.get_conventional_standard_structure()
687 | calculator = XRDCalculator(wavelength="CuKa")
688 | pattern = calculator.get_pattern(conventional_structure)
689 | return str(pattern)
690 | except:
691 | logging.error("Error occurred when function get_diffraction_patterns ")
692 | return f"No diffraction pattern retrieved for material : {material_id}"
693 |
694 |
695 |
696 |
697 | @mcp.tool()
698 | async def get_xRay_absorption_spectra(
699 | material_ids: List[str] = Field(
700 | ...,
701 | description="Material ID of the material"
702 | )
703 | ) -> str:
704 | """
705 | Obtain X-ray Absorption Spectra using single or multiple IDs,
706 | following the methodology as discussed by Mathew et al and Chen et al.
707 |
708 | Args:
709 | material_ids (List[str]) : material_ids of the elements
710 |
711 | Return:
712 | str:
713 |
714 | """
715 | logging.info("")
716 | with _get_mp_rester() as mpr:
717 | xas_doc = mpr.materials.xas.search(material_ids=material_ids)
718 |
719 | if not xas_doc:
720 | logging.info(f"No data retrieve for material(s) : {material_ids}")
721 | return f"No data retrieve for material(s) : {material_ids}"
722 |
723 | data_md = f"|## X-ray absorption spectra for Material IDs |\n\n"
724 | for idx, model in enumerate(xas_doc):
725 | data_md += f"idx : {idx}"
726 | data = model.model_dump()
727 | for key, value in data.items():
728 | data_md += f"| **{key} : {value} |\n\n"
729 |
730 | return data_md
731 |
732 |
733 | @mcp.tool()
734 | async def get_elastic_constants(
735 | material_ids: List[str] = Field(
736 | ...,
737 | description="Material ID of the material"
738 | )
739 | ):
740 | """
741 | Obtain Elastic constants given material IDs.
742 | Elasticity describes a material's ability to resist deformations
743 | (i.e. size and shape) when subjected to external forces.
744 |
745 | :param material_ids : material ID(s) of the elements
746 |
747 | :return:
748 | str: markdown of the elastic constants
749 |
750 | """
751 | logging.info(f"Getting Elastic Constant for material(s): {material_ids}")
752 | with _get_mp_rester() as mpr:
753 | elasticity_doc = mpr.materials.elasticity.search(material_ids=material_ids)
754 |
755 | if not elasticity_doc:
756 | return f"No Elasticity data retrieved for material: {material_ids}"
757 |
758 | data_md = f"|## Elastic Constants |\n\n"
759 | for idx, model in enumerate(elasticity_doc):
760 | data_md += f"idx : {idx}"
761 | data = model.model_dump()
762 | for key, value in data.items():
763 | data_md += f"| **{key} : {value} |\n\n"
764 |
765 | return data_md
766 |
767 |
768 |
769 |
770 |
771 | @mcp.tool()
772 | async def get_suggested_substrates(
773 | material_id: str = Field(
774 | ...,
775 | description="Material ID of the material"
776 | )
777 | ) -> str:
778 | """
779 | Obtains Suggested substrates for a film material.
780 | It helps to find suitable substrate materials for thin films
781 |
782 | Args:
783 | material_id (str): material ID of the material
784 |
785 | Returns:
786 | str: markdown of the data
787 |
788 | """
789 | logging.info(f"Getting suggested substrates for the material : {material_id}")
790 | with _get_mp_rester() as mpr:
791 | substrates_doc = mpr.materials.substrates.search(film_id=material_id)
792 |
793 | if not substrates_doc:
794 | return f"No substrates gotten for material: {material_id}"
795 |
796 | sub_md = f""
797 | for idx, data in enumerate(substrates_doc):
798 | sub_md += f"## Substrate {idx + 1}\n\n"
799 | # Create a detailed view for each substrate
800 | sub_md += f"- **Index**: {idx}\n"
801 | sub_md += f"- **Substrate Formula**: {getattr(data, 'sub_form', 'N/A')}\n"
802 | sub_md += f"- **Substrate ID**: {getattr(data, 'sub_id', 'N/A')}\n"
803 | sub_md += f"- **Film Orientation**: {getattr(data, 'film_orient', 'N/A')}\n"
804 | sub_md += f"- **Area**: {getattr(data, 'area', 'N/A')}\n"
805 | sub_md += f"- **Energy**: {getattr(data, 'energy', 'N/A')}\n"
806 | sub_md += f"- **Film ID**: {getattr(data, 'film_id', 'N/A')}\n"
807 | sub_md += f"- **Orientation**: {getattr(data, 'orient', 'N/A')}\n\n"
808 | sub_md += "---\n\n"
809 |
810 | return sub_md
811 |
812 |
813 |
814 | @mcp.tool()
815 | async def get_thermo_stability(
816 | material_ids: List[str] = Field(
817 | ...,
818 | description="Materials IDs of the material"
819 | ),
820 | thermo_types: List[str] = Field(
821 | default=["GGA_GGA+U_R2SCAN"],
822 | description=""
823 | )
824 | ) -> str:
825 | """
826 | Obtains thermodynamic stability data for a material
827 |
828 | Args:
829 | material_ids (List[str]) : A list of the material ID(s) eg. ["mp-861883"]
830 | thermo_types (List[str]) :
831 |
832 | Returns:
833 | str: Markdown of the thermodynamic stability data
834 |
835 | """
836 | logging.info(f"Getting thermodynamic stability for material(s):{material_ids}")
837 | with _get_mp_rester() as mpr:
838 | thermo_docs = mpr.materials.thermo.search(
839 | material_ids=material_ids,
840 | thermo_types=thermo_types
841 | )
842 |
843 | if not thermo_docs:
844 | logging.info("No thermodynamic stability data retrieved for ")
845 | return f"No thermodynamic stability data retrieved for materials: {material_ids}"
846 |
847 | thermo_md = f"Thermodynamic Stability for: {material_ids}"
848 | for idx, data in enumerate(thermo_docs):
849 | thermo_md += f"\n--- Material {idx + 1} ---\n"
850 |
851 | energy_above_hull = getattr(data, "energy_above_hull", "Not available")
852 | thermo_md += f"| ** | energy_above_hull : {energy_above_hull} | \n"
853 |
854 | formation_energy = getattr(data, "formation_energy_per_atom", "Not available")
855 | thermo_md += f"| ** | formation_energy_per_atom : {formation_energy} | \n"
856 |
857 | thermo_type = getattr(data, "thermo_type", "Not available")
858 | thermo_md += f"| ** | thermo_type : {thermo_type} | \n"
859 |
860 |
861 | is_stable = getattr(data, "is_stable", False)
862 | thermo_md += f"| ** | is_stable : {is_stable} | \n"
863 |
864 | formula_pretty = getattr(data, 'formula_pretty', 'Not available')
865 | thermo_md += f"| ** | formula : {formula_pretty} | \n"
866 |
867 | return thermo_md
868 |
869 |
870 | @mcp.tool()
871 | async def get_surface_properties(
872 | material_id: str = Field(
873 | ...,
874 | description="Material ID of the material"
875 | ),
876 | ) -> str:
877 | """
878 | Gets Surface properties data for materials as discussed by
879 | the methodology by Tran et. al.
880 |
881 | :param
882 | material_id: Material ID for the material
883 | response_limit (int) : Response limit for each call
884 | :return:
885 | Markdown of the surface data
886 |
887 | """
888 | logging.info(f"Getting surface data for material: {material_id}")
889 | with _get_mp_rester() as mpr:
890 | surface_docs = mpr.materials.surface_properties.search(material_id)
891 |
892 | if not surface_docs:
893 | logging.info(f"No surface data retrieved for material: {material_id}")
894 | return f"No surface data retrieved for material: {material_id}"
895 |
896 | surface_md = f"# Surface Properties for material: {material_id}\n\n"
897 |
898 | # surface_docs is a list of surface documents
899 | for idx, surface_doc in enumerate(surface_docs):
900 | # Access the surfaces from each document
901 | if hasattr(surface_doc, 'surfaces') and surface_doc.surfaces:
902 | for surface_idx, surface in enumerate(surface_doc.surfaces):
903 | miller_index = surface.miller_index
904 | miller_str = f"({miller_index[0]}{miller_index[1]}{miller_index[2]})"
905 |
906 | surface_md += f"## Surface {idx + 1}.{surface_idx + 1}: {miller_str}\n\n"
907 | surface_md += f"- **Miller Index:** {miller_index}\n"
908 | surface_md += f"- **Surface Energy:** {surface.surface_energy:.4f} J/m²\n"
909 | surface_md += f"- **Surface Energy (eV/Ų):** {getattr(surface, 'surface_energy_EV_PER_ANG2', 'N/A')}\n"
910 | surface_md += f"- **Work Function:** {getattr(surface, 'work_function', 'N/A')} eV\n"
911 | surface_md += f"- **Fermi Energy:** {getattr(surface, 'efermi', 'N/A')} eV\n"
912 | surface_md += f"- **Area Fraction:** {getattr(surface, 'area_fraction', 'N/A')}\n"
913 | surface_md += f"- **Is Reconstructed:** {'Yes' if getattr(surface, 'is_reconstructed', False) else 'No'}\n"
914 | surface_md += f"- **Has Wulff Shape:** {'Yes' if getattr(surface, 'has_wulff', False) else 'No'}\n\n"
915 |
916 | # Add material properties from the document
917 | surface_md += f"### Material Properties\n\n"
918 | surface_md += f"- **Material ID:** {getattr(surface_doc, 'material_id', 'N/A')}\n"
919 | surface_md += f"- **Formula:** {getattr(surface_doc, 'formula_pretty', 'N/A')}\n"
920 | surface_md += f"- **Crystal System:** {getattr(surface_doc, 'crystal_system', 'N/A')}\n"
921 | surface_md += f"- **Space Group:** {getattr(surface_doc, 'space_group', 'N/A')}\n\n"
922 |
923 | return surface_md
924 |
925 |
926 | @mcp.tool()
927 | async def get_grain_boundaries(
928 | material_id: str = Field(
929 | ...,
930 | description="Material ID of the material"
931 | )
932 | ):
933 |
934 | """
935 | Get Computed Grain Boundaries for a material.
936 |
937 | :param material_id (str): Material ID of the material
938 |
939 | :return:
940 | Markdown of the grain boundaries data
941 | """
942 | logger.info(f"Getting Grain Boundaries for material: {material_id}")
943 | with _get_mp_rester() as mpr:
944 | grain_boundary_docs = mpr.materials.grain_boundaries.search(material_id)
945 |
946 | if not grain_boundary_docs:
947 | logger.info(f"No Grain Boundaries data for material: {material_id}")
948 | return f"No Grain Boundaries data for material: {material_id}"
949 |
950 | grain_md = f"# Grain Boundaries for material: {material_id} \n\n"
951 | for idx, data in enumerate(grain_boundary_docs):
952 | grain_md += f"- **Initial Structure : ** {getattr(data, "initial_structure", "N/A")}\n\n"
953 | grain_md += f"- ** Final Structure : ** {getattr(data, "final_structure", "N/A")} \n\n"
954 | return grain_md
955 |
956 |
957 | @mcp.tool()
958 | async def get_insertion_electrodes(
959 | material_id: str = Field(
960 | ...,
961 | description="Material ID of the material"
962 | )
963 | ) -> str:
964 | """
965 | Get Insertion Electrodes data for a material.
966 |
967 |
968 | :param
969 | material_id (str): Material ID of the material
970 | :return
971 | str: Markdown of the Insertion Electrodes data
972 |
973 | """
974 | logger.info(f"Getting Insertion Electrodes data for material: {material_id}")
975 | with _get_mp_rester() as mpr:
976 | electrodes_docs = mpr.materials.insertion_electrodes.search(material_id)
977 |
978 |
979 | if not electrodes_docs:
980 | logger.info(f"No Insertion Electrodes data for material: {material_id}")
981 | return f"No Insertion Electrodes data for material: {material_id}"
982 |
983 | electrodes_md = f"# Insertion Electrodes for material: {material_id}\n\n"
984 | for idx, data in enumerate(electrodes_docs):
985 | electrodes_md += f"## Electrode {idx + 1}\n\n"
986 | electrodes_md += f"- **Battery Type:** {getattr(data, 'battery_type', 'N/A')}\n"
987 | electrodes_md += f"- **Battery ID:** {getattr(data, 'battery_id', 'N/A')}\n"
988 | electrodes_md += f"- **Battery Formula:** {getattr(data, 'battery_formula', 'N/A')}\n"
989 | electrodes_md += f"- **Working Ion:** {getattr(data, 'working_ion', 'N/A')}\n"
990 | electrodes_md += f"- **Number of Steps:** {getattr(data, 'num_steps', 'N/A')}\n"
991 | electrodes_md += f"- **Max Voltage Step:** {getattr(data, 'max_voltage_step', 'N/A')}\n"
992 | electrodes_md += f"- **Last Updated:** {getattr(data, 'last_updated', 'N/A')}\n"
993 | electrodes_md += f"- **Framework:** {getattr(data, 'framework', 'N/A')}\n"
994 | electrodes_md += f"- **Framework Formula:** {getattr(data, 'framework_formula', 'N/A')}\n"
995 | electrodes_md += f"- **Elements:** {getattr(data, 'elements', 'N/A')}\n"
996 | electrodes_md += f"- **Number of Elements:** {getattr(data, 'nelements', 'N/A')}\n"
997 | electrodes_md += f"- **Chemical System:** {getattr(data, 'chemsys', 'N/A')}\n"
998 | electrodes_md += f"- **Formula Anonymous:** {getattr(data, 'formula_anonymous', 'N/A')}\n"
999 | electrodes_md += f"- **Warnings:** {getattr(data, 'warnings', 'N/A')}\n"
1000 | electrodes_md += f"- **Formula Charge:** {getattr(data, 'formula_charge', 'N/A')}\n"
1001 | electrodes_md += f"- **Formula Discharge:** {getattr(data, 'formula_discharge', 'N/A')}\n"
1002 | electrodes_md += f"- **Max Delta Volume:** {getattr(data, 'max_delta_volume', 'N/A')}\n"
1003 | electrodes_md += f"- **Average Voltage:** {getattr(data, 'average_voltage', 'N/A')}\n"
1004 | electrodes_md += f"- **Capacity Gravimetric:** {getattr(data, 'capacity_grav', 'N/A')}\n"
1005 | electrodes_md += f"- **Capacity Volumetric:** {getattr(data, 'capacity_vol', 'N/A')}\n"
1006 | electrodes_md += f"- **Energy Gravimetric:** {getattr(data, 'energy_grav', 'N/A')}\n"
1007 | electrodes_md += f"- **Energy Volumetric:** {getattr(data, 'energy_vol', 'N/A')}\n"
1008 | electrodes_md += f"- **Fraction A Charge:** {getattr(data, 'fracA_charge', 'N/A')}\n"
1009 | electrodes_md += f"- **Fraction A Discharge:** {getattr(data, 'fracA_discharge', 'N/A')}\n"
1010 | electrodes_md += f"- **Stability Charge:** {getattr(data, 'stability_charge', 'N/A')}\n"
1011 | electrodes_md += f"- **Stability Discharge:** {getattr(data, 'stability_discharge', 'N/A')}\n"
1012 | electrodes_md += f"- **ID Charge:** {getattr(data, 'id_charge', 'N/A')}\n"
1013 | electrodes_md += f"- **ID Discharge:** {getattr(data, 'id_discharge', 'N/A')}\n"
1014 | electrodes_md += f"- **Host Structure:** {getattr(data, 'host_structure', 'N/A')}\n"
1015 |
1016 | # Add adjacent pairs information
1017 | adj_pairs = getattr(data, 'adj_pairs', [])
1018 | if adj_pairs:
1019 | electrodes_md += f"\n### Adjacent Pairs:\n"
1020 | for pair in adj_pairs:
1021 | # Use getattr instead of .get for pydantic/model objects
1022 | electrodes_md += f"- **Formula Charge:** {getattr(pair, 'formula_charge', 'N/A')}\n"
1023 | electrodes_md += f"- **Formula Discharge:** {getattr(pair, 'formula_discharge', 'N/A')}\n"
1024 | electrodes_md += f"- **Max Delta Volume:** {getattr(pair, 'max_delta_volume', 'N/A')}\n"
1025 | electrodes_md += f"- **Average Voltage:** {getattr(pair, 'average_voltage', 'N/A')}\n"
1026 | electrodes_md += f"- **Capacity Gravimetric:** {getattr(pair, 'capacity_grav', 'N/A')}\n"
1027 | electrodes_md += f"- **Capacity Volumetric:** {getattr(pair, 'capacity_vol', 'N/A')}\n"
1028 | electrodes_md += f"- **Energy Gravimetric:** {getattr(pair, 'energy_grav', 'N/A')}\n"
1029 | electrodes_md += f"- **Energy Volumetric:** {getattr(pair, 'energy_vol', 'N/A')}\n"
1030 | electrodes_md += f"- **Fraction A Charge:** {getattr(pair, 'fracA_charge', 'N/A')}\n"
1031 | electrodes_md += f"- **Fraction A Discharge:** {getattr(pair, 'fracA_discharge', 'N/A')}\n"
1032 | electrodes_md += f"- **Stability Charge:** {getattr(pair, 'stability_charge', 'N/A')}\n"
1033 | electrodes_md += f"- **Stability Discharge:** {getattr(pair, 'stability_discharge', 'N/A')}\n"
1034 | electrodes_md += f"- **ID Charge:** {getattr(pair, 'id_charge', 'N/A')}\n"
1035 | electrodes_md += f"- **ID Discharge:** {getattr(pair, 'id_discharge', 'N/A')}\n"
1036 |
1037 | return electrodes_md
1038 |
1039 |
1040 |
1041 | @mcp.tool()
1042 | async def get_oxidation_states(
1043 | material_id : str = Field(
1044 | ...,
1045 | description="Material ID for the material"
1046 | ),
1047 | formula: Optional[str] = Field(
1048 | default=None,
1049 | description="Query by formula including anonymized formula or by including wild cards"
1050 | )
1051 | ) -> str:
1052 | """
1053 | Get oxidation states for a given material ID or formula.
1054 |
1055 | This function retrieves the oxidation states of elements in a material
1056 | from the Materials Project database. It can be queried by material ID or
1057 | by formula, including anonymized formulas or wildcards.
1058 |
1059 | Args:
1060 | material_id: The Materials Project ID of the material (e.g. 'mp-149')
1061 | formula: Optional formula to query oxidation states (e.g. 'LiFeO2')
1062 |
1063 | Returns:
1064 | str: A formatted markdown string containing the oxidation states information
1065 |
1066 | Example:
1067 | >>> get_oxidation_states('mp-149')
1068 | Returns oxidation states for silicon (mp-149)
1069 | """
1070 | logger.info(f"Fetching oxidation states for {material_id} with formula {formula}...")
1071 | with _get_mp_rester() as mpr:
1072 | oxidation_states = mpr.materials.oxidation_states.search(
1073 | material_ids=material_id,
1074 | formula=formula
1075 | )
1076 |
1077 | if not oxidation_states:
1078 | return f"No oxidation states found for {material_id}."
1079 |
1080 | oxidation_md = f"## Oxidation States for {material_id}\n\n"
1081 | for idx, data in enumerate(oxidation_states):
1082 | #oxidation_md = f"## Oxidation States for {material_id}\n\n"
1083 | #oxidation_md += f"- **Material ID**: {material_id}\n"
1084 | oxidation_md += f"- **Formula**: {getattr(data, "formula_pretty", "N/A")}\n\n"
1085 | oxidation_md += f"- **formula_anonymous**: {getattr(data, "formula_anonymous", "N/A")}\n\n"
1086 | oxidation_md += f"- **density** : {getattr(data, "density", "N/A")}\n\n"
1087 | oxidation_md += f"- **volume**: {getattr(data, "volume", "N/A")}\n\n"
1088 | oxidation_md += f"- **symmetry**: {getattr(data, "symmetry", "N/A")}\n\n"
1089 | oxidation_md += f"- **nelements**: {getattr(data, "nelements", "N/A")}\n\n"
1090 | oxidation_md += f"- **density_atomic**: {getattr(data, "density_atomic", "N/A")}\n\n"
1091 | oxidation_md += f"- **property_name: {getattr(data, "property_name", "N/A")}\n\n"
1092 | oxidation_md += f"- **structure**: {getattr(data, "structure", "N/A")}\n\n"
1093 | oxidation_md += f"- **possible_species**: {getattr(data, "possible_species", "N/A")}\n\n"
1094 | oxidation_md += f"- **possible_valances**: {getattr(data, "possible_valences", "N/A")}\n\n"
1095 | oxidation_md += f"- **method**: {getattr(data, "method", "N/A")}\n\n"
1096 |
1097 | return oxidation_md
1098 |
1099 | @mcp.tool()
1100 | async def construct_wulff_shape(
1101 | material_id: str = Field(
1102 | ...,
1103 | description="material ID of the material "
1104 | )
1105 | ):
1106 | """
1107 | Constructs a Wulff shape for a material.
1108 |
1109 | Args:
1110 | material_id (str): Materials Project material_id, e.g. 'mp-123'.
1111 |
1112 | Returns:
1113 | object: image of the wulff shape
1114 |
1115 | """
1116 | logging.info(f"Getting Wulff shape for material: {material_id}")
1117 | with _get_mp_rester() as mpr:
1118 | surface_data = mpr.surface_properties.search(material_id)
1119 |
1120 |
1121 | if not surface_data:
1122 | return f"No surface data collected for wulff shape"
1123 |
1124 | try:
1125 | surface_energies = []
1126 | miller_indices = []
1127 |
1128 | for surface in surface_data[0].surfaces:
1129 | miller_indices.append(surface.miller_index)
1130 | surface_energies.append(surface.surface_energy)
1131 |
1132 | structure = mpr.get_structure_by_material_id(material_id=[material_id])
1133 |
1134 | wulff_shape = WulffShape(
1135 | lattice=structure.lattice,
1136 | miller_list=miller_indices,
1137 | e_surf_list=surface_energies
1138 | )
1139 |
1140 | # plot the shape
1141 | import io
1142 | import base64
1143 | fig = wulff_shape.get_plot()
1144 | #fig.suptitle(f"Wulff Shape\nVolume: {wulff_shape.volume:.3f} Ų", fontsize=14)
1145 | buffer = io.BytesIO()
1146 | fig.savefig(buffer, format='png', dpi=300, bbox_inches='tight')
1147 | buffer.seek(0)
1148 | image_base64 = base64.b64encode(buffer.read()).decode()
1149 | # get the image
1150 | #plt.close(fig)
1151 | return {
1152 | "success": True,
1153 | "material_id": material_id,
1154 | "volume": round(wulff_shape.volume, 3),
1155 | "surface_count": len(surface_energies),
1156 | "miller_indices": miller_indices,
1157 | "surface_energies": surface_energies,
1158 | "image_base64": image_base64,
1159 | "message": f"Wulff shape constructed for {material_id}"
1160 | }
1161 |
1162 | except Exception as e:
1163 | logging.error(f"Error occurred constructing wulff shape: {e}")
1164 | return f"No wulff shape construted for material: {material_id}"
1165 |
1166 |
1167 |
1168 | @mcp.resource(uri="materials_docs://{filename}")
1169 | async def get_materials_docs(
1170 | filename: str
1171 | ) -> str:
1172 | """
1173 | Retrieve docs from the markdown folder
1174 |
1175 | Args:
1176 | filename (str): The name of the file to retrieve from the folder eg. apidocs or docsmaterials
1177 |
1178 | """
1179 | from utils.utility_functions import MarkdownResourceManager, MARKDOWN_FOLDER
1180 | logger.info(f"Retrieving documentation file: {filename}")
1181 | resource_manager = MarkdownResourceManager(MARKDOWN_FOLDER)
1182 | try:
1183 | resource_manager.load_files()
1184 | if filename not in resource_manager.files:
1185 | logger.error(f"File {filename} not found in the documentation resources.")
1186 | return f"File {filename} not found in the documentation resources."
1187 | file_content = resource_manager.files[filename].content
1188 | print(file_content)
1189 | return file_content
1190 | except Exception as e:
1191 | logger.error(f"Error retrieving documentation file {filename}: {e}")
1192 | return f"Error retrieving documentation file {filename}: {e}"
1193 |
1194 |
1195 | @mcp.prompt(name="get_electronic_bandstructure")
1196 | async def BandStructurePrompt() -> str:
1197 | """Prompt for retrieving electronic band structure data."""
1198 | from utils.prompts_templates import ElectronicBandStructurePrompt
1199 | # I want to return
1200 | return ElectronicBandStructurePrompt.template
1201 |
1202 |
1203 | @mcp.tool()
1204 | async def get_doi(
1205 | material_id: str = Field(
1206 | ...,
1207 | description="Material ID of the material"
1208 | )
1209 | ) -> str:
1210 | """
1211 | Get DOI and bibTex reference for a given material ID.
1212 |
1213 | Args:
1214 | material_id (str): The Materials Project ID of the material (e.g. 'mp-149')
1215 | Returns:
1216 | str: A formatted markdown string containing the DOI and bibTex reference
1217 | """
1218 | logger.info("Fetching DOI for material ID {material_id}...")
1219 | with _get_mp_rester() as mpr:
1220 | doi_data = mpr.doi.get_data_by_id(material_id)
1221 |
1222 | if not doi_data:
1223 | return f"No DOI found for material ID {material_id}."
1224 |
1225 | dos_markdown = f"## DOI Information for {material_id}\n\n"
1226 | dos_markdown += f"- **DOI:** {doi_data.doi} \n\n"
1227 | dos_markdown += f"- **BibTeX:**\n```\n{doi_data.bibtex}\n```\n"
1228 |
1229 | return dos_markdown
1230 |
1231 |
1232 |
1233 |
1234 |
1235 |
1236 |
1237 |
1238 | if __name__ == "__main__":
1239 | # Initialize and run the server
1240 | mcp.run(transport='stdio')
```