feat(integrations): SAP S/4HANA (#4301)

* feat(integrations): SAP S/4HANA tools, block, and proxy with multi-deployment support

* fix(sap_s4hana): address PR review comments

- Validate baseUrl/tokenUrl in Zod schema and at runtime to prevent SSRF
  (https-only, deny loopback/link-local/cloud-metadata hosts)
- Cap proxy token cache at 500 entries with LRU eviction
- Add 30s timeout to outbound token, CSRF, and OData fetches
- Make parseJsonInput return T | undefined so missing input is type-safe
- Reset authType when deploymentType changes and surface OAuth fields
  whenever auth is not basic, so cloud_public users always see clientId/
  clientSecret after switching from a basic-auth private deployment
- Reject OData service names that are not uppercase identifiers and
  paths containing ".." or "." traversal segments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): allow versioned service names; tighten proxy SSRF defenses

- Permit ";v=NNNN" suffix on ServiceName regex so the four delivery tools
  (API_OUTBOUND_DELIVERY_SRV;v=0002, API_INBOUND_DELIVERY_SRV;v=0002) pass
  schema validation
- Restrict subdomain to RFC 1123 label characters and region to lowercase
  alphanumeric short codes; run the constructed cloud_public host through
  assertSafeExternalUrl so a crafted subdomain (e.g. "evil.com#") cannot
  redirect requests carrying SAP credentials
- Block RFC-1918 (10/8, 172.16/12, 192.168/16), 127/8, 169.254/16, and
  0.0.0.0 via isPrivateIPv4, plus IPv4-mapped IPv6 variants
  (::ffff:10.0.0.1, ::10.0.0.1) so private internal hosts cannot be
  reached from baseUrl, tokenUrl, or the resolved cloud_public URL

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): catch hex-form IPv4-mapped IPv6 in SSRF check

The WHATWG URL parser normalizes IPv4-mapped IPv6 addresses to hex form
(e.g. [::ffff:169.254.169.254] → [::ffff:a9fe:a9fe]), which slipped past
the dotted-decimal-only extractor. Decode the trailing two 16-bit hex
groups back into IPv4 octets and run them through isPrivateIPv4. Also
add isPrivateOrLoopbackIPv6 so pure IPv6 loopback (::, ::1), unique
local addresses (fc00::/7), and link-local (fe80::/10) cannot be reached.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): scope CSRF metadata fetch and isolate token cache by secret

- buildOdataUrl skips request query params when called with an internal
  pathOverride so the /$metadata CSRF probe never carries user OData
  options ($filter, $top, $select), which were causing write operations
  through the generic odata_query tool to fail.
- tokenCacheKey now mixes a sha256 hash of clientSecret into the cache
  key so two tenants sharing the same tokenUrl + clientId but different
  secrets get isolated entries (no cross-tenant token leak).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): reject ?/# in service path; trim long update tool descriptions

- ServicePath validator now rejects "?" and "#" so a caller can't smuggle
  query options through the path field (e.g.,
  "/A_BusinessPartner?$format=atomsvc"); the Zod refine now reports
  ".." / "." segments, "?", and "#" together.
- Update Customer / Update Supplier / Update Purchase Requisition tool
  descriptions exceeded the docs generator's 600-char regex window, so
  they were rendering with empty descriptions on the integrations
  landing page. Trimmed them to fit while keeping the limited-fields
  note and the If-Match guidance, then regenerated integrations.json
  and tool docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): reject percent-encoded path traversal; widen Set-Cookie split

- ServicePath now also rejects %2e/%2E, %2f/%2F, %5c/%5C, %3f/%3F, %23
  so a caller cannot smuggle ".." / "." / "/" / "\" / "?" / "#" past the
  validator and have SAP's ABAP/ICM gateway decode them server-side.
- joinSetCookies fallback regex now allows the ", " separator that's
  used when multiple Set-Cookie values are folded onto one header line
  (older runtimes without Headers.getSetCookie). Prevents CSRF cookies
  from being concatenated into a single value during write operations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): preserve $ in OData query params; reject empty items array

- buildOdataUrl now constructs query strings manually with
  encodeURIComponent and restores literal "$" so OData system options
  ($filter, $top, $select, $expand, $orderby, $skip, $format) reach
  SAP and any intermediary proxies/WAFs as-is, not as "%24filter".
  URLSearchParams was percent-encoding "$" to "%24" which most ICMs
  decode but some intermediaries silently drop, returning unfiltered
  results.
- create_sales_order now rejects an empty items array (matches
  create_purchase_requisition) so callers get a clear client-side
  error instead of an opaque SAP validation failure on the deep-insert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): ignore baseUrl on cloud_public to prevent token redirection

Why: resolveHost previously preferred baseUrl unconditionally. A caller
sending deploymentType=cloud_public with a baseUrl pointing elsewhere
would obtain a real SAP UAA token, then forward it as Bearer to the
attacker host. Zod superRefine did not validate baseUrl for cloud_public.

Fix: resolveHost now constructs the SAP host from subdomain when
deploymentType is cloud_public and only uses baseUrl for cloud_private
and on_premise (where it is already SSRF-checked in superRefine).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(icons): use useId for SapS4HanaIcon and PipedriveIcon gradients

Why: hardcoded SVG gradient/mask IDs collide when an icon renders more
than once on a page (e.g. integrations listing). All other icons in this
file use React's useId() — these were inconsistent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* icons

* fix(icons): use useId for AWS-style icon gradients

Why: IAMIcon, IdentityCenterIcon, STSIcon, SESIcon, and SecretsManagerIcon
all used hardcoded `id='xxxGradient'` values that collide when an icon
renders more than once on a page (e.g. integrations listing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sap_s4hana): ignore tokenUrl on cloud_public to prevent UAA redirection

Why: resolveTokenUrl previously honored caller-supplied tokenUrl
regardless of deploymentType, mirroring the same redirection class as
the prior baseUrl bug. A cloud_public caller could send tokenUrl to an
attacker host, causing the proxy to POST clientId:clientSecret as Basic
auth to it. superRefine for cloud_public did not validate tokenUrl.

Fix: derive UAA URL from subdomain+region for cloud_public; only honor
tokenUrl for cloud_private/on_premise (already SSRF-checked).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(icons): remove unused mask in PipedriveIcon

Why: the <mask> element had no consumer (no mask='url(#...)' anywhere
in the SVG), so both it and the maskId variable were dead code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-04-27 15:07:00 -07:00
committed by GitHub
parent 8266f0afdb
commit 2502369122
51 changed files with 8495 additions and 30 deletions

View File

@@ -4045,6 +4045,7 @@ export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
}
export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
const pathId = useId()
return (
<svg
{...props}
@@ -4058,7 +4059,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
<defs>
<path
d='M59.6807,81.1772 C59.6807,101.5343 70.0078,123.4949 92.7336,123.4949 C109.5872,123.4949 126.6277,110.3374 126.6277,80.8785 C126.6277,55.0508 113.232,37.7119 93.2944,37.7119 C77.0483,37.7119 59.6807,49.1244 59.6807,81.1772 Z M101.3006,0 C142.0482,0 169.4469,32.2728 169.4469,80.3126 C169.4469,127.5978 140.584,160.60942 99.3224,160.60942 C79.6495,160.60942 67.0483,152.1836 60.4595,146.0843 C60.5063,147.5305 60.5374,149.1497 60.5374,150.8788 L60.5374,215 L18.32565,215 L18.32565,44.157 C18.32565,41.6732 17.53126,40.8873 15.07021,40.8873 L0.5531,40.8873 L0.5531,3.4741 L35.9736,3.4741 C52.282,3.4741 56.4564,11.7741 57.2508,18.1721 C63.8708,10.7524 77.5935,0 101.3006,0 Z'
id='path-1'
id={pathId}
/>
</defs>
<g
@@ -4069,10 +4070,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
>
<g transform='translate(67.000000, 44.000000)'>
<mask id='mask-2' fill='white'>
<use href='#path-1' />
</mask>
<use id='Clip-5' fill='#FFFFFF' xlinkHref='#path-1' />
<use fill='#FFFFFF' xlinkHref={`#${pathId}`} />
</g>
</g>
</svg>
@@ -4098,6 +4096,40 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function SapS4HanaIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 412.38 204'>
<defs>
<linearGradient
id={id}
x1='206.19'
y1='0'
x2='206.19'
y2='204'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#00b1eb' />
<stop offset='.212' stopColor='#009ad9' />
<stop offset='.519' stopColor='#007fc4' />
<stop offset='.792' stopColor='#006eb8' />
<stop offset='1' stopColor='#0069b4' />
</linearGradient>
</defs>
<polyline
fill={`url(#${id})`}
fillRule='evenodd'
points='0 204 208.413 204 412.38 0 0 0 0 204'
/>
<path
fill='#fff'
fillRule='evenodd'
d='m244.727,38.359l-40.593-.025v96.518l-35.46-96.518h-35.16l-30.277,80.716c-3.224-20.352-24.277-27.38-40.84-32.649-10.937-3.512-22.541-8.678-22.434-14.387.091-4.687,6.225-9.04,18.377-8.385,8.17.433,15.373,1.092,29.71,8.006l14.102-24.557c-13.088-6.658-31.169-10.867-45.985-10.883h-.086c-17.277,0-31.677,5.598-40.602,14.824-6.221,6.443-9.572,14.626-9.712,23.679-.227,12.454,4.341,21.292,13.938,28.338,8.104,5.944,18.468,9.794,27.603,12.626,11.27,3.492,20.467,6.526,20.36,13.002-.083,2.355-.977,4.552-2.671,6.337-2.807,2.897-7.124,3.986-13.084,4.098-11.497.243-20.026-1.559-33.61-9.585l-12.536,24.903c13.546,7.705,29.586,12.223,45.952,12.223l2.106-.024c14.247-.256,25.745-4.316,34.929-11.712.527-.416,1.001-.845,1.488-1.277l-4.073,10.874h36.875l6.189-18.822c6.477,2.214,13.847,3.437,21.676,3.437,7.618,0,14.795-1.17,21.156-3.252l5.965,18.637h60.137v-38.969h13.113c31.706,0,50.456-16.147,50.456-43.202,0-30.139-18.219-43.969-57.011-43.969Zm-93.816,82.587c-4.737,0-9.177-.828-13.006-2.275l12.866-40.593h.244l12.643,40.708c-3.801,1.349-8.138,2.16-12.746,2.16Zm96.199-23.324h-8.941v-32.711h8.941c11.927,0,21.437,3.961,21.437,16.139,0,12.602-9.51,16.572-21.437,16.572'
/>
</svg>
)
}
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 71.1 63.6'>
@@ -4694,15 +4726,16 @@ export function DynamoDBIcon(props: SVGProps<SVGSVGElement>) {
}
export function IAMIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='iamGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#iamGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M14,59 L66,59 L66,21 L14,21 L14,59 Z M68,20 L68,60 C68,60.552 67.553,61 67,61 L13,61 C12.447,61 12,60.552 12,60 L12,20 C12,19.448 12.447,19 13,19 L67,19 C67.553,19 68,19.448 68,20 L68,20 Z M44,48 L59,48 L59,46 L44,46 L44,48 Z M57,42 L62,42 L62,40 L57,40 L57,42 Z M44,42 L52,42 L52,40 L44,40 L44,42 Z M29,46 C29,45.449 28.552,45 28,45 C27.448,45 27,45.449 27,46 C27,46.551 27.448,47 28,47 C28.552,47 29,46.551 29,46 L29,46 Z M31,46 C31,47.302 30.161,48.401 29,48.816 L29,51 L27,51 L27,48.815 C25.839,48.401 25,47.302 25,46 C25,44.346 26.346,43 28,43 C29.654,43 31,44.346 31,46 L31,46 Z M19,53.993 L36.994,54 L36.996,50 L33,50 L33,48 L36.996,48 L36.998,45 L33,45 L33,43 L36.999,43 L37,40.007 L19.006,40 L19,53.993 Z M22,38.001 L34,38.006 L34,31 C34.001,28.697 31.197,26.677 28,26.675 L27.996,26.675 C24.804,26.675 22.004,28.696 22.002,31 L22,38.001 Z M17,54.992 L17.006,39 C17.006,38.734 17.111,38.48 17.299,38.292 C17.486,38.105 17.741,38 18.006,38 L20,38.001 L20.002,31 C20.004,27.512 23.59,24.675 27.996,24.675 L28,24.675 C32.412,24.677 36.001,27.515 36,31 L36,38.007 L38,38.008 C38.553,38.008 39,38.456 39,39.008 L38.994,55 C38.994,55.266 38.889,55.52 38.701,55.708 C38.514,55.895 38.259,56 37.994,56 L18,55.992 C17.447,55.992 17,55.544 17,54.992 L17,54.992 Z M60,36 L62,36 L62,34 L60,34 L60,36 Z M44,36 L55,36 L55,34 L44,34 L44,36 Z'
fill='#FFFFFF'
@@ -4712,15 +4745,16 @@ export function IAMIcon(props: SVGProps<SVGSVGElement>) {
}
export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='identityCenterGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#identityCenterGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M46.694,46.8194562 C47.376,46.1374562 47.376,45.0294562 46.694,44.3474562 C46.353,44.0074562 45.906,43.8374562 45.459,43.8374562 C45.01,43.8374562 44.563,44.0074562 44.222,44.3474562 C43.542,45.0284562 43.542,46.1384562 44.222,46.8194562 C44.905,47.5014562 46.013,47.4994562 46.694,46.8194562 M47.718,47.1374562 L51.703,51.1204562 L50.996,51.8274562 L49.868,50.6994562 L48.793,51.7754562 L48.086,51.0684562 L49.161,49.9924562 L47.011,47.8444562 C46.545,48.1654562 46.003,48.3294562 45.458,48.3294562 C44.755,48.3294562 44.051,48.0624562 43.515,47.5264562 C42.445,46.4554562 42.445,44.7124562 43.515,43.6404562 C44.586,42.5714562 46.329,42.5694562 47.401,43.6404562 C48.351,44.5904562 48.455,46.0674562 47.718,47.1374562 M53,44.1014562 C53,46.1684562 51.505,47.0934562 50.023,47.0934562 L50.023,46.0934562 C50.487,46.0934562 52,45.9494562 52,44.1014562 C52,43.0044562 51.353,42.3894562 49.905,42.1084562 C49.68,42.0654562 49.514,41.8754562 49.501,41.6484562 C49.446,40.7444562 48.987,40.1124562 48.384,40.1124562 C48.084,40.1124562 47.854,40.2424562 47.616,40.5464562 C47.506,40.6884562 47.324,40.7594562 47.147,40.7324562 C46.968,40.7054562 46.818,40.5844562 46.755,40.4144562 C46.577,39.9434562 46.211,39.4334562 45.723,38.9774562 C45.231,38.5094562 43.883,37.5074562 41.972,38.2734562 C40.885,38.7054562 40.034,39.9494562 40.034,41.1074562 C40.034,41.2354562 40.043,41.3624562 40.058,41.4884562 C40.061,41.5094562 40.062,41.5304562 40.062,41.5514562 C40.062,41.7994562 39.882,42.0064562 39.645,42.0464562 C38.886,42.2394562 38,42.7454562 38,44.0554562 L38.005,44.2104562 C38.069,45.3254562 39.252,45.9954562 40.358,45.9984562 L41,45.9984562 L41,46.9984562 L40.357,46.9984562 C38.536,46.9944562 37.095,45.8194562 37.006,44.2644562 C37.003,44.1944562 37,44.1244562 37,44.0554562 C37,42.6944562 37.752,41.6484562 39.035,41.1884562 C39.034,41.1614562 39.034,41.1344562 39.034,41.1074562 C39.034,39.5434562 40.138,37.9254562 41.602,37.3434562 C43.298,36.6654562 45.095,37.0034562 46.409,38.2494562 C46.706,38.5274562 47.076,38.9264562 47.372,39.4134562 C47.673,39.2124562 48.008,39.1124562 48.384,39.1124562 C49.257,39.1124562 50.231,39.7714562 50.458,41.2074562 C52.145,41.6324562 53,42.6054562 53,44.1014562 M27,53 L27,27 L53,27 L53,34 L51,34 L51,29 L29,29 L29,51 L51,51 L51,46 L53,46 L53,53 Z'
fill='#FFFFFF'
@@ -4730,15 +4764,16 @@ export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
}
export function STSIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='stsGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#stsGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M14,59 L66,59 L66,21 L14,21 L14,59 Z M68,20 L68,60 C68,60.552 67.553,61 67,61 L13,61 C12.447,61 12,60.552 12,60 L12,20 C12,19.448 12.447,19 13,19 L67,19 C67.553,19 68,19.448 68,20 L68,20 Z M44,48 L59,48 L59,46 L44,46 L44,48 Z M57,42 L62,42 L62,40 L57,40 L57,42 Z M44,42 L52,42 L52,40 L44,40 L44,42 Z M29,46 C29,45.449 28.552,45 28,45 C27.448,45 27,45.449 27,46 C27,46.551 27.448,47 28,47 C28.552,47 29,46.551 29,46 L29,46 Z M31,46 C31,47.302 30.161,48.401 29,48.816 L29,51 L27,51 L27,48.815 C25.839,48.401 25,47.302 25,46 C25,44.346 26.346,43 28,43 C29.654,43 31,44.346 31,46 L31,46 Z M19,53.993 L36.994,54 L36.996,50 L33,50 L33,48 L36.996,48 L36.998,45 L33,45 L33,43 L36.999,43 L37,40.007 L19.006,40 L19,53.993 Z M22,38.001 L34,38.006 L34,31 C34.001,28.697 31.197,26.677 28,26.675 L27.996,26.675 C24.804,26.675 22.004,28.696 22.002,31 L22,38.001 Z M17,54.992 L17.006,39 C17.006,38.734 17.111,38.48 17.299,38.292 C17.486,38.105 17.741,38 18.006,38 L20,38.001 L20.002,31 C20.004,27.512 23.59,24.675 27.996,24.675 L28,24.675 C32.412,24.677 36.001,27.515 36,31 L36,38.007 L38,38.008 C38.553,38.008 39,38.456 39,39.008 L38.994,55 C38.994,55.266 38.889,55.52 38.701,55.708 C38.514,55.895 38.259,56 37.994,56 L18,55.992 C17.447,55.992 17,55.544 17,54.992 L17,54.992 Z M60,36 L62,36 L62,34 L60,34 L60,36 Z M44,36 L55,36 L55,34 L44,34 L44,36 Z'
fill='#FFFFFF'
@@ -4748,15 +4783,16 @@ export function STSIcon(props: SVGProps<SVGSVGElement>) {
}
export function SESIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='sesGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#sesGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M57,60.999875 C57,59.373846 55.626,57.9998214 54,57.9998214 C52.374,57.9998214 51,59.373846 51,60.999875 C51,62.625904 52.374,63.9999286 54,63.9999286 C55.626,63.9999286 57,62.625904 57,60.999875 L57,60.999875 Z M40,59.9998571 C38.374,59.9998571 37,61.3738817 37,62.9999107 C37,64.6259397 38.374,65.9999643 40,65.9999643 C41.626,65.9999643 43,64.6259397 43,62.9999107 C43,61.3738817 41.626,59.9998571 40,59.9998571 L40,59.9998571 Z M26,57.9998214 C24.374,57.9998214 23,59.373846 23,60.999875 C23,62.625904 24.374,63.9999286 26,63.9999286 C27.626,63.9999286 29,62.625904 29,60.999875 C29,59.373846 27.626,57.9998214 26,57.9998214 L26,57.9998214 Z M28.605,42.9995536 L51.395,42.9995536 L43.739,36.1104305 L40.649,38.7584778 C40.463,38.9194807 40.23,38.9994821 39.999,38.9994821 C39.768,38.9994821 39.535,38.9194807 39.349,38.7584778 L36.26,36.1104305 L28.605,42.9995536 Z M27,28.1732888 L27,41.7545313 L34.729,34.7984071 L27,28.1732888 Z M51.297,26.9992678 L28.703,26.9992678 L39.999,36.6824408 L51.297,26.9992678 Z M53,41.7545313 L53,28.1732888 L45.271,34.7974071 L53,41.7545313 Z M59,60.999875 C59,63.7099234 56.71,65.9999643 54,65.9999643 C51.29,65.9999643 49,63.7099234 49,60.999875 C49,58.6308327 50.75,56.5837961 53,56.1057876 L53,52.9997321 L41,52.9997321 L41,58.1058233 C43.25,58.5838319 45,60.6308684 45,62.9999107 C45,65.7099591 42.71,68 40,68 C37.29,68 35,65.7099591 35,62.9999107 C35,60.6308684 36.75,58.5838319 39,58.1058233 L39,52.9997321 L27,52.9997321 L27,56.1057876 C29.25,56.5837961 31,58.6308327 31,60.999875 C31,63.7099234 28.71,65.9999643 26,65.9999643 C23.29,65.9999643 21,63.7099234 21,60.999875 C21,58.6308327 22.75,56.5837961 25,56.1057876 L25,51.9997143 C25,51.4477044 25.447,50.9996964 26,50.9996964 L39,50.9996964 L39,44.9995893 L26,44.9995893 C25.447,44.9995893 25,44.5515813 25,43.9995714 L25,25.99925 C25,25.4472401 25.447,24.9992321 26,24.9992321 L54,24.9992321 C54.553,24.9992321 55,25.4472401 55,25.99925 L55,43.9995714 C55,44.5515813 54.553,44.9995893 54,44.9995893 L41,44.9995893 L41,50.9996964 L54,50.9996964 C54.553,50.9996964 55,51.4477044 55,51.9997143 L55,56.1057876 C57.25,56.5837961 59,58.6308327 59,60.999875 L59,60.999875 Z M68,39.9995 C68,45.9066055 66.177,51.5597064 62.727,56.3447919 L61.104,55.174771 C64.307,50.7316916 66,45.4845979 66,39.9995 C66,25.664244 54.337,14.0000357 40.001,14.0000357 C25.664,14.0000357 14,25.664244 14,39.9995 C14,45.4845979 15.693,50.7316916 18.896,55.174771 L17.273,56.3447919 C13.823,51.5597064 12,45.9066055 12,39.9995 C12,24.5612243 24.561,12 39.999,12 C55.438,12 68,24.5612243 68,39.9995 L68,39.9995 Z'
fill='#FFFFFF'
@@ -4766,15 +4802,16 @@ export function SESIcon(props: SVGProps<SVGSVGElement>) {
}
export function SecretsManagerIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='secretsManagerGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#secretsManagerGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M38.76,43.36 C38.76,44.044 39.317,44.6 40,44.6 C40.684,44.6 41.24,44.044 41.24,43.36 C41.24,42.676 40.684,42.12 40,42.12 C39.317,42.12 38.76,42.676 38.76,43.36 L38.76,43.36 Z M36.76,43.36 C36.76,41.573 38.213,40.12 40,40.12 C41.787,40.12 43.24,41.573 43.24,43.36 C43.24,44.796 42.296,46.002 41,46.426 L41,49 L39,49 L39,46.426 C37.704,46.002 36.76,44.796 36.76,43.36 L36.76,43.36 Z M49,38 L31,38 L31,51 L49,51 L49,48 L46,48 L46,46 L49,46 L49,43 L46,43 L46,41 L49,41 L49,38 Z M34,36 L45.999,36 L46,31 C46.001,28.384 43.143,26.002 40.004,26 L40.001,26 C38.472,26 36.928,26.574 35.763,27.575 C34.643,28.537 34,29.786 34,31.001 L34,36 Z M48,31.001 L47.999,36 L50,36 C50.553,36 51,36.448 51,37 L51,52 C51,52.552 50.553,53 50,53 L30,53 C29.447,53 29,52.552 29,52 L29,37 C29,36.448 29.447,36 30,36 L32,36 L32,31 C32.001,29.202 32.897,27.401 34.459,26.058 C35.982,24.75 38.001,24 40.001,24 L40.004,24 C44.265,24.002 48.001,27.273 48,31.001 L48,31.001 Z M19.207,55.049 L20.828,53.877 C18.093,50.097 16.581,45.662 16.396,41 L19,41 L19,39 L16.399,39 C16.598,34.366 18.108,29.957 20.828,26.198 L19.207,25.025 C16.239,29.128 14.599,33.942 14.399,39 L12,39 L12,41 L14.396,41 C14.582,46.086 16.224,50.926 19.207,55.049 L19.207,55.049 Z M53.838,59.208 C50.069,61.936 45.648,63.446 41,63.639 L41,61 L39,61 L39,63.639 C34.352,63.447 29.93,61.937 26.159,59.208 L24.988,60.828 C29.1,63.805 33.928,65.445 39,65.639 L39,68 L41,68 L41,65.639 C46.072,65.445 50.898,63.805 55.01,60.828 L53.838,59.208 Z M26.159,20.866 C29.93,18.138 34.352,16.628 39,16.436 L39,19 L41,19 L41,16.436 C45.648,16.628 50.069,18.138 53.838,20.866 L55.01,19.246 C50.898,16.27 46.072,14.63 41,14.436 L41,12 L39,12 L39,14.436 C33.928,14.629 29.1,16.269 24.988,19.246 L26.159,20.866 Z M65.599,39 C65.399,33.942 63.759,29.128 60.79,25.025 L59.169,26.198 C61.89,29.957 63.4,34.366 63.599,39 L61,39 L61,41 L63.602,41 C63.416,45.662 61.905,50.097 59.169,53.877 L60.79,55.049 C63.774,50.926 65.415,46.086 65.602,41 L68,41 L68,39 L65.599,39 Z M56.386,25.064 L64.226,17.224 L62.812,15.81 L54.972,23.65 L56.386,25.064 Z M23.612,55.01 L15.772,62.85 L17.186,64.264 L25.026,56.424 L23.612,55.01 Z M28.666,27.253 L13.825,12.413 L12.411,13.827 L27.252,28.667 L28.666,27.253 Z M54.193,52.78 L67.586,66.173 L66.172,67.587 L52.779,54.194 L54.193,52.78 Z'
fill='#FFFFFF'

View File

@@ -154,6 +154,7 @@ import {
RootlyIcon,
S3Icon,
SalesforceIcon,
SapS4HanaIcon,
SESIcon,
SearchIcon,
SecretsManagerIcon,
@@ -369,6 +370,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
rootly: RootlyIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
sap_s4hana: SapS4HanaIcon,
search: SearchIcon,
secrets_manager: SecretsManagerIcon,
sendgrid: SendgridIcon,

View File

@@ -150,6 +150,7 @@
"rootly",
"s3",
"salesforce",
"sap_s4hana",
"search",
"secrets_manager",
"sendgrid",

File diff suppressed because it is too large Load Diff

View File

@@ -154,6 +154,7 @@ import {
RootlyIcon,
S3Icon,
SalesforceIcon,
SapS4HanaIcon,
SESIcon,
SearchIcon,
SecretsManagerIcon,
@@ -351,6 +352,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
rootly: RootlyIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
sap_s4hana: SapS4HanaIcon,
search: SearchIcon,
secrets_manager: SecretsManagerIcon,
sendgrid: SendgridIcon,

View File

@@ -11379,6 +11379,173 @@
"integrationTypes": ["crm", "customer-support", "sales"],
"tags": ["sales-engagement", "customer-support"]
},
{
"type": "sap_s4hana",
"slug": "sap-s-4hana",
"name": "SAP S/4HANA",
"description": "Read and write SAP S/4HANA Cloud business data via OData",
"longDescription": "Connect SAP S/4HANA Cloud Public Edition with per-tenant OAuth 2.0 client credentials configured in your Communication Arrangements. Read and create business partners, customers, suppliers, sales orders, deliveries (inbound/outbound), billing documents, products, stock and material documents, purchase requisitions, purchase orders, and supplier invoices, or run arbitrary OData v2 queries against any whitelisted Communication Scenario.",
"bgColor": "#0A6ED1",
"iconName": "SapS4HanaIcon",
"docsUrl": "https://docs.sim.ai/tools/sap_s4hana",
"operations": [
{
"name": "List Business Partners",
"description": "List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Business Partner",
"description": "Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner)."
},
{
"name": "Create Business Partner",
"description": "Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner). For Person category 1 provide FirstName and LastName. For Organization category 2 provide OrganizationBPName1."
},
{
"name": "Update Business Partner",
"description": "Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Customers",
"description": "List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Customer",
"description": "Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer)."
},
{
"name": "Update Customer",
"description": "Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Customer PATCH is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlock, BillingIsBlockedForCustomer, PostingIsBlocked, and DeletionIndicator. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Suppliers",
"description": "List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Supplier",
"description": "Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier)."
},
{
"name": "Update Supplier",
"description": "Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Supplier PATCH is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Sales Orders",
"description": "List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Sales Order",
"description": "Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder)."
},
{
"name": "Create Sales Order",
"description": "Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) with deep insert of sales order items via to_Item."
},
{
"name": "Update Sales Order",
"description": "Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "Delete Sales Order",
"description": "Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only orders without subsequent documents (deliveries, invoices) can be deleted; otherwise reject items via update instead."
},
{
"name": "List Outbound Deliveries",
"description": "List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=0002, A_OutbDeliveryHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Outbound Delivery",
"description": "Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=0002, A_OutbDeliveryHeader)."
},
{
"name": "List Inbound Deliveries",
"description": "List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, A_InbDeliveryHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Inbound Delivery",
"description": "Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, A_InbDeliveryHeader)."
},
{
"name": "List Billing Documents",
"description": "List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_DOCUMENT_SRV, A_BillingDocument) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Billing Document",
"description": "Retrieve a single billing document (customer invoice) by BillingDocument key from SAP S/4HANA Cloud (API_BILLING_DOCUMENT_SRV, A_BillingDocument)."
},
{
"name": "List Products",
"description": "List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Product",
"description": "Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product)."
},
{
"name": "Update Product",
"description": "Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PATCH only sends the fields you provide; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV PATCH/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET."
},
{
"name": "List Material Stock",
"description": "List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A_MatlStkInAcctMod). The entity uses an 11-field composite key (Material, Plant, StorageLocation, Batch, Supplier, Customer, WBSElementInternalID, SDDocument, SDDocumentItem, InventorySpecialStockType, InventoryStockType) — query with $filter on these fields instead of a direct key lookup."
},
{
"name": "List Material Documents",
"description": "List material document headers (goods movements) from SAP S/4HANA Cloud (API_MATERIAL_DOCUMENT_SRV, A_MaterialDocumentHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "List Purchase Requisitions",
"description": "List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand. Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled."
},
{
"name": "Get Purchase Requisition",
"description": "Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader). Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled."
},
{
"name": "Create Purchase Requisition",
"description": "Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader). PurchaseRequisition is auto-assigned by SAP from the document number range; provide line items via the to_PurchaseReqnItem deep-insert array. Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled."
},
{
"name": "Update Purchase Requisition",
"description": "Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Purchase Orders",
"description": "List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Purchase Order",
"description": "Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder)."
},
{
"name": "Create Purchase Order",
"description": "Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder). PurchaseOrder is auto-assigned by SAP from the document number range; provide line items via the body parameter."
},
{
"name": "Update Purchase Order",
"description": "Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates."
},
{
"name": "List Supplier Invoices",
"description": "List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, A_SupplierInvoice) with optional OData $filter, $top, $skip, $orderby, $select, $expand."
},
{
"name": "Get Supplier Invoice",
"description": "Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYear) from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, A_SupplierInvoice)."
},
{
"name": "OData Query (advanced)",
"description": "Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping."
}
],
"operationCount": 37,
"triggers": [],
"triggerCount": 0,
"authType": "none",
"category": "tools",
"integrationTypes": ["other", "developer-tools"],
"tags": ["automation"]
},
{
"type": "search",
"slug": "search",

View File

@@ -0,0 +1,614 @@
import { createHash } from 'node:crypto'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('SapS4HanaProxyAPI')
const HttpMethod = z.enum(['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'MERGE'])
const DeploymentType = z.enum(['cloud_public', 'cloud_private', 'on_premise'])
const AuthType = z.enum(['oauth_client_credentials', 'basic'])
const ServiceName = z
.string()
.min(1, 'service is required')
.regex(
/^[A-Z][A-Z0-9_]*(;v=\d+)?$/,
'service must be an uppercase OData service name optionally suffixed with ";v=NNNN" (e.g., API_BUSINESS_PARTNER, API_OUTBOUND_DELIVERY_SRV;v=0002)'
)
const ServicePath = z
.string()
.min(1, 'path is required')
.refine(
(p) =>
!p.split(/[/\\]/).some((seg) => seg === '..' || seg === '.') &&
!p.includes('?') &&
!p.includes('#') &&
!/%(?:2[eEfF]|5[cC]|3[fF]|23)/.test(p),
{
message:
'path must not contain ".." or "." segments, "?", "#", or percent-encoded path/query/fragment characters',
}
)
const Subdomain = z
.string()
.regex(
/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i,
'subdomain must contain only letters, digits, and hyphens (1-63 chars)'
)
const ProxyRequestSchema = z
.object({
deploymentType: DeploymentType.default('cloud_public'),
authType: AuthType.default('oauth_client_credentials'),
subdomain: Subdomain.optional(),
region: z
.string()
.regex(/^[a-z]{2,4}\d{1,3}$/i, 'region must be an SAP BTP region code (e.g., eu10, us30)')
.optional(),
baseUrl: z.string().optional(),
tokenUrl: z.string().optional(),
clientId: z.string().optional(),
clientSecret: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
service: ServiceName,
path: ServicePath,
method: HttpMethod.default('GET'),
query: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
body: z.unknown().optional(),
ifMatch: z.string().optional(),
})
.superRefine((req, ctx) => {
if (req.deploymentType === 'cloud_public') {
if (!req.subdomain) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['subdomain'],
message: 'subdomain is required for cloud_public deployment',
})
}
if (!req.region) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['region'],
message: 'region is required for cloud_public deployment',
})
}
if (req.authType !== 'oauth_client_credentials') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['authType'],
message: 'cloud_public deployment only supports oauth_client_credentials',
})
}
if (!req.clientId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['clientId'],
message: 'clientId is required',
})
}
if (!req.clientSecret) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['clientSecret'],
message: 'clientSecret is required',
})
}
} else {
if (!req.baseUrl) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['baseUrl'],
message: 'baseUrl is required for cloud_private and on_premise deployments',
})
} else {
const baseUrlCheck = checkExternalUrlSafety(req.baseUrl, 'baseUrl')
if (!baseUrlCheck.ok) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['baseUrl'],
message: baseUrlCheck.message,
})
}
}
if (req.authType === 'oauth_client_credentials') {
if (!req.tokenUrl) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['tokenUrl'],
message: 'tokenUrl is required for OAuth on cloud_private/on_premise',
})
} else {
const tokenUrlCheck = checkExternalUrlSafety(req.tokenUrl, 'tokenUrl')
if (!tokenUrlCheck.ok) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['tokenUrl'],
message: tokenUrlCheck.message,
})
}
}
if (!req.clientId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['clientId'],
message: 'clientId is required for OAuth',
})
}
if (!req.clientSecret) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['clientSecret'],
message: 'clientSecret is required for OAuth',
})
}
} else {
if (!req.username) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['username'],
message: 'username is required for Basic auth',
})
}
if (!req.password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['password'],
message: 'password is required for Basic auth',
})
}
}
}
})
type ProxyRequest = z.infer<typeof ProxyRequestSchema>
interface CachedToken {
accessToken: string
expiresAt: number
}
const TOKEN_CACHE = new Map<string, CachedToken>()
const TOKEN_CACHE_MAX_ENTRIES = 500
const TOKEN_SAFETY_WINDOW_MS = 60_000
const OUTBOUND_FETCH_TIMEOUT_MS = 30_000
const FORBIDDEN_HOSTS = new Set([
'localhost',
'0.0.0.0',
'127.0.0.1',
'169.254.169.254',
'metadata.google.internal',
'metadata',
'[::1]',
'[::]',
'[::ffff:127.0.0.1]',
'[fd00:ec2::254]',
])
function isPrivateIPv4(host: string): boolean {
const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
if (!match) return false
const octets = match.slice(1, 5).map(Number) as [number, number, number, number]
if (octets.some((o) => o < 0 || o > 255)) return false
const [a, b] = octets
if (a === 10) return true
if (a === 172 && b >= 16 && b <= 31) return true
if (a === 192 && b === 168) return true
if (a === 127) return true
if (a === 169 && b === 254) return true
if (a === 0) return true
return false
}
function extractIPv4MappedHost(host: string): string | null {
const stripped = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host
const lower = stripped.toLowerCase()
for (const prefix of ['::ffff:', '::']) {
if (lower.startsWith(prefix)) {
const candidate = lower.slice(prefix.length)
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(candidate)) return candidate
}
}
const hexMatch = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
if (hexMatch) {
const high = Number.parseInt(hexMatch[1] as string, 16)
const low = Number.parseInt(hexMatch[2] as string, 16)
if (high >= 0 && high <= 0xffff && low >= 0 && low <= 0xffff) {
const a = (high >> 8) & 0xff
const b = high & 0xff
const c = (low >> 8) & 0xff
const d = low & 0xff
return `${a}.${b}.${c}.${d}`
}
}
return null
}
function isPrivateOrLoopbackIPv6(host: string): boolean {
const stripped = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host
const lower = stripped.toLowerCase()
if (lower === '::' || lower === '::1') return true
if (/^fc[0-9a-f]{2}:/.test(lower) || /^fd[0-9a-f]{2}:/.test(lower)) return true
if (lower.startsWith('fe80:')) return true
return false
}
function checkExternalUrlSafety(
rawUrl: string,
label: string
): { ok: true; url: URL } | { ok: false; message: string } {
let parsed: URL
try {
parsed = new URL(rawUrl)
} catch {
return { ok: false, message: `${label} must be a valid URL` }
}
if (parsed.protocol !== 'https:') {
return { ok: false, message: `${label} must use https://` }
}
const host = parsed.hostname.toLowerCase()
if (FORBIDDEN_HOSTS.has(host) || FORBIDDEN_HOSTS.has(`[${host}]`)) {
return { ok: false, message: `${label} host is not allowed` }
}
if (isPrivateIPv4(host)) {
return { ok: false, message: `${label} host is not allowed (private/loopback range)` }
}
const mapped = extractIPv4MappedHost(host)
if (mapped && isPrivateIPv4(mapped)) {
return { ok: false, message: `${label} host is not allowed (IPv4-mapped private range)` }
}
if (isPrivateOrLoopbackIPv6(host)) {
return { ok: false, message: `${label} host is not allowed (IPv6 private/loopback)` }
}
return { ok: true, url: parsed }
}
function assertSafeExternalUrl(rawUrl: string, label: string): URL {
const result = checkExternalUrlSafety(rawUrl, label)
if (!result.ok) throw new Error(result.message)
return result.url
}
function resolveTokenUrl(req: ProxyRequest): string {
if (req.deploymentType === 'cloud_public') {
return `https://${req.subdomain}.authentication.${req.region}.hana.ondemand.com/oauth/token`
}
if (!req.tokenUrl) {
throw new Error('tokenUrl is required for OAuth on cloud_private/on_premise')
}
return req.tokenUrl
}
function tokenCacheKey(req: ProxyRequest): string {
const secretHash = req.clientSecret
? createHash('sha256').update(req.clientSecret).digest('hex').slice(0, 16)
: ''
return `${resolveTokenUrl(req)}::${req.clientId ?? ''}::${secretHash}`
}
function rememberToken(key: string, token: CachedToken): void {
if (TOKEN_CACHE.has(key)) TOKEN_CACHE.delete(key)
TOKEN_CACHE.set(key, token)
while (TOKEN_CACHE.size > TOKEN_CACHE_MAX_ENTRIES) {
const oldestKey = TOKEN_CACHE.keys().next().value
if (oldestKey === undefined) break
TOKEN_CACHE.delete(oldestKey)
}
}
async function fetchAccessToken(req: ProxyRequest, requestId: string): Promise<string> {
const cacheKey = tokenCacheKey(req)
const cached = TOKEN_CACHE.get(cacheKey)
if (cached && cached.expiresAt - TOKEN_SAFETY_WINDOW_MS > Date.now()) {
return cached.accessToken
}
const tokenUrl = assertSafeExternalUrl(resolveTokenUrl(req), 'tokenUrl').toString()
const basic = Buffer.from(`${req.clientId}:${req.clientSecret}`).toString('base64')
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: 'grant_type=client_credentials',
signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS),
})
if (!response.ok) {
const text = await response.text().catch(() => '')
logger.warn(`[${requestId}] Token fetch failed (${response.status}): ${text}`)
throw new Error(`SAP token request failed: HTTP ${response.status}`)
}
const data = (await response.json()) as {
access_token?: string
expires_in?: number
}
if (!data.access_token) {
throw new Error('SAP token response missing access_token')
}
const expiresInMs = (data.expires_in ?? 3600) * 1000
rememberToken(cacheKey, {
accessToken: data.access_token,
expiresAt: Date.now() + expiresInMs,
})
return data.access_token
}
interface CsrfBundle {
token: string
cookie: string
}
function joinSetCookies(headers: Headers): string {
const cookies =
typeof (headers as { getSetCookie?: () => string[] }).getSetCookie === 'function'
? (headers as { getSetCookie: () => string[] }).getSetCookie()
: (headers.get('set-cookie') ?? '').split(/,\s*(?=[^=,;\s]+=)/)
return cookies
.map((c) => c.split(';')[0]?.trim())
.filter(Boolean)
.join('; ')
}
function buildAuthHeader(req: ProxyRequest, accessToken: string | null): string {
if (req.authType === 'basic') {
const basic = Buffer.from(`${req.username}:${req.password}`).toString('base64')
return `Basic ${basic}`
}
return `Bearer ${accessToken}`
}
async function fetchCsrf(
req: ProxyRequest,
accessToken: string | null,
requestId: string
): Promise<CsrfBundle | null> {
const url = buildOdataUrl(req, '/$metadata')
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: buildAuthHeader(req, accessToken),
Accept: 'application/xml',
'X-CSRF-Token': 'Fetch',
},
signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS),
})
if (!response.ok) {
const text = await response.text().catch(() => '')
logger.warn(`[${requestId}] CSRF fetch failed (${response.status}): ${text}`)
return null
}
const token = response.headers.get('x-csrf-token')
const cookie = joinSetCookies(response.headers)
if (!token) return null
return { token, cookie }
}
function resolveHost(req: ProxyRequest): string {
if (req.deploymentType === 'cloud_public') {
const constructed = `https://${req.subdomain}-api.s4hana.ondemand.com`
return assertSafeExternalUrl(constructed, 'subdomain').toString().replace(/\/+$/, '')
}
if (!req.baseUrl) {
throw new Error('baseUrl is required for cloud_private and on_premise deployments')
}
const trimmed = req.baseUrl.replace(/\/+$/, '')
return assertSafeExternalUrl(trimmed, 'baseUrl').toString().replace(/\/+$/, '')
}
function buildOdataUrl(req: ProxyRequest, pathOverride?: string): string {
const host = resolveHost(req)
const servicePath = `/sap/opu/odata/sap/${req.service}`
const subPath = pathOverride ?? req.path
const normalized = subPath.startsWith('/') ? subPath : `/${subPath}`
const base = `${host}${servicePath}${normalized}`
if (pathOverride !== undefined) {
return base
}
if (!req.query || Object.keys(req.query).length === 0) {
return base
}
const encode = (s: string) => encodeURIComponent(s).replace(/%24/g, '$')
const parts: string[] = []
for (const [key, value] of Object.entries(req.query)) {
if (value === undefined || value === null) continue
parts.push(`${encode(key)}=${encode(String(value))}`)
}
const queryString = parts.join('&')
if (!queryString) return base
return base.includes('?') ? `${base}&${queryString}` : `${base}?${queryString}`
}
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE', 'MERGE'])
interface OdataInvocation {
status: number
body: unknown
raw: string
csrfHeader: string
}
async function callOdata(
req: ProxyRequest,
accessToken: string | null,
csrf: CsrfBundle | null
): Promise<OdataInvocation> {
const url = buildOdataUrl(req)
const headers: Record<string, string> = {
Authorization: buildAuthHeader(req, accessToken),
Accept: 'application/json',
}
const isWrite = WRITE_METHODS.has(req.method)
const hasBody = req.body !== undefined && req.body !== null
if (hasBody) headers['Content-Type'] = 'application/json'
if (req.ifMatch) headers['If-Match'] = req.ifMatch
if (isWrite && csrf) {
headers['X-CSRF-Token'] = csrf.token
if (csrf.cookie) headers.Cookie = csrf.cookie
}
const response = await fetch(url, {
method: req.method,
headers,
body: hasBody ? JSON.stringify(req.body) : undefined,
signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS),
})
const raw = await response.text()
let parsed: unknown = null
if (raw.length > 0) {
try {
parsed = JSON.parse(raw)
} catch {
parsed = raw
}
}
const csrfHeader = response.headers.get('x-csrf-token')?.toLowerCase() ?? ''
return { status: response.status, body: parsed, raw, csrfHeader }
}
function isCsrfRequired(invocation: OdataInvocation): boolean {
if (invocation.status !== 403) return false
if (invocation.csrfHeader === 'required') return true
if (typeof invocation.body !== 'object' || invocation.body === null) return false
const errorObj = (invocation.body as { error?: { message?: { value?: string } | string } }).error
const messageField = errorObj?.message
const message = typeof messageField === 'string' ? messageField : (messageField?.value ?? '')
return message.toLowerCase().includes('csrf')
}
function extractOdataError(body: unknown, status: number): string {
if (body && typeof body === 'object') {
const err = (
body as {
error?: {
message?: { value?: string } | string
code?: string
innererror?: {
errordetails?: Array<{ code?: string; message?: string; severity?: string }>
}
}
}
).error
if (err) {
const messageField = err.message
const base =
typeof messageField === 'string' ? messageField : (messageField?.value ?? err.code ?? '')
const prefix = err.code ? `[${err.code}] ` : ''
const details = err.innererror?.errordetails
?.filter((d) => d.message && (!d.severity || d.severity.toLowerCase() !== 'info'))
.map((d) => {
const tag = d.code ? `[${d.code}] ` : ''
return `${tag}${d.message}`
})
.filter((m): m is string => Boolean(m))
if (details && details.length > 0) {
const extras = details.filter((d) => !d.endsWith(base))
return extras.length > 0 ? `${prefix}${base} (${extras.join('; ')})` : `${prefix}${base}`
}
if (base) return `${prefix}${base}`
}
}
if (typeof body === 'string' && body.length > 0) return body
return `SAP request failed with HTTP ${status}`
}
function unwrapOdata(body: unknown): unknown {
if (!body || typeof body !== 'object') return body
const root = (body as { d?: unknown }).d
if (root === undefined) return body
if (root && typeof root === 'object' && 'results' in (root as Record<string, unknown>)) {
const rootObj = root as { results: unknown; __count?: string; __next?: string }
if (rootObj.__count !== undefined || rootObj.__next !== undefined) {
return {
results: rootObj.results,
...(rootObj.__count !== undefined && { __count: rootObj.__count }),
...(rootObj.__next !== undefined && { __next: rootObj.__next }),
}
}
return rootObj.results
}
return root
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SAP proxy request: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const json = await request.json()
const proxyReq = ProxyRequestSchema.parse(json)
const isWrite = WRITE_METHODS.has(proxyReq.method)
const accessToken =
proxyReq.authType === 'oauth_client_credentials'
? await fetchAccessToken(proxyReq, requestId)
: null
const csrf = isWrite ? await fetchCsrf(proxyReq, accessToken, requestId) : null
let invocation = await callOdata(proxyReq, accessToken, csrf)
if (isWrite && isCsrfRequired(invocation)) {
logger.info(`[${requestId}] CSRF token rejected, refetching and retrying`)
const refreshed = await fetchCsrf(proxyReq, accessToken, requestId)
if (refreshed) {
invocation = await callOdata(proxyReq, accessToken, refreshed)
}
}
if (invocation.status >= 200 && invocation.status < 300) {
const data = invocation.status === 204 ? null : unwrapOdata(invocation.body)
return NextResponse.json({ success: true, output: { status: invocation.status, data } })
}
const message = extractOdataError(invocation.body, invocation.status)
logger.warn(
`[${requestId}] SAP API error (${invocation.status}) ${proxyReq.service}${proxyReq.path}: ${message}`
)
return NextResponse.json(
{ success: false, error: message, status: invocation.status },
{ status: invocation.status }
)
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Validation error:`, error.errors)
return NextResponse.json(
{ success: false, error: error.errors[0]?.message || 'Validation failed' },
{ status: 400 }
)
}
logger.error(`[${requestId}] Unexpected SAP proxy error:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -169,6 +169,7 @@ import { RouterBlock, RouterV2Block } from '@/blocks/blocks/router'
import { RssBlock } from '@/blocks/blocks/rss'
import { S3Block } from '@/blocks/blocks/s3'
import { SalesforceBlock } from '@/blocks/blocks/salesforce'
import { SapS4HanaBlock } from '@/blocks/blocks/sap_s4hana'
import { ScheduleBlock } from '@/blocks/blocks/schedule'
import { SearchBlock } from '@/blocks/blocks/search'
import { SecretsManagerBlock } from '@/blocks/blocks/secrets_manager'
@@ -419,6 +420,7 @@ export const registry: Record<string, BlockConfig> = {
rss: RssBlock,
s3: S3Block,
salesforce: SalesforceBlock,
sap_s4hana: SapS4HanaBlock,
schedule: ScheduleBlock,
search: SearchBlock,
sendgrid: SendGridBlock,

View File

@@ -4045,6 +4045,7 @@ export function AsanaIcon(props: SVGProps<SVGSVGElement>) {
}
export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
const pathId = useId()
return (
<svg
{...props}
@@ -4058,7 +4059,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
<defs>
<path
d='M59.6807,81.1772 C59.6807,101.5343 70.0078,123.4949 92.7336,123.4949 C109.5872,123.4949 126.6277,110.3374 126.6277,80.8785 C126.6277,55.0508 113.232,37.7119 93.2944,37.7119 C77.0483,37.7119 59.6807,49.1244 59.6807,81.1772 Z M101.3006,0 C142.0482,0 169.4469,32.2728 169.4469,80.3126 C169.4469,127.5978 140.584,160.60942 99.3224,160.60942 C79.6495,160.60942 67.0483,152.1836 60.4595,146.0843 C60.5063,147.5305 60.5374,149.1497 60.5374,150.8788 L60.5374,215 L18.32565,215 L18.32565,44.157 C18.32565,41.6732 17.53126,40.8873 15.07021,40.8873 L0.5531,40.8873 L0.5531,3.4741 L35.9736,3.4741 C52.282,3.4741 56.4564,11.7741 57.2508,18.1721 C63.8708,10.7524 77.5935,0 101.3006,0 Z'
id='path-1'
id={pathId}
/>
</defs>
<g
@@ -4069,10 +4070,7 @@ export function PipedriveIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
>
<g transform='translate(67.000000, 44.000000)'>
<mask id='mask-2' fill='white'>
<use href='#path-1' />
</mask>
<use id='Clip-5' fill='#FFFFFF' xlinkHref='#path-1' />
<use fill='#FFFFFF' xlinkHref={`#${pathId}`} />
</g>
</g>
</svg>
@@ -4098,6 +4096,40 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function SapS4HanaIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 412.38 204'>
<defs>
<linearGradient
id={id}
x1='206.19'
y1='0'
x2='206.19'
y2='204'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#00b1eb' />
<stop offset='.212' stopColor='#009ad9' />
<stop offset='.519' stopColor='#007fc4' />
<stop offset='.792' stopColor='#006eb8' />
<stop offset='1' stopColor='#0069b4' />
</linearGradient>
</defs>
<polyline
fill={`url(#${id})`}
fillRule='evenodd'
points='0 204 208.413 204 412.38 0 0 0 0 204'
/>
<path
fill='#fff'
fillRule='evenodd'
d='m244.727,38.359l-40.593-.025v96.518l-35.46-96.518h-35.16l-30.277,80.716c-3.224-20.352-24.277-27.38-40.84-32.649-10.937-3.512-22.541-8.678-22.434-14.387.091-4.687,6.225-9.04,18.377-8.385,8.17.433,15.373,1.092,29.71,8.006l14.102-24.557c-13.088-6.658-31.169-10.867-45.985-10.883h-.086c-17.277,0-31.677,5.598-40.602,14.824-6.221,6.443-9.572,14.626-9.712,23.679-.227,12.454,4.341,21.292,13.938,28.338,8.104,5.944,18.468,9.794,27.603,12.626,11.27,3.492,20.467,6.526,20.36,13.002-.083,2.355-.977,4.552-2.671,6.337-2.807,2.897-7.124,3.986-13.084,4.098-11.497.243-20.026-1.559-33.61-9.585l-12.536,24.903c13.546,7.705,29.586,12.223,45.952,12.223l2.106-.024c14.247-.256,25.745-4.316,34.929-11.712.527-.416,1.001-.845,1.488-1.277l-4.073,10.874h36.875l6.189-18.822c6.477,2.214,13.847,3.437,21.676,3.437,7.618,0,14.795-1.17,21.156-3.252l5.965,18.637h60.137v-38.969h13.113c31.706,0,50.456-16.147,50.456-43.202,0-30.139-18.219-43.969-57.011-43.969Zm-93.816,82.587c-4.737,0-9.177-.828-13.006-2.275l12.866-40.593h.244l12.643,40.708c-3.801,1.349-8.138,2.16-12.746,2.16Zm96.199-23.324h-8.941v-32.711h8.941c11.927,0,21.437,3.961,21.437,16.139,0,12.602-9.51,16.572-21.437,16.572'
/>
</svg>
)
}
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 71.1 63.6'>
@@ -4694,15 +4726,16 @@ export function DynamoDBIcon(props: SVGProps<SVGSVGElement>) {
}
export function IAMIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='iamGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#iamGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M14,59 L66,59 L66,21 L14,21 L14,59 Z M68,20 L68,60 C68,60.552 67.553,61 67,61 L13,61 C12.447,61 12,60.552 12,60 L12,20 C12,19.448 12.447,19 13,19 L67,19 C67.553,19 68,19.448 68,20 L68,20 Z M44,48 L59,48 L59,46 L44,46 L44,48 Z M57,42 L62,42 L62,40 L57,40 L57,42 Z M44,42 L52,42 L52,40 L44,40 L44,42 Z M29,46 C29,45.449 28.552,45 28,45 C27.448,45 27,45.449 27,46 C27,46.551 27.448,47 28,47 C28.552,47 29,46.551 29,46 L29,46 Z M31,46 C31,47.302 30.161,48.401 29,48.816 L29,51 L27,51 L27,48.815 C25.839,48.401 25,47.302 25,46 C25,44.346 26.346,43 28,43 C29.654,43 31,44.346 31,46 L31,46 Z M19,53.993 L36.994,54 L36.996,50 L33,50 L33,48 L36.996,48 L36.998,45 L33,45 L33,43 L36.999,43 L37,40.007 L19.006,40 L19,53.993 Z M22,38.001 L34,38.006 L34,31 C34.001,28.697 31.197,26.677 28,26.675 L27.996,26.675 C24.804,26.675 22.004,28.696 22.002,31 L22,38.001 Z M17,54.992 L17.006,39 C17.006,38.734 17.111,38.48 17.299,38.292 C17.486,38.105 17.741,38 18.006,38 L20,38.001 L20.002,31 C20.004,27.512 23.59,24.675 27.996,24.675 L28,24.675 C32.412,24.677 36.001,27.515 36,31 L36,38.007 L38,38.008 C38.553,38.008 39,38.456 39,39.008 L38.994,55 C38.994,55.266 38.889,55.52 38.701,55.708 C38.514,55.895 38.259,56 37.994,56 L18,55.992 C17.447,55.992 17,55.544 17,54.992 L17,54.992 Z M60,36 L62,36 L62,34 L60,34 L60,36 Z M44,36 L55,36 L55,34 L44,34 L44,36 Z'
fill='#FFFFFF'
@@ -4712,15 +4745,16 @@ export function IAMIcon(props: SVGProps<SVGSVGElement>) {
}
export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='identityCenterGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#identityCenterGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M46.694,46.8194562 C47.376,46.1374562 47.376,45.0294562 46.694,44.3474562 C46.353,44.0074562 45.906,43.8374562 45.459,43.8374562 C45.01,43.8374562 44.563,44.0074562 44.222,44.3474562 C43.542,45.0284562 43.542,46.1384562 44.222,46.8194562 C44.905,47.5014562 46.013,47.4994562 46.694,46.8194562 M47.718,47.1374562 L51.703,51.1204562 L50.996,51.8274562 L49.868,50.6994562 L48.793,51.7754562 L48.086,51.0684562 L49.161,49.9924562 L47.011,47.8444562 C46.545,48.1654562 46.003,48.3294562 45.458,48.3294562 C44.755,48.3294562 44.051,48.0624562 43.515,47.5264562 C42.445,46.4554562 42.445,44.7124562 43.515,43.6404562 C44.586,42.5714562 46.329,42.5694562 47.401,43.6404562 C48.351,44.5904562 48.455,46.0674562 47.718,47.1374562 M53,44.1014562 C53,46.1684562 51.505,47.0934562 50.023,47.0934562 L50.023,46.0934562 C50.487,46.0934562 52,45.9494562 52,44.1014562 C52,43.0044562 51.353,42.3894562 49.905,42.1084562 C49.68,42.0654562 49.514,41.8754562 49.501,41.6484562 C49.446,40.7444562 48.987,40.1124562 48.384,40.1124562 C48.084,40.1124562 47.854,40.2424562 47.616,40.5464562 C47.506,40.6884562 47.324,40.7594562 47.147,40.7324562 C46.968,40.7054562 46.818,40.5844562 46.755,40.4144562 C46.577,39.9434562 46.211,39.4334562 45.723,38.9774562 C45.231,38.5094562 43.883,37.5074562 41.972,38.2734562 C40.885,38.7054562 40.034,39.9494562 40.034,41.1074562 C40.034,41.2354562 40.043,41.3624562 40.058,41.4884562 C40.061,41.5094562 40.062,41.5304562 40.062,41.5514562 C40.062,41.7994562 39.882,42.0064562 39.645,42.0464562 C38.886,42.2394562 38,42.7454562 38,44.0554562 L38.005,44.2104562 C38.069,45.3254562 39.252,45.9954562 40.358,45.9984562 L41,45.9984562 L41,46.9984562 L40.357,46.9984562 C38.536,46.9944562 37.095,45.8194562 37.006,44.2644562 C37.003,44.1944562 37,44.1244562 37,44.0554562 C37,42.6944562 37.752,41.6484562 39.035,41.1884562 C39.034,41.1614562 39.034,41.1344562 39.034,41.1074562 C39.034,39.5434562 40.138,37.9254562 41.602,37.3434562 C43.298,36.6654562 45.095,37.0034562 46.409,38.2494562 C46.706,38.5274562 47.076,38.9264562 47.372,39.4134562 C47.673,39.2124562 48.008,39.1124562 48.384,39.1124562 C49.257,39.1124562 50.231,39.7714562 50.458,41.2074562 C52.145,41.6324562 53,42.6054562 53,44.1014562 M27,53 L27,27 L53,27 L53,34 L51,34 L51,29 L29,29 L29,51 L51,51 L51,46 L53,46 L53,53 Z'
fill='#FFFFFF'
@@ -4730,15 +4764,16 @@ export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
}
export function STSIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='stsGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#stsGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M14,59 L66,59 L66,21 L14,21 L14,59 Z M68,20 L68,60 C68,60.552 67.553,61 67,61 L13,61 C12.447,61 12,60.552 12,60 L12,20 C12,19.448 12.447,19 13,19 L67,19 C67.553,19 68,19.448 68,20 L68,20 Z M44,48 L59,48 L59,46 L44,46 L44,48 Z M57,42 L62,42 L62,40 L57,40 L57,42 Z M44,42 L52,42 L52,40 L44,40 L44,42 Z M29,46 C29,45.449 28.552,45 28,45 C27.448,45 27,45.449 27,46 C27,46.551 27.448,47 28,47 C28.552,47 29,46.551 29,46 L29,46 Z M31,46 C31,47.302 30.161,48.401 29,48.816 L29,51 L27,51 L27,48.815 C25.839,48.401 25,47.302 25,46 C25,44.346 26.346,43 28,43 C29.654,43 31,44.346 31,46 L31,46 Z M19,53.993 L36.994,54 L36.996,50 L33,50 L33,48 L36.996,48 L36.998,45 L33,45 L33,43 L36.999,43 L37,40.007 L19.006,40 L19,53.993 Z M22,38.001 L34,38.006 L34,31 C34.001,28.697 31.197,26.677 28,26.675 L27.996,26.675 C24.804,26.675 22.004,28.696 22.002,31 L22,38.001 Z M17,54.992 L17.006,39 C17.006,38.734 17.111,38.48 17.299,38.292 C17.486,38.105 17.741,38 18.006,38 L20,38.001 L20.002,31 C20.004,27.512 23.59,24.675 27.996,24.675 L28,24.675 C32.412,24.677 36.001,27.515 36,31 L36,38.007 L38,38.008 C38.553,38.008 39,38.456 39,39.008 L38.994,55 C38.994,55.266 38.889,55.52 38.701,55.708 C38.514,55.895 38.259,56 37.994,56 L18,55.992 C17.447,55.992 17,55.544 17,54.992 L17,54.992 Z M60,36 L62,36 L62,34 L60,34 L60,36 Z M44,36 L55,36 L55,34 L44,34 L44,36 Z'
fill='#FFFFFF'
@@ -4748,15 +4783,16 @@ export function STSIcon(props: SVGProps<SVGSVGElement>) {
}
export function SESIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='sesGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#sesGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M57,60.999875 C57,59.373846 55.626,57.9998214 54,57.9998214 C52.374,57.9998214 51,59.373846 51,60.999875 C51,62.625904 52.374,63.9999286 54,63.9999286 C55.626,63.9999286 57,62.625904 57,60.999875 L57,60.999875 Z M40,59.9998571 C38.374,59.9998571 37,61.3738817 37,62.9999107 C37,64.6259397 38.374,65.9999643 40,65.9999643 C41.626,65.9999643 43,64.6259397 43,62.9999107 C43,61.3738817 41.626,59.9998571 40,59.9998571 L40,59.9998571 Z M26,57.9998214 C24.374,57.9998214 23,59.373846 23,60.999875 C23,62.625904 24.374,63.9999286 26,63.9999286 C27.626,63.9999286 29,62.625904 29,60.999875 C29,59.373846 27.626,57.9998214 26,57.9998214 L26,57.9998214 Z M28.605,42.9995536 L51.395,42.9995536 L43.739,36.1104305 L40.649,38.7584778 C40.463,38.9194807 40.23,38.9994821 39.999,38.9994821 C39.768,38.9994821 39.535,38.9194807 39.349,38.7584778 L36.26,36.1104305 L28.605,42.9995536 Z M27,28.1732888 L27,41.7545313 L34.729,34.7984071 L27,28.1732888 Z M51.297,26.9992678 L28.703,26.9992678 L39.999,36.6824408 L51.297,26.9992678 Z M53,41.7545313 L53,28.1732888 L45.271,34.7974071 L53,41.7545313 Z M59,60.999875 C59,63.7099234 56.71,65.9999643 54,65.9999643 C51.29,65.9999643 49,63.7099234 49,60.999875 C49,58.6308327 50.75,56.5837961 53,56.1057876 L53,52.9997321 L41,52.9997321 L41,58.1058233 C43.25,58.5838319 45,60.6308684 45,62.9999107 C45,65.7099591 42.71,68 40,68 C37.29,68 35,65.7099591 35,62.9999107 C35,60.6308684 36.75,58.5838319 39,58.1058233 L39,52.9997321 L27,52.9997321 L27,56.1057876 C29.25,56.5837961 31,58.6308327 31,60.999875 C31,63.7099234 28.71,65.9999643 26,65.9999643 C23.29,65.9999643 21,63.7099234 21,60.999875 C21,58.6308327 22.75,56.5837961 25,56.1057876 L25,51.9997143 C25,51.4477044 25.447,50.9996964 26,50.9996964 L39,50.9996964 L39,44.9995893 L26,44.9995893 C25.447,44.9995893 25,44.5515813 25,43.9995714 L25,25.99925 C25,25.4472401 25.447,24.9992321 26,24.9992321 L54,24.9992321 C54.553,24.9992321 55,25.4472401 55,25.99925 L55,43.9995714 C55,44.5515813 54.553,44.9995893 54,44.9995893 L41,44.9995893 L41,50.9996964 L54,50.9996964 C54.553,50.9996964 55,51.4477044 55,51.9997143 L55,56.1057876 C57.25,56.5837961 59,58.6308327 59,60.999875 L59,60.999875 Z M68,39.9995 C68,45.9066055 66.177,51.5597064 62.727,56.3447919 L61.104,55.174771 C64.307,50.7316916 66,45.4845979 66,39.9995 C66,25.664244 54.337,14.0000357 40.001,14.0000357 C25.664,14.0000357 14,25.664244 14,39.9995 C14,45.4845979 15.693,50.7316916 18.896,55.174771 L17.273,56.3447919 C13.823,51.5597064 12,45.9066055 12,39.9995 C12,24.5612243 24.561,12 39.999,12 C55.438,12 68,24.5612243 68,39.9995 L68,39.9995 Z'
fill='#FFFFFF'
@@ -4766,15 +4802,16 @@ export function SESIcon(props: SVGProps<SVGSVGElement>) {
}
export function SecretsManagerIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
return (
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='secretsManagerGradient'>
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id={id}>
<stop stopColor='#BD0816' offset='0%' />
<stop stopColor='#FF5252' offset='100%' />
</linearGradient>
</defs>
<rect fill='url(#secretsManagerGradient)' width='80' height='80' />
<rect fill={`url(#${id})`} width='80' height='80' />
<path
d='M38.76,43.36 C38.76,44.044 39.317,44.6 40,44.6 C40.684,44.6 41.24,44.044 41.24,43.36 C41.24,42.676 40.684,42.12 40,42.12 C39.317,42.12 38.76,42.676 38.76,43.36 L38.76,43.36 Z M36.76,43.36 C36.76,41.573 38.213,40.12 40,40.12 C41.787,40.12 43.24,41.573 43.24,43.36 C43.24,44.796 42.296,46.002 41,46.426 L41,49 L39,49 L39,46.426 C37.704,46.002 36.76,44.796 36.76,43.36 L36.76,43.36 Z M49,38 L31,38 L31,51 L49,51 L49,48 L46,48 L46,46 L49,46 L49,43 L46,43 L46,41 L49,41 L49,38 Z M34,36 L45.999,36 L46,31 C46.001,28.384 43.143,26.002 40.004,26 L40.001,26 C38.472,26 36.928,26.574 35.763,27.575 C34.643,28.537 34,29.786 34,31.001 L34,36 Z M48,31.001 L47.999,36 L50,36 C50.553,36 51,36.448 51,37 L51,52 C51,52.552 50.553,53 50,53 L30,53 C29.447,53 29,52.552 29,52 L29,37 C29,36.448 29.447,36 30,36 L32,36 L32,31 C32.001,29.202 32.897,27.401 34.459,26.058 C35.982,24.75 38.001,24 40.001,24 L40.004,24 C44.265,24.002 48.001,27.273 48,31.001 L48,31.001 Z M19.207,55.049 L20.828,53.877 C18.093,50.097 16.581,45.662 16.396,41 L19,41 L19,39 L16.399,39 C16.598,34.366 18.108,29.957 20.828,26.198 L19.207,25.025 C16.239,29.128 14.599,33.942 14.399,39 L12,39 L12,41 L14.396,41 C14.582,46.086 16.224,50.926 19.207,55.049 L19.207,55.049 Z M53.838,59.208 C50.069,61.936 45.648,63.446 41,63.639 L41,61 L39,61 L39,63.639 C34.352,63.447 29.93,61.937 26.159,59.208 L24.988,60.828 C29.1,63.805 33.928,65.445 39,65.639 L39,68 L41,68 L41,65.639 C46.072,65.445 50.898,63.805 55.01,60.828 L53.838,59.208 Z M26.159,20.866 C29.93,18.138 34.352,16.628 39,16.436 L39,19 L41,19 L41,16.436 C45.648,16.628 50.069,18.138 53.838,20.866 L55.01,19.246 C50.898,16.27 46.072,14.63 41,14.436 L41,12 L39,12 L39,14.436 C33.928,14.629 29.1,16.269 24.988,19.246 L26.159,20.866 Z M65.599,39 C65.399,33.942 63.759,29.128 60.79,25.025 L59.169,26.198 C61.89,29.957 63.4,34.366 63.599,39 L61,39 L61,41 L63.602,41 C63.416,45.662 61.905,50.097 59.169,53.877 L60.79,55.049 C63.774,50.926 65.415,46.086 65.602,41 L68,41 L68,39 L65.599,39 Z M56.386,25.064 L64.226,17.224 L62.812,15.81 L54.972,23.65 L56.386,25.064 Z M23.612,55.01 L15.772,62.85 L17.186,64.264 L25.026,56.424 L23.612,55.01 Z M28.666,27.253 L13.825,12.413 L12.411,13.827 L27.252,28.667 L28.666,27.253 Z M54.193,52.78 L67.586,66.173 L66.172,67.587 L52.779,54.194 L54.193,52.78 Z'
fill='#FFFFFF'

View File

@@ -2248,6 +2248,45 @@ import {
salesforceUpdateOpportunityTool,
salesforceUpdateTaskTool,
} from '@/tools/salesforce'
import {
createBusinessPartnerTool as sapS4HanaCreateBusinessPartnerTool,
createPurchaseOrderTool as sapS4HanaCreatePurchaseOrderTool,
createPurchaseRequisitionTool as sapS4HanaCreatePurchaseRequisitionTool,
createSalesOrderTool as sapS4HanaCreateSalesOrderTool,
deleteSalesOrderTool as sapS4HanaDeleteSalesOrderTool,
getBillingDocumentTool as sapS4HanaGetBillingDocumentTool,
getBusinessPartnerTool as sapS4HanaGetBusinessPartnerTool,
getCustomerTool as sapS4HanaGetCustomerTool,
getInboundDeliveryTool as sapS4HanaGetInboundDeliveryTool,
getOutboundDeliveryTool as sapS4HanaGetOutboundDeliveryTool,
getProductTool as sapS4HanaGetProductTool,
getPurchaseOrderTool as sapS4HanaGetPurchaseOrderTool,
getPurchaseRequisitionTool as sapS4HanaGetPurchaseRequisitionTool,
getSalesOrderTool as sapS4HanaGetSalesOrderTool,
getSupplierInvoiceTool as sapS4HanaGetSupplierInvoiceTool,
getSupplierTool as sapS4HanaGetSupplierTool,
listBillingDocumentsTool as sapS4HanaListBillingDocumentsTool,
listBusinessPartnersTool as sapS4HanaListBusinessPartnersTool,
listCustomersTool as sapS4HanaListCustomersTool,
listInboundDeliveriesTool as sapS4HanaListInboundDeliveriesTool,
listMaterialDocumentsTool as sapS4HanaListMaterialDocumentsTool,
listMaterialStockTool as sapS4HanaListMaterialStockTool,
listOutboundDeliveriesTool as sapS4HanaListOutboundDeliveriesTool,
listProductsTool as sapS4HanaListProductsTool,
listPurchaseOrdersTool as sapS4HanaListPurchaseOrdersTool,
listPurchaseRequisitionsTool as sapS4HanaListPurchaseRequisitionsTool,
listSalesOrdersTool as sapS4HanaListSalesOrdersTool,
listSupplierInvoicesTool as sapS4HanaListSupplierInvoicesTool,
listSuppliersTool as sapS4HanaListSuppliersTool,
odataQueryTool as sapS4HanaOdataQueryTool,
updateBusinessPartnerTool as sapS4HanaUpdateBusinessPartnerTool,
updateCustomerTool as sapS4HanaUpdateCustomerTool,
updateProductTool as sapS4HanaUpdateProductTool,
updatePurchaseOrderTool as sapS4HanaUpdatePurchaseOrderTool,
updatePurchaseRequisitionTool as sapS4HanaUpdatePurchaseRequisitionTool,
updateSalesOrderTool as sapS4HanaUpdateSalesOrderTool,
updateSupplierTool as sapS4HanaUpdateSupplierTool,
} from '@/tools/sap_s4hana'
import { searchTool } from '@/tools/search'
import {
secretsManagerCreateSecretTool,
@@ -5286,6 +5325,43 @@ export const tools: Record<string, ToolConfig> = {
salesforce_query_more: salesforceQueryMoreTool,
salesforce_describe_object: salesforceDescribeObjectTool,
salesforce_list_objects: salesforceListObjectsTool,
sap_s4hana_create_business_partner: sapS4HanaCreateBusinessPartnerTool,
sap_s4hana_create_purchase_order: sapS4HanaCreatePurchaseOrderTool,
sap_s4hana_create_purchase_requisition: sapS4HanaCreatePurchaseRequisitionTool,
sap_s4hana_create_sales_order: sapS4HanaCreateSalesOrderTool,
sap_s4hana_delete_sales_order: sapS4HanaDeleteSalesOrderTool,
sap_s4hana_get_billing_document: sapS4HanaGetBillingDocumentTool,
sap_s4hana_get_business_partner: sapS4HanaGetBusinessPartnerTool,
sap_s4hana_get_customer: sapS4HanaGetCustomerTool,
sap_s4hana_get_inbound_delivery: sapS4HanaGetInboundDeliveryTool,
sap_s4hana_get_outbound_delivery: sapS4HanaGetOutboundDeliveryTool,
sap_s4hana_get_product: sapS4HanaGetProductTool,
sap_s4hana_get_purchase_order: sapS4HanaGetPurchaseOrderTool,
sap_s4hana_get_purchase_requisition: sapS4HanaGetPurchaseRequisitionTool,
sap_s4hana_get_sales_order: sapS4HanaGetSalesOrderTool,
sap_s4hana_get_supplier: sapS4HanaGetSupplierTool,
sap_s4hana_get_supplier_invoice: sapS4HanaGetSupplierInvoiceTool,
sap_s4hana_list_billing_documents: sapS4HanaListBillingDocumentsTool,
sap_s4hana_list_business_partners: sapS4HanaListBusinessPartnersTool,
sap_s4hana_list_customers: sapS4HanaListCustomersTool,
sap_s4hana_list_inbound_deliveries: sapS4HanaListInboundDeliveriesTool,
sap_s4hana_list_material_documents: sapS4HanaListMaterialDocumentsTool,
sap_s4hana_list_material_stock: sapS4HanaListMaterialStockTool,
sap_s4hana_list_outbound_deliveries: sapS4HanaListOutboundDeliveriesTool,
sap_s4hana_list_products: sapS4HanaListProductsTool,
sap_s4hana_list_purchase_orders: sapS4HanaListPurchaseOrdersTool,
sap_s4hana_list_purchase_requisitions: sapS4HanaListPurchaseRequisitionsTool,
sap_s4hana_list_sales_orders: sapS4HanaListSalesOrdersTool,
sap_s4hana_list_supplier_invoices: sapS4HanaListSupplierInvoicesTool,
sap_s4hana_list_suppliers: sapS4HanaListSuppliersTool,
sap_s4hana_odata_query: sapS4HanaOdataQueryTool,
sap_s4hana_update_business_partner: sapS4HanaUpdateBusinessPartnerTool,
sap_s4hana_update_customer: sapS4HanaUpdateCustomerTool,
sap_s4hana_update_product: sapS4HanaUpdateProductTool,
sap_s4hana_update_purchase_order: sapS4HanaUpdatePurchaseOrderTool,
sap_s4hana_update_purchase_requisition: sapS4HanaUpdatePurchaseRequisitionTool,
sap_s4hana_update_sales_order: sapS4HanaUpdateSalesOrderTool,
sap_s4hana_update_supplier: sapS4HanaUpdateSupplierTool,
sqs_send: sqsSendTool,
sts_assume_role: stsAssumeRoleTool,
sts_get_caller_identity: stsGetCallerIdentityTool,

View File

@@ -0,0 +1,162 @@
import type { CreateBusinessPartnerParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const createBusinessPartnerTool: ToolConfig<CreateBusinessPartnerParams, SapProxyResponse> =
{
id: 'sap_s4hana_create_business_partner',
name: 'SAP S/4HANA Create Business Partner',
description:
'Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner). For Person category 1 provide FirstName and LastName. For Organization category 2 provide OrganizationBPName1.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
businessPartnerCategory: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BusinessPartnerCategory: "1" Person, "2" Organization, "3" Group',
},
businessPartnerGrouping: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'BusinessPartnerGrouping (number range / role grouping configured in S/4HANA, e.g. "0001")',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'FirstName (required for Person)',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'LastName (required for Person)',
},
organizationBPName1: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OrganizationBPName1 (required for Organization)',
},
body: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Optional additional A_BusinessPartner fields merged into the create payload',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const extra = parseJsonInput<Record<string, unknown>>(params.body, 'body') ?? {}
const extraHasName = (key: string) => Object.hasOwn(extra, key) && Boolean(extra[key])
if (params.businessPartnerCategory === '1') {
const hasFirst = Boolean(params.firstName) || extraHasName('FirstName')
const hasLast = Boolean(params.lastName) || extraHasName('LastName')
if (!hasFirst || !hasLast) {
throw new Error('BusinessPartnerCategory "1" (Person) requires FirstName and LastName')
}
} else if (params.businessPartnerCategory === '2') {
const hasOrgName =
Boolean(params.organizationBPName1) || extraHasName('OrganizationBPName1')
if (!hasOrgName) {
throw new Error(
'BusinessPartnerCategory "2" (Organization) requires OrganizationBPName1'
)
}
}
const payload: Record<string, unknown> = {
...extra,
BusinessPartnerCategory: params.businessPartnerCategory,
BusinessPartnerGrouping: params.businessPartnerGrouping,
}
if (params.firstName) payload.FirstName = params.firstName
if (params.lastName) payload.LastName = params.lastName
if (params.organizationBPName1) payload.OrganizationBPName1 = params.organizationBPName1
return {
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: '/A_BusinessPartner',
method: 'POST',
query: { $format: 'json' },
body: payload,
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Created A_BusinessPartner entity' },
},
}

View File

@@ -0,0 +1,145 @@
import type { CreatePurchaseOrderParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const createPurchaseOrderTool: ToolConfig<CreatePurchaseOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_create_purchase_order',
name: 'SAP S/4HANA Create Purchase Order',
description:
'Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder). PurchaseOrder is auto-assigned by SAP from the document number range; provide line items via the body parameter.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
purchaseOrderType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchaseOrderType (e.g., "NB" Standard PO)',
},
companyCode: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'CompanyCode (4 chars, e.g., "1010")',
},
purchasingOrganization: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchasingOrganization (4 chars)',
},
purchasingGroup: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchasingGroup (3 chars)',
},
supplier: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Supplier business partner key (up to 10 chars)',
},
body: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Additional A_PurchaseOrder fields and to_PurchaseOrderItem deep-insert items merged into the create payload (e.g., {"to_PurchaseOrderItem":[{"PurchaseOrderItem":"10","Material":"TG11","OrderQuantity":"5","Plant":"1010","PurchaseOrderQuantityUnit":"PC","NetPriceAmount":"100.00","DocumentCurrency":"USD"}]}).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const extra = parseJsonInput<Record<string, unknown>>(params.body, 'body') ?? {}
const payload: Record<string, unknown> = {
...extra,
PurchaseOrderType: params.purchaseOrderType,
CompanyCode: params.companyCode,
PurchasingOrganization: params.purchasingOrganization,
PurchasingGroup: params.purchasingGroup,
Supplier: params.supplier,
}
return {
...baseProxyBody(params),
service: 'API_PURCHASEORDER_PROCESS_SRV',
path: '/A_PurchaseOrder',
method: 'POST',
query: { $format: 'json' },
body: payload,
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Created A_PurchaseOrder entity' },
},
}

View File

@@ -0,0 +1,132 @@
import type { CreatePurchaseRequisitionParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const createPurchaseRequisitionTool: ToolConfig<
CreatePurchaseRequisitionParams,
SapProxyResponse
> = {
id: 'sap_s4hana_create_purchase_requisition',
name: 'SAP S/4HANA Create Purchase Requisition',
description:
'Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader). PurchaseRequisition is auto-assigned by SAP from the document number range; provide line items via the to_PurchaseReqnItem deep-insert array. Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
purchaseRequisitionType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchaseRequisitionType (e.g., "NB" Standard PR)',
},
items: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'to_PurchaseReqnItem deep-insert array (e.g., [{"PurchaseRequisitionItem":"10","Material":"TG11","RequestedQuantity":"5","Plant":"1010","BaseUnit":"PC","DeliveryDate":"/Date(1735689600000)/"}])',
},
body: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Additional A_PurchaseRequisitionHeader fields merged into the create payload (e.g., {"PurchaseRequisitionDescription":"Office supplies"})',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const items = parseJsonInput<Array<Record<string, unknown>>>(params.items, 'items')
if (!Array.isArray(items) || items.length === 0) {
throw new Error('items must be a non-empty JSON array of purchase requisition items')
}
const extra = parseJsonInput<Record<string, unknown>>(params.body, 'body') ?? {}
const payload: Record<string, unknown> = {
...extra,
PurchaseRequisitionType: params.purchaseRequisitionType,
to_PurchaseReqnItem: items,
}
return {
...baseProxyBody(params),
service: 'API_PURCHASEREQ_PROCESS_SRV',
path: '/A_PurchaseRequisitionHeader',
method: 'POST',
query: { $format: 'json' },
body: payload,
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Created A_PurchaseRequisitionHeader entity' },
},
}

View File

@@ -0,0 +1,159 @@
import type { CreateSalesOrderParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const createSalesOrderTool: ToolConfig<CreateSalesOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_create_sales_order',
name: 'SAP S/4HANA Create Sales Order',
description:
'Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) with deep insert of sales order items via to_Item.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
salesOrderType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SalesOrderType (e.g., "OR" Standard Order)',
},
salesOrganization: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SalesOrganization (4 chars, e.g., "1010")',
},
distributionChannel: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DistributionChannel (2 chars, e.g., "10")',
},
organizationDivision: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'OrganizationDivision (2 chars, e.g., "00")',
},
soldToParty: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SoldToParty business partner key (up to 10 chars)',
},
items: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of sales order items for to_Item deep insert. Each item should include Material and RequestedQuantity (e.g., [{"Material":"TG11","RequestedQuantity":"1"}]).',
},
body: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Optional additional A_SalesOrder fields merged into the create payload',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const items = parseJsonInput<Array<Record<string, unknown>>>(params.items, 'items')
if (!Array.isArray(items) || items.length === 0) {
throw new Error('items must be a non-empty JSON array of sales order item objects')
}
const extra = parseJsonInput<Record<string, unknown>>(params.body, 'body') ?? {}
const payload: Record<string, unknown> = {
...extra,
SalesOrderType: params.salesOrderType,
SalesOrganization: params.salesOrganization,
DistributionChannel: params.distributionChannel,
OrganizationDivision: params.organizationDivision,
SoldToParty: params.soldToParty,
to_Item: items,
}
return {
...baseProxyBody(params),
service: 'API_SALES_ORDER_SRV',
path: '/A_SalesOrder',
method: 'POST',
query: { $format: 'json' },
body: payload,
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: {
type: 'json',
description: 'Created A_SalesOrder entity (with deep-inserted items if expanded by SAP)',
},
},
}

View File

@@ -0,0 +1,108 @@
import type { DeleteSalesOrderParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteSalesOrderTool: ToolConfig<DeleteSalesOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_delete_sales_order',
name: 'SAP S/4HANA Delete Sales Order',
description:
'Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only orders without subsequent documents (deliveries, invoices) can be deleted; otherwise reject items via update instead.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
salesOrder: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SalesOrder key to delete (string, up to 10 characters)',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_SALES_ORDER_SRV',
path: `/A_SalesOrder(${quoteOdataKey(params.salesOrder)})`,
method: 'DELETE',
ifMatch: params.ifMatch || '*',
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: { type: 'json', description: 'Null on successful deletion' },
},
}

View File

@@ -0,0 +1,115 @@
import type { GetBillingDocumentParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getBillingDocumentTool: ToolConfig<GetBillingDocumentParams, SapProxyResponse> = {
id: 'sap_s4hana_get_billing_document',
name: 'SAP S/4HANA Get Billing Document',
description:
'Retrieve a single billing document (customer invoice) by BillingDocument key from SAP S/4HANA Cloud (API_BILLING_DOCUMENT_SRV, A_BillingDocument).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
billingDocument: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BillingDocument key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_Item,to_Partner")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BILLING_DOCUMENT_SRV',
path: `/A_BillingDocument(${quoteOdataKey(params.billingDocument)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_BillingDocument entity' },
},
}

View File

@@ -0,0 +1,115 @@
import type { GetBusinessPartnerParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getBusinessPartnerTool: ToolConfig<GetBusinessPartnerParams, SapProxyResponse> = {
id: 'sap_s4hana_get_business_partner',
name: 'SAP S/4HANA Get Business Partner',
description:
'Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
businessPartner: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BusinessPartner key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand ($expand)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: `/A_BusinessPartner(${quoteOdataKey(params.businessPartner)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_BusinessPartner entity' },
},
}

View File

@@ -0,0 +1,116 @@
import type { GetCustomerParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getCustomerTool: ToolConfig<GetCustomerParams, SapProxyResponse> = {
id: 'sap_s4hana_get_customer',
name: 'SAP S/4HANA Get Customer',
description:
'Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
customer: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Customer key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_CustomerCompany,to_CustomerSalesArea")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: `/A_Customer(${quoteOdataKey(params.customer)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_Customer entity' },
},
}

View File

@@ -0,0 +1,116 @@
import type { GetInboundDeliveryParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getInboundDeliveryTool: ToolConfig<GetInboundDeliveryParams, SapProxyResponse> = {
id: 'sap_s4hana_get_inbound_delivery',
name: 'SAP S/4HANA Get Inbound Delivery',
description:
'Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, A_InbDeliveryHeader).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
deliveryDocument: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DeliveryDocument key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_DeliveryDocumentItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_INBOUND_DELIVERY_SRV;v=0002',
path: `/A_InbDeliveryHeader(${quoteOdataKey(params.deliveryDocument)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_InbDeliveryHeader entity' },
},
}

View File

@@ -0,0 +1,116 @@
import type { GetOutboundDeliveryParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getOutboundDeliveryTool: ToolConfig<GetOutboundDeliveryParams, SapProxyResponse> = {
id: 'sap_s4hana_get_outbound_delivery',
name: 'SAP S/4HANA Get Outbound Delivery',
description:
'Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=0002, A_OutbDeliveryHeader).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
deliveryDocument: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DeliveryDocument key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_DeliveryDocumentItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_OUTBOUND_DELIVERY_SRV;v=0002',
path: `/A_OutbDeliveryHeader(${quoteOdataKey(params.deliveryDocument)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_OutbDeliveryHeader entity' },
},
}

View File

@@ -0,0 +1,115 @@
import type { GetProductParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getProductTool: ToolConfig<GetProductParams, SapProxyResponse> = {
id: 'sap_s4hana_get_product',
name: 'SAP S/4HANA Get Product',
description:
'Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
product: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Product key (string, up to 40 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_Description")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_PRODUCT_SRV',
path: `/A_Product(${quoteOdataKey(params.product)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_Product entity' },
},
}

View File

@@ -0,0 +1,115 @@
import type { GetPurchaseOrderParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getPurchaseOrderTool: ToolConfig<GetPurchaseOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_get_purchase_order',
name: 'SAP S/4HANA Get Purchase Order',
description:
'Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
purchaseOrder: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchaseOrder key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_PurchaseOrderItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_PURCHASEORDER_PROCESS_SRV',
path: `/A_PurchaseOrder(${quoteOdataKey(params.purchaseOrder)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_PurchaseOrder entity' },
},
}

View File

@@ -0,0 +1,118 @@
import type { GetPurchaseRequisitionParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getPurchaseRequisitionTool: ToolConfig<
GetPurchaseRequisitionParams,
SapProxyResponse
> = {
id: 'sap_s4hana_get_purchase_requisition',
name: 'SAP S/4HANA Get Purchase Requisition',
description:
'Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader). Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
purchaseRequisition: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchaseRequisition key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_PurchaseReqnItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_PURCHASEREQ_PROCESS_SRV',
path: `/A_PurchaseRequisitionHeader(${quoteOdataKey(params.purchaseRequisition)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_PurchaseRequisitionHeader entity' },
},
}

View File

@@ -0,0 +1,115 @@
import type { GetSalesOrderParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getSalesOrderTool: ToolConfig<GetSalesOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_get_sales_order',
name: 'SAP S/4HANA Get Sales Order',
description:
'Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
salesOrder: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SalesOrder key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_Item")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_SALES_ORDER_SRV',
path: `/A_SalesOrder(${quoteOdataKey(params.salesOrder)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_SalesOrder entity' },
},
}

View File

@@ -0,0 +1,116 @@
import type { GetSupplierParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getSupplierTool: ToolConfig<GetSupplierParams, SapProxyResponse> = {
id: 'sap_s4hana_get_supplier',
name: 'SAP S/4HANA Get Supplier',
description:
'Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
supplier: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Supplier key (string, up to 10 characters)',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_SupplierCompany,to_SupplierPurchasingOrg")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: `/A_Supplier(${quoteOdataKey(params.supplier)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_Supplier entity' },
},
}

View File

@@ -0,0 +1,121 @@
import type { GetSupplierInvoiceParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildEntityQuery,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const getSupplierInvoiceTool: ToolConfig<GetSupplierInvoiceParams, SapProxyResponse> = {
id: 'sap_s4hana_get_supplier_invoice',
name: 'SAP S/4HANA Get Supplier Invoice',
description:
'Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYear) from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, A_SupplierInvoice).',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
supplierInvoice: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SupplierInvoice key (string, up to 10 characters)',
},
fiscalYear: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'FiscalYear (4-character year, e.g., "2024")',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand ($expand)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_SUPPLIERINVOICE_PROCESS_SRV',
path: `/A_SupplierInvoice(SupplierInvoice=${quoteOdataKey(params.supplierInvoice)},FiscalYear=${quoteOdataKey(params.fiscalYear)})`,
method: 'GET',
query: buildEntityQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'A_SupplierInvoice entity' },
},
}

View File

@@ -0,0 +1,37 @@
export { createBusinessPartnerTool } from '@/tools/sap_s4hana/create_business_partner'
export { createPurchaseOrderTool } from '@/tools/sap_s4hana/create_purchase_order'
export { createPurchaseRequisitionTool } from '@/tools/sap_s4hana/create_purchase_requisition'
export { createSalesOrderTool } from '@/tools/sap_s4hana/create_sales_order'
export { deleteSalesOrderTool } from '@/tools/sap_s4hana/delete_sales_order'
export { getBillingDocumentTool } from '@/tools/sap_s4hana/get_billing_document'
export { getBusinessPartnerTool } from '@/tools/sap_s4hana/get_business_partner'
export { getCustomerTool } from '@/tools/sap_s4hana/get_customer'
export { getInboundDeliveryTool } from '@/tools/sap_s4hana/get_inbound_delivery'
export { getOutboundDeliveryTool } from '@/tools/sap_s4hana/get_outbound_delivery'
export { getProductTool } from '@/tools/sap_s4hana/get_product'
export { getPurchaseOrderTool } from '@/tools/sap_s4hana/get_purchase_order'
export { getPurchaseRequisitionTool } from '@/tools/sap_s4hana/get_purchase_requisition'
export { getSalesOrderTool } from '@/tools/sap_s4hana/get_sales_order'
export { getSupplierTool } from '@/tools/sap_s4hana/get_supplier'
export { getSupplierInvoiceTool } from '@/tools/sap_s4hana/get_supplier_invoice'
export { listBillingDocumentsTool } from '@/tools/sap_s4hana/list_billing_documents'
export { listBusinessPartnersTool } from '@/tools/sap_s4hana/list_business_partners'
export { listCustomersTool } from '@/tools/sap_s4hana/list_customers'
export { listInboundDeliveriesTool } from '@/tools/sap_s4hana/list_inbound_deliveries'
export { listMaterialDocumentsTool } from '@/tools/sap_s4hana/list_material_documents'
export { listMaterialStockTool } from '@/tools/sap_s4hana/list_material_stock'
export { listOutboundDeliveriesTool } from '@/tools/sap_s4hana/list_outbound_deliveries'
export { listProductsTool } from '@/tools/sap_s4hana/list_products'
export { listPurchaseOrdersTool } from '@/tools/sap_s4hana/list_purchase_orders'
export { listPurchaseRequisitionsTool } from '@/tools/sap_s4hana/list_purchase_requisitions'
export { listSalesOrdersTool } from '@/tools/sap_s4hana/list_sales_orders'
export { listSupplierInvoicesTool } from '@/tools/sap_s4hana/list_supplier_invoices'
export { listSuppliersTool } from '@/tools/sap_s4hana/list_suppliers'
export { odataQueryTool } from '@/tools/sap_s4hana/odata_query'
export { updateBusinessPartnerTool } from '@/tools/sap_s4hana/update_business_partner'
export { updateCustomerTool } from '@/tools/sap_s4hana/update_customer'
export { updateProductTool } from '@/tools/sap_s4hana/update_product'
export { updatePurchaseOrderTool } from '@/tools/sap_s4hana/update_purchase_order'
export { updatePurchaseRequisitionTool } from '@/tools/sap_s4hana/update_purchase_requisition'
export { updateSalesOrderTool } from '@/tools/sap_s4hana/update_sales_order'
export { updateSupplierTool } from '@/tools/sap_s4hana/update_supplier'

View File

@@ -0,0 +1,132 @@
import type { ListBillingDocumentsParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listBillingDocumentsTool: ToolConfig<ListBillingDocumentsParams, SapProxyResponse> = {
id: 'sap_s4hana_list_billing_documents',
name: 'SAP S/4HANA List Billing Documents',
description:
'List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_DOCUMENT_SRV, A_BillingDocument) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "SoldToParty eq \'10100001\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_Item,to_Partner")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BILLING_DOCUMENT_SRV',
path: '/A_BillingDocument',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_BillingDocument entities' },
},
}

View File

@@ -0,0 +1,132 @@
import type { ListBusinessPartnersParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listBusinessPartnersTool: ToolConfig<ListBusinessPartnersParams, SapProxyResponse> = {
id: 'sap_s4hana_list_business_partners',
name: 'SAP S/4HANA List Business Partners',
description:
'List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessPartner) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "BusinessPartnerCategory eq \'1\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand ($expand)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: '/A_BusinessPartner',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_BusinessPartner entities' },
},
}

View File

@@ -0,0 +1,133 @@
import type { ListCustomersParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listCustomersTool: ToolConfig<ListCustomersParams, SapProxyResponse> = {
id: 'sap_s4hana_list_customers',
name: 'SAP S/4HANA List Customers',
description:
'List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "CustomerAccountGroup eq \'Z001\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_CustomerCompany,to_CustomerSalesArea")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: '/A_Customer',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_Customer entities' },
},
}

View File

@@ -0,0 +1,134 @@
import type { ListInboundDeliveriesParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listInboundDeliveriesTool: ToolConfig<ListInboundDeliveriesParams, SapProxyResponse> =
{
id: 'sap_s4hana_list_inbound_deliveries',
name: 'SAP S/4HANA List Inbound Deliveries',
description:
'List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, A_InbDeliveryHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "ReceivingPlant eq \'1010\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_DeliveryDocumentItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_INBOUND_DELIVERY_SRV;v=0002',
path: '/A_InbDeliveryHeader',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_InbDeliveryHeader entities' },
},
}

View File

@@ -0,0 +1,135 @@
import type { ListMaterialDocumentsParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listMaterialDocumentsTool: ToolConfig<ListMaterialDocumentsParams, SapProxyResponse> =
{
id: 'sap_s4hana_list_material_documents',
name: 'SAP S/4HANA List Material Documents',
description:
'List material document headers (goods movements) from SAP S/4HANA Cloud (API_MATERIAL_DOCUMENT_SRV, A_MaterialDocumentHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"OData $filter expression (e.g., \"MaterialDocumentYear eq '2024' and PostingDate ge datetime'2024-01-01T00:00:00'\")",
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_MaterialDocumentItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_MATERIAL_DOCUMENT_SRV',
path: '/A_MaterialDocumentHeader',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_MaterialDocumentHeader entities' },
},
}

View File

@@ -0,0 +1,133 @@
import type { ListMaterialStockParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listMaterialStockTool: ToolConfig<ListMaterialStockParams, SapProxyResponse> = {
id: 'sap_s4hana_list_material_stock',
name: 'SAP S/4HANA List Material Stock',
description:
'List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A_MatlStkInAcctMod). The entity uses an 11-field composite key (Material, Plant, StorageLocation, Batch, Supplier, Customer, WBSElementInternalID, SDDocument, SDDocumentItem, InventorySpecialStockType, InventoryStockType) — query with $filter on these fields instead of a direct key lookup.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"OData $filter expression (e.g., \"Material eq 'TG10' and Plant eq '1010' and InventoryStockType eq '01'\")",
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand ($expand)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_MATERIAL_STOCK_SRV',
path: '/A_MatlStkInAcctMod',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_MatlStkInAcctMod stock entries' },
},
}

View File

@@ -0,0 +1,136 @@
import type { ListOutboundDeliveriesParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listOutboundDeliveriesTool: ToolConfig<
ListOutboundDeliveriesParams,
SapProxyResponse
> = {
id: 'sap_s4hana_list_outbound_deliveries',
name: 'SAP S/4HANA List Outbound Deliveries',
description:
'List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=0002, A_OutbDeliveryHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "OverallDeliveryStatus eq \'C\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_DeliveryDocumentItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_OUTBOUND_DELIVERY_SRV;v=0002',
path: '/A_OutbDeliveryHeader',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_OutbDeliveryHeader entities' },
},
}

View File

@@ -0,0 +1,132 @@
import type { ListProductsParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listProductsTool: ToolConfig<ListProductsParams, SapProxyResponse> = {
id: 'sap_s4hana_list_products',
name: 'SAP S/4HANA List Products',
description:
'List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "ProductType eq \'FERT\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand ($expand)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_PRODUCT_SRV',
path: '/A_Product',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_Product entities' },
},
}

View File

@@ -0,0 +1,132 @@
import type { ListPurchaseOrdersParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listPurchaseOrdersTool: ToolConfig<ListPurchaseOrdersParams, SapProxyResponse> = {
id: 'sap_s4hana_list_purchase_orders',
name: 'SAP S/4HANA List Purchase Orders',
description:
'List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_PurchaseOrder) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "CompanyCode eq \'1010\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_PurchaseOrderItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_PURCHASEORDER_PROCESS_SRV',
path: '/A_PurchaseOrder',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_PurchaseOrder entities' },
},
}

View File

@@ -0,0 +1,135 @@
import type { ListPurchaseRequisitionsParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listPurchaseRequisitionsTool: ToolConfig<
ListPurchaseRequisitionsParams,
SapProxyResponse
> = {
id: 'sap_s4hana_list_purchase_requisitions',
name: 'SAP S/4HANA List Purchase Requisitions',
description:
'List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, A_PurchaseRequisitionHeader) with optional OData $filter, $top, $skip, $orderby, $select, $expand. Note: API_PURCHASEREQ_PROCESS_SRV is deprecated since S/4HANA Cloud Public Edition 2402; the successor is API_PURCHASEREQUISITION_2 (OData v4). This tool still works against tenants where the legacy service is enabled.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "PurchaseRequisitionType eq \'NB\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_PurchaseReqnItem")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_PURCHASEREQ_PROCESS_SRV',
path: '/A_PurchaseRequisitionHeader',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_PurchaseRequisitionHeader entities' },
},
}

View File

@@ -0,0 +1,132 @@
import type { ListSalesOrdersParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listSalesOrdersTool: ToolConfig<ListSalesOrdersParams, SapProxyResponse> = {
id: 'sap_s4hana_list_sales_orders',
name: 'SAP S/4HANA List Sales Orders',
description:
'List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "SalesOrganization eq \'1010\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand (e.g., "to_Item,to_Partner")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_SALES_ORDER_SRV',
path: '/A_SalesOrder',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_SalesOrder entities' },
},
}

View File

@@ -0,0 +1,132 @@
import type { ListSupplierInvoicesParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listSupplierInvoicesTool: ToolConfig<ListSupplierInvoicesParams, SapProxyResponse> = {
id: 'sap_s4hana_list_supplier_invoices',
name: 'SAP S/4HANA List Supplier Invoices',
description:
'List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, A_SupplierInvoice) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "InvoicingParty eq \'17300001\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated navigation properties to expand ($expand)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_SUPPLIERINVOICE_PROCESS_SRV',
path: '/A_SupplierInvoice',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_SupplierInvoice entities' },
},
}

View File

@@ -0,0 +1,133 @@
import type { ListSuppliersParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
buildOdataQuery,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const listSuppliersTool: ToolConfig<ListSuppliersParams, SapProxyResponse> = {
id: 'sap_s4hana_list_suppliers',
name: 'SAP S/4HANA List Suppliers',
description:
'List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with optional OData $filter, $top, $skip, $orderby, $select, $expand.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $filter expression (e.g., "SupplierAccountGroup eq \'BP02\'")',
},
top: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum results to return ($top)',
},
skip: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to skip ($skip)',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'OData $orderby expression',
},
select: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to return ($select)',
},
expand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated navigation properties to expand (e.g., "to_SupplierCompany,to_SupplierPurchasingOrg")',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: '/A_Supplier',
method: 'GET',
query: buildOdataQuery(params),
}),
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: { type: 'json', description: 'Array of A_Supplier entities' },
},
}

