Hosting a SciFM with Nomad

This guide walks through the shortest credible path from a scientific model to a running Nomad tool. The goal is to package the model code, describe it in nomad.yml, and verify that Nomad can start the server and expose the tool through MCP.

By the end of the guide, nomad serve ... should start cleanly, your tool should appear in MCP Inspector, and a test invocation should return output.

What you are building

Nomad serves tools from configuration rather than from an ad hoc Python entry point. In practice, connecting a SciFM means supplying four pieces:

  • Importable Python code for the tool class/function in a pip-installable package.

  • A reachable model directory for the weights and configuration (name_or_path),

  • A model card based on container/model-card.md.

  • A config entry (fmod_models or tools) in nomad.yml.

This page focuses on getting that integration working end to end. Specific deployment rules and packaging constraints live in the Deployment Specification.

This guide uses uv for package management. uv is not required to deploy a model with Nomad, but it makes the setup steps easier.

Create an Importable Package

nomad serve loads your class or function by dotted import path, so that exact symbol must be importable in the same Python environment that launches the server.

Minimal repository shape:

my_pkg/
  pyproject.toml
  src/my_pkg/mcp.py

You can create this quickly with uv init --package ./my_pkg. Replace ./my_pkg with your desired package name.

To quickly check, from the top-level ./my_pkg directory, run:

uv run python -c "from my_pkg.mcp import MyModelTool; print(MyModelTool)"

For local development, install your package into the same environment you use to run nomad serve. For demo deployments, that environment is defined by container/deploy/demo/pyproject.toml, so your package must be added there as a pinned dependency. If the package comes from another repository or a local checkout, also add a matching [tool.uv.sources] entry so the demo build can resolve it. Do not add model-specific dependencies to the root project unless they are required outside the deployment package.

Writing a TorchModuleTool

A nomad.fm_base_tool.TorchModuleTool is the adapter between Nomad’s MCP-facing tool interface and your underlying PyTorch model. In practice, you are not exposing your raw training interface. You are defining an agent-friendly scientific query and then wiring that query into the model.

Before you write code, answer three questions:

  • What is the scientific query you want the tool to answer?

  • What should one tool call take as input?

  • What should one tool call return?

For agent workflows, prefer JSON-friendly inputs and outputs over raw tensors or large binary payloads. A good pattern is to make one tool call represent one scientific query and return one structured answer. That keeps the interface easy for an agent to supply, inspect, and reuse.

For rollout, PDE, or grid-based models, start by looking at src/nomad/well_format.py. In particular, nomad.well_format.AutoRegressiveInput and nomad.well_format.WellFormat provide a ready-made contract for stateful scientific model inputs and outputs. PDE tools should also document the model dt in their tool description, prefer t_start=0.0, and ensure t_final >= duration when using rollout-style inputs.

The four methods you usually implement

Most model integrations only need to define:

Minimal skeleton

from collections.abc import Sequence

import torch
from pydantic import BaseModel, Field

from nomad.fm_base_tool import TorchModuleTool

# # For PDE Models
# from nomad.well_format import AutoRegressiveInput, WellFormat

# # For SciFM's needing tensor input/outputs
# from nomad.well_format import Tensor


class MyModelInput(BaseModel):
    molecule: str = Field(description="Input query for one prediction")


class MyModelOutput(BaseModel):
    score: float = Field(description="Predicted value for the input query")


class MyModelTool(
    TorchModuleTool[
        MyModelInput,
        MyModelOutput,
        dict[str, torch.Tensor],
        torch.Tensor,
    ]
):
    args_schema = MyModelInput
    output_schema = MyModelOutput

    @classmethod
    def from_pretrained(cls, name_or_path: str, **kwargs):
        model = load_model_somehow(name_or_path, **kwargs)
        return cls(
            name="my-model",
            description="Predicts a score for one molecule.",
            fm=model,
            batch_size=32,
            device=model.device,
        )

    def preprocess(self, inputs: Sequence[MyModelInput]) -> dict[str, torch.Tensor]:
        encoded = encode_batch([item.molecule for item in inputs])
        return {key: value.to(self.device) for key, value in encoded.items()}

    def postprocess(self, model_output: torch.Tensor):
        for row in model_output:
            yield MyModelOutput(score=float(row.item()))

Implementation notes

  • Define args_schema and output_schema as Pydantic models that describe the MCP tool interface, not your raw model tensors.

  • Put heavyweight loading logic in TorchModuleTool.from_pretrained, not __init__.

  • In TorchModuleTool.preprocess, assume you are receiving a batch. Even if each call looks scalar at the CLI or in MCP Inspector, Nomad may batch several requests together.

  • Move tensors to self.device in TorchModuleTool.preprocess. TorchModuleTool execution should use only that device. Multi-GPU models are not supported.

  • In TorchModuleTool.postprocess, yield one output object at a time rather than returning the whole batch as one result.

  • Use the tool description and field descriptions carefully. Agents and human operators both rely on that text to decide when and how to call the tool. Keep descriptions concise and put detailed model information in the model card.

  • If your model produces very large outputs, rethink the tool interface before exposing it directly. Large payloads are awkward for both MCP clients and language-model-driven agents.

  • Do not write to stdout from model or tool code. Use Python logging instead.

  • Tools should not write persistent files. If a temporary file is unavoidable, create it per invocation with the tempfile module.

Overriding __init__ is highly discouraged because TorchModuleTool is a pydantic.BaseModel. Custom __init__ behavior is possible, but considerably more complex than loading the model in TorchModuleTool.from_pretrained.

