Agent & BTP Tutorials 12 min read

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.

S
s4ready.ai Team

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 · EntitySet A_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-partner and @modelcontextprotocol/sdk packages

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.

Joule agent to MCP server to S/4HANA architectureA vertical flow. A business user talks to SAP Joule chat in natural language. Joule routes the intent to a Joule Studio agent. The agent calls a custom Node.js MCP server on BTP Cloud Foundry, which exposes read, create, update and block tools. The MCP server uses the SAP Cloud SDK and a BTP Destination to call API_BUSINESS_PARTNER on S/4HANA, entity set A_BusinessPartner over OData v2.Business user → SAP Joule chatNatural language: “Block Business Partner 1000099”Joule Studio AgentRoutes the intent to the right toolMCP Server · Node.js + SAP Cloud SDKTools: read_bp · create_bp · update_bp · block_bp (BTP Cloud Foundry)BTP DestinationOAuth2 / Basic Auth to S/4HANAS/4HANA · API_BUSINESS_PARTNERA_BusinessPartner · OData v2 · SAP_COM_0008
Natural language flows down; results flow back up. The MCP server is the only custom code you write.

💡 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 the If-Match header. 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 NameTypeMaxRequiredNotes
BusinessPartnerString (key)10read, update, block
BusinessPartnerCategoryString1create1=Person, 2=Org, 3=Group
BusinessPartnerGroupingString4createDetermines number range
OrganizationBPName1String40create (org)Organisation name
FirstNameString40create (person)Person first name
LastNameString40create (person)Person last name
BusinessPartnerFullNameString81Read / search result
SearchTerm1String20Search filter field
BusinessPartnerIsBlockedBooleanblockReplaces physical delete
CorrespondenceLanguageString2Language key e.g. EN
Navigation PropertyChild EntityKey Fields
to_BusinessPartnerAddressA_BusinessPartnerAddressCountry, CityName, PostalCode, StreetName, HouseNumber, Language
to_AddressUsage (nested)A_AddressEmailAddressAddressUsage = ‘XXDEFAULT’ marks default address
to_BusinessPartnerRoleA_BusinessPartnerRoleBusinessPartnerRole e.g. FLCU01 (customer), FLVN01 (vendor)
to_BusinessPartnerTaxNumberA_BusinessPartnerTaxNumberBPTaxType, 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.

ToolHTTPEndpointJoule example prompt
read_bpGET/A_BusinessPartner or /A_BusinessPartner('{id}')Show me Business Partner 1000001
create_bpPOST/A_BusinessPartnerCreate an organisation BP called Acme Ltd
update_bpPATCH/A_BusinessPartner('{BusinessPartner}')Update the name of BP 1000001 to Acme Corp
block_bpPATCH/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_PARTNER service does not support physical deletion of Business Partners (the SDK docs explicitly confirm this). The correct enterprise pattern is to set BusinessPartnerIsBlocked = 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' }.

FieldValue
NameS4H_BP_DEST
TypeHTTP
URLhttps://{your-s4hana-host}/sap/opu/odata/sap/API_BUSINESS_PARTNER
AuthenticationBasicAuthentication or OAuth2ClientCredentials
UserCommunication 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

  1. Open Joule Studio from your SAP BTP subaccount → AI Services → Joule Studio.
  2. Create a new Agent → click ‘New Agent’ → give it a name, e.g. BP Manager Agent.
  3. Add MCP Server → click ‘Add Tool Source’ → select ‘MCP Server’.
  4. 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.
  5. 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.
  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:

IntentSample promptExpected tool
List BPsShow me all Business Partners with search term ACMEread_bp
Read singleGet details for Business Partner 1000001read_bp
Create orgCreate a new organisation Business Partner called Global Logistics Ltdcreate_bp
Create personCreate a person Business Partner: John Smith, grouping 0001create_bp
UpdateUpdate the name of Business Partner 1000042 to Acme Corporationupdate_bp
BlockBlock Business Partner 1000099 — it is no longer activeblock_bp

10. Common errors & fixes

ErrorCauseFix
400 Bad Request on createMissing BusinessPartnerGroupingAlways pass grouping — it determines the number range
403 on PATCH / updateMissing CSRF token or wrong ETagUse SDK’s .getByKey() before .update() — SDK handles token + ETag
404 on readWrong BP number or destination URLCheck the destination URL includes /sap/opu/odata/sap/API_BUSINESS_PARTNER
401 UnauthorizedWrong communication user or passwordVerify SAP_COM_0008 arrangement and communication user credentials
Cannot delete BPPhysical delete not supported by APIUse block_bp tool — set BusinessPartnerIsBlocked = true
Address not defaultAddressUsage not setPass to_AddressUsage: [{ addressUsage: 'XXDEFAULT' }] in deep create

11. References & further reading


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