TravelService
by clouddnagmbh
README.md
# GenAI & MCP Integration in SAP CAP
Companion repository for the live session **"GenAI & MCP Integration in SAP CAP"**.
`main` holds the base CAP application. **Every demo is its own branch**, and the
branches form a linear chain — each builds on the previous one, so
`git diff main..<branch>` shows exactly what that step added.
> All facts, versions and annotations in this repo were verified against plugin
> **source code and integration tests** on 2026-09-07 — not against READMEs.
> Where the official docs and the code disagree, the code wins and the branch
> README says so.
## The base app
A deliberately small travel domain: `Travels` (draft-enabled) with `Bookings`,
`TravelAgencies`, `Customers` and a `TravelStatus` code list.
```
300 travels · 752 bookings · 20 agencies · 60 customers
```
The seed data is **generated deterministically** and carries two learnable
patterns, so AI recommendations have something real to find:
| Pattern | Strength |
|---|---|
| an agency always bills in its country's currency | 100% (1 currency per agency) |
| customers rebook with their "home" agency | 78.7% |
| expensive travels get rejected more often | correlated with `TotalPrice` |
All three status codes are populated (168 Open / 100 Accepted / 32 Rejected), so
no filter demo returns an empty list. The database deploys with
`assert_integrity: "DB"`, which proves every foreign key resolves.
## Branches
Each branch carries its own `docs/<branch-name>.md` with the exact commands, the
`verify:` checks, and what the official docs get wrong.
### Track A — GenAI
| Branch | Demo | Runs offline? |
|---|---|---|
| `01-recommendations-mock` | SAP-RPT-1 field recommendations via `@cap-js/ai` — one `npm add`, zero handler code | yes (mock) |
| `02-recommendations-control` | `@UI.RecommendationState`, and proof it is **not** a privacy control | yes (mock) |
| `03-aicore-service` | `AICore` as a CAP service; a hand-crafted RPT-1 call | yes (predictions) |
| `04-genai-orchestration` | a **real generative call** via SAP Cloud SDK for AI, behind a mockable CAP service | yes (local impl) |
| `05-vector-rag` | `cds.Vector` + similarity search + a full RAG chain | yes |
### Track B — MCP
| Branch | Demo | Runs offline? |
|---|---|---|
| `06-mcp-basics` | expose the service via `@mcp`; explore it with `mcp-rpc.sh` | yes |
| `07-mcp-query` | `describe` / `query` in CQL **and** CQN, TOON, row limits | yes |
| `08-mcp-actions` | writes via unbound actions, the `call` tool | yes |
| `09-mcp-security` | `@requires` / `@restrict` / `@cds.api.ignore`, tailored projections | yes |
| `10-agent-clients` | autowiring into Claude Code; manual VS Code / Copilot config | yes |
| `11-genai-mcp-together` | one service, two consumers — an agent invoking CAP's own RAG | yes |
**No BTP entitlement is required for any demo.** Every branch runs offline; the
cloud paths are documented and, where possible, wired so that switching to them
is a profile change rather than a code change.
## What the upstream exercises get wrong
This repo started as a verification of SAP's `recap2026` exercises 08 and 09.
The corrections are load-bearing, not cosmetic:
| Claim | Reality |
|---|---|
| MCP tools are `describe`, `query`, `call_action` | `call_action` was renamed **`call`** in `@cap-js/mcp` 1.3.0 and appears nowhere in shipped code. And `call` only exists once the service has an unbound action |
| `query` takes `entity` / `select` / `where` / `limit` | Default is `format: "cql"` — one `cql` string, `additionalProperties: false`. The exercise's payload fails Zod validation |
| `cds.mcp.toon_format: false` gives JSON | Removed in 1.4.2. TOON is unconditional; JSON only via `structuredContent` |
| MCP Inspector: "Transport Type" → "Via Proxy" → "Connect" | That is the v1 UI. `npx` gives 2.5.0, needs Node ≥ 22.19.0, and `--transport http` is mandatory |
| `@UI.RecommendationState: 0` keeps a field out of the payload | It is dropped as a prediction *target* but **still sent** to SAP AI Core |
| "You should see a message indicating that the AI plugin is active" | The plugin logs nothing — not on startup, not on mock fallback |
| `claude "prompt"` | Opens an interactive session and never exits. Use `claude -p` |
| `annotate … with @odata @hcql @mcp` | `@cap-js/hcql` does not exist on npm; HCQL ships inside `@sap/cds` |
Two problems are ours to report upstream, found by building this repo:
- **`@cds.api.ignore` on an element is not access control** in MCP's default
`cql` mode — hidden from `describe`, still returned by `query`, while OData and
`cqn` mode both refuse it. See `docs/09-mcp-security.md`.
- **Querying AI Core admin entities with no binding terminates the CAP
process** — an uncaught `TypeError` in `AICoreService._getToken()`, not a 500.
See `docs/03-aicore-service.md`.
## Quick start
```bash
npm ci
npm run watch
```
Then open:
| What | URL |
|---|---|
| Service index | http://localhost:4004 |
| Fiori preview (list report) | http://localhost:4004/$fiori-preview/TravelService/Travels |
| OData | http://localhost:4004/odata/v4/travel/Travels |
| MCP (full service) | `POST http://localhost:4004/mcp/travel` (from branch `06`) |
| MCP (curated agent surface) | `POST http://localhost:4004/mcp/travel-agent` (from branch `09`) |
Local auth uses CAP's mocked users — `alice` (role `admin`) with an empty
password, i.e. `Authorization: Basic YWxpY2U6`.
No UI5 application is generated: CAP 10's built-in `$fiori-preview` serves a
full Fiori elements list report and object page straight from the annotations
in `app/`. That keeps the UI out of the branch diffs entirely.
## Maintaining the chain
The branches form a linear chain, so a change to `main` needs re-stacking:
```bash
./rebase-chain.sh
```
It rebases each branch onto its predecessor, using each branch's **own first
parent** as the base.
Two invariants it relies on, both learned the hard way:
1. **One commit per branch.** The script uses `<branch>^` as the rebase base, so
a second commit on a branch makes it silently drop the first. Amend instead of
adding commits, and squash if you slip:
`git reset --soft <branch>~2 && git commit`.
2. **Do not substitute `git merge-base`.** Once the parent branch has been
rewritten, the merge base moves backwards and the parent's commit is replayed
a second time — which conflicts on `package.json`.
Check the invariant before running it:
```bash
git log --oneline main..11-genai-mcp-together | wc -l # must equal the branch count
```
## Prerequisites
```
node >= 22.19.0 (MCP Inspector v2 needs this; @sap/cds 10 needs >= 22)
@sap/cds 10.0.6 pinned
@cap-js/sqlite 3.0.2 pinned
@sap/cds-dk 10.x global (npm i -g @sap/cds-dk)
```
Versions are **pinned exactly**, not floated. Between `@cap-js/mcp` 1.2.0 and
1.4.3 — about six weeks — a tool was renamed, the default query input format
changed, and a config flag was deleted. Floating those ranges breaks demos.
## Layout
```
db/schema.cds domain model
db/data/*.csv deterministic seed data (generated, committed)
srv/travel-service.cds the service
app/annotations.cds draft + value helps <- gates the AI recommendations feature
app/fiori.cds Fiori elements UI annotations
docs/<branch>.md one per branch: commands, verify checks, doc corrections
mcp-rpc.sh minimal MCP client (branch 06+) -- no Inspector needed
```
## Verifying MCP without the Inspector
`mcp-rpc.sh` posts one JSON-RPC call and unwraps the SSE frame. No download, no
Node version floor, fully deterministic — the safest thing to run on stage:
```bash
./mcp-rpc.sh '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq -r '.result.tools[].name'
MCP_USER=viewer ./mcp-rpc.sh '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
MCP_URL=http://localhost:4004/mcp/travel-agent ./mcp-rpc.sh '…'
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues