Skip to content

ToolHive agent scenario: FHIR → Google Drive → email

This guide walks you through running the platform as an MCP server using ToolHive and driving the FHIR → Google Drive → email workflow with the bundled agent (agents.toolhive).

What you'll build: An AI agent that can autonomously fetch patient data from an EHR (Epic or Cerner), upload a summary to Google Drive, and send a notification email — all driven by a single natural language prompt.

Time to complete: ~20 minutes (plus credential procurement time)


Table of contents


What is ToolHive?

ToolHive is a desktop application that:

  • Runs MCP servers inside secure Docker containers
  • Injects secrets as environment variables (so they never appear in config files)
  • Exposes an HTTP proxy so any MCP-compatible client can connect

You can think of it as a local "MCP server manager" — you register your server once, and ToolHive handles starting it, securing its secrets, and making it available to AI tools.


Architecture

The bundled scenario wires four tools across three external systems. Steps are numbered in the order the agent calls them:

flowchart TB
    Agent["Agent script<br/>agents/toolhive.py"]
    LLM["LLM provider"]
    Agent <-- "tool-calling loop" --> LLM

    subgraph th["ToolHive"]
        Proxy["HTTP proxy"]
        Secrets[/"Secrets → env"/]
        subgraph box["MCP container · node-wire"]
            T1["fhir_*_read_patient"]
            T2["google_drive_files_upload"]
            T3["smtp_send_email"]
        end
    end

    EHR["Epic / Cerner<br/>FHIR R4"]
    Drive["Google Drive"]
    Mail["SMTP relay"]

    Agent -- "MCP / HTTP" --> Proxy
    Proxy -- "stdio" --> box
    Secrets -. "env" .-> box
    T1 -- "1 · fetch patient" --> EHR
    T2 -- "2 · upload summary" --> Drive
    T3 -- "3 · email the link" --> Mail

    classDef agent fill:#eceff3,stroke:#5b7387,stroke-width:1px,color:#1c2733
    classDef tool fill:#ddf1fb,stroke:#1a88b0,stroke-width:1px,color:#0d2f3d
    classDef store fill:#f2f4f7,stroke:#8a9bac,stroke-width:1px,color:#1c2733
    classDef extn fill:#ddf5f0,stroke:#12867a,stroke-width:1px,color:#0b2f2a
    class Agent,LLM agent
    class T1,T2,T3,Proxy tool
    class Secrets store
    class EHR,Drive,Mail extn
    style th fill:#f4fbfe,stroke:#1a88b0,stroke-width:1px,color:#0d2f3d
    style box fill:#e8f6fd,stroke:#1a88b0,stroke-width:1px,color:#0d2f3d

ToolHive runs the connector platform in a secure Docker container, injects secrets as environment variables, and exposes an HTTP proxy. The agent script connects to that proxy, discovers tools via tools/list, and orchestrates the workflow using an LLM's tool-calling capability.


What does the Node Wire MCP server expose?

When running this scenario’s minimal multi-connector stack (one MCP server per connector image registered in ToolHive), each per-connector server (agents.stripe_mcp and the others) advertises that connector's full action set, not only the one this workflow calls — Cerner and Epic 5 each, Google Drive 7 (files.* plus permissions.create), Stripe 5, SMTP 1, so about 23 tools in total.

The unified MCP server (python -m agents.mcp_entrypoint) exposes all manifest actions for every registered connector enabled for MCP in config/connectors.yaml — about 35 with the bundled connectors, plus the nw_* tenant/config tools when multi-tenancy is on.

The following table lists the five tools this section covers: the four the workflow calls (Cerner read patient, Epic read patient, Drive upload, SMTP send), which the architecture diagram shows, plus a Stripe charge that the stack registers but the workflow does not call. See nw-mcp-builder for the full surface.

Tool Description
fhir_cerner_read_patient Fetch a patient's record from a Cerner FHIR R4 endpoint
fhir_epic_read_patient Fetch a patient's record from an Epic FHIR R4 endpoint
google_drive_files_upload Create and upload a text file to Google Drive
stripe_charge Process a payment — registered by the stack, but not used by the FHIR → Drive → email workflow
smtp_send_email Send an email via SMTP

Advertised names use underscores (connector_id_action). Legacy dotted names (for example, fhir_epic.read_patient) still work on tools/call.

The agent uses an LLM's tool-calling capability to decide which tools to call, in what order, and with what parameters.

PHI masking in the agent prompt

The agent's system prompt (src/agents/toolhive.py) instructs the LLM to mask sensitive health information before it uploads or emails anything. The MCP server itself does not mask FHIR data, so masking depends on the LLM following the prompt:

  • Date of birth: year is masked (****-MM-DD)
  • Patient IDs: all but the first three digits are masked (8877665 becomes 887****)
  • When emailing summaries, the agent uploads data to Drive first and sends a link — not raw patient data

Prerequisites

You need the following:

Requirement Notes
ToolHive UI installed Download for macOS / Linux / Windows
Docker running Only needed to build the image
Cerner FHIR credentials client_id, kid, private key, tenant URL
Google Drive service account JSON service_account.json or file path
SMTP credentials Gmail App Password recommended
Groq API key (free) console.groq.com

Quick start (non-developers)

These two methods let a non-developer get the agent running quickly: the recommended path uses the ToolHive UI (no code), and the local path is for someone who can run a single PowerShell script.

  1. Open the ToolHive UI on your machine.
  2. Build or pull the Docker image node-wire:latest (admins can do this for you), then add a Server / Container.
  3. Name it node-wire-connectors. Set Transport to stdio.
  4. In the server's Environment / Secrets section, add the variables from Environment variables. For GOOGLE_DRIVE_SA_JSON paste the entire service account JSON into the secret value (do not upload a file path here).
  5. Start the server. ToolHive shows an Endpoint URL like http://localhost:<PORT>/sse or a proxy URL that contains /sse or /mcp.
  6. Copy the proxy URL and paste it into a local .env or give it to the person running the agent as TOOLHIVE_MCP_URL.

Option B: Local quick run (Windows PowerShell)

Prerequisite: Install Python 3.11, 3.12 or 3.13, and Git. If you cannot install, ask an administrator to run Option A.

  1. Open PowerShell and clone or navigate to the project folder.
  2. Create a .env file in the project root (replace placeholder values):

    Set-Content -Path .env -Value @'
    TOOLHIVE_MCP_URL=http://localhost:7977/mcp
    LLM_PROVIDER=groq
    GROQ_API_KEY=<your-groq-api-key>
    GROQ_MODEL=openai/gpt-oss-120b
    FROM_EMAIL=from@example.com
    SMTP_HOST=sandbox.smtp.mailtrap.io
    SMTP_PORT=2525
    SMTP_USERNAME=<your-smtp-username>
    SMTP_PASSWORD=<your-smtp-password>
    SMTP_USE_TLS=true
    GOOGLE_DRIVE_FOLDER_ID=<your-folder-id>
    '@
    

    Replace the following:

    • <your-groq-api-key>: your Groq API key.
    • <your-smtp-username> and <your-smtp-password>: your SMTP credentials.
    • <your-folder-id>: the ID of the Drive folder shared with the service account.
  3. Install the Python extras (one-time):

    pip install -e ".[agents]"
    
  4. Run the agent (example):

    #$env:TOOLHIVE_MCP_URL="http://localhost:7977/mcp"
    python -m agents.toolhive --patient-id 12724066 --recipient-email you@example.com
    

Notes for non-developers:

  • If you see errors about missing credentials, re-open .env and ensure each secret is filled in. Use the ToolHive UI option if you can't edit .env safely.
  • For email testing you can use Mailtrap or Mailhog (no real emails sent).

Step 1: Build the Docker image

From the root of the repository, build the Python wheels first (required for the Docker image):

bash scripts/build-packages.sh

Then build the container image:

docker build -t node-wire:latest .

This packages the MCP server and all dependencies into a self-contained image. The image's default command is python -m agents.mcp_entrypoint.

Verify the build succeeded:

docker images | grep node-wire

The output is similar to the following:

