Building a Joule Agent + MCP Server for Business Partner CRUD on SAP BTP
A step-by-step guide to building a production MCP server on BTP Cloud Foundry that exposes four Business Partner tools to a SAP Joule Studio agent — create, read, update, and block Business Partners in S/4HANA via natural language, using API_BUSINESS_PARTNER, Node.js and the SAP Cloud SDK.
This step-by-step guide walks you through building a production-ready MCP (Model Context Protocol) server on SAP BTP Cloud Foundry, exposing four Business Partner tools to a SAP Joule Studio agent. Once deployed, business users can create, read, update, and block Business Partners in S/4HANA via natural-language prompts — no UI required.
At a glance
- API:
API_BUSINESS_PARTNER· EntitySetA_BusinessPartner(OData v2)- Runtime: Node.js 20 + SAP Cloud SDK, deployed on BTP Cloud Foundry
- Agent: Joule Studio custom agent, registered via BTP Destination
- Important: Business Partners cannot be physically deleted — “block” (
BusinessPartnerIsBlocked) is the correct pattern
1. Prerequisites
SAP System & BTP
- SAP S/4HANA (Cloud Public/Private or on-premise with REST API access)
- Communication Arrangement for scenario SAP_COM_0008 (Business Partner, Customer and Supplier Integration) configured and active
- BTP Subaccount with Cloud Foundry environment enabled
- BTP Destination pointing to your S/4HANA system with Basic Auth or OAuth2
- Joule Studio access (SAP Business AI entitlement)
Developer Tools
- Node.js 20+ and npm installed locally or in BAS
- SAP Business Application Studio (BAS) or VS Code with SAP extensions
- Cloud Foundry CLI (
cf) installed and logged in to your BTP subaccount @sap-cloud-sdk/api-business-partnerand@modelcontextprotocol/sdkpackages
2. Architecture overview
The solution follows a four-layer flow. A business user speaks to SAP Joule in natural language. Joule routes the intent to a registered Joule Studio Agent, which calls the appropriate tool on your MCP Server deployed on BTP Cloud Foundry. The MCP Server uses the SAP Cloud SDK to call API_BUSINESS_PARTNER on S/4HANA via a BTP Destination.
💡 Why SAP Cloud SDK instead of raw fetch?
The SAP Cloud SDK type-safe client handles CSRF token fetching and ETag management automatically on PATCH/DELETE calls. Without it, you must manually fetch a token (
x-csrf-token: fetch) on a GET call, then re-send it on write calls plus include an ETag in theIf-Matchheader. The SDK does all of this for you, eliminating a major source of runtime errors.
3. API & entity field verification
Service: API_BUSINESS_PARTNER · EntitySet: A_BusinessPartner · Protocol: OData v2
Metadata URL: /sap/opu/odata/sap/API_BUSINESS_PARTNER/$metadata
A_BusinessPartner — key fields used in this guide
| Field Name | Type | Max | Required | Notes |
|---|---|---|---|---|
| BusinessPartner | String (key) | 10 | — | read, update, block |
| BusinessPartnerCategory | String | 1 | create | 1=Person, 2=Org, 3=Group |
| BusinessPartnerGrouping | String | 4 | create | Determines number range |
| OrganizationBPName1 | String | 40 | create (org) | Organisation name |
| FirstName | String | 40 | create (person) | Person first name |
| LastName | String | 40 | create (person) | Person last name |
| BusinessPartnerFullName | String | 81 | — | Read / search result |
| SearchTerm1 | String | 20 | — | Search filter field |
| BusinessPartnerIsBlocked | Boolean | — | block | Replaces physical delete |
| CorrespondenceLanguage | String | 2 | — | Language key e.g. EN |
Navigation properties used (deep create)
| Navigation Property | Child Entity | Key Fields |
|---|---|---|
| to_BusinessPartnerAddress | A_BusinessPartnerAddress | Country, CityName, PostalCode, StreetName, HouseNumber, Language |
| to_AddressUsage (nested) | A_AddressEmailAddress | AddressUsage = ‘XXDEFAULT’ marks default address |
| to_BusinessPartnerRole | A_BusinessPartnerRole | BusinessPartnerRole e.g. FLCU01 (customer), FLVN01 (vendor) |
| to_BusinessPartnerTaxNumber | A_BusinessPartnerTaxNumber | BPTaxType, BPTaxNumber |
4. Project setup in BAS
Open SAP Business Application Studio (or VS Code) and create a new Node.js project.
Step 1 — Initialise the project
mkdir bp-mcp-server && cd bp-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk \
@sap-cloud-sdk/api-business-partner \
@sap-cloud-sdk/connectivity \
@sap-cloud-sdk/http-client
npm install -D typescript @types/node ts-node
npx tsc --init
Step 2 — Project structure
bp-mcp-server/
├── src/
│ ├── tools/
│ │ ├── read_bp.ts
│ │ ├── create_bp.ts
│ │ ├── update_bp.ts
│ │ └── block_bp.ts
│ └── index.ts ← MCP server entry point
├── manifest.yml ← BTP Cloud Foundry config
├── package.json
└── .env ← destination name, local dev only
Step 3 — MCP server entry point (index.ts)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerReadBpTool } from "./tools/read_bp.js";
import { registerCreateBpTool } from "./tools/create_bp.js";
import { registerUpdateBpTool } from "./tools/update_bp.js";
import { registerBlockBpTool } from "./tools/block_bp.js";
const server = new McpServer({
name: "bp-mcp-server",
version: "1.0.0",
description: "CRUD operations on SAP Business Partners via API_BUSINESS_PARTNER"
});
registerReadBpTool(server);
registerCreateBpTool(server);
registerUpdateBpTool(server);
registerBlockBpTool(server);
const transport = new StdioServerTransport();
await server.connect(transport);
console.log("BP MCP Server running");
5. The four MCP tools
Each tool maps to a specific HTTP method on the A_BusinessPartner entity set.
| Tool | HTTP | Endpoint | Joule example prompt |
|---|---|---|---|
| read_bp | GET | /A_BusinessPartner or /A_BusinessPartner('{id}') | Show me Business Partner 1000001 |
| create_bp | POST | /A_BusinessPartner | Create an organisation BP called Acme Ltd |
| update_bp | PATCH | /A_BusinessPartner('{BusinessPartner}') | Update the name of BP 1000001 to Acme Corp |
| block_bp | PATCH | /A_BusinessPartner('{BusinessPartner}') | Block Business Partner 1000001 |
Tool 1: read_bp — read Business Partners
Retrieves a list of BPs or a single BP by number. Supports optional filtering by name.
// tools/read_bp.ts
import { businessPartnerService } from "@sap-cloud-sdk/api-business-partner";
export function registerReadBpTool(server: McpServer) {
server.tool("read_bp", {
businessPartnerId: { type: "string", optional: true,
description: "BP number. Omit to list all." },
searchTerm: { type: "string", optional: true,
description: "Filter by SearchTerm1 (max 20 chars)" }
}, async ({ businessPartnerId, searchTerm }) => {
const { businessPartnerApi } = businessPartnerService();
const req = businessPartnerApi.requestBuilder().getAll()
.select(
businessPartnerApi.schema.BUSINESS_PARTNER,
businessPartnerApi.schema.BUSINESS_PARTNER_FULL_NAME,
businessPartnerApi.schema.BUSINESS_PARTNER_CATEGORY,
businessPartnerApi.schema.SEARCH_TERM_1,
businessPartnerApi.schema.BUSINESS_PARTNER_IS_BLOCKED
);
if (searchTerm) req.filter(
businessPartnerApi.schema.SEARCH_TERM_1.equals(searchTerm)
);
const result = await req.execute({ destinationName: "S4H_BP_DEST" });
return { content: [{ type: "text",
text: JSON.stringify(result.map(bp => ({
id: bp.businessPartner,
name: bp.businessPartnerFullName,
category: bp.businessPartnerCategory,
blocked: bp.businessPartnerIsBlocked
}))) }] };
});
}
Tool 2: create_bp — create a new Business Partner
BusinessPartnerCategory and BusinessPartnerGrouping are required on every create call. The grouping controls which number range is used to assign the BP number. Missing it causes a runtime error.
// tools/create_bp.ts
import { businessPartnerService } from "@sap-cloud-sdk/api-business-partner";
export function registerCreateBpTool(server: McpServer) {
server.tool("create_bp", {
category: { type: "string", description: "1=Person, 2=Organisation, 3=Group" },
grouping: { type: "string", description: "BP grouping key (number range)" },
name: { type: "string", description: "OrganizationBPName1 (org) or FirstName (person)" },
lastName: { type: "string", optional: true, description: "LastName for persons" },
country: { type: "string", optional: true, description: "Country key e.g. DE, SG, IN" },
city: { type: "string", optional: true, description: "CityName for address" },
language: { type: "string", optional: true, description: "Correspondence language e.g. EN" },
}, async ({ category, grouping, name, lastName, country, city, language }) => {
const { businessPartnerApi, businessPartnerAddressApi } = businessPartnerService();
const builder = businessPartnerApi.entityBuilder()
.businessPartnerCategory(category)
.businessPartnerGrouping(grouping);
// Set name fields based on category
if (category === "2") builder.organizationBpName1(name); // Organisation
else { builder.firstName(name); if (lastName) builder.lastName(lastName); }
if (language) builder.correspondenceLanguage(language);
// Deep insert address if provided
if (country && city) {
const address = businessPartnerAddressApi.entityBuilder()
.country(country).cityName(city).language(language ?? "EN")
.toAddressUsage([{ addressUsage: "XXDEFAULT" }]) // mark as default
.build();
builder.toBusinessPartnerAddress([address]);
}
const bp = builder.build();
const result = await businessPartnerApi.requestBuilder()
.create(bp).execute({ destinationName: "S4H_BP_DEST" });
return { content: [{ type: "text",
text: `Business Partner created successfully. BP Number: ${result.businessPartner}` }] };
});
}
Tool 3: update_bp — update Business Partner fields
PATCH calls require a CSRF token and an ETag in the If-Match header. The SAP Cloud SDK fetches and attaches these automatically — fetch the entity first with .getByKey(), then call .update() on the modified entity.
// tools/update_bp.ts
import { businessPartnerService } from "@sap-cloud-sdk/api-business-partner";
export function registerUpdateBpTool(server: McpServer) {
server.tool("update_bp", {
businessPartnerId: { type: "string", description: "BP number to update" },
newName: { type: "string", optional: true, description: "New OrganizationBPName1" },
searchTerm: { type: "string", optional: true, description: "New SearchTerm1 (max 20)" },
}, async ({ businessPartnerId, newName, searchTerm }) => {
const { businessPartnerApi } = businessPartnerService();
const dest = { destinationName: "S4H_BP_DEST" };
// 1. Fetch existing entity (SDK stores ETag automatically)
const bp = await businessPartnerApi.requestBuilder()
.getByKey(businessPartnerId).execute(dest);
// 2. Apply changes
if (newName) bp.organizationBpName1 = newName;
if (searchTerm) bp.searchTerm1 = searchTerm;
// 3. PATCH — SDK sends If-Match + CSRF token automatically
await businessPartnerApi.requestBuilder().update(bp).execute(dest);
return { content: [{ type: "text",
text: `Business Partner ${businessPartnerId} updated successfully.` }] };
});
}
Tool 4: block_bp — block a Business Partner (replaces delete)
⚠️ Why block instead of delete?
The
API_BUSINESS_PARTNERservice does not support physical deletion of Business Partners (the SDK docs explicitly confirm this). The correct enterprise pattern is to setBusinessPartnerIsBlocked = true, which prevents the BP from being used in new transactions while preserving audit history.
// tools/block_bp.ts
import { businessPartnerService } from "@sap-cloud-sdk/api-business-partner";
export function registerBlockBpTool(server: McpServer) {
server.tool("block_bp", {
businessPartnerId: { type: "string", description: "BP number to block" },
}, async ({ businessPartnerId }) => {
const { businessPartnerApi } = businessPartnerService();
const dest = { destinationName: "S4H_BP_DEST" };
// Fetch first so SDK has the ETag for the PATCH
const bp = await businessPartnerApi.requestBuilder()
.getByKey(businessPartnerId).execute(dest);
bp.businessPartnerIsBlocked = true;
await businessPartnerApi.requestBuilder().update(bp).execute(dest);
return { content: [{ type: "text",
text: `Business Partner ${businessPartnerId} has been blocked.` }] };
});
}
6. Configure the BTP Destination
Create a Destination in your BTP subaccount that points to your S/4HANA system. This is what the SAP Cloud SDK uses when you pass { destinationName: 'S4H_BP_DEST' }.
| Field | Value |
|---|---|
| Name | S4H_BP_DEST |
| Type | HTTP |
| URL | https://{your-s4hana-host}/sap/opu/odata/sap/API_BUSINESS_PARTNER |
| Authentication | BasicAuthentication or OAuth2ClientCredentials |
| User | Communication user from SAP_COM_0008 arrangement |
| sap-client (property) | Your SAP client number e.g. 100 |
SAP_COM_0008 — Communication Arrangement. Before creating the Destination, ensure a Communication Arrangement for scenario SAP_COM_0008 (Business Partner, Customer and Supplier Integration) is active in your S/4HANA system. This arrangement creates the communication user and activates the API_BUSINESS_PARTNER OData service endpoint.
7. Deploy to BTP Cloud Foundry
manifest.yml
applications:
- name: bp-mcp-server
memory: 256M
buildpacks:
- nodejs_buildpack
command: node dist/index.js
env:
NODE_ENV: production
services:
- destination-service-instance # binds BTP Destination service
- xsuaa-service-instance # binds XSUAA for OAuth2
Build and push
# Compile TypeScript
npx tsc
# Login to BTP Cloud Foundry
cf login -a https://api.cf.{region}.hana.ondemand.com
# Deploy
cf push
# Note your app URL from the output:
# routes: bp-mcp-server.cfapps.{region}.hana.ondemand.com
8. Register the MCP server in Joule Studio
- Open Joule Studio from your SAP BTP subaccount → AI Services → Joule Studio.
- Create a new Agent → click ‘New Agent’ → give it a name, e.g. BP Manager Agent.
- Add MCP Server → click ‘Add Tool Source’ → select ‘MCP Server’.
- Enter the server URL:
https://bp-mcp-server.cfapps.{region}.hana.ondemand.com→ the four tools (read_bp, create_bp, update_bp, block_bp) will auto-discover. - Configure the system prompt — describe the agent’s purpose:
You are an SAP Business Partner management assistant. You can:
- Read and search Business Partners
- Create new Business Partners (organisation or person)
- Update Business Partner details
- Block a Business Partner (note: BPs cannot be deleted, only blocked)
Always confirm the BP number returned after a successful create.
For organisations, category is 2. For persons, category is 1.
- Save and activate the agent — it is now available in Joule.
9. Test with natural language
Open Joule in your Fiori Launchpad or SAP BTP cockpit and try these prompts:
| Intent | Sample prompt | Expected tool |
|---|---|---|
| List BPs | Show me all Business Partners with search term ACME | read_bp |
| Read single | Get details for Business Partner 1000001 | read_bp |
| Create org | Create a new organisation Business Partner called Global Logistics Ltd | create_bp |
| Create person | Create a person Business Partner: John Smith, grouping 0001 | create_bp |
| Update | Update the name of Business Partner 1000042 to Acme Corporation | update_bp |
| Block | Block Business Partner 1000099 — it is no longer active | block_bp |
10. Common errors & fixes
| Error | Cause | Fix |
|---|---|---|
| 400 Bad Request on create | Missing BusinessPartnerGrouping | Always pass grouping — it determines the number range |
| 403 on PATCH / update | Missing CSRF token or wrong ETag | Use SDK’s .getByKey() before .update() — SDK handles token + ETag |
| 404 on read | Wrong BP number or destination URL | Check the destination URL includes /sap/opu/odata/sap/API_BUSINESS_PARTNER |
| 401 Unauthorized | Wrong communication user or password | Verify SAP_COM_0008 arrangement and communication user credentials |
| Cannot delete BP | Physical delete not supported by API | Use block_bp tool — set BusinessPartnerIsBlocked = true |
| Address not default | AddressUsage not set | Pass to_AddressUsage: [{ addressUsage: 'XXDEFAULT' }] in deep create |
11. References & further reading
- API_BUSINESS_PARTNER reference — SAP Business Accelerator Hub
- SAP Help Portal — Business Partner (A2X) OData service
- SAP Cloud SDK — OData v2 type-safe client
- SAP Community — ADT for VS Code & the MCP Server
Want the bigger picture on where Joule, Joule Studio and the MCP Server fit together? Read our plain-English map of SAP’s AI stack, and see how ABAP in VS Code went GA. Planning an S/4HANA move? Start with a free readiness scan.
Keep reading
Building a Joule Agent + MCP Server for Sales Order Creation on SAP BTP
A step-by-step guide to building an MCP server on BTP Cloud Foundry that lets a SAP Joule Studio agent create, read, update, and delete Sales Orders in S/4HANA via natural language — using API_SALES_ORDER_SRV, header-plus-item deep insert, Node.js and the SAP Cloud SDK.
Read more →SAP BTP Architecture for S/4HANA Migration Projects
A technical walkthrough of SAP BTP's four pillars, how BTP connects to S/4HANA via destinations and APIs, CAP vs RAP, Cloud Foundry vs Kyma, and where migration teams should actually start.
Read more →Building AI Agents for SAP: Architecture, Tool Use, and Practical Pitfalls
How to wire AI agents — including LLM-based orchestrators — into SAP systems via BTP destinations, function calling for ABAP analysis, and multi-agent workflows for assessment, remediation, and testing.
Read more →