indicus-ai-mcp-tools
README.md
# indicus-ai-mcp-tools
Demo services for the IndicusAI platform, served two ways at once:
- **REST**, at `/soc`, `/utility`, `/payer`, `/iam` — registered as built-in
custom HTTP tools, one tool per endpoint
- **MCP**, at `/mcp` — one connector carrying every tool on the server
Both surfaces call the same functions over the same fixtures. That is the
point: a demo comparing built-in tools against MCP proves nothing if the two
run on different data, because any difference could be blamed on the data.
## Run it
```bash
python3 -m venv .venv && .venv/bin/pip install -e .
.venv/bin/uvicorn mcp_server:app --port 8304
curl -s localhost:8304/health | python3 -m json.tool
```
Or `docker compose up -d`, which joins the platform's network so the platform
reaches it as `http://mcp-tools:8304`.
## Adding a tool
One edit:
```python
@app.get("/widgets/{widget_id}", operation_id="getWidget",
summary="Fetch one widget")
def get_widget(widget_id: str) -> dict:
"""What the model reads to decide whether to call this."""
...
```
It is now a REST endpoint **and** an MCP tool called `soc_getWidget`. Nothing
else to register — `mcp_server` walks the routes and every one carrying an
`operation_id` becomes a tool. A route without one is skipped, which is how the
audio download stays out of the tool list.
Three things the wrapper handles, each learned by hitting it:
- **Names are `{service}_{operationId}`.** MCP names are flat across a server
while operation_ids are unique only per service. SOC and IAM both define
`getIdentity`; unprefixed, the second silently replaced the first and an
agent asking for a SOC identity got IAM's answer.
- **`HTTPException` becomes a value, not a protocol error.** Over REST a 404
*is* the response; over MCP there are no status codes, so an uncaught one
reads as a broken server. `{"error": "No alert ALT-9999.", "status": 404}` is
a fact the model can act on.
- **Annotations are resolved eagerly.** `from __future__ import annotations`
makes them strings, which pydantic resolves through the handler's own module
globals — not something a wrapper can inherit.
### A new service
Write the FastAPI app in `services/`, then add it to `SERVICES` in
`mcp_server.py`. That is the whole procedure.
## Onboarding these tools into IndicusAI
What a tool is on that platform, the three ways one gets there, and how to
build both demos' agents and pipelines in the UI:
[demo-data-setup.md](https://github.com/shatrunjaysingh/IndicusAI/blob/main/demo-data-setup.md), in the platform
repository — that side of it is all IndicusAI, and this repository only
supplies the endpoints.
## Testing the DISCOM agents
[discom-agent-tests.md](https://github.com/shatrunjaysingh/IndicusAI/blob/main/discom-agent-tests.md), in the platform
repository — the walkthrough is done in that UI, and sits with the other
platform documents. It takes all ten through the browser one input at a time,
with what a correct answer contains and the wrong answer each case is designed
to catch.
The expected figures in it come from `services/discom_data.py` here, so a
change to a fixture means a change to that document.
## Seeding a demo
`seeds/` builds the workspace, skills, agents and pipeline in the platform.
They log in as `demo@example.com`, so `scripts/seed.py` in the platform repo
runs first.
**On a deployment**, run them inside the container — it already has the code
and its dependencies, and the compose file sets both hostnames:
```bash
docker compose exec mcp-tools python seeds/soc.py
docker compose exec mcp-tools python seeds/utility.py
```
**On a laptop**, where the platform and these services are both on localhost:
```bash
.venv/bin/python seeds/soc.py
.venv/bin/python seeds/utility.py
```
Two hostnames decide where a seed points, and both default to `127.0.0.1`,
which is correct only on a laptop — inside a container it *is* the container:
- `PLATFORM_API_URL` — the platform being seeded, `http://api:8000/api/v1`
over the compose network
- `DEMO_HOST` — what goes into the registered tool URLs and their
`allowed_hosts` pin, `mcp-tools` over the compose network
## The recordings
`services/generate_visit_recordings.py` uses macOS `say` and **cannot run on
Linux**. Generate on a Mac, copy `data/recordings/` to the deployment. `data/`
is gitignored — the fixtures are derived, not source.
Only the utility demo needs them, and only to run a visit end to end; seeding
works without.
## Sample client back offices: /shop, /miso, /power, /service
Three pretend businesses for the chat bot's **data connectors**: an online store
(Kettle & Co), an electricity utility (Brightline Power), and an appliance and
battery service company (FixRight Service). A connector is built and tested
against these exactly as against a real client's API, so no client is needed,
and no customer data is ever uploaded to IndicusAI.
| Business | Reads | Changes |
|---|---|---|
| `/shop` | customer by mobile, orders, one order | cancel, return, change address (refused in the wrong state) |
| `/miso` | the same API as `/shop`, with Miso's 300 orders (`IDS-240001` … `IDS-240300`, on `+919000…` test numbers) | the same |
| `/power` | accounts by mobile, bills, payment link, outage at the account's address, complaints | raise a complaint |
| `/service` | products with warranty, warranty by serial, service requests | book a visit, move a visit |
- **Auth:** `X-Api-Key: sample-client-key` (set `SAMPLE_CLIENT_API_KEY`). It is declared
as an API-key security scheme, so an OpenAPI import treats it as a stored credential.
- **Specs:** `/shop/openapi.json`, `/miso/openapi.json`, `/power/openapi.json`, `/service/openapi.json`.
- **Test people** (the same in all three): `+919000000999` Asha Rao (Bengaluru),
`+919000000111` Ravi Sen (Kolkata, which has a power cut today), `+919000000222` Meera Iyer
(Chennai), `+919000000333` Arjun Mehta (Mumbai).
- **Data** is synthetic, in memory, and dated relative to today. `/power` and `/service`
changes last until a restart.
- **Changing the stores' data for a test** (`/shop`, `/miso`): on the Swagger page
(`/miso/docs`, `/shop/docs`), **Authorize** with the key, then under **Test data**:
`PATCH /orders/{order_id}` changes any field you give — a new `phone` / `customer_name`
moves the order to that customer; `POST /orders` adds one; `POST /reset` undoes every
change. Changes (and the bot's cancels, returns and address changes) are saved in
`state/<store>.json` and survive a restart until a reset. On the server the port is only
on the box: `ssh -N -L 8304:127.0.0.1:8304 <server>` and open `localhost:8304/miso/docs`.
```bash
curl -s -H "X-Api-Key: sample-client-key" "localhost:8304/power/accounts?phone=9000000999"
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues