A minimal Model Context Protocol (MCP) server that connects an AI assistant to AVEVA Connect Data Services — specifically the Sequential Data Store (SDS).
This project is a training reference for AVEVA system integrators. It is intentionally small. Every design decision is explained so you can read the code, understand why it is structured the way it is, and apply the same patterns to your own integrations.
AI assistants do not natively know how to query your historian. MCP is the bridge. It lets you expose any data source — a REST API, a database, a COM object, anything — as a set of callable tools that an AI can discover and use during a conversation.
Building those tools is not a black box. This project shows the full picture:
- How to wrap a real industrial REST API as MCP tools
- How OAuth 2.0 authentication works in this context
- How to write tool descriptions that give the AI enough context to use them correctly
- How to run and test the server without an AVEVA account
When no credentials are configured the server runs in demo mode, returning pre-generated data for a fictional utility: Aveva Water Authority (AWA).
The AWA namespace contains six streams:
| Stream ID | Description | Units |
|---|---|---|
FIT-101.PV |
Influent flow rate — diurnal pattern | gpm |
AIT-301.PV |
Aeration basin dissolved oxygen | mg/L |
PDT-401.PV |
Filter 1 head loss — drifting toward backwash threshold | ft |
AIT-501.PV |
Effluent turbidity — spike ~6 hours ago, now recovered | NTU |
FIC-601.PV |
Chlorine dosing rate — correlated with influent flow | lb/day |
P-201.RunStatus |
Transfer pump 201 run/stop status | Boolean |
The data is designed to give the AI something meaningful to say. The filter head loss drift, the turbidity exceedance, the pump trip, and the dosing correlation are all present and discoverable through normal tool use.
Demo data is deterministic, generated from a fixed seed, so the AI sees the same values every time, making training sessions reproducible.
1. Clone the repository
git clone https://github.com/smslavin/connect-data-services-mcp.git
cd connect-data-services-mcp2. Create a virtual environment
python -m venv .venv3. Activate the virtual environment
Windows:
.venv\Scripts\Activate.ps1macOS/Linux:
source .venv/bin/activate4. Install dependencies
pip install -r requirements.txt5. Run the server
python server.pyYou should see:
connect-data-services MCP server starting (DEMO — Aveva Water Authority mock data)
The server is now running over stdio, ready for an MCP client to connect.
The configuration file location depends on your OS:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"connect-data-services": {
"command": "python",
"args": ["C:/path/to/connect-data-services-mcp/server.py"]
}
}
}Claude Desktop runs on Windows but can launch the server inside WSL. Use the full path to the Python executable inside the virtual environment — this avoids any dependency on the WSL shell's PATH.
{
"mcpServers": {
"connect-data-services": {
"command": "wsl",
"args": [
"/home/username/connect-data-services-mcp/.venv/bin/python",
"/home/username/connect-data-services-mcp/server.py"
]
}
}
}Replace username with your WSL username. If you cloned the repo to a Windows
path, the WSL equivalent is /mnt/c/Users/<WindowsUser>/path/to/connect-data-services-mcp.
Restart Claude Desktop after saving. The four tools (list_namespaces,
list_streams, get_stream_metadata, get_values) will appear in Claude's
tool list automatically.
1. Get credentials from the Connect portal
In the portal (connect.aveva.com):
- Your Tenant ID is shown in Account Settings (top-right corner)
- Client ID and Secret are created under Security → Clients
2. Configure the environment
Copy .env.example to .env and fill in your values:
CONNECT_TENANT_ID=your-tenant-id
CONNECT_CLIENT_ID=your-client-id
CONNECT_CLIENT_SECRET=your-client-secret
CONNECT_REGION=your-region-slug
All four variables are required. The server falls back to demo mode if any are missing rather than connecting to the wrong endpoint.
Your region slug is the subdomain prefix of your Connect portal URL:
https://euno.datahub.connect.aveva.com → euno. Common values:
uswe (US West), euno (EU North), apso (Asia Pacific South).
3. Run the server
python server.pyYou should see:
connect-data-services MCP server starting (LIVE — tenant your-tenant-id)
All four tools now make real calls to your tenant's SDS namespace.
With the server running and connected to Claude Desktop, try this prompt:
"What process data is available in the AWA namespace, and is anything worth my attention right now?"
Claude will call list_namespaces, list_streams, get_stream_metadata for
each stream of interest, and get_values across a 48-hour window — all from
that single question. The response will look something like this:
I found six streams in the AWA-Production namespace covering influent flow, aeration, filtration, effluent quality, chemical dosing, and pump status. A few things stand out:
Filter 1 head loss (PDT-401.PV) has climbed from 1.4 ft to 6.7 ft over the past 48 hours and is approaching the 8.0 ft backwash threshold. At the current rate it will reach that limit within the next several hours.
Effluent turbidity (AIT-501.PV) spiked to 1.8 NTU approximately six hours ago — above the 1.0 NTU permit limit — before recovering to normal. Transfer pump 201 (P-201.RunStatus) tripped at the same time and was offline for about 30 minutes. The two events appear related.
Chlorine dosing (FIC-601.PV) is tracking the expected diurnal flow pattern — nothing unusual there.
Would you like a closer look at the filter trend or the turbidity event?
This is the pattern you are building toward with your own customers: a single question that spans multiple streams, surfaces correlations a human might miss, and offers a clear next step — without any custom application code.
Returns the namespaces in your tenant. A namespace is a partitioned
time-series store — typically one per site or operational area. The Id
field from the response is used in every subsequent call.
Lists streams in a namespace. A stream is the SDS equivalent of a PI tag or
historian point — a time-ordered sequence of values for a single measurement.
The optional query parameter filters by Id, Name, Description, or Tags.
Returns the full metadata record for one stream: its type, description, engineering units, operating limits, and interpolation mode. Call this before querying values so the AI has the context to interpret what it gets back.
The InterpolationMode field is worth understanding:
Continuous— values between stored points are linearly interpolated (analog sensors, flow rates, temperatures)StepwiseContinuousLeading— the value holds until the next stored point (discrete signals like pump run/stop status)
Queries values from a stream between a start and end timestamp (ISO 8601 UTC).
Returns up to count values, evenly sampled across the window. The default
count of 100 is appropriate for trend analysis; increase it for finer
resolution over a short event window.
Why four tools and not one?
Each tool does one thing. The AI builds up context incrementally. First learn what namespaces exist, then what streams they contain, then what a specific stream means, then fetch its values. This mirrors how a human analyst would approach an unfamiliar historian. It also gives the AI natural checkpoints to confirm it is querying the right thing before consuming data.
Why stdio transport?
The MCP specification supports both stdio (subprocess) and HTTP transports. Stdio is simpler to distribute. One command, no port to configure, no firewall rules. For a server that runs locally alongside Claude Desktop, stdio is the right default.
Why auto-detect demo vs live mode?
Removing friction is important for adoption. An SI who clones this repo and
runs python server.py gets a working server immediately. When they are ready
to connect real data, they add a .env file with all four required variables
(CONNECT_TENANT_ID, CONNECT_CLIENT_ID, CONNECT_CLIENT_SECRET,
CONNECT_REGION) and nothing else changes.
Why use OpenID Connect discovery for the token endpoint?
Rather than hardcoding the OAuth token URL, the server fetches it at startup
from the well-known OpenID Connect configuration document at
{BASE_URL}/identity/.well-known/openid-configuration. This is the pattern
AVEVA uses in their own authentication samples. It stays correct if AVEVA
moves the endpoint in a future release, and it avoids the need to know the
exact URL path in advance. The discovery document is the authoritative source.
Why are descriptions long?
Tool descriptions are the AI's primary source of context. A description that says "returns flow values" tells the AI what the tool returns but not what flow means, what units it is in or what a normal value looks like. Richer descriptions reduce hallucination and lead to better questions from the AI. This is more important for smaller models that have less world knowledge to draw on.
Why is the mock data deterministic?
Training and demos require reproducibility. If the data changed every time you ran the server, you could not build a consistent narrative around it or use screenshots from one session in a slide deck shown in the next. Seeding random from the stream ID gives each stream its own independent history while keeping results stable across restarts.
Why does the mock data tell a story?
Data with correlations and anomalies gives the AI something to reason about. A filter trending toward its backwash threshold, a past turbidity exceedance that coincided with a pump trip, dosing that tracks influent flow. These are the kinds of patterns an operator cares about and they are the patterns that demonstrate the value of putting an AI over your historian.
connect-data-services-mcp/
├── server.py Main file: FastMCP app, tool definitions, OAuth client
├── mock_data.py Demo data: stream definitions and time-series generation
├── requirements.txt
├── .env.example Credential template — copy to .env, never commit .env
└── .gitignore
This server covers read-only access to the SDS API. The same pattern extends naturally to:
- Event frames — query operational events and correlate them with process data
- Assets — navigate the asset hierarchy to find streams by equipment rather than tag name
- Annotations — attach AI-generated observations back to the historian as a record
- Write tools — with appropriate authorization, update setpoints or acknowledge alarms
Each of these is a new tool following the same structure you see in
server.py. The pattern does not change; the scope expands.