node-wire   latest   <image-id>   ...

Step 2: Prepare your secrets

ToolHive injects these as environment variables inside the container at runtime. They are never stored in config files or Docker images.

Gather the following values before proceeding:

Secret name Description Where to get it
CERNER_FHIR_BASE_URL Your Cerner FHIR R4 base URL Cerner developer portal, format: https://fhir-ehr-code.cerner.com/r4/<tenant-id>
CERNER_CLIENT_ID Cerner app client ID Cerner developer portal
CERNER_KID Key ID for your RSA key pair You choose this when registering the app
CERNER_PRIVATE_KEY RSA private key (full PEM block) Generated when you registered your app
CERNER_TOKEN_URL Cerner OAuth2 token endpoint Format: https://authorization.cerner.com/tenants/<tenant-id>/...
GOOGLE_DRIVE_SA_JSON Contents of your service account JSON (not the file path — paste the full JSON string) See Google Drive service account setup
SMTP_USERNAME Full email address (must include @) Your email address, for example, you@gmail.com
SMTP_PASSWORD App password for SMTP For Gmail: create an App Password
SMTP_HOST SMTP server hostname smtp.gmail.com for Gmail
SMTP_PORT SMTP port 587 for STARTTLS, 465 for implicit TLS

If you use Epic instead of Cerner, replace the CERNER_* variables with their EPIC_* equivalents (EPIC_FHIR_BASE_URL, EPIC_TOKEN_URL, EPIC_CLIENT_ID, EPIC_KID, EPIC_PRIVATE_KEY).

For GOOGLE_DRIVE_SA_JSON, paste the entire contents of the service account JSON file as the secret value — not the file path. The Docker container doesn't have access to your local filesystem.


Step 3: Store secrets in ToolHive

  1. Open the ToolHive application.
  2. Navigate to Secrets (or Environment Variables) in the sidebar.
  3. For each secret in the Step 2 table, click Add Secret and enter the name and value.

Option B: ToolHive CLI (thv)

If you have the thv CLI installed:

thv secret set CERNER_FHIR_BASE_URL
# You'll be prompted to enter the value securely

thv secret set CERNER_CLIENT_ID
thv secret set CERNER_KID
thv secret set CERNER_PRIVATE_KEY
thv secret set CERNER_TOKEN_URL
thv secret set GOOGLE_DRIVE_SA_JSON
thv secret set SMTP_USERNAME
thv secret set SMTP_PASSWORD
thv secret set SMTP_HOST
thv secret set SMTP_PORT

Step 4: Register the MCP server in ToolHive

Option A: ToolHive UI

  1. In the ToolHive app, go to Servers → Add Server.
  2. Select Docker Container or Custom Server.
  3. Fill in the fields:
  4. Name: node-wire-connectors
  5. Image: node-wire:latest
  6. Transport: stdio
  7. Under Environment, set variables from the following table (multi-tenancy) or link flat secrets from Step 3 (single-tenant).
  8. If using multi-tenancy, add a volume: host config/tenants.yaml → container /app/config/tenants.yaml (read-only).
  9. Click Start or Deploy.

ToolHive starts the container and sets up a stdio-to-HTTP proxy on a local port.

For the unified MCP server, multi-tenancy is the recommended setup. Set the following variables:

Name Value
NW_MCP_TRANSPORT stdio
NW_ALLOWED_CONNECTORS for example, google_drive,fhir_epic
NW_MULTITENANCY_ENABLED true
NW_TENANTS_PATH /app/config/tenants.yaml
NW_MCP_AUTH_DISABLED true
NW_MCP_SCOPE_POLICY_DEFAULT allow
NW_MCP_TENANT_PIN_LOCKED false

Use nw_select_tenant / nw_select_config (or agent --tenant-id / --config-name) before connector calls. One config name applies to every connector — pick a name that exists on all connectors you use. See Tenancy — MCP tools.

Option B: ToolHive CLI (single-tenant secrets)

Run the server with each secret mapped into the container:

