|
| 1 | +# OCI Enterprise AI Agents — OAC MCP Server |
| 2 | + |
| 3 | +A React assistant that lets [OCI Enterprise AI](https://www.oracle.com/artificial-intelligence/enterprise-ai/) query **governed Oracle Analytics Cloud data** through the **OAC MCP server**. The model reasons through the OCI Generative AI Responses API, the app executes the delegated OAC MCP tool calls, and every numeric result is rendered as a chart — with the Logical SQL and the full tool timeline shown next to it. |
| 4 | + |
| 5 | +Built with **Next.js 16**, **React 19**, **Recharts**, and a small **Python bridge**. |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Quick Start |
| 12 | + |
| 13 | +```bash |
| 14 | +cd files |
| 15 | +npm install |
| 16 | +python3 -m venv .venv |
| 17 | +.venv/bin/pip install -r scripts/requirements.txt |
| 18 | +cp .env.example .env # fill in your values (see Configuration) |
| 19 | +npm run dev |
| 20 | +``` |
| 21 | + |
| 22 | +Open [http://localhost:3000](http://localhost:3000). Click the **Graph Demo** quick prompt and **Run Analysis** to verify the whole React → Python → chart path *without any OAC credentials*. Then configure OAC access and run a real analysis (e.g. the **Sample Sales** quick prompt). |
| 23 | + |
| 24 | +### Requirements |
| 25 | +- Node.js 22+ |
| 26 | +- Python 3.9+ |
| 27 | +- An OCI tenancy with **Generative AI** access: the OpenAI-compatible Responses endpoint, an API key, and a Generative AI project OCID |
| 28 | +- An **Oracle Analytics Cloud** instance whose MCP endpoint (`/api/mcp`) is enabled, and a user with access to at least one subject area or dataset (the demo prompts use `Sample Sales Lite` / `Sample Targets Lite`) |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## Configuration |
| 33 | + |
| 34 | +### Required environment variables |
| 35 | + |
| 36 | +Create `files/.env` (copy `.env.example`): |
| 37 | + |
| 38 | +```env |
| 39 | +# OCI Generative AI — OpenAI-compatible Responses API for your region |
| 40 | +GENAI_BASE_URL=https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com/openai/v1 |
| 41 | +GENAI_API_KEY=<oci-genai-api-key> |
| 42 | +GENAI_PROJECT_ID=ocid1.generativeaiproject.oc1..xxxxx |
| 43 | +
|
| 44 | +# Oracle Analytics Cloud MCP endpoint of your instance |
| 45 | +OAC_MCP_SERVER_URL=https://<your-oac-instance>.analytics.ocp.oraclecloud.com/api/mcp |
| 46 | +
|
| 47 | +# Optional — derived from OAC_MCP_SERVER_URL when not set |
| 48 | +# OAC_TOKEN_REFRESH_URL=https://<your-oac-instance>.analytics.ocp.oraclecloud.com/api/dv/api/v1/tokens/token/refresh |
| 49 | +
|
| 50 | +# Optional — python used by the API route (default: files/.venv/bin/python, then python3) |
| 51 | +# PYTHON_BIN=/usr/bin/python3 |
| 52 | +``` |
| 53 | + |
| 54 | +### OAC authentication |
| 55 | + |
| 56 | +Two options, in the order the bridge resolves them: |
| 57 | + |
| 58 | +1. **Access token** — set `OAC_ACCESS_TOKEN` (or `MCP_BEARER_TOKEN`) in `.env`, or paste a token in the UI. OAC access tokens expire after ~1 hour. |
| 59 | +2. **`tokens.json` (recommended)** — in OAC go to **Profile → Mobile Authentication → Download token file** and save it as `files/tokens.json` (shape in `tokens.example.json`). The bridge reads it on every request and **refreshes it automatically** ~10 minutes before expiry, persisting the rotated refresh token back to the file. This survives well beyond the 1-hour access-token window. |
| 60 | + |
| 61 | +> The refresh endpoint authenticates with the *access token itself*, so once a downloaded token file fully expires it cannot be refreshed — download a fresh `tokens.json` from OAC. |
| 62 | +
|
| 63 | +--- |
| 64 | + |
| 65 | +## Architecture |
| 66 | + |
| 67 | +``` |
| 68 | +Browser (React + Recharts) |
| 69 | + │ POST /api/oac-demo { prompt, previousResponseId, ... } |
| 70 | + ▼ |
| 71 | +Next.js API route (files/src/app/api/oac-demo/route.js) |
| 72 | + │ spawns one process per request, JSON over stdin/stdout |
| 73 | + ▼ |
| 74 | +Python bridge (files/scripts/oac_react_api.py) |
| 75 | + ├──► OCI Generative AI Responses API (model reasoning, function calling) |
| 76 | + └──► OAC MCP server /api/mcp (JSON-RPC 2.0: initialize, tools/list, tools/call) |
| 77 | +``` |
| 78 | + |
| 79 | +The model never talks to OAC directly. The bridge advertises the three OAC MCP tools to the model as function tools, executes each requested call against the OAC MCP JSON-RPC endpoint, compacts the result (large metadata is trimmed before it re-enters the context), and chains it back to the Responses API with `previous_response_id`. After the loop finishes, the bridge extracts the last Logical SQL result rows into a Recharts-friendly `{categoryKey, valueKeys, data}` payload for the UI. |
| 80 | + |
| 81 | +Only three read-only OAC MCP tools are allowed: |
| 82 | + |
| 83 | +| Tool | Purpose | |
| 84 | +|---|---| |
| 85 | +| `discover_data` | List governed Subject Areas and uploaded datasets | |
| 86 | +| `describe_data` | Inspect columns, measures, dimensions of one model | |
| 87 | +| `execute_logical_sql` | Run read-only Logical SQL against the governed model | |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Features |
| 92 | + |
| 93 | +### Chat with chained sessions |
| 94 | +Follow-up questions keep `previous_response_id`, so "break that down by month" works. **New Session** resets the chain. |
| 95 | + |
| 96 | + |
| 97 | + |
| 98 | +### Governed analytics through MCP |
| 99 | +The assistant discovers the right subject area or dataset, describes only the relevant metadata, and builds Logical SQL from the exact described column names — including the `XSA('owner'.'Dataset')` syntax for uploaded datasets. |
| 100 | + |
| 101 | +### Automatic result charts |
| 102 | +Any executed Logical SQL result with at least one category column and one numeric measure is rendered as a bar chart. A **Graph Demo** quick prompt renders a deterministic local chart to verify the UI path with no OAC access at all. |
| 103 | + |
| 104 | +### Tool timeline + Logical SQL panel |
| 105 | +Every MCP call is listed with status, arguments, and compacted output; all executed Logical SQL statements are shown verbatim. |
| 106 | + |
| 107 | +### Token diagnostics |
| 108 | +On 401/403 the bridge reports the token source used, whether the JWT is readable/expired, and whether the token audience matches the OAC host — the usual failure causes, stated directly in the error message. |
| 109 | + |
| 110 | +### Display masking |
| 111 | +Dataset owner e-mails, OCIDs, and `XSA('owner'...)` identifiers are masked in the rendered UI so demo recordings don't leak identifiers. |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## Project Structure |
| 116 | + |
| 117 | +``` |
| 118 | +oci-enterprise-ai-agents-oac-mcp-server/ |
| 119 | +├── README.md |
| 120 | +├── LICENSE |
| 121 | +├── images/ # screenshots used in this README |
| 122 | +└── files/ |
| 123 | + ├── package.json # next, react, recharts, react-markdown, remark-gfm, lucide-react |
| 124 | + ├── next.config.mjs |
| 125 | + ├── jsconfig.json |
| 126 | + ├── .env.example # copy to .env and fill in |
| 127 | + ├── tokens.example.json # shape of the downloaded OAC token file |
| 128 | + ├── src/app/ |
| 129 | + │ ├── layout.js # minimal root layout |
| 130 | + │ ├── globals.css |
| 131 | + │ ├── page.js # the assistant UI (chat, stages, chart, timeline) |
| 132 | + │ ├── page.module.css |
| 133 | + │ └── api/oac-demo/route.js # spawns the Python bridge per request |
| 134 | + └── scripts/ |
| 135 | + ├── oac_react_api.py # JSON bridge: Responses API + OAC MCP + chart builder |
| 136 | + ├── refresh_oac_tokens.py # CLI + importable helper for the tokens.json refresh flow |
| 137 | + └── requirements.txt # openai, requests |
| 138 | +``` |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +## Data Flow |
| 143 | + |
| 144 | +### One chat turn |
| 145 | + |
| 146 | +1. UI POSTs `{action: "chat", prompt, previousResponseId, ...}` to `/api/oac-demo` |
| 147 | +2. The route spawns `scripts/oac_react_api.py` and writes the payload to stdin |
| 148 | +3. The bridge resolves an OAC token (see below) and preflights `initialize` against the OAC MCP endpoint — 401/403 fail fast with token diagnostics |
| 149 | +4. The bridge lists the OAC MCP tools and exposes them to the model as function tools via the Responses API |
| 150 | +5. Each model tool call is executed by the bridge against OAC MCP (`tools/call`), compacted, and returned to the model; the loop repeats until the model produces a final answer |
| 151 | +6. The bridge extracts Logical SQL, builds the chart payload from the last SQL result rows, and emits one JSON object to stdout; the route returns it to the UI |
| 152 | + |
| 153 | +### Token resolution order |
| 154 | + |
| 155 | +1. `accessToken` pasted in the UI payload |
| 156 | +2. `files/tokens.json` — used directly while valid, auto-refreshed (and persisted) when within 10 minutes of expiry |
| 157 | +3. `OAC_ACCESS_TOKEN` / `MCP_BEARER_TOKEN` environment variables |
| 158 | + |
| 159 | +### Chart building |
| 160 | + |
| 161 | +`build_chart_payload` walks the tool timeline backwards to the last successful `execute_logical_sql`, normalizes rows (JSON batches, column/value arrays, or markdown tables), picks the first text column as category and up to three numeric columns as series, and caps the data at 40 rows. |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## OCI API endpoints used |
| 166 | + |
| 167 | +### Generative AI (inference) |
| 168 | +- `POST {GENAI_BASE_URL}/responses` — model reasoning with function tools, chained via `previous_response_id` (OpenAI-compatible Responses API; authenticated with `GENAI_API_KEY` + `GENAI_PROJECT_ID`) |
| 169 | + |
| 170 | +### Oracle Analytics Cloud |
| 171 | +- `POST {OAC_MCP_SERVER_URL}` — MCP JSON-RPC 2.0: `initialize`, `tools/list`, `tools/call` (Bearer token) |
| 172 | +- `POST .../api/dv/api/v1/tokens/token/refresh` — refreshes a downloaded token file: current access token in the `Authorization` header, refresh token as `text/plain` body; returns rotated `accessToken`/`refreshToken` |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Commands |
| 177 | + |
| 178 | +```bash |
| 179 | +npm run dev # Dev server (Turbopack) |
| 180 | +npm run build # Production build |
| 181 | +npm run start # Production server |
| 182 | +npm run lint # ESLint |
| 183 | + |
| 184 | +# Refresh a downloaded token file manually |
| 185 | +.venv/bin/python scripts/refresh_oac_tokens.py tokens.json |
| 186 | + |
| 187 | +# Smoke-test the bridge without the UI |
| 188 | +echo '{"action":"config"}' | .venv/bin/python scripts/oac_react_api.py |
| 189 | +``` |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Troubleshooting |
| 194 | + |
| 195 | +**`OAC MCP initialize returned HTTP 401/403`** |
| 196 | +The token is expired, malformed, or issued for a different OAC instance. The error includes the token source used and JWT diagnostics. Fix: paste a fresh access token, or download a new `tokens.json` from the *same* OAC instance as `OAC_MCP_SERVER_URL`. |
| 197 | + |
| 198 | +**`401` from the token refresh endpoint** |
| 199 | +The refresh flow authenticates with the access token itself, so a fully expired `tokens.json` cannot be refreshed. Download a fresh one from OAC. |
| 200 | + |
| 201 | +**`Missing oacMcpUrl.`** |
| 202 | +`OAC_MCP_SERVER_URL` is not set in `files/.env` and no URL was supplied by the UI. |
| 203 | + |
| 204 | +**`Python bridge returned no JSON.`** |
| 205 | +The route couldn't run the bridge. Ensure the venv exists at `files/.venv` (or set `PYTHON_BIN`) and that `pip install -r scripts/requirements.txt` succeeded. The stderr excerpt is included in the error response. |
| 206 | + |
| 207 | +**Pasted MCP URL ends in `/ui/api/mcp`** |
| 208 | +That's the browser UI route; programmatic calls get a 302 to the login page. The bridge rewrites it to `/api/mcp` automatically. |
| 209 | + |
| 210 | +**`EADDRINUSE: address already in use`** |
| 211 | +Another dev server holds port 3000. Run `npm run dev -- --port 3001`. |
| 212 | + |
| 213 | +**No chart appears after an analysis** |
| 214 | +The chart needs at least one text/category column and one numeric measure in the *executed* SQL result. The chart panel explains which condition failed (no SQL executed, SQL failed, or no plottable rows). |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## Tech Stack |
| 219 | + |
| 220 | +- **Framework** — Next.js 16 (App Router, Turbopack) |
| 221 | +- **UI** — React 19, Lucide icons, CSS Modules |
| 222 | +- **Charts** — Recharts |
| 223 | +- **Markdown** — react-markdown + remark-gfm |
| 224 | +- **Bridge** — Python 3, `openai` (OCI Responses API), `requests` (OAC MCP JSON-RPC + token refresh) |
| 225 | +- **Protocols** — MCP (JSON-RPC 2.0 over streamable HTTP), OpenAI-compatible Responses API |
| 226 | + |
| 227 | +--- |
| 228 | + |
| 229 | +## License |
| 230 | + |
| 231 | +Copyright (c) 2026 Oracle and/or its affiliates. |
| 232 | + |
| 233 | +Licensed under the Universal Permissive License (UPL), Version 1.0. |
| 234 | + |
| 235 | +See [LICENSE](LICENSE) for more details. |
| 236 | + |
| 237 | +ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK. |
0 commit comments