View File

@@ -0,0 +1,163 @@
import type { ODataQueryParams, SapProxyResponse } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
function normalizeQuery(
query: ODataQueryParams['query']
): Record<string, string | number | boolean> | undefined {
if (!query) return undefined
if (typeof query === 'object') return query
if (typeof query !== 'string') return undefined
const trimmed = query.trim()
if (!trimmed) return undefined
if (trimmed.startsWith('{')) {
return parseJsonInput<Record<string, string | number | boolean>>(trimmed, 'query')
}
const search = new URLSearchParams(trimmed.startsWith('?') ? trimmed.slice(1) : trimmed)
const result: Record<string, string> = {}
for (const [key, value] of search.entries()) result[key] = value
return result
}
export const odataQueryTool: ToolConfig<ODataQueryParams, SapProxyResponse> = {
id: 'sap_s4hana_odata_query',
name: 'SAP S/4HANA OData Query',
description:
'Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
service: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'OData service name (e.g., "API_BUSINESS_PARTNER", "API_SALES_ORDER_SRV")',
},
path: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Path inside the service (e.g., "/A_BusinessPartner" or "/A_BusinessPartner(\'1000123\')")',
},
method: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'HTTP method: GET (default), POST, PATCH, PUT, DELETE, MERGE',
},
query: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'OData query parameters as JSON object or query string (e.g., {"$filter":"BusinessPartnerCategory eq \'1\'","$top":10}). $format=json is added automatically when omitted.',
},
body: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'JSON request body for write operations',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'ETag value for the If-Match header (required by SAP for PATCH/PUT/DELETE on existing entities)',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const query = normalizeQuery(params.query) ?? {}
if (!('$format' in query)) query.$format = 'json'
const requestBody: Record<string, unknown> = {
...baseProxyBody(params),
service: params.service,
path: params.path,
method: params.method || 'GET',
query,
}
const parsedBody = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (parsedBody !== undefined) requestBody.body = parsedBody
if (params.ifMatch) requestBody.ifMatch = params.ifMatch
return requestBody
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP' },
data: {
type: 'json',
description: 'Parsed OData payload (entity, collection, or null on 204)',
},
},
}