thv run \
  --name node-wire-connectors \
  --transport stdio \
  --secret CERNER_FHIR_BASE_URL,target=CERNER_FHIR_BASE_URL \
  --secret CERNER_CLIENT_ID,target=CERNER_CLIENT_ID \
  --secret CERNER_KID,target=CERNER_KID \
  --secret CERNER_PRIVATE_KEY,target=CERNER_PRIVATE_KEY \
  --secret CERNER_TOKEN_URL,target=CERNER_TOKEN_URL \
  --secret GOOGLE_DRIVE_SA_JSON,target=GOOGLE_DRIVE_SA_JSON \
  --secret SMTP_USERNAME,target=SMTP_USERNAME \
  --secret SMTP_PASSWORD,target=SMTP_PASSWORD \
  --secret SMTP_HOST,target=SMTP_HOST \
  --secret SMTP_PORT,target=SMTP_PORT \
  node-wire:latest

For multi-tenancy, add -e flags for the multi-tenancy table in Option A and mount tenants.yaml (ToolHive volume UI or Docker bind).

What you should see

In the ToolHive UI under Installed, you should see:

Field Value
Name node-wire-connectors
Status Running
Tools Manifest-driven <connector_id>_<action> (for example, fhir_cerner_read_patient, fhir_epic_read_patient, google_drive_files_upload, smtp_send_email; with MT also nw_list_tenants, nw_select_tenant, nw_list_configs, nw_select_config)
Endpoint http://localhost:<auto-port>/sse

Step 5: Configure the agent

