Skip to main content
Glama
yanivshoval0104

siebel-mcp-gateway

README.md
# Siebel MCP Gateway

Exposes the Oracle Siebel REST API as MCP tools over streamable HTTP, so an
agentic client can query/create/update/delete Siebel records and pull the
object catalog without holding Siebel credentials itself.

Mock mode models a synthetic healthcare-referral demo schema (patient →
community/hospital referral → Form 17 commitment → treatment history),
built to faithfully carry a set of documented, deliberate data-quality
findings rather than smooth them over — duplicate patient records across
two orgs, a status field that actually holds urgency, a script that
silently overrides a Workflow's stated limits, two "visits remaining"
fields that drift apart. All data is synthetic.

## Stack

Python 3.12+, the official `mcp` SDK (`MCPServer`, the current name for what
used to be called `FastMCP` in older SDK versions), `httpx` for outbound
Siebel calls, `uvicorn` as the ASGI server.

## Local run

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Fill in .env, or for a first run without a live Siebel instance:
#   MOCK_MODE=true
#   MCP_GATEWAY_TOKEN=<any string you'll also give your client>
MOCK_MODE=true MCP_GATEWAY_TOKEN=dev-token \
  uvicorn app.server:app --host 0.0.0.0 --port 8000
```

Health check: `curl http://localhost:8000/healthz` → `{"status":"ok"}` (no
auth required, so platform health checkers work).

MCP endpoint: `http://localhost:8000/mcp` — every request needs
`Authorization: Bearer <MCP_GATEWAY_TOKEN>`, since the endpoint itself has no
other access control once deployed publicly.

## Tests

```bash
python3 -m pytest -v
```

All tests run against the in-memory mock store or a mocked HTTP transport —
no network calls, no live Siebel instance required.

## Deploying to Render

1. Push this repo to GitHub.
2. **Grant Render access to the repo first, if it's not already connected.**
   Render's GitHub App only sees repos it's been explicitly given access to
   — a brand-new repo won't show up in Render's repo picker just because
   you own it. Go to
   [github.com/settings/installations](https://github.com/settings/installations)
   → find **Render** → **Configure** → either switch to "All repositories"
   or add this repo to the allowed list → Save. Only then will it appear
   back in Render's connect screen.
3. In the Render dashboard: **New → Blueprint** (not "Web Service" — this
   repo has a `render.yaml`, and Blueprint is what reads it). Connect the
   repo, confirm branch `main` and the default `render.yaml` path.
4. Render shows a form for every env var marked `sync: false` in
   `render.yaml` — fill these in before deploying:
   - `MCP_GATEWAY_TOKEN` — generate one, e.g. `openssl rand -hex 32`
   - `MOCK_MODE` — `true` to start serving mock data immediately (recommended
     while the real Siebel instance isn't ready yet), `false` if you already
     have real Siebel credentials to enter below
   - `SIEBEL_BASE_URL` / `SIEBEL_USERNAME` / `SIEBEL_PASSWORD` — only
     required if `MOCK_MODE=false`; leave blank if starting in mock mode
5. Click **Deploy Blueprint**. Render assigns `https://<your-service>.onrender.com`.

To change any of these later (e.g. flip `MOCK_MODE` once the real Siebel
instance is ready): open the service (not the Blueprint) → **Environment**
tab → edit the value → **Save Changes**, which triggers a redeploy.

## Pointing your MCP client at the deployed gateway

- URL: `https://<your-service>.onrender.com/mcp`
- Transport: streamable HTTP
- Auth: a static bearer token/API key, not OAuth — set the header to
  `Authorization: Bearer <MCP_GATEWAY_TOKEN>` (the same value from step 4
  above). If your client's auth UI wants a header name and a raw value
  separately rather than one combined header, header name is `Authorization`
  and value is `Bearer <token>` (include the word "Bearer") — if that gets
  a 401, try giving it just the raw token instead, since some clients add
  the `Bearer ` prefix themselves.

## Notes from actually deploying this

- **The mock store is in-memory only.** Anything created/updated/deleted
  during a session persists only as long as that server process stays up.
  A redeploy, or Render's free-tier instance spinning down after ~15
  minutes idle and cold-starting on the next request, resets it back to
  the original seeded data. That's expected mock-mode behavior, not a bug.
- **The `mcp` Python SDK's client-side transport dependency is `httpx2`**,
  not plain `httpx` — only relevant if you're writing your own MCP client
  against this gateway using the SDK's `streamable_http_client` helper
  rather than a higher-level client app; it expects an `httpx2.AsyncClient`
  for the `http_client=` argument, not a regular `httpx.AsyncClient`.

## Flip-to-live checklist

Once the real Siebel instance is up:

- [ ] Set `SIEBEL_BASE_URL` to the real instance (no trailing slash), e.g.
      `https://<siebel-host>/siebel/v1.0`
- [ ] Set `SIEBEL_USERNAME` / `SIEBEL_PASSWORD`
- [ ] Set `SIEBEL_VERIFY_TLS=false` only if the instance is still on a
      self-signed cert — flip back to `true` once it has a real one
- [ ] Set `MOCK_MODE=false`
- [ ] Redeploy, then smoke-test with `siebel_list_objects` and
      `search_facilities` before pointing real agent traffic at it

## Tools

Generic (work against any Business Component: `Contact`, `Employee`,
`Medical Facility`, `Appointment Slot`, `Referral Request`,
`Commitment Form`, `Treatment History`):

| Tool | Purpose |
|---|---|
| `siebel_query` | List/search records: `searchspec`, `fields`, `page_size`, `start_row` |
| `siebel_get` | Fetch one record by `row_id` |
| `siebel_create` | Create a record from a `fields` dict |
| `siebel_update` | Update a record's `fields` by `row_id` |
| `siebel_delete` | Delete a record by `row_id` |
| `siebel_list_objects` | List the business components the account exposes |

Convenience wrappers, thinner surface for common demo asks:

| Tool | Purpose |
|---|---|
| `search_facilities` | By specialty code and/or exact city |
| `search_contacts` | By last-name prefix |
| `create_referral` | Patient + doctor + specialty + urgency; starts at Stage Code = COMMUNITY_SEARCH |

## Notes on the Siebel REST API assumptions baked in here

- Auth is HTTP Basic on every outbound call (separate from this gateway's
  own bearer-token check on inbound MCP requests — two different auth
  layers, don't conflate them).
- URL grammar is `{BASE}/data/{BusinessObject}/{BusinessComponent}`. BO and
  BC are **not** always the same name here — e.g. `Referral Request` is a
  child BC under the `Patient Referral` BO, `Appointment Slot` is a child
  BC under `Appointment Management`. Tools take the BC name; the client
  looks up the right BO internally. Path segments are URL-encoded, so
  multi-word names work.
- List responses arrive as `{"items": [...]}`; the `"links"` array on each
  record is stripped before returning to the model, to save tokens.
- Non-2xx responses are surfaced as the HTTP status code plus Siebel's own
  message text; a 401 gets a clear "check Siebel credentials" prefix.
  Outbound calls time out at 30s.
- Several fields are **computed, not stored** (Age, Days Waiting, Visits
  Remaining, Is Expired, Entry Gap Days, and the Facility/Doctor/Patient
  join fields) — they're derived fresh on every read, matching how they'd
  behave as real Business Component calculated/join fields rather than
  physical columns.