PSI Machine Intelligence MCP Server
MCP server that gives AI agents (Claude Code, Claude Desktop, custom agents) tool access to all PSI machine data — BOM, PLC programs, electrical drawings, project provenance, supply chain, fault diagnostics.
Overview
The PSI Machine Intelligence MCP Server exposes 78 tools over the Model Context Protocol standard. It connects to the UniData REST API for ERP data, reads network file shares (LDS/CAD via UNC paths) for PLC programs, electrical drawings, and engineering files, and uses Azure OpenAI for AI-powered assessments.
The vision: An engineer asks “Machine 2399 is throwing a low air pressure fault — what sensor is that, where’s the wiring diagram, and have we seen this on other machines?” and gets a complete answer.
| Feature | Description |
|---|---|
| Production URL | http://ps-proxy:3100/mcp |
| Source | ProgressiveSurface/psi-machine-mcp |
| Dev Location | C:\git\PLC\mcp-server\ |
| Runtime | Node.js (CommonJS) + ffmpeg (for video transcription) |
| SDK | @modelcontextprotocol/sdk v1.26.0 |
| Tools | 64 tools across 14 categories |
| Transport | Stdio (local) or Streamable HTTP (network) |
| HTTP Port | 3100 (configurable via MCP_PORT env var) |
| Deployed | PS-PROXY as Windows Service via NSSM |
| Auto-deploy | Push to master triggers GitHub Actions deploy |
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ MCP Clients │
├──────────────────────────────────────────────────────────────────┤
│ │
│ PSI Explorer "Ask the Fleet" Claude Code Claude │
│ (explorer.progressivesurface (developer CLI) Desktop │
│ .com — any PSI employee) (GUI) │
│ │ │ │ │
│ │ HTTP (MCP SDK) │ stdio │ HTTP │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PSI Machine Intelligence MCP Server │ │
│ │ (Node.js, PS-PROXY:3100, 78 tools) │ │
│ └────┬──────────┬──────────────┬───────────────┬──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ REST API File Shares PLC Tools Analysis │
│ (93+ eps) (LDS, CAD) (ACD, L5X) (reports) │
└──────────────────────────────────────────────────────────────────┘
Clients
| Client | Transport | Users | Setup Required |
|---|---|---|---|
| Ask the Fleet (PSI Explorer) | HTTP | Any PSI employee | None — open the website |
| Claude Code | HTTP (Entra bearer via headersHelper) | Developers | psi-claude-skills mcp — see Connect AI Agents |
| Claude Desktop | Stdio, through a local bridge | Anyone with the app | psi-claude-skills mcp — Desktop cannot connect over HTTP (see below) |
| Custom agents | HTTP | Developers | @modelcontextprotocol/sdk client |
Ask the Fleet is the first production web application consuming the MCP server. It embeds an AI chat panel inside PSI Explorer where users ask questions in plain English. The Express.js backend connects to PS-PROXY:3100 as an MCP client, passes the 78 tools to Azure OpenAI (GPT 5.2), and streams responses back to the browser via SSE. No AI expertise or developer tools required — anyone on VPN can use it at explorer.progressivesurface.com.
IMPORTANT
Claude Desktop cannot connect to this server directly. It only launches stdio servers, and its “custom connector” path negotiates OAuth using dynamic client registration (RFC 7591), which Entra ID does not support — it fails with
Incompatible auth server: does not support dynamic client registration. Desktop is connected viapsi-mcp-bridge.py, a dependency-free stdio↔HTTP bridge that attaches a token minted from the user’s ownaz login. Both the bridge and the installer that wires it up ship in thePSI.ClaudeSkillsbundle (claude-code/mcp/inProgressiveSurface/psi-claude-org-skills); users runpsi-claude-skills mcp.
Two Transport Modes
| Mode | Flag | Use Case |
|---|---|---|
| Stdio | node mcp-server.js (default) | Single-user, local Claude Code / Claude Desktop |
| HTTP | node mcp-server.js --http | Multi-user, network access, remote agents |
Stdio mode — Claude Code spawns the server as a child process. Communication is JSON-RPC over stdin/stdout. This is the standard MCP pattern for local tools.
HTTP mode — Runs an Express server on port 3100 supporting both the newer Streamable HTTP protocol (POST/GET/DELETE /mcp) and legacy SSE (GET /sse, POST /messages). Any MCP client on the network can connect.
Tools Reference (51 Tools)
Category 1: Project & Machine Identity
| Tool | Description |
|---|---|
get_project_info | Project metadata with resolved machine type names, customer, team names, schedule, hours |
find_similar_machines | Sibling projects with same machine type — paginated, with resolved machine type names, customer names, status labels |
get_project_lineage | REF.PROJ.NO chain — follow project ancestry (uses /api/project/dev/{job}/lineage) |
list_machine_types | All 50 machine types with project counts (uses /api/project/dev/machine-types) |
Category 2: Bill of Materials & Supply Chain
| Tool | Description |
|---|---|
get_bom | Full BOM explosion for a job or part number |
get_part_details | Part info including description, MRP code, cost, drawing number |
get_part_manufacturer | OEM manufacturer details (code, name, address, design category) |
get_part_vendors | Purchase vendors with open POs, lead times, costs |
search_parts | Find parts by number or description keyword |
Category 3: PLC & Controls
| Tool | Description |
|---|---|
get_plc_analysis | Full PLC analysis (modules, tags, routines, faults). Reads from fleet archive first, falls back to ACD analyzer |
analyze_acd | Directly analyze an ACD file using the Python ACD analyzer |
lookup_fault | Search fault catalog by tag name, severity, or keyword — returns classified faults with abort/hold/inhibit severity. On zero results, suggests available keywords and sample faults |
get_io_map | I/O cross-reference enriched with tag descriptions from analysis (tag ↔ slot ↔ device ↔ drawing ↔ BOM part) |
lookup_plc_tag | Look up a specific PLC tag (data type, scope, usage) |
lookup_pc_read_write | Decode PC_Read[n] or PC_Write[n] from IO spreadsheet |
get_robot_interface | FANUC UOP signal mapping and fault signals |
get_plc_changes | Change history from plc-fleet-archive (what changed and when) |
get_plc_history | Version timeline with attribution (who changed it) |
search_fleet_tags | Cross-fleet tag/module/program search (88 projects, 5,423 tags) |
compare_plc_versions | Side-by-side structural diff between two projects |
get_fleet_summary | Aggregate fleet statistics (overview, modules, changes, processors) |
Category 4: Engineering Files & Drawings
| Tool | Description |
|---|---|
list_project_files | K: drive catalog for a project (categorized: PLC, robot, safety, tags, etc.) |
check_commissioning_readiness | 9-point readiness check from K: drive scanner |
find_drawing | Look up drawing/part on X: drive (SolidWorks, DWG, PDF) |
get_electrical_drawing_index | Parse .wdp file for drawing list, check DWG and PDF availability |
read_drawing_pdf | Read a mechanical or electrical drawing PDF — metadata, text extraction, or binary |
read_bom_file | Read a BOM export file (.xls) from X:\BOMFiles — parsed component list |
get_drawing_metadata | Check what engineering files exist for a part across all CAD directories |
Category 5: Change Tracking & History
| Tool | Description |
|---|---|
get_rfcs | Redbook RFCs for a project (with details, status, costs) |
get_vendor_history | AP payment history for a vendor |
get_work_orders | Work order list for a project |
Category 6: Analysis & Diagnostics
| Tool | Description |
|---|---|
diagnose_fault | Given a fault condition, trace: fault → tag → I/O → device → drawing → BOM → vendor. Reports supply chain lookup errors when vendor/manufacturer APIs fail |
compare_projects | BOM delta with GT code breakdown, shared parts, schedule comparison between two projects |
assess_obsolescence_risk | Check INC/OBS flags + manufacturer lifecycle for a project’s purchased parts |
Category 7: Digital Thread
| Tool | Description |
|---|---|
get_digital_thread | Full digital thread report for a project (runs digital_thread_generator.js) |
Category 8: ACD Discovery
| Tool | Description |
|---|---|
find_acd_files | Find ACD files for a project on K: drive (9,293 ACD files across 467 projects) |
Category 9: Inventory & Material Flow
| Tool | Description |
|---|---|
get_inventory_status | Current on-hand, allocated, available quantities + bin/lot detail for a part |
get_inventory_transactions | Transaction history from INVHIST linked list (PO receipts, WO issues, cycle count adj, etc.) |
get_inventory_history | 24-month summary: monthly balance, receipts, issues, returns, adjustments, scrap, sales |
analyze_inventory_discrepancy | Root cause analysis combining all sources — flags CHRONIC_SHRINKAGE, WO_OVERPICK_LIKELY, RTS_LIKELY, etc. |
Category 10: Cost Analysis & Floor Stock
| Tool | Description |
|---|---|
analyze_part_cost_leakage | Classify every material transaction by WO type (Fabrication, Assembly, FloorStock, GlDirect, etc.), compute extended cost per category, and quantify cost leakage — material costs not attributed to any job |
get_job_cost_attribution | Given a job number, classify all its work orders by type and show cost attribution breakdown (how many Fabrication vs Assembly vs Misc vs Floor Stock WOs) |
Category 11: Customer Lookup & Sales Orders
| Tool | Description |
|---|---|
search_customers | Fuzzy search 4,600+ PSI customers by name — paginated (offset/limit, max 200). Returns customer number, name, location, account type. Call this first to resolve a customer name to a number before using quote/sales tools |
get_open_quotes | List open spare parts quotes by customer number, contact number, or contact email. Returns quote date, value, description, status (open/ordered/expired) |
get_quote_detail | Full detail for a specific quote: header + all line items with part numbers, pricing, quantities, and linked sales orders |
get_sales_orders | Open sales orders for a customer with linked quote numbers. Use to check if a customer PO has been received for a quote |
get_sales_history | Invoiced sales history (VB_SODET.REV4): what was sold to a customer, with quantities, prices, ship dates, and 24-field line detail |
Category 12: Fleet Intelligence & Templates
| Tool | Description |
|---|---|
recommend_template | Given specs (robot type, I/O range, safety required, machine family), scores fleet candidates and returns top matches with gap analysis per area (I/O, safety, quality, robot type) |
audit_safety_compliance | Fleet-wide safety audit: finds GuardLogix without safety task, scores below threshold, no E-stop detection, no fault aggregation |
get_fleet_overview | Fleet-wide statistics and trends |
fleet_parsing_summary | Summary of parsed/unparsed projects across the fleet |
fleet_reuse_summary | Module and tag reuse analysis across projects |
fleet_scorecard_summary | Scorecard distribution and trends across fleet |
search_fleet_summaries | Search fleet summaries by keyword or criteria |
get_machine_summary | Summary for a specific machine type across fleet |
compare_machine_summaries | Compare two machine type summaries side-by-side |
get_plc_scorecard | Quality scorecard for a specific project |
Category 13: AI-Powered Assessment
| Tool | Description |
|---|---|
get_ai_assessment | Azure OpenAI-powered deep analysis of a PLC program — generates fault catalog with test procedures, safety assessment narrative, design quality review, and commissioning checklist. Results cached as ai_assessment.json in fleet archive. Sections: faultCatalog, safetyAssessment, designQuality, commissioningChecklist |
Authentication
The HTTP transport is an OAuth 2.1 Resource Server secured with Microsoft Entra ID bearer
tokens (added 2026-06-23). Every route is protected except /health and the discovery doc.
- App registration:
PSI Machine MCP— client/app id0dfb7d0a-b815-4611-93ba-ccdf3213187b, App ID URIapi://0dfb7d0a-b815-4611-93ba-ccdf3213187b. - Required: delegated scope
Mcp.Invoke(user tokens) or app roleMcp.Invoke.App(daemon / managed-identity tokens). - Validation: RS256 signature via tenant JWKS, plus
iss(PSI tenant),aud(the app), and scope/role checks. Missing/invalid token →401; valid token without the scope/role →403. Fail-closed — ifMCP_APP_CLIENT_IDis unset the server rejects all protected requests. - Discovery: RFC 9728 Protected Resource Metadata at
/.well-known/oauth-protected-resource, plus a401 WWW-Authenticate: Bearer resource_metadata=…header.
How clients get a token
| Client | Mechanism |
|---|---|
| Claude Code | headersHelper runs az account get-access-token --resource api://0dfb7d0a-… per connection (see Option 1). The Azure CLI client is pre-authorized, so any PSI user with az login gets a delegated Mcp.Invoke token. |
Daemon / web backend (e.g. PSI Explorer bom-explorer-web) | App Service managed identity granted the Mcp.Invoke.App app role; DefaultAzureCredential.getToken('api://0dfb7d0a-…/.default'). |
| VS Code / Copilot | Static Authorization header via a .vscode/mcp.json input (no headersHelper support). |
Network posture
Private by default — bound to the PSI network with the firewall scoped to private ranges; no public exposure. stdio connections (Option 2) bypass HTTP auth entirely (local child process, launching user’s identity). Public access for cloud clients (e.g. M365 Copilot) requires a separate public front (gateway / App Proxy) — not configured.
Upstream auth (MCP server → UniData API) — current gap
The section above secures the inbound leg (clients → MCP server). The outbound leg (MCP
server → UniData REST API) is not authenticated yet: lib/api-client.js issues
plain HTTPS GETs with no token against the API’s unauthenticated /api/.../dev/* routes, which
the API resolves through its service account to MRP-PROD. So today every fleet read the MCP
server performs is attributed to the service account, not to a person.
| Leg | Mechanism today | Identity seen downstream |
|---|---|---|
| Browser → Ask the Fleet backend | MSAL (Entra) sign-in | the end user |
| Ask the Fleet backend → MCP server | App Service managed identity, app role Mcp.Invoke.App | the explorer app (app-only — not the user) |
| MCP server → UniData API | anonymous HTTP → /dev/* routes | the service account (not the user) |
Identity is lost after the first hop. The end user authenticates to the Ask the Fleet backend, but from there on the chain is app-only: the MCP server only knows “the explorer app called me,” and the UniData API only knows “the service account asked.” There is no per-user attribution past the explorer backend.
Why this blocks audited capabilities (e.g. service tickets). SERVICE.1287 reads via
/api/service are deliberately gated on a per-user UPN (per-user AFTEC audit; see
unidata-api). An app-only or anonymous caller is rejected. So a search_service_tickets MCP tool
cannot be added on the current plumbing without one of:
-
Service account — turn on an anonymous
/api/service/dev/*route (matches the existing data/dev/*pattern). Fast, but attributes all reads to the service account. PSI’s direction is to avoid this for anything audited. -
Trusted delegation / asserted identity (the intended direction) — the user signs in once at the Ask the Fleet backend (Entra/MSAL); that is the trusted validation point. The user’s Entra token does not need to be propagated all the way down (this is not token-exchange OBO). Instead each hop authenticates to the next as a trusted app (App Service managed identity / app role) and asserts the acting user: explorer → MCP → API, each saying “I’m a trusted caller and I have validated this is user XYZ — process as XYZ.” The API authenticates the immediate caller as a trusted app and, on that basis, honors the asserted UPN — opening a per-user UniData session via the existing
CreateConnection(upn)path (which resolves the user’s AFTEC credentials from AD). Result: correct per-user audit, no service account, and no user Entra/UniData credentials needed downstream.Trust is transitive — the API accepts an asserted identity only because the caller holds the trusted app role, so every hop must remain a trusted, authenticated subsystem. A sufficient bar for reads; audited writes (e.g. creating/closing a service ticket) warrant tighter scrutiny.
Status: inbound auth ✅ done (2026-06). Outbound auth — functional but unauthenticated (anonymous → service account); not an outage, just below the no-service-account bar. Trusted- delegation user assertion — intended, not yet built; required before MCP tools touch audited tables like
SERVICE.1287.
Setup
Option 1: Connect to Production Server (Recommended)
The MCP server runs on PS-PROXY as a Windows service, reachable from the PSI network (LAN/VPN).
The HTTP transport requires an Entra bearer token (see Authentication) — an
unauthenticated request returns 401.
Claude Code — use headersHelper to mint a token per connection via az (requires az login):
{
"mcpServers": {
"psi-machine": {
"type": "http",
"url": "http://ps-proxy:3100/mcp",
"headersHelper": "az account get-access-token --resource api://0dfb7d0a-b815-4611-93ba-ccdf3213187b --query \"{Authorization:join(' ',['Bearer',accessToken])}\" -o json"
}
}
}VS Code / GitHub Copilot — uses a different schema (.vscode/mcp.json, servers key, no
headersHelper); supply a static Authorization header via an input (token from the same az
command). See the MCP standard.
Option 2: Local Stdio (Dev/Offline)
For local development or when PS-PROXY is unavailable:
{
"mcpServers": {
"psi-machine": {
"command": "node",
"args": ["C:/git/PLC/mcp-server/mcp-server.js"]
}
}
}Requirements: Node.js, network access to \\ad.ptihome.com\DFS\ shares, Python 3.11 (for ACD analysis).
Health Check
curl http://ps-proxy:3100/health{
"status": "ok",
"server": "psi-machine-intelligence",
"version": "1.0.0",
"transport": "streamable-http",
"activeSessions": 0,
"uptime": 123.45
}HTTP Endpoints
| Method | Path | Description |
|---|---|---|
POST | /mcp | Initialize session or send MCP messages (Streamable HTTP) |
GET | /mcp | Establish SSE stream for responses (Streamable HTTP) |
DELETE | /mcp | Terminate session (Streamable HTTP) |
GET | /sse | Legacy SSE connection (older MCP clients) |
POST | /messages?sessionId=<id> | Legacy message endpoint (older MCP clients) |
GET | /health | Server health check (anonymous) |
GET | /.well-known/oauth-protected-resource | RFC 9728 OAuth discovery doc (anonymous) |
All paths except /health and the discovery doc require a valid Entra bearer token (see Authentication).
Project Structure
C:\git\PLC\mcp-server\ (source)
C:\Services\PSI.MCP.Server\ (deployed on PS-PROXY)
├── mcp-server.js # Main server -- tool definitions + transport layer
├── package.json # Dependencies: @modelcontextprotocol/sdk, express, xlsx, zod
├── lib/
│ ├── api-client.js # REST API wrapper (apiGet, apiPost, apiExportAll) with 10s timeout
│ ├── file-tools.js # File access via UNC paths (LDS, CAD shares)
│ ├── fleet-summary-tools.js # Fleet intelligence (recommend_template, audit_safety_compliance)
│ ├── generate-summary.js # Summary generation with fault severity classification
│ └── ai-assessment.js # Azure OpenAI integration for AI-powered PLC assessment
├── acd_analyzer.py # Python ACD parser (uses hutcheb/acd-tools) — see [[l5x-tools|L5X Tools]]
├── .mcp.json # Claude Code integration config
├── deploy/
│ └── manual-deploy.ps1 # Manual deploy script for PS-PROXY
└── .github/workflows/
├── deploy-ps-proxy.yml # Auto-deploy on push to master
└── diagnose.yml # Diagnostic workflow for troubleshooting
Dependencies
| Package | Purpose |
|---|---|
@modelcontextprotocol/sdk | MCP protocol implementation (stdio + HTTP transports) |
express | HTTP server for network mode |
xlsx | Read IO spreadsheets and BOM export files |
pdf-parse | Extract text from mechanical/electrical drawing PDFs |
zod | Tool input schema validation |
External Dependencies
| Dependency | Path / URL | Purpose |
|---|---|---|
| UniData API | https://api.progressivesurface.com | ERP data (93+ endpoints) |
| LDS Share | \\ad.ptihome.com\DFS\LDS\PROJECT\{job}\ | Project folders (PLC, robot, safety, tags) |
| CAD Share | \\ad.ptihome.com\DFS\CAD\ | Engineering files (187K SolidWorks, 230K DWG) |
| Python 3.11 | C:\Python311\python.exe | ACD file analysis (acd-tools library) |
| Azure OpenAI | adeve-midqp8v8-eastus2.cognitiveservices.azure.com | AI-powered PLC assessment (GPT 5.2) |
| L5X Tools | See L5X Tools page | ACD→L5X conversion, L5X parsing, fleet analysis |
Note: File shares use UNC paths, not drive letters. This works on both workstations (with K:/X: mapped) and servers (PS-PROXY).
Data Flow
Example: “What’s the air pressure sensor on machine 2399?”
1. Agent calls get_project_info(2399)
└→ GET /api/project/dev/2399/info
└→ Returns: GE GREENVILLE, Large Auto Door, team names
2. Agent calls lookup_fault(2399, "air pressure")
└→ Reads plc_analysis_2399.json (cached or auto-analyzed from ACD)
└→ Returns: Fault #23, tag AirPressure_Low, Slot 4 Module 7
3. Agent calls get_io_map(2399)
└→ Reads io_crossref_2399.json
└→ Returns: Tag → Slot 4:7 → Drawing 359912 → BOM Part 039496
4. Agent calls get_part_details(039496)
└→ GET /api/parts/dev/039496
└→ Returns: SMC ISE30A Pressure Switch
5. Agent calls find_drawing(039496)
└→ Checks X:\PDF\039496.pdf, X:\MasterDwgs\039496.slddrw
└→ Returns: PDF available, SolidWorks drawing available
Deployment
Production (PS-PROXY)
The server runs on PS-PROXY (192.9.201.217) as a Windows service managed by NSSM.
| Component | Detail |
|---|---|
| Service name | PSI.MCP.Server |
| Service manager | NSSM 2.24 (C:\tools\nssm\nssm.exe) |
| Install path | C:\Services\PSI.MCP.Server\ |
| Node.js | v22.14.0 |
| Port | 3100 (TCP, firewall rule: “PSI MCP Server”) |
| Logs | C:\Services\PSI.MCP.Server\logs\ (stdout.log, stderr.log) |
| Auto-start | Yes (SERVICE_AUTO_START) |
| Log rotation | 10 MB per file |
Auto-Deploy (CI/CD)
Pushing to master on psi-machine-mcp automatically deploys to PS-PROXY via GitHub Actions:
- Checks out code
- Ensures Node.js is installed
- Stops existing service
- Copies files to
C:\Services\PSI.MCP.Server\ - Runs
npm install --production - Installs NSSM if needed
- Registers/starts Windows service
- Runs health check
The workflow uses the ps-proxy self-hosted runner (org-level, labels: ps-proxy, dotnet-8, node, unidata-access).
Manual Deploy
For first-time setup or troubleshooting:
# Run on PS-PROXY as Administrator
Invoke-Command -ComputerName PS-PROXY -FilePath .\deploy\manual-deploy.ps1Environment Variables
Set via NSSM AppEnvironmentExtra (configured by CI/CD from GitHub Actions secrets):
| Variable | Description |
|---|---|
MCP_PORT | HTTP listen port (default: 3100) |
MCP_AUTH_MODE | entra (default) enforces Entra bearer auth; none disables it (local/dev only) |
MCP_APP_CLIENT_ID | Client id of the PSI Machine MCP app reg (0dfb7d0a-…). Unset → server fails closed. GitHub secret. |
ENTRA_TENANT_ID | Tenant GUID (default = PSI tenant). Repo variable. |
MCP_REQUIRED_SCOPE | Required delegated scope (default Mcp.Invoke); app role Mcp.Invoke.App is also accepted. Repo variable. |
MCP_ALLOWED_REMOTE | Comma-separated firewall source allowlist (default RFC1918 private ranges). Repo variable. |
AZURE_OPENAI_ENDPOINT | Azure OpenAI endpoint URL (for get_ai_assessment tool) |
AZURE_OPENAI_API_KEY | Azure OpenAI API key |
AZURE_OPENAI_API_VERSION | API version (default: 2024-05-01-preview) |
AZURE_OPENAI_MODEL | Model deployment name (default: gpt-5.2-chat) |
Service Management
# On PS-PROXY:
Get-Service PSI.MCP.Server # Check status
Start-Service PSI.MCP.Server # Start
Stop-Service PSI.MCP.Server # Stop
Restart-Service PSI.MCP.Server # Restart
Get-Content C:\Services\PSI.MCP.Server\logs\stderr.log -Tail 50 # View logsVerify
curl http://ps-proxy:3100/healthRelated
- Ask the Fleet — Production web chat powered by these 78 tools (first web client)
- UniData API — Backend REST API providing ERP data (93+ endpoints)
- PSI Explorer — React frontend with embedded AI chat
- Project Explorer — Project tracking web app
- Digital Thread — Conceptual architecture for machine analysis
- Claude Code — AI integration and MCP setup
- Connect AI Agents — MCP client configuration guide