An MCP (Model Context Protocol) server for SAP BTP administration, powered by odata-mcp-proxy. It exposes BTP Core Services APIs as MCP tools, allowing AI assistants like Claude to manage your BTP landscape through natural language — and ships interactive MCP-UI dashboards (subaccount overview with drill-down, a geomap of data-center locations, consumption and users/roles dashboards) rendered directly in the chat.
There is no application code in this project. Both the API tools and the interactive views are declared in a single JSON config file (btp-admin-api-config.json) alongside self-contained HTML templates in ui/. npm start runs the odata-mcp-proxy binary against that config.
This project uses the odata-mcp-proxy npm package, which maps OData/REST services to MCP tools based on a configuration file. You provide a config describing your APIs and entity sets, and the proxy generates the corresponding MCP tools automatically.
The config's ui section declares five additional UI_* tools. For each one the proxy fetches the listed data sources concurrently through the same OData clients (forwarding the caller's JWT), inlines the shared CSS/JS partials into the named template, bakes the payload in, and returns it as an mcp-ui embedded resource — with the MCP Apps adapter enabled, so it renders on both MCP Apps hosts like Claude and classic mcp-ui hosts.
AI Assistant (Claude, Cursor, etc.)
|
| MCP Protocol (HTTP or stdio)
v
odata-mcp-proxy ──[apis]──> entity tools (Subaccounts_list, …)
| └─[ui]────> UI_* tools + ui:// templates (ui/*.html)
|
| REST + OAuth2 (via BTP Destination Service)
v
BTP Core Services / Usage Data Management / XSUAA APIs
The server stays declarative: it fetches and injects, but does no aggregation. Rolling raw API responses up into charts and tables is each template's job.
Think of it like the SAP Application Router -- a ready-made runtime you configure, not code you write.
Five tools return interactive HTML views instead of raw JSON. Each result contains a short text summary for the model plus a ui:// embedded resource the host renders in an iframe; buttons and table rows inside the views call the next view's tool and swap it into the frame, so you can navigate between views without going back through the model.
| Tool | What it shows | Navigation |
|---|---|---|
UI_SubaccountsOverview |
Stat tiles, subaccounts-per-region chart, searchable/sortable table of all subaccounts | Row click → UI_SubaccountDetails; header buttons → the dashboards |
UI_SubaccountDetails |
One subaccount: state, region/location, labels, and its entitled services & plans (quotas) | Back to overview / map |
UI_GeomapDashboard |
World map with data-center locations sized by subaccount count, plus table view and unmapped-region list | Circle/row click → subaccounts at that location → details |
UI_ConsumptionDashboard |
Monthly usage trends per service/plan/metric (small multiples), service adoption, and cost-per-month stacked by subaccount when cost data is available | Window presets (3/6/12 months) re-run the tool |
UI_UsersRolesDashboard |
Users per identity provider, role collections by assigned users, searchable users / role-collections tables | — |
Notes:
- All views are self-contained (no CDN dependencies), respect light/dark mode, include tooltips, and every chart has a table twin.
- The same templates are also registered as
ui://btp-mcp-server/*resources with tool metadata (ui/resourceUri) for MCP Apps hosts that pre-fetch templates; tool results carry the data asstructuredContentas well. ui/_shared.jstalks to the host itself rather than relying on the adapter@mcp-ui/serverinjects: it probes with aui/initializehandshake and, on MCP Apps hosts (Claude), sendstools/calland size updates as JSON-RPC — the adapter bridges those by patchingparent.postMessage, which throws in the cross-origin sandboxed frame those hosts use, so every click was dropped. Hosts that stay silent get classic mcp-uitoolmessages instead and render the next view themselves. Because a view's script can run again in a frame that already ran it, injected partials must not declare top-levelconst/let.UI_ConsumptionDashboardneeds theBTP_USAGE_SERVICEdestination (Usage Data Management API); cost data additionally requires a consumption-based contract. Its reporting window is derived in the config via{$monthsAgo(months-1):yyyymm}/{$now:yyyymm}, so themonthsinput (1–24, default 6) needs no server-side date code.UI_UsersRolesDashboarduses the XSUAA SCIM and Authorization APIs viaBTP_XSUAA_SERVICE. The SCIM collection is paged by the proxy (paginate: 100 per request, capped at 500); when the cap is hit the view says so rather than under-reporting.- Geo coordinates for the map come from a BTP region table (
ui/_regions.js), injected into the templates as a partial; unknown regions are listed under the map instead of being dropped.
Registering every operation of every entity set as its own tool produced 40 tools and ~8,600 tokens of tools/list — around 4% of a 200k context window before the user has said anything, and past the point where models start picking worse tools.
The config enables the proxy's hybrid discovery mode instead:
"discovery": {
"mode": "hybrid",
"alwaysRegister": ["Subaccounts"],
"maxResults": 25,
"maxFullResults": 5
}| Tools | tools/list |
|
|---|---|---|
| One tool per operation (previous) | 40 | ~8,600 tokens |
hybrid, Subaccounts pinned (current) |
12 | ~2,610 tokens |
search (nothing pinned) |
7 | ~1,441 tokens |
What stays directly visible: the five Subaccounts_* tools (the hot path for mutations), the five UI_* views (the entry points the model should prefer), and two meta-tools — search_operations to find the right api/entitySet/operation, and execute_operation to run it. Everything else is reachable through those two, plus one odata://{api}/{entitySet} schema resource per entity set for hosts that pre-fetch resources.
Remove the discovery block and every entity set goes back to having its own tools; nothing else in the config changes.
The config file (btp-admin-api-config.json) defines three BTP Core Services and the XSUAA Authorization API:
| Tool | Operations | Description |
|---|---|---|
GlobalAccount |
list | View global account details |
Subaccounts |
list, get, create, update, delete | Manage subaccounts |
Directories |
list, get, create, update, delete | Organize subaccounts into directories |
| Tool | Operations | Description |
|---|---|---|
Assignments |
list, update | Manage service plan entitlement assignments |
AllowedDataCenters |
list | View available data centers |
| Tool | Operations | Description |
|---|---|---|
EnvironmentInstances |
list, get, create, delete | Manage runtime environments (e.g. Cloud Foundry orgs) |
AvailableEnvironments |
list | View environment types available for provisioning |
| Tool | Operations | Description |
|---|---|---|
MonthlyUsage |
list | Monthly service usage per subaccount/service/plan/metric (?fromDate=YYYYMM&toDate=YYYYMM) |
MonthlySubaccountsCost |
list | Monthly cost per subaccount for consumption-based licenses |
| Tool | Operations | Description |
|---|---|---|
Users |
list, get | Subaccount users via the XSUAA SCIM API (SCIM query params, not OData) |
Applications |
list, get | View XSUAA application registrations |
RoleCollections |
list, get, create, update, delete | Manage role collections |
Roles |
list, get, create, delete | Manage roles derived from role templates |
RoleTemplates |
list, get | View role templates defined by applications |
All _list tools support OData query parameters: $filter, $select, $expand, $orderby, $top, $skip.
The repository ships a Claude Code skill at .claude/skills/btp-admin/SKILL.md that teaches Claude how to use these tools efficiently:
- Tool selection -- maps common admin tasks ("entitle a service", "create a CF org") to the right tool on the first try
- Token discipline -- always use
$select,$filter, and$topso verbose BTP list responses stay small - Payload shapes -- documents request bodies the JSON schemas can't fully express (entitlement
amountvsenable, environment instanceparameters, role keys) - Workflows and pitfalls -- multi-step recipes (subaccount onboarding) and async-operation handling
Skills use progressive disclosure: only the name and description are loaded into context until a BTP admin task triggers the full skill, so the standing cost is a few dozen tokens. Anyone cloning this repository and using Claude Code gets it automatically; it also works when the repo is added as a project in claude.ai.
- Node.js 18+ (20+ recommended)
- SAP BTP account with a Cloud Foundry environment
- BTP Destinations configured for the CIS APIs (Accounts, Entitlements, Provisioning) with OAuth2 authentication
- Cloud Foundry CLI (
cf) and MBT Build Tool (mbt) for deployment
btp-mcp-server/
├── package.json # Start script + the single odata-mcp-proxy dependency
├── btp-admin-api-config.json # Everything: `apis` (entity tools) + `ui` (view tools)
├── ui/ # Self-contained HTML view templates
│ ├── subaccounts-overview.html
│ ├── subaccount-details.html
│ ├── dashboard-geomap.html
│ ├── dashboard-consumption.html
│ ├── dashboard-users-roles.html
│ ├── _shared.css / _shared.js# chart chrome, palette, helpers (injected as partials)
│ ├── _regions.js # BTP region → city/coordinates table (injected)
│ └── _world-path.txt # world outline (Natural Earth 110m, public domain)
├── .claude/skills/btp-admin/ # Claude Code skill: tool selection + token-usage guidance
├── mta.yaml # BTP Cloud Foundry deployment descriptor
├── xs-security.json # XSUAA OAuth2 configuration
├── default-env.json # Local dev credentials (gitignored)
└── LICENSE
npm installCreate BTP Destinations pointing to the CIS, Usage Data Management and XSUAA APIs:
| Destination | URL | Used by |
|---|---|---|
BTP_ACCOUNTS_SERVICE |
https://accounts-service.cfapps.<region>.hana.ondemand.com |
Account tools, all UI views |
BTP_ENTITLEMENTS_SERVICE |
https://entitlements-service.cfapps.<region>.hana.ondemand.com |
Entitlement tools, subaccount details UI |
BTP_PROVISIONING_SERVICE |
https://provisioning-service.cfapps.<region>.hana.ondemand.com |
Environment tools |
BTP_USAGE_SERVICE |
https://uas-reporting.cfapps.<region>.hana.ondemand.com |
Consumption tools + dashboard |
BTP_XSUAA_SERVICE |
https://<subdomain>.authentication.<region>.hana.ondemand.com |
Authorization tools, users & roles dashboard (XSUAA apiaccess service key) |
Each destination should use OAuth2 client credentials authentication.
Create a default-env.json with your BTP service bindings (XSUAA, Destination, Connectivity) to run locally:
npm startThis runs odata-mcp-proxy --config btp-admin-api-config.json, which registers the entity tools from the config's apis section and the interactive views from its ui section.
npm run build:btp # Build MTA archive
npm run deploy:btp # Deploy to Cloud FoundryThe MTA deployment provisions three service instances:
- Destination (lite) -- resolves API endpoints and manages OAuth2 tokens
- Connectivity (lite) -- enables secure backend connectivity
- XSUAA (application) -- handles OAuth2 authentication with role-based access control
The XSUAA configuration (xs-security.json) defines three role templates:
| Role | Scopes | Description |
|---|---|---|
MCPViewer |
read | Read-only access |
MCPEditor |
read, write | Read and write access |
MCPAdmin |
read, write, admin | Full administrative access |
OAuth2 redirect URIs are pre-configured for Claude.ai, Cursor, Microsoft Teams, and local development.
This project demonstrates how easy it is to create a custom MCP server using odata-mcp-proxy. To build your own:
-
Create a new project and install the dependency:
mkdir my-mcp-server && cd my-mcp-server npm init -y npm install odata-mcp-proxy
-
Add a start script to
package.json:{ "scripts": { "start": "odata-mcp-proxy --config my-api-config.json" } } -
Define your APIs in a config file (
my-api-config.json):{ "server": { "name": "my-mcp-server", "version": "1.0.0", "description": "My custom MCP server" }, "apis": [ { "name": "my-api", "destination": "MY_BTP_DESTINATION", "pathPrefix": "/api/v1", "csrfProtected": true, "entitySets": [ { "entitySet": "Products", "description": "Product catalog", "category": "master-data", "keys": [{ "name": "Id", "type": "string" }], "operations": { "list": true, "get": true, "create": false, "update": false, "delete": false } } ] } ] } -
Add your
mta.yaml,xs-security.json, and BTP Destinations, then deploy. That's it -- no code to write.
MIT