Skip to main content

Development Guide

Table of Contents

We sincerely welcome your contribution of new tools. This guide walks you through the steps to create, test, and submit a new MCP-compatible tool in the ChemMCP package.

Step 0: Setup

To begin with, fork our Github repo into your own account and clone to your local machine. You could install uv and do uv sync as introduced here, which creates a Python environment for you and smooths your development.

Step 1: Choose Your Tool’s Names

You need to determine a tool name and a tool function name for your tool. Specifically:

  • Tool Name: The name of the tool.

    • Use PascalCase (every word head capitalized), noun-phrase style.
    • Examples: WebSearch, BbbpPredictor, Smiles2Iupac.
  • Function Name: The Python function-like name of the tool, used in the MCP server.

    • Use snake-case, verb-phrase style.
    • Examples: search_web, predict_bbbp, convert_smiles_to_iupac.
  • File Name (or module name): The Python module name, matching your tool name.

    • Lowercase, snake-case.
    • Examples: web_search.py, bbbp_predictor.py, smiles2iupac.py.

Step 2: Create Your Tool File and Register

Create a Python file with your file name, under src/chemmcp/tools/. Then, register your tool to the module map in src/chemmcp/tools/__init__.py:

_tool_module_map = {
    # … existing entries …
    "MyNewTool": "my_new_tool",  # Tool Name: File Name.
}

Step 3: Implement Your Tool

Copy the following template to the file, and implement the mentioned variables and functions for your tool:

import os
import logging
from typing import Optional

from openai import OpenAI

from ..utils.base_tool import BaseTool
from ..utils.errors import ChemMCPToolError  # your custom errors
from ..utils.mcp_app import ChemMCPManager, run_mcp_server

# Use `logger.info("msg")` or `logger.debug("msg")` to print messages if neceesary, instead of `print`.
logger = logging.getLogger(__name__)  


@ChemMCPManager.register_tool  # Use this decorator if you want it discoverable in MCP mode
class MyNewTool(BaseTool):  # Class name must match the tool name
    __version__      = "0.1.0"            # semver: MAJOR.MINOR.PATCH
    name             = "MyNewTool"        # must match class name
    func_name        = "do_something"     # snake-case verb phrase
    description      = "Brief description of what MyNewTool does."
    categories       = ["General"]        # choose from Molecule | Reaction | General
    tags             = ["API", "Neural Networks", "Molecular Properties", "LLMs"]  # keywords for this tool
    required_envs    = [                  # if this tool needs users to set API keys or any environment variables; otherwise, leave it an empty list
        ("OPENAI_API_KEY", "API key for OpenAI"),
    ]

    # the function signature for the following _run_base function
    # four elements presented in strings:  input_domain_name,    type,   default value ("N/A" if no default),   the description of this input
    code_input_sig   = [
        ("smiles", "str", "N/A", "SMILES string of the molecule."),
        ("threshold",      "float", "0.5", "Cutoff threshold."),
    ]

    # the function signature for the following _run_text function
    # four elements presented in strings:  input_domain_name,    type,   default value ("N/A" if no default),   the description of this input
    text_input_sig   = [
        ("smiles_and_threshold", "str", "N/A", "The SMILES string of the moledule and the cutoff threshold, separated by a space."),
    ]

    # the output for both the _run_base and _run_code function
    # three elements presented in strings: output_domain_name,   type,   the description of the output
    output_sig       = [
        ("result", "float", "The score of the input molecule."),
    ]

    # the concrete example(s) of the input and output of this tool
    # at least one example
    examples         = [
        {
            "code_input": {  # the input to the _run_base function. must match your defined `code_input_sig`
                "molecule_smiles": "CCO",
                "threshold": 0.7
            },
            "text_input": {  # the input to the _run_text function. must match your defined `text_input_sig`
                "smiles_and_threshold": "CCO  0.7"
            },
            "output": {  # the output of both of the function. must match your defined `output_sig`
                "result": 0.13
            }
        },
    ]

    # You need to define your __init__ function, if you have any customized settings for this tool
    # Note: every argument must have a default value
    def __init__(
        self,
        openai_api_key: Optional[str] = None,  # if your tool needs api_keys or any envs
        init: bool = True,                     # whether to run _init_modules. must have, just put this line
        interface: str = "code"                # whether to use the code interface or text interface. must have, just put this line
    ):
        # Load API key or environment variable if any
        if openai_api_key is None:  # first from the environment variables
            openai_api_key = os.getenv("MY_TOOL_API_KEY")
        if openai_api_key is None:  # if no api_key found, raise an error
            # we recommend using the errors defined in `src/chemmcp/utils/errors.py`
            raise ChemMCPToolError("Please set `MY_TOOL_API_KEY`.")
        self.openai_api_key = openai_api_key
        
        super().__init__(init=init, interface=interface)

    # Optional. Recommeneded to load some checkpoints or initialize some external clients here.
    # This function will be called in super().__init__ if init==True.
    def _init_modules(self):
        """
        Load some checkpoints or initialize some external clients.
        """
        self.openai_client = OpenAI(self.openai_api_key)

    # Your must implement this function.
    # The input arguments are the input or your tool.
    # The output is the output of your tool.
    def _run_base(self, smiles: str, threshold: float = 0.5) -> float:
        """
        Core logic of your tool.
        """
        # … your implementation of the tool function …
        # You could call external APIs, load any models, or implement your own algorithms.
        score: float = self.openai_client.predict(smiles)  # this is just a simplified example

        return score

    # This function is for those applications that only support one string as the input.
    # Typically, you just need to parse the input string and get the inputs to the _run_base function, then call _run_base.
    # This is optional: if your _run_base only has one str input argument, then you don't need to manually implement this -- ChemMCP will do it for you.
    # Otherwise, please implement it.
    def _run_text(self, smiles_and_threshold: str) -> str:
        """
        Parse the only text str input, and then call _run_base. Return the result of _run_base.
        """

        # parse the text query into _run_base's input arguments
        smiles, threshold = smiles_and_threshold.strip().split()
        smiles = str(smiles)
        threshold = float(threshold)

        # call _run_base
        result = self._run_base(smiles, threshold)

        return result

At the bottom of your file, add:

if __name__ == "__main__":
    run_mcp_server()

Step 4: Test Your Tool

You can write a script, or use Jupyter Notebook, or any other ways you like, to test your tool. Basically, initialize an instance of your tool class, and check if the tool works as expected.

import os
from chemmcp.tools import MyNewTool
os.environ['OPENAI_API_KEY'] = "YOUR_API_KEY"

my_new_tool = MyNewTool()

# it calls your _run_base function
result = my_new_tool.run_code(smiles="CCO", threshold=0.5)

# it calls your _run_text function
result = my_new_tool.run_text(smiles_and_threshold="CCO 0.5")

If your tool supports MCP (i.e., you used @ChemMCPManager.register_tool), then to ensure it works well under the MCP mode, use MCP Inspector to test your tool.

# Assume your file name is `my_new_tool.py`
npx @modelcontextprotocol/inspector uv run -m src.chemmcp.tools.my_new_tool

Step 5: Submit Your Tool

After testing your tool, you can now use git to submit to your forked repo, and then submit a pull request to our repo. We will check and merge your awesome work to ChemMCP.

Thank you very much 🥰

Contact

Have questions or feedback?

  • Open an issue for bug reports or feature requests on our GitHub repository.

  • Email us at yu.3737 at osu.edu – we are eager to know your ideas and suggestions!