Skip to main content
Glama
README.md
# Innergy MCP Gateway

A production-oriented, read-only remote Model Context Protocol (MCP) server that lets ChatGPT retrieve selected Innergy project, budget, work-order, finance, inventory, labor, scheduling, and added-expense data without exposing the Innergy API key.

## Safety model

- The public MCP transport is served at `POST/GET/DELETE /mcp`.
- The MCP transport accepts either an Auth0 RS256 access token or the existing static Bearer token fallback.
- The safe operational health check is `GET /health`.
- Every MCP tool is annotated as read-only, non-destructive, and idempotent.
- No create, update, or delete Innergy route is registered.
- The Innergy key is read from the server environment only and is sent upstream in the documented `Api-Key` request header.
- Secrets and authorization headers are redacted from server logs.
- Browser and MCP responses never include the API key or base URL.
- Normal responses are projected into concise records. `debug: true` returns a redacted, size-limited upstream payload.

## Architecture

```text
ChatGPT ── Streamable HTTP MCP ──> /mcp
                                      │
                                      ├── tool validation and read-only policy
                                      ├── concise response projection
                                      └── modular Innergy API client
                                                │
                                                └── Api-Key header ──> Innergy

Browser ──> / ──> deployment/health dashboard
Browser ──> /health ──> safe configuration and capability status
```

The React app is a monitoring and connection-information surface. All credentials and Innergy requests remain in the Node.js API service.

## Confirmed Innergy API contract

The following GET routes were confirmed from the supplied Innergy OpenAPI 3.0.1 document:

| MCP tool | Confirmed Innergy route |
| --- | --- |
| `get_projects` | `GET /api/projects` |
| `get_project` | `GET /api/projects/{id}` |
| `search_projects` | `GET /api/projects`, then local matching |
| `get_work_orders` | `GET /api/projectWorkOrders` |
| `get_project_work_orders` | `GET /api/v2-unstable/project-management/work-orders` with read-only fallback to `GET /api/projects/{id}/workOrders` |
| `get_added_expenses` | `GET /api/addedExpenses`, then optional local project matching |
| Project detail | Project by work order, change orders, tasks, SOV, budget groups, job costing, budget products/materials/operations/overheads/distributed costs, work-order base information |
| Work-order detail | Single work order, base information, tasks, impediments, notes, shipment items, job costing |
| Finance | Invoices, purchase orders, project invoice draws, cost-transaction history, work in progress |
| Inventory | Master inventory, inventory by material, material requests, material lookup, materials to buy |
| Labor and scheduling | Labor Kanban items, monthly time tracking, monthly date management |

The gateway exposes 45 curated MCP tools, all mapped to GET operations confirmed in the supplied OpenAPI document. No undocumented Innergy route is called. Search, workflow summaries, schedules, exception analysis, joinery grouping, and expense project filtering are performed locally when the confirmed upstream endpoint does not provide that capability.

### OpenAPI GET catalog

At build time the supplied v2 OpenAPI document is copied as an immutable dist-relative
asset. Server startup fails if that asset is missing, invalid, or does not contain the
expected 219 GET operations. The primary generic tools are `search_endpoints`,
`describe_endpoint`, `query_endpoint`, and `count_endpoint`. They only resolve catalog
GET templates; URLs, hosts, and mutation operations are never accepted. Generic grids
validate documented path/query values and fields, use `skip`/`take` pages of at most 500,
and scan all natural pages when local list filtering, text search, fetch-all, or exact
counting is required. Results remain windowed (default 100) with total/count/paging
metadata, while selected raw schema fields are preserved.

`get_project_work_orders` is v2-first. It uses the specification-defined `GET /api/v2-unstable/project-management/work-orders` grid operation with `skip`, `take` (maximum 500 per upstream request), `requireTotalCount=true`, encoded `filter` conditions, encoded `sort` objects, and a fixed `select` projection containing only fields needed for concise MCP work-order results. It supports project UUIDs through `projectIds`; project sequence identifiers through the schema-defined `ProjectSequenceIdentifier` grid field; `Status` values `Open` and `Closed`; the documented `WorkOrderType` values `Sales`, `Drafting`, `Production`, and `Installation`; the documented `WorkflowPhaseType` values `Opportunity`, `Drafting`, `Production`, and `Installation`; `workFlowStepId`; and exact `WorkflowStepName` filtering. `countOnly` uses a one-field `WorkOrderId` projection, while `summaryBy=Status` or `summaryBy=WorkOrderType` uses server-side total-count queries without downloading complete work-order rows.

