Skip to content

Commit fb383bb

Browse files
Using async code for tools. (#20)
* Rebase. * image for mcp servers. * tiny changes. * edit gitignore file for a while. * add changes. * remove unnecessary import. * tiny change. * All the prsers are working well. * clean a bit.
1 parent 40aeeb7 commit fb383bb

9 files changed

Lines changed: 647 additions & 224 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,40 @@ To execute any script, use `uv run python <filepath>` (automatically calls `uv s
4444
To try out individual Python sessions open a new bash shell.
4545
Then activate the virtual environment via `. .venv/bin/activate`.You can now initiate your Python session (`python`).
4646

47+
## Manual Testing setup
48+
MCP primitives developed for a single server can be tested using [MCP instructor](https://github.com/modelcontextprotocol/inspector). The problem with it can not handle multiple servers at once. So, it requires to test each server individually. (We did not find the way to inspect multiple servers at once using MCP inspector).
49+
```
50+
uv run mcp dev <path-to-server-file e.g., src/nomad/__main__.py>
51+
```
52+
Then click on the url link appeared on the terminal to open the MCP inspector in the browser or inspector will automatically open in browser.
53+
54+
To run and test all the servers together in VS Code, as a client, list your servers in a `json` file e.g., `.vscode/mcp.json` and VSCode will be capable of detect the servers and can be run on the clicking button above the server (appeared in the VSCode UI).
55+
56+
![mcp.json](images/mcp_json.png)
57+
58+
Example of `mcp.json` listing all the servers:
59+
```Json
60+
{
61+
"servers": {
62+
"parse-patrol": {
63+
"command": "uv",
64+
"args": ["run", "python", "src/parse_patrol/__main__.py"],
65+
"cwd": "${workspaceFolder}"
66+
},
67+
"cclib": {
68+
"command": "uv",
69+
"args": ["run", "python", "src/cclib/__main__.py"],
70+
"cwd": "${workspaceFolder}"
71+
},
72+
"nomad": {
73+
"command": "uv",
74+
"args": ["run", "python", "src/nomad/__main__.py"],
75+
"cwd": "${workspaceFolder}"
76+
}
77+
}
78+
}
79+
```
80+
4781
## Project Structure
4882

4983
```

images/mcp_json.png

65.3 KB
Loading

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ dependencies = [
1010
"pydantic>=2.11.7",
1111
"qc-iodata>=1.0.0a8",
1212
"requests>=2.32.5",
13+
"asyncio>=4.0.0",
14+
"aiofiles>=24.1.0",
15+
"aiohttp",
1316
]
1417

1518

src/cclib/__main__.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import cclib
22
from mcp.server.fastmcp import FastMCP # pyright: ignore[reportMissingImports]
3+
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
4+
35

46
from typing import Optional, Dict, List, Any
57
from pydantic import BaseModel, Field
68

79

10+
configure_logging("INFO")
11+
logger = get_logger(__name__)
12+
13+
814
class CCDataModel(BaseModel):
915
class Config:
1016
# Prevent model registration conflicts when imported multiple times
@@ -84,7 +90,7 @@ class Config:
8490
zpve: Optional[float] = Field(None, description="Zero-point vibrational energy correction (hartree/particle, float)")
8591

8692

87-
def ccdata_to_model(ccdata: cclib.parser.data.ccData) -> CCDataModel: # type: ignore
93+
def ccdata_to_model(ccdata: cclib.parser.data.ccData) -> CCDataModel: # type: ignore
8894
"""Convert ccData object to CCDataModel (Pydantic) format.
8995
9096
Args:
@@ -93,6 +99,7 @@ def ccdata_to_model(ccdata: cclib.parser.data.ccData) -> CCDataModel: # type: ig
9399
Returns:
94100
CCDataModel with converted data types for JSON serialization
95101
"""
102+
logger.info("Converting ccData to CCDataModel...")
96103
result = {}
97104
for field_name in CCDataModel.model_fields.keys():
98105
if hasattr(ccdata, field_name):
@@ -109,11 +116,12 @@ def ccdata_to_model(ccdata: cclib.parser.data.ccData) -> CCDataModel: # type: ig
109116
result[field_name] = value
110117
return CCDataModel(**result)
111118

119+
112120
mcp = FastMCP("CCLib Chemistry Parser")
113121

114122

115123
@mcp.tool()
116-
def cclib_parse_file_to_model(filepath: str) -> CCDataModel:
124+
async def cclib_parse_file_to_model(filepath: str) -> CCDataModel:
117125
"""Parse chemistry file and return as CCDataModel for JSON serialization.
118126
119127
Args:
@@ -122,17 +130,18 @@ def cclib_parse_file_to_model(filepath: str) -> CCDataModel:
122130
Returns:
123131
CCDataModel with parsed data converted for JSON serialization
124132
"""
125-
filereader = cclib.io.ccopen(filepath) # type: ignore
133+
logger.info("Parsing file: %s ...", filepath)
134+
filereader = cclib.io.ccopen(filepath) # type: ignore
126135
if filereader is None:
136+
logger.error("File not found: %s", filepath)
127137
return CCDataModel()
128138
ccdata = filereader.parse()
129139
return ccdata_to_model(ccdata)
130140

131141

132142
@mcp.prompt()
133-
def cclib_test_prompt(
134-
file_description: str,
135-
output_format: str="a CCDataModel for JSON serialization"
143+
async def cclib_test_prompt(
144+
file_description: str, output_format: str = "a CCDataModel for JSON serialization"
136145
) -> str:
137146
"""Generate a prompt for parsing chemistry files using cclib.
138147

src/custom_gaussian/__main__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from pydantic import BaseModel, Field
66
import periodictable
77
from mcp.server.fastmcp import FastMCP # pyright: ignore[reportMissingImports]
8+
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
9+
10+
11+
configure_logging("INFO")
12+
logger = get_logger(__name__)
813

914

1015
class CustomGaussianDataModel(BaseModel):
@@ -417,7 +422,7 @@ def read_block(start_idx: int) -> Tuple[int, List[str]]:
417422

418423

419424
@mcp.tool()
420-
def gauss_parse_file_to_model(filepath: str) -> CustomGaussianDataModel:
425+
async def gauss_parse_file_to_model(filepath: str) -> CustomGaussianDataModel:
421426
"""
422427
Parse a Gaussian file (.log/.out, .gjf/.com, .fchk) and return a CustomGaussianDataModel.
423428
@@ -432,8 +437,11 @@ def gauss_parse_file_to_model(filepath: str) -> CustomGaussianDataModel:
432437
Returns:
433438
CustomGaussianDataModel with parsed contents.
434439
"""
440+
441+
logger.info("Parsing Gaussian file: %s ...", filepath)
435442
path = Path(filepath)
436443
if not path.exists():
444+
logger.error("File not found: %s", filepath)
437445
return CustomGaussianDataModel(metadata={"error": f"File not found: {filepath}"})
438446

439447
ext = path.suffix.lower()
@@ -459,9 +467,8 @@ def gauss_parse_file_to_model(filepath: str) -> CustomGaussianDataModel:
459467

460468

461469
@mcp.prompt()
462-
def custom_gaussian_test_prompt(
463-
file_description: str,
464-
analysis_type: str = "comprehensive analysis"
470+
async def custom_gaussian_test_prompt(
471+
file_description: str, analysis_type: str = "comprehensive analysis"
465472
) -> str:
466473
"""Generate a prompt for parsing Gaussian files with custom analysis.
467474

src/iodata_parser/__main__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def iodata_to_model(ext_data: iodata_package.IOData) -> IODataModel:
7676
return IODataModel(**result)
7777

7878
@mcp.tool()
79-
def iodata_parse_file_to_model(filepath: str) -> IODataModel:
79+
async def iodata_parse_file_to_model(filepath: str) -> IODataModel:
8080
"""Parse chemistry file and return as IODataModel for JSON serialization.
8181
8282
Args:
@@ -89,9 +89,8 @@ def iodata_parse_file_to_model(filepath: str) -> IODataModel:
8989
return iodata_to_model(data)
9090

9191
@mcp.prompt()
92-
def iodata_test_prompt(
93-
file_description: str,
94-
output_format: str="a IOData for JSON serialization"
92+
async def iodata_test_prompt(
93+
file_description: str, output_format: str = "a IOData for JSON serialization"
9594
) -> str:
9695
"""Generate a prompt for parsing chemistry files using IOData.
9796
@@ -104,7 +103,7 @@ def iodata_test_prompt(
104103
"""
105104

106105
return (
107-
f"Use `iodata_parse_file_to_model` to parse the file with description {file_description} "
106+
f"Use `iodata_parse_file_to_model` to parse the file (with extension .fck) with description {file_description} "
108107
f"and return the data as {output_format}."
109108
)
110109

0 commit comments

Comments
 (0)