Choose fmod_models vs tools

If you are serving one SciFM, start with a complete nomad.yml like this:

fmod_models:
  - model_class: my_pkg.mcp.MyModelTool
    name_or_path: my-org/my-model-v2
    tool_name: my-model-v2
    batch_size: 32

Add entries under tools only when you also want regular Python callables registered alongside the model.

Hosting Your Model Weights

The weights and configuration for your model must be stored in a HuggingFace-style model directory. Prefer hosting that directory as an external model source. The path to this directory is passed to TorchModuleTool.from_pretrained to load your model. This directory is referenced by the name_or_path field for the model under fmod_models in the config.

TorchModuleTool.from_pretrained(name_or_path) should accept the resolved name_or_path without requiring extra keyword arguments and should return a fresh, independent tool instance each time it is called. The tool manager may call it repeatedly while serving a model; see Tool Manager Reference for lifecycle details.

Recommended name_or_path values:

Source

Example

HuggingFace

hf://my-org/my-model-v2

ORAS artifact

oras://registry.example.org/my-org/my-model:v1#models/my-model-v1

Git/Git LFS

git+ssh://git@example.org/my-org/models.git@main#models/my-model-v1

You may use a local path for name_or_path during testing, but this is not supported for demo deployments. ORAS artifacts are preferred.

If you use a local model directory for development, choose a directory name that satisfies MCP tool naming constraints, use a distinct directory name for each weight revision, and include a model card matching the template.

Upload Weights To HuggingFace

Use HuggingFace when you want to publish the model directory as a Hub model repository. Follow HuggingFace’s uploading models guide to create the repository and upload the files.

Once the model is uploaded, reference it with an hf:// URI:

name_or_path: hf://my-org/my-model-v2

If the model files are inside a subdirectory of the repository, add that subdirectory as a fragment:

name_or_path: hf://my-org/my-model-v2#models/my-model-v2

Push Weights To ORAS

Use ORAS when you want model weights in an OCI-compatible registry, separate from your source repository. Start with a HuggingFace-style model directory:

models/my-model-v1/
  config.json
  model.safetensors
  tokenizer.json

Install ORAS, log in to your registry, then push the model directory from the project root:

oras login registry.example.org
oras push \
  registry.example.org/my-org/my-model:v1 \
  models/my-model-v1/

Reference the pushed artifact in nomad.yml with the model directory as a subpath:

name_or_path: oras://registry.example.org/my-org/my-model:v1#models/my-model-v1

For reproducible deployments, resolve the tag to a digest and use the pinned URI:

oras resolve registry.example.org/my-org/my-model:v1
name_or_path: oras://registry.example.org/my-org/my-model@sha256:...#models/my-model-v1

If the registry is private, make sure the environment that runs Nomad can authenticate to it before startup.

Store Weights With Git LFS

Weights hosted within your project’s repository MUST be stored with Git LFS. First, install Git LFS. The model directory should still match the HuggingFace model repository format. Then, within your project’s repository:

git lfs install
git lfs track '*.safetensors' '*.pth' # Plus any other large files used by your model
git add .gitattributes
git add path/to/model_directory
git commit -m "Adding model"
git push

Validate

Run:

nomad serve --transport streamable-http --host localhost --port 8000 path/to/nomad.yml

Expected output (truncated):

INFO:     Application startup complete.
INFO:     Uvicorn running on http://localhost:8000

If your tool accepts ordinary JSON-friendly inputs, connect with MCP Inspector and run a quick tool call to verify that it behaves correctly.

Structured Tensor Inputs

If your TorchModuleTool accepts WellFormat, AutoRegressiveInput, or nomad.well_format.Tensor fields, MCP Inspector is usually the wrong testing surface. Those values are sent as compressed base64-encoded torch serializations, which makes hand-written Inspector payloads cumbersome and error-prone.

In that case, test the model through nomad code-mode-exec instead.

Create gateway.yml:

servers:
  nomad:
    transport: streamable_http
    url: http://localhost:8000/mcp

Then create test_rollout.py. The generated Python wrapper name is the sanitized form of your MCP tool name, so my-model-v2 becomes my_model_v2:

from mcp_tools.nomad import my_model_v2
from nomad.well_format import WellFormat

initial_state = WellFormat.from_file("path/to/input.h5")

# Nomad decodes tensor fields returned by wrappers, but does not
# reconstruct the outer Pydantic object automatically.
rollout = my_model_v2(
    duration=10,
    initial_state=initial_state,
)

# Reconstruct WellFormat from the returned plain-dict output
rollout = WellFormat.model_validate(rollout)
rollout.to_file("path/to/output.h5")

Run the script in the code-mode sandbox:

nomad code-mode-exec --config gateway.yml test_rollout.py

For a fuller worked example using WellFormat inputs and a PDE surrogate, see the Nomad Inference notebook.

Demo Deployment Additions

If you are wiring a new model into the repository’s demo deployment scaffold, update these files together:

  • container/deploy/demo/pyproject.toml: add your model package to dependencies, and add or update the matching [tool.uv.sources] entry if the package comes from Git or a local checkout.

  • container/deploy/demo/nomad.yml: add your fmod_models entry, plus any helper tools entries and tool_manager settings needed for batching or queue depth.

  • container/model-card.md: use this template when you are shipping a local model directory that needs a model card in the exported bundle.

Paths in container/deploy/demo/nomad.yml are resolved relative to that file. During image builds, the demo image build process runs nomad export before building the container, so local model assets referenced by name_or_path are copied into the build context automatically.

For the full demo deployment workflow, see the Deployment Guide.