View File

@@ -0,0 +1,302 @@
import type { ToolResponse } from '@/tools/types'
export type SapDeploymentType = 'cloud_public' | 'cloud_private' | 'on_premise'
export type SapAuthType = 'oauth_client_credentials' | 'basic'
export interface SapBaseParams {
deploymentType?: SapDeploymentType
authType?: SapAuthType
subdomain?: string
region?: string
baseUrl?: string
tokenUrl?: string
clientId?: string
clientSecret?: string
username?: string
password?: string
}
export interface ProxyOutput {
status: number
data: unknown
}
export interface SapProxyResponse extends ToolResponse {
output: ProxyOutput
}
export interface ListBusinessPartnersParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetBusinessPartnerParams extends SapBaseParams {
businessPartner: string
select?: string
expand?: string
}
export interface CreateBusinessPartnerParams extends SapBaseParams {
businessPartnerCategory: string
businessPartnerGrouping: string
firstName?: string
lastName?: string
organizationBPName1?: string
body?: Record<string, unknown> | string
}
export interface ListSalesOrdersParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetSalesOrderParams extends SapBaseParams {
salesOrder: string
select?: string
expand?: string
}
export interface CreateSalesOrderParams extends SapBaseParams {
salesOrderType: string
salesOrganization: string
distributionChannel: string
organizationDivision: string
soldToParty: string
items: string | Array<Record<string, unknown>>
body?: Record<string, unknown> | string
}
export interface ListProductsParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetProductParams extends SapBaseParams {
product: string
select?: string
expand?: string
}
export interface ListPurchaseOrdersParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetPurchaseOrderParams extends SapBaseParams {
purchaseOrder: string
select?: string
expand?: string
}
export interface CreatePurchaseOrderParams extends SapBaseParams {
purchaseOrderType: string
companyCode: string
purchasingOrganization: string
purchasingGroup: string
supplier: string
body?: Record<string, unknown> | string
}
export interface ListSupplierInvoicesParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetSupplierInvoiceParams extends SapBaseParams {
supplierInvoice: string
fiscalYear: string
select?: string
expand?: string
}
export interface ListOutboundDeliveriesParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetOutboundDeliveryParams extends SapBaseParams {
deliveryDocument: string
select?: string
expand?: string
}
export interface ListBillingDocumentsParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetBillingDocumentParams extends SapBaseParams {
billingDocument: string
select?: string
expand?: string
}
export interface ListPurchaseRequisitionsParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetPurchaseRequisitionParams extends SapBaseParams {
purchaseRequisition: string
select?: string
expand?: string
}
export interface ListMaterialStockParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface ListSuppliersParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetSupplierParams extends SapBaseParams {
supplier: string
select?: string
expand?: string
}
export interface ListCustomersParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetCustomerParams extends SapBaseParams {
customer: string
select?: string
expand?: string
}
export interface ListInboundDeliveriesParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface GetInboundDeliveryParams extends SapBaseParams {
deliveryDocument: string
select?: string
expand?: string
}
export interface ListMaterialDocumentsParams extends SapBaseParams {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}
export interface UpdateBusinessPartnerParams extends SapBaseParams {
businessPartner: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface UpdateCustomerParams extends SapBaseParams {
customer: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface UpdateSupplierParams extends SapBaseParams {
supplier: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface UpdateProductParams extends SapBaseParams {
product: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface UpdateSalesOrderParams extends SapBaseParams {
salesOrder: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface DeleteSalesOrderParams extends SapBaseParams {
salesOrder: string
ifMatch?: string
}
export interface UpdatePurchaseOrderParams extends SapBaseParams {
purchaseOrder: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface UpdatePurchaseRequisitionParams extends SapBaseParams {
purchaseRequisition: string
body: Record<string, unknown> | string
ifMatch?: string
}
export interface CreatePurchaseRequisitionParams extends SapBaseParams {
purchaseRequisitionType: string
items: string | Array<Record<string, unknown>>
body?: Record<string, unknown> | string
}
export type ODataMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'MERGE'
export interface ODataQueryParams extends SapBaseParams {
service: string
path: string
method?: ODataMethod
query?: string | Record<string, string | number | boolean>
body?: Record<string, unknown> | string
ifMatch?: string
}

View File

@@ -0,0 +1,128 @@
import type { SapProxyResponse, UpdateBusinessPartnerParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updateBusinessPartnerTool: ToolConfig<UpdateBusinessPartnerParams, SapProxyResponse> =
{
id: 'sap_s4hana_update_business_partner',
name: 'SAP S/4HANA Update Business Partner',
description:
'Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
businessPartner: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BusinessPartner key to update (string, up to 10 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_BusinessPartner fields to update (e.g., {"FirstName":"Jane","SearchTerm1":"VIP"})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: `/A_BusinessPartner(${quoteOdataKey(params.businessPartner)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description: 'Null on 204 success, or updated A_BusinessPartner entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,127 @@
import type { SapProxyResponse, UpdateCustomerParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updateCustomerTool: ToolConfig<UpdateCustomerParams, SapProxyResponse> = {
id: 'sap_s4hana_update_customer',
name: 'SAP S/4HANA Update Customer',
description:
'Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Customer PATCH is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlock, BillingIsBlockedForCustomer, PostingIsBlocked, and DeletionIndicator. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
customer: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Customer key to update (string, up to 10 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_Customer fields to update (e.g., {"OrderIsBlockedForCustomer":true,"DeletionIndicator":false})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: `/A_Customer(${quoteOdataKey(params.customer)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description: 'Null on 204 success, or updated A_Customer entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,127 @@
import type { SapProxyResponse, UpdateProductParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updateProductTool: ToolConfig<UpdateProductParams, SapProxyResponse> = {
id: 'sap_s4hana_update_product',
name: 'SAP S/4HANA Update Product',
description:
'Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PATCH only sends the fields you provide; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV PATCH/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
product: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Product key to update (string, up to 40 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_Product fields to update (e.g., {"ProductGroup":"L001","IsMarkedForDeletion":false})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_PRODUCT_SRV',
path: `/A_Product(${quoteOdataKey(params.product)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description: 'Null on 204 success, or updated A_Product entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,127 @@
import type { SapProxyResponse, UpdatePurchaseOrderParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updatePurchaseOrderTool: ToolConfig<UpdatePurchaseOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_update_purchase_order',
name: 'SAP S/4HANA Update Purchase Order',
description:
'Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
purchaseOrder: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchaseOrder key to update (string, up to 10 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_PurchaseOrder fields to update (e.g., {"PurchasingGroup":"002","PurchaseOrderDate":"/Date(1735689600000)/"})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_PURCHASEORDER_PROCESS_SRV',
path: `/A_PurchaseOrder(${quoteOdataKey(params.purchaseOrder)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description: 'Null on 204 success, or updated A_PurchaseOrder entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,131 @@
import type { SapProxyResponse, UpdatePurchaseRequisitionParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updatePurchaseRequisitionTool: ToolConfig<
UpdatePurchaseRequisitionParams,
SapProxyResponse
> = {
id: 'sap_s4hana_update_purchase_requisition',
name: 'SAP S/4HANA Update Purchase Requisition',
description:
'Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
purchaseRequisition: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'PurchaseRequisition key to update (string, up to 10 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_PurchaseRequisitionHeader fields to update (e.g., {"PurchaseRequisitionType":"NB"})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_PURCHASEREQ_PROCESS_SRV',
path: `/A_PurchaseRequisitionHeader(${quoteOdataKey(params.purchaseRequisition)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description:
'Null on 204 success, or updated A_PurchaseRequisitionHeader entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,127 @@
import type { SapProxyResponse, UpdateSalesOrderParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updateSalesOrderTool: ToolConfig<UpdateSalesOrderParams, SapProxyResponse> = {
id: 'sap_s4hana_update_sales_order',
name: 'SAP S/4HANA Update Sales Order',
description:
'Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
salesOrder: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SalesOrder key to update (string, up to 10 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_SalesOrder fields to update (e.g., {"PurchaseOrderByCustomer":"PO-12345","HeaderBillingBlockReason":"01"})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_SALES_ORDER_SRV',
path: `/A_SalesOrder(${quoteOdataKey(params.salesOrder)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description: 'Null on 204 success, or updated A_SalesOrder entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,127 @@
import type { SapProxyResponse, UpdateSupplierParams } from '@/tools/sap_s4hana/types'
import {
baseProxyBody,
parseJsonInput,
quoteOdataKey,
SAP_PROXY_URL,
transformSapProxyResponse,
} from '@/tools/sap_s4hana/utils'
import type { ToolConfig } from '@/tools/types'
export const updateSupplierTool: ToolConfig<UpdateSupplierParams, SapProxyResponse> = {
id: 'sap_s4hana_update_supplier',
name: 'SAP S/4HANA Update Supplier',
description:
'Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Supplier PATCH is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates.',
version: '1.0.0',
params: {
subdomain: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)',
},
region: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'BTP region (e.g. eu10, us10)',
},
clientId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client ID from the S/4HANA Communication Arrangement',
},
clientSecret: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'OAuth client secret from the S/4HANA Communication Arrangement',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Deployment type: cloud_public (default), cloud_private, or on_premise',
},
authType: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Authentication type: oauth_client_credentials (default) or basic',
},
baseUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Base URL of the S/4HANA host (Cloud Private / On-Premise)',
},
tokenUrl: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'OAuth token URL (Cloud Private / On-Premise + OAuth)',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Username for HTTP Basic auth',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Password for HTTP Basic auth',
},
supplier: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Supplier key to update (string, up to 10 characters)',
},
body: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'JSON object with A_Supplier fields to update (e.g., {"PaymentIsBlockedForSupplier":true,"PostingIsBlocked":true})',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'If-Match ETag for optimistic concurrency. Defaults to "*" (unconditional).',
},
},
request: {
url: SAP_PROXY_URL,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => {
const payload = parseJsonInput<Record<string, unknown>>(params.body, 'body')
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('body must be a JSON object with the fields to update')
}
return {
...baseProxyBody(params),
service: 'API_BUSINESS_PARTNER',
path: `/A_Supplier(${quoteOdataKey(params.supplier)})`,
method: 'PATCH',
query: { $format: 'json' },
body: payload,
ifMatch: params.ifMatch || '*',
}
},
},
transformResponse: transformSapProxyResponse,
outputs: {
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
data: {
type: 'json',
description: 'Null on 204 success, or updated A_Supplier entity if SAP returns one',
},
},
}

View File

@@ -0,0 +1,90 @@
import type { SapBaseParams } from '@/tools/sap_s4hana/types'
export const SAP_PROXY_URL = '/api/tools/sap_s4hana/proxy'
export function baseProxyBody(params: SapBaseParams) {
const body: Record<string, unknown> = {}
if (params.deploymentType) body.deploymentType = params.deploymentType
if (params.authType) body.authType = params.authType
if (params.subdomain) body.subdomain = params.subdomain
if (params.region) body.region = params.region
if (params.baseUrl) body.baseUrl = params.baseUrl
if (params.tokenUrl) body.tokenUrl = params.tokenUrl
if (params.clientId) body.clientId = params.clientId
if (params.clientSecret) body.clientSecret = params.clientSecret
if (params.username) body.username = params.username
if (params.password) body.password = params.password
return body
}
export function buildOdataQuery(opts: {
filter?: string
top?: number
skip?: number
orderBy?: string
select?: string
expand?: string
}): Record<string, string | number> {
const query: Record<string, string | number> = { $format: 'json' }
if (opts.filter) query.$filter = opts.filter
if (typeof opts.top === 'number') query.$top = opts.top
if (typeof opts.skip === 'number') query.$skip = opts.skip
if (opts.orderBy) query.$orderby = opts.orderBy
if (opts.select) query.$select = opts.select
if (opts.expand) query.$expand = opts.expand
return query
}
export function buildEntityQuery(opts: {
select?: string
expand?: string
}): Record<string, string> {
const query: Record<string, string> = { $format: 'json' }
if (opts.select) query.$select = opts.select
if (opts.expand) query.$expand = opts.expand
return query
}
export function parseJsonInput<T = unknown>(input: unknown, fieldName: string): T | undefined {
if (input === undefined || input === null || input === '') {
return undefined
}
if (typeof input === 'object') return input as T
if (typeof input !== 'string') {
throw new Error(`Invalid ${fieldName}: expected JSON object or string`)
}
try {
return JSON.parse(input) as T
} catch {
throw new Error(`Invalid ${fieldName}: must be valid JSON`)
}
}
export function quoteOdataKey(value: string): string {
return `'${String(value).trim().replace(/'/g, "''")}'`
}
export interface SapProxyToolOutput {
status: number
data: unknown
}
export async function transformSapProxyResponse(
response: Response
): Promise<{ success: boolean; output: SapProxyToolOutput; error?: string }> {
const data = (await response.json().catch(() => ({}))) as {
success?: boolean
output?: SapProxyToolOutput
error?: string
status?: number
}
if (!response.ok || data.success === false) {
throw new Error(data.error || `SAP request failed: HTTP ${response.status}`)
}
return {
success: true,
output: data.output ?? { status: response.status, data: null },
}
}