Files
sage-mcp-server/spike/soap-spike-results.md
2026-03-10 16:57:26 +00:00

7.0 KiB

SOAP Spike Results — Sage X3 WSDL Validation

Date: 2026-03-10

Method

  • Created a mock WSDL (mock-x3.wsdl) replicating X3's CAdxWebServiceXmlCC structure
  • Mock uses RPC/encoded binding style with SOAP 1.1, soapenc:Array types, Adonix/WSS namespace
  • Ran a local mock SOAP server + client using soap@1.8.0 to validate all 5 questions
  • All 6 tests passed (5 questions + 1 bonus envelope inspection)

Validation Questions

Q1: Can soap parse X3's WSDL? YES

The soap library successfully parses a WSDL with:

  • style="rpc" and use="encoded" binding
  • soapenc:Array complex types (ArrayOfCAdxMessage, ArrayOfCAdxParamKeyValue, etc.)
  • Complex nested types (CAdxCallContext, CAdxResultXml)
  • Multiple operations with different message signatures

All 5 operations discovered via client.describe():

  • getDescription, read, query, save, run

Each operation's input/output types were correctly introspected including the CAdxCallContext parameter structure.

Q2: Can it construct getDescription with CAdxCallContext? YES

The generated SOAP envelope correctly contains:

<tns:getDescription>
  <callContext>
    <codeLang>ENG</codeLang>
    <codeUser></codeUser>
    <password></password>
    <poolAlias>SEED</poolAlias>
    <poolId></poolId>
    <requestConfig>adxwss.optreturn=JSON&amp;adxwss.beautify=true</requestConfig>
  </callContext>
  <publicName>SIH</publicName>
</tns:getDescription>

Key observations:

  • CAdxCallContext fields are correctly nested inside <callContext>
  • requestConfig value is properly XML-escaped (&amp;)
  • RPC namespace http://www.adonix.com/WSS is present as tns
  • Uses tns:getDescription wrapper element (RPC style, not document/literal)

Q3: Does adxwss.optreturn=JSON work? PARTIALLY TESTABLE

What we confirmed:

  • resultXml is always a string field in the SOAP response
  • The soap library passes the string value through without parsing it
  • When the server puts JSON inside resultXml, JSON.parse(resultXml) works
  • When the server puts XML inside resultXml, we need fast-xml-parser

What requires a real X3 server to fully validate:

  • Whether X3 actually returns valid JSON when adxwss.optreturn=JSON is set
  • The exact JSON structure X3 produces (field names, nesting)

Implementation approach: Always try JSON.parse(resultXml) first, fall back to fast-xml-parser.

Q4: What format does resultXml come in? STRING

Key findings from the spike:

Field Type returned by soap lib Notes
resultXml string Raw string, NOT parsed. We parse it ourselves.
status string Returns "1" not 1. Must parseInt().
messages undefined Empty arrays come back as undefined. Must default to [].
technicalInfos undefined Same as messages — default to [].

The raw SOAP response correctly HTML-encodes XML inside <resultXml>:

<resultXml>&lt;SINVOICE&gt;&lt;FLD NAME=&quot;NUM&quot;&gt;INV001&lt;/FLD&gt;&lt;/SINVOICE&gt;</resultXml>

The soap library correctly decodes this back to the original XML string.

Q5: Does Basic Auth work at HTTP level? YES

  • client.setSecurity(new soap.BasicAuthSecurity(user, password)) works
  • Auth header is added at HTTP transport level (not in SOAP body)
  • CAdxCallContext codeUser and password should remain empty strings for V12
  • The SOAP envelope correctly shows empty <codeUser></codeUser> and <password></password>

Recommendation: Use soap library

The soap@1.8.0 library is the right choice for Sage X3 SOAP integration.

Why soap library wins:

  1. Correctly parses RPC/encoded WSDL with soapenc:Array types
  2. Generates proper SOAP 1.1 envelopes with CAdxCallContext
  3. Built-in BasicAuthSecurity matches X3 V12 auth pattern
  4. client.describe() enables runtime operation discovery
  5. Handles XML entity encoding/decoding in resultXml automatically
  6. Promise-based API via *Async methods (getDescriptionAsync, readAsync, etc.)

Still needed: fast-xml-parser as companion

  • Parse resultXml content when X3 returns XML (no JSON flag, or flag unsupported)
  • Fallback for any edge cases where JSON parsing fails

Caveats discovered:

  1. status returns as string "1", not number — always parseInt(status, 10)
  2. Empty arrays (messages, technicalInfos) return as undefined — always default to []
  3. The soap lib does NOT add xsi:type or encodingStyle attributes to individual elements — X3 may or may not require these (test with real server)
  4. No soapenc:arrayType attribute on array elements in the request — monitor for compatibility

Implementation Notes

SOAP client creation pattern:

const client = await soap.createClientAsync(wsdlUrl, {});
client.setSecurity(new soap.BasicAuthSecurity(user, password));
client.setEndpoint(soapEndpoint);

Calling X3 operations:

const [response] = await client.readAsync({
  callContext: {
    codeLang: "ENG",
    codeUser: "",
    password: "",
    poolAlias: "SEED",
    poolId: "",
    requestConfig: "adxwss.optreturn=JSON",
  },
  publicName: "SIH",
  objectKeys: [{ key: "NUM", value: "INV001" }],
});

const status = parseInt(response.readReturn.status, 10);
const resultXml = response.readReturn.resultXml;
const data = tryParseJson(resultXml) ?? parseXml(resultXml);
const messages = response.readReturn.messages ?? [];

Response normalization:

function normalizeResult(raw: any): SoapResult {
  return {
    status: parseInt(raw.status, 10),
    data: tryParseJson(raw.resultXml) ?? parseXml(raw.resultXml),
    messages: (raw.messages ?? []).map(normalizeMessage),
    technicalInfos: (raw.technicalInfos ?? []).map(normalizeTechInfo),
  };
}

Fallback Plan: Raw HTTP POST

If the soap library fails with a real X3 server (e.g., missing xsi:type attributes cause rejection), the fallback is:

const envelope = `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema"
               xmlns:wss="http://www.adonix.com/WSS"
               xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
  <soap:Body>
    <wss:read soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
      <callContext xsi:type="wss:CAdxCallContext">
        <codeLang xsi:type="xsd:string">ENG</codeLang>
        <poolAlias xsi:type="xsd:string">SEED</poolAlias>
        ...
      </callContext>
      ...
    </wss:read>
  </soap:Body>
</soap:Envelope>`;

const response = await fetch(soapEndpoint, {
  method: "POST",
  headers: {
    "Content-Type": "text/xml; charset=utf-8",
    "SOAPAction": '""',
    "Authorization": `Basic ${btoa(`${user}:${password}`)}`,
  },
  body: envelope,
});

This approach gives full control over xsi:type annotations and encodingStyle attributes, but requires manual XML construction and parsing. Use only if the soap library proves incompatible with the real X3 server.