Skip to content

Connect MCP servers

Model Context Protocol servers appear as additional tool providers. Once configured, their tools are called exactly like local ones — await self.call_tool(...) — and recorded in provenance the same way.

Install the client

pip install "./science-adk/python[mcp]"

Stdio: a server launched as a subprocess

science.toml
[tools.pubchem]
command = "uvx mcp-pubchem"

The command is split with shell-like tokenisation and launched as a subprocess; Science ADK speaks JSON-RPC over its stdin and stdout. The section name (pubchem) becomes the provider prefix.

science-adk tools
pubchem.get_compound_by_name(name: str) -> object
pubchem.search_compounds(query: str, limit: integer = 10) -> object
local.power_law_fit(x: list, y: list) -> dict

HTTP: a remote server

science.toml
[tools.remote_compute]
url = "https://mcp.example.org/api"
token_env = "COMPUTE_API_TOKEN"

token_env names the environment variable holding the credential. The secret itself never goes into science.toml, which is a file you commit.

export COMPUTE_API_TOKEN="..."
science-adk tools

If the variable is unset, the registry refuses to build the provider rather than silently running without authentication:

ToolError: Tool provider 'remote_compute' needs COMPUTE_API_TOKEN environment
variable, which is not set.

The token is sent as a bearer token on each request.

Using the tools

class FetchCompound(DataAgent):
    source_kind: ClassVar[str] = "measured"

    ports: Ports = Ports(
        inputs=[Port("settings", "json")],
        outputs=[Port("compound", "json")],
    )

    async def execute(self):
        settings = await self.input("settings")
        compound = await self.call_tool(
            "pubchem.get_compound_by_name", name=settings["name"]
        )
        await self.log(f"retrieved {settings['name']} from PubChem")
        return {"compound": compound}

Declare it on the node, as always:

{ "id": "fetch", "kind": "data", "tools": ["pubchem.get_compound_by_name"] }

Qualify names across providers

A bare name resolves only if it is unambiguous. As soon as more than one provider is configured, prefer provider.tool — it documents where the data came from at the call site.

When a server is unavailable

The registry warns and keeps going rather than failing the whole project:

warning: tool provider 'pubchem' unavailable: Cannot reach MCP server
'pubchem' at https://...: [Errno -2] Name or service not known

The run then fails where the tool was actually needed, and the node that declared it fails tool_use_verified. That is the intended behaviour: a missing data source must never quietly become a run with fewer inputs.

Provenance is identical

An MCP tool call is recorded exactly like a local one:

"provenance": {
  "tool_calls": [{"tool": "pubchem.get_compound_by_name", "ok": true}]
}

A remote dependency does not weaken the integrity guarantees; it just widens what the run depended on, which the trace now says out loud.

Reproducibility caveats

A remote server can change under you in a way a local function cannot. Two habits help:

  1. Record the response, not just the conclusion. Let the data node output the retrieved payload so it lands in the trace (or spills to data/), and keep the derivation in a downstream node.
  2. Pin the server version where the protocol allows it, and say which version you used in HYPOTHESIS.md.

Checklist

science-adk tools               # the server's tools appear, correctly prefixed
science-adk validate            # the DAG still checks out
science-adk run --only fetch    # the call goes through and is recorded
jq '.nodes[] | select(.node_id=="fetch") | .provenance' research/*/runs/*/trace.json

See also