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?
- Architecture
- What does the Node Wire MCP server expose?
- Prerequisites
- Quick start (non-developers)
- Step 1: Build the Docker image
- Step 2: Prepare your secrets
- Step 3: Store secrets in ToolHive
- Step 4: Register the MCP server in ToolHive
- Step 5: Configure the agent
- Step 6: Run the agent
- Local testing (no ToolHive required)
- Claude Desktop and Cursor integration
- Environment variables (complete list)
- Individual connector MCP servers
- Troubleshooting
- Running tests
- File layout (
agents) - Related documentation
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 (
8877665becomes887****) - 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.
Option A: ToolHive UI (recommended, no code)¶
- Open the ToolHive UI on your machine.
- Build or pull the Docker image
node-wire:latest(admins can do this for you), then add a Server / Container. - Name it
node-wire-connectors. Set Transport tostdio. - In the server's Environment / Secrets section, add the variables from Environment variables. For
GOOGLE_DRIVE_SA_JSONpaste the entire service account JSON into the secret value (do not upload a file path here). - Start the server. ToolHive shows an Endpoint URL like
http://localhost:<PORT>/sseor a proxy URL that contains/sseor/mcp. - Copy the proxy URL and paste it into a local
.envor give it to the person running the agent asTOOLHIVE_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.
- Open PowerShell and clone or navigate to the project folder.
-
Create a
.envfile 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.
-
Install the Python extras (one-time):
-
Run the agent (example):
Notes for non-developers:
- If you see errors about missing credentials, re-open
.envand ensure each secret is filled in. Use the ToolHive UI option if you can't edit.envsafely. - 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):
Then build the container image:
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:
The output is similar to the following:
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¶
Option A: ToolHive UI (recommended for beginners)¶
- Open the ToolHive application.
- Navigate to Secrets (or Environment Variables) in the sidebar.
- 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¶
- In the ToolHive app, go to Servers → Add Server.
- Select Docker Container or Custom Server.
- Fill in the fields:
- Name:
node-wire-connectors - Image:
node-wire:latest - Transport:
stdio - Under Environment, set variables from the following table (multi-tenancy) or link flat secrets from Step 3 (single-tenant).
- If using multi-tenancy, add a volume: host
config/tenants.yaml→ container/app/config/tenants.yaml(read-only). - 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:
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-idand--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:
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:
Update your .env (for a local agent, not inside ToolHive's container):
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):
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
Related documentation¶
- Installation guide — Full platform setup guide
- Configuration guide — Environment variables and settings
- google_drive_connector.md — Google Drive service account setup and REST API reference