Because API v2 is explicitly marked unstable, a failed v2 grid request automatically falls back to the existing read-only `GET /api/projects/{id}/workOrders` implementation. Responses disclose whether `v2-unstable` or `v1-fallback` supplied the data. The fallback endpoint remains unpaginated and is filtered locally where possible.

The v2 operation documents these required permissions: `ProjectManagement → Project → WorkOrder → View`, `ProjectManagement → ProductionWorkOrders → View`, `ProjectManagement → InstallationWorkOrders → View`, `ProjectManagement → DraftingWorkOrders → View`, and `ShopFloor → WorkOrders → View`.

`get_project_work_orders` also accepts exact `type`, `query`, and `includeSummary` compatibility inputs. `type` is a compatibility alias for the documented `workOrderType` query value; status and step use native grid predicates. `query` uses documented text matching on the `Name` and `SequenceIdentifier` LinkDto fields. `Tags` is a `ListDto`, whose OpenAPI contract does not document a grid-filter value shape, so a requested `tag` is exact-matched locally only after the v2 response and is disclosed in `filters`. `get_work_order_summary` returns exact named total/Open/Closed/type breakdowns and workflow-step counts rather than work-order rows. It uses narrow projections and server total-count queries; projectId is optional except for an exact coordinator display-name match. The upstream grid has no coordinator parameter, so coordinator matching is explicitly local and exact (for example, Aaron Clarke cannot match Aaron Ashworth) and requires a project ID to keep that scan bounded.

### Additional v2 grids

The gateway also uses the v2 grid contract (encoded filters, fixed projections, `take <= 500`, and `requireTotalCount`) for projects, shop drawings and drawing publish logs, purchase orders and purchase-order line items, shipment items, project time tracking, library materials, and material associations. `get_projects` is v2-first and retains the v1 `/api/projects` fallback only for unavailable/malformed/not-found v2 responses—not 400, 401, or 403. `get_my_projects` uses only documented `employeeId` plus `employeeType` (`SalesPerson`, `Estimator`, or `ProjectManager`); it has no coordinator parameter or default-person lookup.

`get_project_slippage` calls the documented dashboard slippage grid. That operation does **not** have a `projectId` parameter, so the tool cannot claim a project-specific server-side query. `get_project_labour` projects activity, duration, employee, project, work-order, and labor-item fields only; no compensation or pay fields are selected. `search_innergy` intentionally searches only the efficiently server-filterable implemented project and material name grids.

### Project coordination tools

| MCP tool | Purpose |
| --- | --- |
| `get_project_workflow_summary` | Group project work orders by exact Innergy type, step, step index, and status, with straightforward counts and planned-date range. |
| `search_work_orders` | Search work-order number, name, joinery number, and description; filter locally by project, type, exact step, status, and tag. |
| `get_work_order` | Retrieve one work order through the documented direct endpoint using its Innergy sequence identifier (normally the work-order number returned by `search_work_orders`), with concise coordination, schedule, hours, cost, impediment, and custom-field data. |
| `get_project_exceptions` | Report explicit overdue/approaching milestones, impediments, remaining hours after planned end, disclosed hours overruns, and long-unchanged workflow steps. |
| `get_project_joinery_register` | Group only work orders with a clear shared JO-style reference; return ambiguous and unmatched work orders separately. |
| `get_project_schedule` | Preserve original Innergy dates and add auditable duration, overdue-milestone, and planned-versus-actual variance calculations. |

Exception thresholds are returned with every response: approaching means within seven days, an unchanged workflow step is flagged at 14 days, and actual hours are considered materially over plan above 20%. These are transparent analysis thresholds, not invented Innergy workflow meanings.

## Replit configuration

Set these in Replit Secrets/environment variables:

| Name | Storage | Purpose |
| --- | --- | --- |
| `INNERGY_API_KEY` | Replit Secret | Innergy API key. Never put it in source or chat. |
| `MCP_AUTH_TOKEN` | Replit Secret | Bearer token required by every `/mcp` request. |
| `INNERGY_BASE_URL` | Environment variable | Innergy API origin, such as `https://your-company-api.example.com`. Do not include `/api/projects`. |
| `AUTH0_DOMAIN` | Environment variable | `dev-scza6vay6a66c87e.au.auth0.com` |
| `AUTH0_ISSUER` | Environment variable | `https://dev-scza6vay6a66c87e.au.auth0.com/` |
| `AUTH0_AUDIENCE` | Environment variable | `https://repulsive-nimble-editor.replit.app/mcp` |
| `AUTH0_SCOPE` | Environment variable | `innergy.read` |
| `MCP_SERVER_URL` | Environment variable | Canonical public MCP URL: `https://repulsive-nimble-editor.replit.app/mcp` |

`INNERGY_BASE_URL` must use HTTP or HTTPS. HTTPS is strongly recommended outside local development.

## Local usage

The managed Replit workflows provide ports and routing. Start:

```bash
pnpm --filter @workspace/api-server run dev
pnpm --filter @workspace/innergy-mcp run dev
```

Useful checks through Replit's shared preview routing:

```bash
curl -sS https://$REPLIT_DEV_DOMAIN/health
curl -i -X POST https://$REPLIT_DEV_DOMAIN/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke-test","version":"1.0.0"}}}'
```

Use the returned `mcp-session-id` header for `tools/list` and `tools/call`.

## Deployment and final MCP URL

Publish the Replit project after the health and MCP checks pass. The final connector URL is:

```text
https://<your-published-replit-domain>/mcp
```

Do not use the temporary `.replit.dev` preview domain as the final ChatGPT connector URL.

## Connect to ChatGPT Developer Mode

Current OpenAI guidance supports remote MCP servers over Streamable HTTP:

1. Publish this project and copy its published `/mcp` URL.
2. In ChatGPT on the web, open **Settings → Security and login** and enable **Developer mode**. Workspace-managed accounts may instead expose the setting under workspace permissions.
3. Open **ChatGPT Plugins**, select the plus button, and create a developer-mode app.
4. Enter the published MCP URL ending in `/mcp`.
5. Choose **OAuth**. ChatGPT discovers Auth0 through the MCP protected-resource metadata and requests the `innergy.read` scope.
6. Save the app and enable it from the Developer mode tool picker in a conversation.
7. Review and approve tool calls as appropriate for your ChatGPT account or workspace policy.

Example prompts:

- “Show me the work orders for Brunswick.”
- “What projects have incomplete work orders?”
- “Show me added expenses for project X.”
- “Find project number 12345.”
- “Show project 12345’s schedule of values and job-costing summary.”
- “List open purchase orders and invoice draws for project 12345.”
- “Which materials are requested for work order WO-100?”
- “Show labor Kanban items for project 12345.”

ChatGPT can combine `search_projects` with `get_project_work_orders` when a prompt identifies a project by name rather than by Innergy sequence ID.

## Error behavior

Tools return concise JSON error objects with stable codes:

- `not_configured`
- `invalid_request`
- `invalid_auth`
- `forbidden`
- `not_found`
- `rate_limited`
- `malformed_response`
- `response_too_large`
- `upstream_timeout`
- `upstream_unavailable`
- `upstream_error`

Upstream response bodies are not copied into errors, preventing accidental disclosure of sensitive provider details.

If the bearer token is lost, replace `MCP_AUTH_TOKEN` in Replit Secrets and update the ChatGPT app configuration with the new token.

## Auth0 OAuth resource-server configuration

This gateway is an OAuth protected resource, not an OAuth login application. ChatGPT is the OAuth client and Auth0 is the authorization server, so the gateway does not expose a local `/callback` or login route and does not need an Auth0 client secret.

Public protected-resource metadata is available at:

```text
GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-protected-resource/mcp
```

Auth0 access tokens must be signed with RS256 and match:

```text
Issuer:   https://dev-scza6vay6a66c87e.au.auth0.com/
Audience: https://repulsive-nimble-editor.replit.app/mcp
Scope:    innergy.read
```

The existing `MCP_AUTH_TOKEN` remains accepted as a temporary fallback. Authorization headers and token values are never logged.

Verify the deployed metadata:

```bash
curl -i https://repulsive-nimble-editor.replit.app/.well-known/oauth-protected-resource/mcp
```

Verify the unauthenticated discovery challenge:

```bash
curl -i -X POST https://repulsive-nimble-editor.replit.app/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"oauth-check","version":"1.0.0"}}}'
```