Copy the proxy URL from the ToolHive UI (shown in the server's details page) and add it to your local .env file:

# Replace <port> with the port number shown in ToolHive
TOOLHIVE_MCP_URL=http://localhost:<port>/mcp

# LLM provider (groq is recommended — it's fast and has a free tier)
LLM_PROVIDER=groq
GROQ_API_KEY=<your-groq-api-key>

Step 6: Run the agent

Install agent dependencies (one-time)

Install the agent extras:

pip install -e ".[agents]"

Execute the scenario

Run the scenario for a patient:

python -m agents.toolhive \
  --patient-id 12724066 \
  --recipient-email your-email@example.com \
  --drive-folder-id "<your-folder-id>"

Replace <your-folder-id> with the ID of the Drive folder shared with the service account.

The command takes the following arguments:

Argument Required Description
--patient-id Yes (or --patient-family + --patient-given) Numeric patient ID in the EHR system
--patient-family Alternative to --patient-id Patient last name for name-based search
--patient-given Used with --patient-family Patient first name
--recipient-email Yes Where to send the patient summary
--drive-folder-id No Google Drive folder ID; defaults to GOOGLE_DRIVE_FOLDER_ID env var
--max-steps No Maximum LLM reasoning steps (default: 10)
--max-tool-failures No Stop after this many failed calls to the same tool name (default: TOOLHIVE_MAX_TOOL_FAILURES env, else 2)
--tenant-id No Pin MCP tenant (X-Tenant-ID on HTTP; NW_TENANT_ID for --local). Defaults from NW_TENANT_ID env.
--config-name No Calls nw_select_config at start so every connector uses that name

With multitenancy enabled, MCP loads config/tenants.yaml and advertises the nw_* tenant/config tools. How they behave on each transport: Tenancy — MCP tools.

Switching LLM providers

Set LLM_PROVIDER and the provider's API key on the command line:

# Use OpenAI
LLM_PROVIDER=openai OPENAI_API_KEY=<openai-api-key> python -m agents.toolhive <arguments>

# Use Claude (Anthropic)
LLM_PROVIDER=anthropic ANTHROPIC_API_KEY=<anthropic-api-key> python -m agents.toolhive <arguments>

# Use Google Gemini
LLM_PROVIDER=gemini GEMINI_API_KEY=<gemini-api-key> python -m agents.toolhive <arguments>

# Use NVIDIA (OpenAI-compatible endpoint)
LLM_PROVIDER=nvidia NVIDIA_API_KEY=<nvidia-api-key> python -m agents.toolhive <arguments>

Replace the following:

  • <openai-api-key>, <anthropic-api-key>, <gemini-api-key>, and <nvidia-api-key>: the API key for the provider you select.
  • <arguments>: the arguments from the preceding table, for example, --patient-id and --recipient-email.

Sample output

A successful run prints output similar to the following:

============================================================
Node Wire ToolHive Agent
Provider : groq
MCP info : http://localhost:34567/mcp
============================================================
Task: fetch patient details from FHIR, create a summary file in Google Drive, and email it to the recipient. (Patient identifiers and recipient address are withheld from logs.)

============================================================
RESULT
============================================================
✅ Success  | trace_id=3e4f5a6b-...

Final Answer:
I have completed all three steps:
1. Fetched patient Nancy Smart (DOB: ****-03-15) from Cerner FHIR.
2. Created 'patient_summary_12724066.txt' in Google Drive (file_id: 1XYZ...).
3. Sent a summary email to your-email@example.com with a link to the file.

Steps executed (3):
  ✓ Step 1: fhir_cerner_read_patient
       args   : {"resource_id": "12724066"}
       result : {"patient_id": "12724066", "full_name": "Nancy Smart", ...}
  ✓ Step 2: google_drive_files_upload
       args   : {...}
       result : {"file_id": "1XYZ...", "web_view_link": "https://docs.google.com/..."}
  ✓ Step 3: smtp_send_email
       args   : {...}
       result : {"sent": true}

Local testing (no ToolHive required)

You can test the full end-to-end flow on your local machine without ToolHive.

Option A: stdio mode (--local flag)

This launches the MCP server as a subprocess and connects to it directly:

# Set your credentials in .env first, then:
export GROQ_API_KEY="<your-groq-api-key>"

python -m agents.toolhive \
  --local \
  --patient-id 12724066 \
  --recipient-email your-email@example.com

The --local flag skips the ToolHive proxy and talks to the MCP server over stdio, so you can test without Docker.

Option B: MCP Inspector

The MCP Inspector is a browser-based tool for inspecting and manually calling MCP tools:

npx @modelcontextprotocol/inspector python -m agents.mcp_entrypoint

Open the URL it prints (usually http://localhost:5173) to browse the available tools and send test requests.

Option C: local email testing with Mailhog

If you don't want to use a real SMTP server while testing:

# Start a local fake SMTP server
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog

Update your .env (for a local agent, not inside ToolHive's container):

SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=

View captured emails at http://localhost:8025.

ToolHive + Mailhog: If the MCP server runs in Docker and Mailhog runs on the host, point ToolHive secrets at the host SMTP port (for example, SMTP_HOST = host.docker.internal, SMTP_PORT = 1025 on macOS/Windows Docker Desktop) and disable TLS if your Mailhog setup expects plain SMTP. Re-register or restart the server after changing secrets.


Claude Desktop and Cursor integration

Once ToolHive is running node-wire-connectors, any MCP-compatible client can connect to it.

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "node-wire-connectors": {
      "url": "http://localhost:<PORT>/sse"
    }
  }
}

Replace <PORT> with the port shown in ToolHive UI. The Node Wire connector tools appear in Claude's tool sidebar automatically.

Cursor

In Cursor's MCP settings, add the same endpoint URL. The tools appear in the agent's tool list.


Environment variables (complete list)

This is the full set of environment variables used by the connector platform and the agent. Values marked "Required" must be provided for the matching connector to work.

Variable Required for Notes / Example value
CERNER_CLIENT_ID Cerner connector Required for Cerner FHIR OAuth
CERNER_FHIR_BASE_URL Cerner connector Base FHIR URL for Cerner
CERNER_KID Cerner connector Key ID for private key (kid)
CERNER_PRIVATE_KEY Cerner connector PEM private key (paste full block)
CERNER_TOKEN_URL Cerner connector Token endpoint URL
EPIC_CLIENT_ID Epic connector Required for Epic OAuth
EPIC_FHIR_BASE_URL Epic connector Base FHIR URL for Epic
EPIC_KID Epic connector Key ID for private key (kid)
EPIC_PRIVATE_KEY Epic connector PEM private key (paste full block)
EPIC_TOKEN_URL Epic connector for example, https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token
FROM_EMAIL Email sending Example: from@example.com
GROQ_API_KEY LLM (Groq) Your Groq API key
GROQ_MODEL LLM Example: openai/gpt-oss-120b
NW_MCP_TRANSPORT ToolHive / local stdio when running in ToolHive container
PYTHONPATH Runtime for example, /app/src for container; **/node-wire/src locally
SMTP_HOST SMTP connector Example: sandbox.smtp.mailtrap.io
SMTP_PORT SMTP connector Example: 2525
SMTP_USERNAME SMTP connector Mailtrap / SMTP user
SMTP_PASSWORD SMTP connector Mailtrap / SMTP password
SMTP_USE_TLS SMTP connector true or false
GOOGLE_DRIVE_SA_JSON Google Drive Either paste full JSON into ToolHive secret or provide absolute file path to the service account JSON

Individual connector MCP servers

For modular deployments, each connector can be run as an independent MCP server container:

  • nw-google-drive (Google Drive)
  • nw-smartonfhir-epic (Epic SMART on FHIR)
  • nw-smartonfhir-cerner (Cerner SMART on FHIR)
  • nw-smtp (SMTP email)
  • nw-stripe (Stripe)
  • nw-salesforce (Salesforce)
  • nw-slack (Slack)

When running multiple MCP servers, configure the agent with TOOLHIVE_MCP_URLS (comma-separated list of ToolHive proxy URLs). The agent merges tools across servers.

Full guide (pre-built per-connector Docker images): Local wheels → images


Troubleshooting

The following table lists common problems and their fixes:

Problem Likely cause Fix
TOOLHIVE_MCP_URL (single) or TOOLHIVE_MCP_URLS (comma-separated) is not set Missing env var Copy the endpoint URL from ToolHive UI → Installed → node-wire-connectors and add to .env, or use --local
Failed to list MCP tools: Connection refused ToolHive server stopped Restart via ToolHive UI, or run thv run ... again; check thv list to see running servers
Secret 'CERNER_PRIVATE_KEY' is not configured Secret not stored in ToolHive Run thv secret set CERNER_PRIVATE_KEY or add it via the ToolHive UI
google_drive connector: authentication failed GOOGLE_DRIVE_SA_JSON is a file path, not JSON content For ToolHive, paste the actual JSON contents of the file (not the file path) as the secret value; for local .env, use an absolute path to the JSON file per Google Drive service account setup
SMTP authentication failed Wrong username or password For Gmail, use an App Password not your regular password; confirm SMTP_USERNAME includes @
groq SDK not installed Missing optional dependency pip install -e ".[agents]"
Agent loops forever without completing LLM reasoning issue Try increasing --max-steps; try a different LLM provider; check that the expected tools are visible in ToolHive (tools/list); refresh after MCP image upgrades
docker: Cannot connect to the Docker daemon Docker not running Start Docker Desktop
Container starts but shows 0 tools MCP server failed to start Check container logs: docker logs <container-id>; verify the image built successfully

Running tests

The test suite covers the agent and MCP server without making any real API calls:

# dev is not a pip extra (it's a uv dependency group) — for the dev toolchain use:
uv sync --frozen --extra agents --dev
pytest tests/test_toolhive_agent.py -v

Expect every test collected from tests/test_toolhive_agent.py to pass (names and count change as the suite evolves). If a test fails, re-run with -v and compare against the current definitions in that file.


File layout (agents)

The agent-related files:

node-wire/
├── Dockerfile                              ← Docker image for ToolHive
├── pyproject.toml                          ← [agents] extras added
├── sample.env                              ← env var reference
└── src/
    └── agents/
        ├── __init__.py
        ├── mcp_entrypoint.py               ← MCP stdio server (manifest; all MCP connectors)
        ├── toolhive.py                     ← ReAct agent + CLI
        ├── llm_factory.py                  ← Provider factory
        └── providers/
            ├── groq_provider.py            ← Default (Groq)
            ├── openai_provider.py
            ├── gemini_provider.py
            └── anthropic_provider.py