#
tokens: 48243/50000 7/825 files (page 32/61)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 32 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .DS_Store
├── .github
│   ├── dependabot.yml
│   ├── instructions
│   │   ├── mcp-node-tests.instructions.md
│   │   └── mcp-yml-tests.instructions.md
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── documentation.yml
│   │   ├── feature_request.yml
│   │   └── question.yml
│   ├── PULL_REQUEST_TEMPLATE
│   │   ├── bug_fix.md
│   │   ├── documentation.md
│   │   └── new_tool.md
│   ├── pull_request_template.md
│   └── workflows
│       ├── ci.yml
│       ├── deploy-pages.yml
│       ├── publish.yml
│       └── update-docs.yml
├── .gitignore
├── .husky
│   └── pre-commit
├── aegis.config.docs-only.json
├── aegis.config.json
├── aegis.config.with-dw.json
├── AGENTS.md
├── ai-instructions
│   ├── claude-desktop
│   │   └── claude_custom_instructions.md
│   ├── cursor
│   │   └── .cursor
│   │       └── rules
│   │           ├── debugging-workflows.mdc
│   │           ├── hooks-development.mdc
│   │           ├── isml-templates.mdc
│   │           ├── job-framework.mdc
│   │           ├── performance-optimization.mdc
│   │           ├── scapi-endpoints.mdc
│   │           ├── security-patterns.mdc
│   │           ├── sfcc-development.mdc
│   │           ├── sfra-controllers.mdc
│   │           ├── sfra-models.mdc
│   │           ├── system-objects.mdc
│   │           └── testing-patterns.mdc
│   └── github-copilot
│       └── copilot-instructions.md
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docs
│   ├── best-practices
│   │   ├── cartridge_creation.md
│   │   ├── isml_templates.md
│   │   ├── job_framework.md
│   │   ├── localserviceregistry.md
│   │   ├── ocapi_hooks.md
│   │   ├── performance.md
│   │   ├── scapi_custom_endpoint.md
│   │   ├── scapi_hooks.md
│   │   ├── security.md
│   │   ├── sfra_client_side_js.md
│   │   ├── sfra_controllers.md
│   │   ├── sfra_models.md
│   │   └── sfra_scss.md
│   ├── dw_campaign
│   │   ├── ABTest.md
│   │   ├── ABTestMgr.md
│   │   ├── ABTestSegment.md
│   │   ├── AmountDiscount.md
│   │   ├── ApproachingDiscount.md
│   │   ├── BonusChoiceDiscount.md
│   │   ├── BonusDiscount.md
│   │   ├── Campaign.md
│   │   ├── CampaignMgr.md
│   │   ├── CampaignStatusCodes.md
│   │   ├── Coupon.md
│   │   ├── CouponMgr.md
│   │   ├── CouponRedemption.md
│   │   ├── CouponStatusCodes.md
│   │   ├── Discount.md
│   │   ├── DiscountPlan.md
│   │   ├── FixedPriceDiscount.md
│   │   ├── FixedPriceShippingDiscount.md
│   │   ├── FreeDiscount.md
│   │   ├── FreeShippingDiscount.md
│   │   ├── PercentageDiscount.md
│   │   ├── PercentageOptionDiscount.md
│   │   ├── PriceBookPriceDiscount.md
│   │   ├── Promotion.md
│   │   ├── PromotionMgr.md
│   │   ├── PromotionPlan.md
│   │   ├── SlotContent.md
│   │   ├── SourceCodeGroup.md
│   │   ├── SourceCodeInfo.md
│   │   ├── SourceCodeStatusCodes.md
│   │   └── TotalFixedPriceDiscount.md
│   ├── dw_catalog
│   │   ├── Catalog.md
│   │   ├── CatalogMgr.md
│   │   ├── Category.md
│   │   ├── CategoryAssignment.md
│   │   ├── CategoryLink.md
│   │   ├── PriceBook.md
│   │   ├── PriceBookMgr.md
│   │   ├── Product.md
│   │   ├── ProductActiveData.md
│   │   ├── ProductAttributeModel.md
│   │   ├── ProductAvailabilityLevels.md
│   │   ├── ProductAvailabilityModel.md
│   │   ├── ProductInventoryList.md
│   │   ├── ProductInventoryMgr.md
│   │   ├── ProductInventoryRecord.md
│   │   ├── ProductLink.md
│   │   ├── ProductMgr.md
│   │   ├── ProductOption.md
│   │   ├── ProductOptionModel.md
│   │   ├── ProductOptionValue.md
│   │   ├── ProductPriceInfo.md
│   │   ├── ProductPriceModel.md
│   │   ├── ProductPriceTable.md
│   │   ├── ProductSearchHit.md
│   │   ├── ProductSearchModel.md
│   │   ├── ProductSearchRefinementDefinition.md
│   │   ├── ProductSearchRefinements.md
│   │   ├── ProductSearchRefinementValue.md
│   │   ├── ProductVariationAttribute.md
│   │   ├── ProductVariationAttributeValue.md
│   │   ├── ProductVariationModel.md
│   │   ├── Recommendation.md
│   │   ├── SearchModel.md
│   │   ├── SearchRefinementDefinition.md
│   │   ├── SearchRefinements.md
│   │   ├── SearchRefinementValue.md
│   │   ├── SortingOption.md
│   │   ├── SortingRule.md
│   │   ├── Store.md
│   │   ├── StoreGroup.md
│   │   ├── StoreInventoryFilter.md
│   │   ├── StoreInventoryFilterValue.md
│   │   ├── StoreMgr.md
│   │   ├── Variant.md
│   │   └── VariationGroup.md
│   ├── dw_content
│   │   ├── Content.md
│   │   ├── ContentMgr.md
│   │   ├── ContentSearchModel.md
│   │   ├── ContentSearchRefinementDefinition.md
│   │   ├── ContentSearchRefinements.md
│   │   ├── ContentSearchRefinementValue.md
│   │   ├── Folder.md
│   │   ├── Library.md
│   │   ├── MarkupText.md
│   │   └── MediaFile.md
│   ├── dw_crypto
│   │   ├── CertificateRef.md
│   │   ├── CertificateUtils.md
│   │   ├── Cipher.md
│   │   ├── Encoding.md
│   │   ├── JWE.md
│   │   ├── JWEHeader.md
│   │   ├── JWS.md
│   │   ├── JWSHeader.md
│   │   ├── KeyRef.md
│   │   ├── Mac.md
│   │   ├── MessageDigest.md
│   │   ├── SecureRandom.md
│   │   ├── Signature.md
│   │   ├── WeakCipher.md
│   │   ├── WeakMac.md
│   │   ├── WeakMessageDigest.md
│   │   ├── WeakSignature.md
│   │   └── X509Certificate.md
│   ├── dw_customer
│   │   ├── AddressBook.md
│   │   ├── AgentUserMgr.md
│   │   ├── AgentUserStatusCodes.md
│   │   ├── AuthenticationStatus.md
│   │   ├── Credentials.md
│   │   ├── Customer.md
│   │   ├── CustomerActiveData.md
│   │   ├── CustomerAddress.md
│   │   ├── CustomerCDPData.md
│   │   ├── CustomerContextMgr.md
│   │   ├── CustomerGroup.md
│   │   ├── CustomerList.md
│   │   ├── CustomerMgr.md
│   │   ├── CustomerPasswordConstraints.md
│   │   ├── CustomerPaymentInstrument.md
│   │   ├── CustomerStatusCodes.md
│   │   ├── EncryptedObject.md
│   │   ├── ExternalProfile.md
│   │   ├── OrderHistory.md
│   │   ├── ProductList.md
│   │   ├── ProductListItem.md
│   │   ├── ProductListItemPurchase.md
│   │   ├── ProductListMgr.md
│   │   ├── ProductListRegistrant.md
│   │   ├── Profile.md
│   │   └── Wallet.md
│   ├── dw_extensions.applepay
│   │   ├── ApplePayHookResult.md
│   │   └── ApplePayHooks.md
│   ├── dw_extensions.facebook
│   │   ├── FacebookFeedHooks.md
│   │   └── FacebookProduct.md
│   ├── dw_extensions.paymentrequest
│   │   ├── PaymentRequestHookResult.md
│   │   └── PaymentRequestHooks.md
│   ├── dw_extensions.payments
│   │   ├── SalesforceBancontactPaymentDetails.md
│   │   ├── SalesforceCardPaymentDetails.md
│   │   ├── SalesforceEpsPaymentDetails.md
│   │   ├── SalesforceIdealPaymentDetails.md
│   │   ├── SalesforceKlarnaPaymentDetails.md
│   │   ├── SalesforcePaymentDetails.md
│   │   ├── SalesforcePaymentIntent.md
│   │   ├── SalesforcePaymentMethod.md
│   │   ├── SalesforcePaymentRequest.md
│   │   ├── SalesforcePaymentsHooks.md
│   │   ├── SalesforcePaymentsMgr.md
│   │   ├── SalesforcePaymentsSiteConfiguration.md
│   │   ├── SalesforcePayPalOrder.md
│   │   ├── SalesforcePayPalOrderAddress.md
│   │   ├── SalesforcePayPalOrderPayer.md
│   │   ├── SalesforcePayPalPaymentDetails.md
│   │   ├── SalesforceSepaDebitPaymentDetails.md
│   │   └── SalesforceVenmoPaymentDetails.md
│   ├── dw_extensions.pinterest
│   │   ├── PinterestAvailability.md
│   │   ├── PinterestFeedHooks.md
│   │   ├── PinterestOrder.md
│   │   ├── PinterestOrderHooks.md
│   │   └── PinterestProduct.md
│   ├── dw_io
│   │   ├── CSVStreamReader.md
│   │   ├── CSVStreamWriter.md
│   │   ├── File.md
│   │   ├── FileReader.md
│   │   ├── FileWriter.md
│   │   ├── InputStream.md
│   │   ├── OutputStream.md
│   │   ├── PrintWriter.md
│   │   ├── RandomAccessFileReader.md
│   │   ├── Reader.md
│   │   ├── StringWriter.md
│   │   ├── Writer.md
│   │   ├── XMLIndentingStreamWriter.md
│   │   ├── XMLStreamConstants.md
│   │   ├── XMLStreamReader.md
│   │   └── XMLStreamWriter.md
│   ├── dw_job
│   │   ├── JobExecution.md
│   │   └── JobStepExecution.md
│   ├── dw_net
│   │   ├── FTPClient.md
│   │   ├── FTPFileInfo.md
│   │   ├── HTTPClient.md
│   │   ├── HTTPRequestPart.md
│   │   ├── Mail.md
│   │   ├── SFTPClient.md
│   │   ├── SFTPFileInfo.md
│   │   ├── WebDAVClient.md
│   │   └── WebDAVFileInfo.md
│   ├── dw_object
│   │   ├── ActiveData.md
│   │   ├── CustomAttributes.md
│   │   ├── CustomObject.md
│   │   ├── CustomObjectMgr.md
│   │   ├── Extensible.md
│   │   ├── ExtensibleObject.md
│   │   ├── Note.md
│   │   ├── ObjectAttributeDefinition.md
│   │   ├── ObjectAttributeGroup.md
│   │   ├── ObjectAttributeValueDefinition.md
│   │   ├── ObjectTypeDefinition.md
│   │   ├── PersistentObject.md
│   │   ├── SimpleExtensible.md
│   │   └── SystemObjectMgr.md
│   ├── dw_order
│   │   ├── AbstractItem.md
│   │   ├── AbstractItemCtnr.md
│   │   ├── Appeasement.md
│   │   ├── AppeasementItem.md
│   │   ├── Basket.md
│   │   ├── BasketMgr.md
│   │   ├── BonusDiscountLineItem.md
│   │   ├── CouponLineItem.md
│   │   ├── CreateAgentBasketLimitExceededException.md
│   │   ├── CreateBasketFromOrderException.md
│   │   ├── CreateCouponLineItemException.md
│   │   ├── CreateOrderException.md
│   │   ├── CreateTemporaryBasketLimitExceededException.md
│   │   ├── GiftCertificate.md
│   │   ├── GiftCertificateLineItem.md
│   │   ├── GiftCertificateMgr.md
│   │   ├── GiftCertificateStatusCodes.md
│   │   ├── Invoice.md
│   │   ├── InvoiceItem.md
│   │   ├── LineItem.md
│   │   ├── LineItemCtnr.md
│   │   ├── Order.md
│   │   ├── OrderAddress.md
│   │   ├── OrderItem.md
│   │   ├── OrderMgr.md
│   │   ├── OrderPaymentInstrument.md
│   │   ├── OrderProcessStatusCodes.md
│   │   ├── PaymentCard.md
│   │   ├── PaymentInstrument.md
│   │   ├── PaymentMethod.md
│   │   ├── PaymentMgr.md
│   │   ├── PaymentProcessor.md
│   │   ├── PaymentStatusCodes.md
│   │   ├── PaymentTransaction.md
│   │   ├── PriceAdjustment.md
│   │   ├── PriceAdjustmentLimitTypes.md
│   │   ├── ProductLineItem.md
│   │   ├── ProductShippingCost.md
│   │   ├── ProductShippingLineItem.md
│   │   ├── ProductShippingModel.md
│   │   ├── Return.md
│   │   ├── ReturnCase.md
│   │   ├── ReturnCaseItem.md
│   │   ├── ReturnItem.md
│   │   ├── Shipment.md
│   │   ├── ShipmentShippingCost.md
│   │   ├── ShipmentShippingModel.md
│   │   ├── ShippingLineItem.md
│   │   ├── ShippingLocation.md
│   │   ├── ShippingMethod.md
│   │   ├── ShippingMgr.md
│   │   ├── ShippingOrder.md
│   │   ├── ShippingOrderItem.md
│   │   ├── SumItem.md
│   │   ├── TaxGroup.md
│   │   ├── TaxItem.md
│   │   ├── TaxMgr.md
│   │   ├── TrackingInfo.md
│   │   └── TrackingRef.md
│   ├── dw_order.hooks
│   │   ├── CalculateHooks.md
│   │   ├── OrderHooks.md
│   │   ├── PaymentHooks.md
│   │   ├── ReturnHooks.md
│   │   └── ShippingOrderHooks.md
│   ├── dw_rpc
│   │   ├── SOAPUtil.md
│   │   ├── Stub.md
│   │   └── WebReference.md
│   ├── dw_suggest
│   │   ├── BrandSuggestions.md
│   │   ├── CategorySuggestions.md
│   │   ├── ContentSuggestions.md
│   │   ├── CustomSuggestions.md
│   │   ├── ProductSuggestions.md
│   │   ├── SearchPhraseSuggestions.md
│   │   ├── SuggestedCategory.md
│   │   ├── SuggestedContent.md
│   │   ├── SuggestedPhrase.md
│   │   ├── SuggestedProduct.md
│   │   ├── SuggestedTerm.md
│   │   ├── SuggestedTerms.md
│   │   ├── Suggestions.md
│   │   └── SuggestModel.md
│   ├── dw_svc
│   │   ├── FTPService.md
│   │   ├── FTPServiceDefinition.md
│   │   ├── HTTPFormService.md
│   │   ├── HTTPFormServiceDefinition.md
│   │   ├── HTTPService.md
│   │   ├── HTTPServiceDefinition.md
│   │   ├── LocalServiceRegistry.md
│   │   ├── Result.md
│   │   ├── Service.md
│   │   ├── ServiceCallback.md
│   │   ├── ServiceConfig.md
│   │   ├── ServiceCredential.md
│   │   ├── ServiceDefinition.md
│   │   ├── ServiceProfile.md
│   │   ├── ServiceRegistry.md
│   │   ├── SOAPService.md
│   │   └── SOAPServiceDefinition.md
│   ├── dw_system
│   │   ├── AgentUserStatusCodes.md
│   │   ├── Cache.md
│   │   ├── CacheMgr.md
│   │   ├── HookMgr.md
│   │   ├── InternalObject.md
│   │   ├── JobProcessMonitor.md
│   │   ├── Log.md
│   │   ├── Logger.md
│   │   ├── LogNDC.md
│   │   ├── OrganizationPreferences.md
│   │   ├── Pipeline.md
│   │   ├── PipelineDictionary.md
│   │   ├── RemoteInclude.md
│   │   ├── Request.md
│   │   ├── RequestHooks.md
│   │   ├── Response.md
│   │   ├── RESTErrorResponse.md
│   │   ├── RESTResponseMgr.md
│   │   ├── RESTSuccessResponse.md
│   │   ├── SearchStatus.md
│   │   ├── Session.md
│   │   ├── Site.md
│   │   ├── SitePreferences.md
│   │   ├── Status.md
│   │   ├── StatusItem.md
│   │   ├── System.md
│   │   └── Transaction.md
│   ├── dw_util
│   │   ├── ArrayList.md
│   │   ├── Assert.md
│   │   ├── BigInteger.md
│   │   ├── Bytes.md
│   │   ├── Calendar.md
│   │   ├── Collection.md
│   │   ├── Currency.md
│   │   ├── DateUtils.md
│   │   ├── Decimal.md
│   │   ├── FilteringCollection.md
│   │   ├── Geolocation.md
│   │   ├── HashMap.md
│   │   ├── HashSet.md
│   │   ├── Iterator.md
│   │   ├── LinkedHashMap.md
│   │   ├── LinkedHashSet.md
│   │   ├── List.md
│   │   ├── Locale.md
│   │   ├── Map.md
│   │   ├── MapEntry.md
│   │   ├── MappingKey.md
│   │   ├── MappingMgr.md
│   │   ├── PropertyComparator.md
│   │   ├── SecureEncoder.md
│   │   ├── SecureFilter.md
│   │   ├── SeekableIterator.md
│   │   ├── Set.md
│   │   ├── SortedMap.md
│   │   ├── SortedSet.md
│   │   ├── StringUtils.md
│   │   ├── Template.md
│   │   └── UUIDUtils.md
│   ├── dw_value
│   │   ├── EnumValue.md
│   │   ├── MimeEncodedText.md
│   │   ├── Money.md
│   │   └── Quantity.md
│   ├── dw_web
│   │   ├── ClickStream.md
│   │   ├── ClickStreamEntry.md
│   │   ├── Cookie.md
│   │   ├── Cookies.md
│   │   ├── CSRFProtection.md
│   │   ├── Form.md
│   │   ├── FormAction.md
│   │   ├── FormElement.md
│   │   ├── FormElementValidationResult.md
│   │   ├── FormField.md
│   │   ├── FormFieldOption.md
│   │   ├── FormFieldOptions.md
│   │   ├── FormGroup.md
│   │   ├── FormList.md
│   │   ├── FormListItem.md
│   │   ├── Forms.md
│   │   ├── HttpParameter.md
│   │   ├── HttpParameterMap.md
│   │   ├── LoopIterator.md
│   │   ├── PageMetaData.md
│   │   ├── PageMetaTag.md
│   │   ├── PagingModel.md
│   │   ├── Resource.md
│   │   ├── URL.md
│   │   ├── URLAction.md
│   │   ├── URLParameter.md
│   │   ├── URLRedirect.md
│   │   ├── URLRedirectMgr.md
│   │   └── URLUtils.md
│   ├── sfra
│   │   ├── account.md
│   │   ├── address.md
│   │   ├── billing.md
│   │   ├── cart.md
│   │   ├── categories.md
│   │   ├── content.md
│   │   ├── locale.md
│   │   ├── order.md
│   │   ├── payment.md
│   │   ├── price-default.md
│   │   ├── price-range.md
│   │   ├── price-tiered.md
│   │   ├── product-bundle.md
│   │   ├── product-full.md
│   │   ├── product-line-items.md
│   │   ├── product-search.md
│   │   ├── product-tile.md
│   │   ├── querystring.md
│   │   ├── render.md
│   │   ├── request.md
│   │   ├── response.md
│   │   ├── server.md
│   │   ├── shipping.md
│   │   ├── store.md
│   │   ├── stores.md
│   │   └── totals.md
│   └── TopLevel
│       ├── APIException.md
│       ├── arguments.md
│       ├── Array.md
│       ├── ArrayBuffer.md
│       ├── BigInt.md
│       ├── Boolean.md
│       ├── ConversionError.md
│       ├── DataView.md
│       ├── Date.md
│       ├── Error.md
│       ├── ES6Iterator.md
│       ├── EvalError.md
│       ├── Fault.md
│       ├── Float32Array.md
│       ├── Float64Array.md
│       ├── Function.md
│       ├── Generator.md
│       ├── global.md
│       ├── Int16Array.md
│       ├── Int32Array.md
│       ├── Int8Array.md
│       ├── InternalError.md
│       ├── IOError.md
│       ├── Iterable.md
│       ├── Iterator.md
│       ├── JSON.md
│       ├── Map.md
│       ├── Math.md
│       ├── Module.md
│       ├── Namespace.md
│       ├── Number.md
│       ├── Object.md
│       ├── QName.md
│       ├── RangeError.md
│       ├── ReferenceError.md
│       ├── RegExp.md
│       ├── Set.md
│       ├── StopIteration.md
│       ├── String.md
│       ├── Symbol.md
│       ├── SyntaxError.md
│       ├── SystemError.md
│       ├── TypeError.md
│       ├── Uint16Array.md
│       ├── Uint32Array.md
│       ├── Uint8Array.md
│       ├── Uint8ClampedArray.md
│       ├── URIError.md
│       ├── WeakMap.md
│       ├── WeakSet.md
│       ├── XML.md
│       ├── XMLList.md
│       └── XMLStreamError.md
├── docs-site
│   ├── .gitignore
│   ├── App.tsx
│   ├── components
│   │   ├── Badge.tsx
│   │   ├── BreadcrumbSchema.tsx
│   │   ├── CodeBlock.tsx
│   │   ├── Collapsible.tsx
│   │   ├── ConfigBuilder.tsx
│   │   ├── ConfigHero.tsx
│   │   ├── ConfigModeTabs.tsx
│   │   ├── icons.tsx
│   │   ├── Layout.tsx
│   │   ├── LightCodeContainer.tsx
│   │   ├── NewcomerCTA.tsx
│   │   ├── NextStepsStrip.tsx
│   │   ├── OnThisPage.tsx
│   │   ├── Search.tsx
│   │   ├── SEO.tsx
│   │   ├── Sidebar.tsx
│   │   ├── StructuredData.tsx
│   │   ├── ToolCard.tsx
│   │   ├── ToolFilters.tsx
│   │   ├── Typography.tsx
│   │   └── VersionBadge.tsx
│   ├── constants.tsx
│   ├── index.html
│   ├── main.tsx
│   ├── metadata.json
│   ├── package-lock.json
│   ├── package.json
│   ├── pages
│   │   ├── AIInterfacesPage.tsx
│   │   ├── ConfigurationPage.tsx
│   │   ├── DevelopmentPage.tsx
│   │   ├── ExamplesPage.tsx
│   │   ├── FeaturesPage.tsx
│   │   ├── HomePage.tsx
│   │   ├── SecurityPage.tsx
│   │   ├── ToolsPage.tsx
│   │   └── TroubleshootingPage.tsx
│   ├── postcss.config.js
│   ├── public
│   │   ├── .well-known
│   │   │   └── security.txt
│   │   ├── 404.html
│   │   ├── android-chrome-192x192.png
│   │   ├── android-chrome-512x512.png
│   │   ├── apple-touch-icon.png
│   │   ├── explain-product-pricing-methods-no-mcp.png
│   │   ├── explain-product-pricing-methods.png
│   │   ├── favicon-16x16.png
│   │   ├── favicon-32x32.png
│   │   ├── favicon.ico
│   │   ├── llms.txt
│   │   ├── robots.txt
│   │   ├── site.webmanifest
│   │   └── sitemap.xml
│   ├── README.md
│   ├── scripts
│   │   ├── generate-search-index.js
│   │   ├── generate-sitemap.js
│   │   └── search-dev.js
│   ├── src
│   │   └── styles
│   │       ├── input.css
│   │       └── prism-theme.css
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── types.ts
│   ├── utils
│   │   ├── search.ts
│   │   └── toolsData.ts
│   └── vite.config.ts
├── eslint.config.js
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── convert-docs.js
├── SECURITY.md
├── server.json
├── src
│   ├── clients
│   │   ├── base
│   │   │   ├── http-client.ts
│   │   │   ├── oauth-token.ts
│   │   │   └── ocapi-auth-client.ts
│   │   ├── best-practices-client.ts
│   │   ├── cartridge-generation-client.ts
│   │   ├── docs
│   │   │   ├── class-content-parser.ts
│   │   │   ├── class-name-resolver.ts
│   │   │   ├── documentation-scanner.ts
│   │   │   ├── index.ts
│   │   │   └── referenced-types-extractor.ts
│   │   ├── docs-client.ts
│   │   ├── log-client.ts
│   │   ├── logs
│   │   │   ├── index.ts
│   │   │   ├── log-analyzer.ts
│   │   │   ├── log-client.ts
│   │   │   ├── log-constants.ts
│   │   │   ├── log-file-discovery.ts
│   │   │   ├── log-file-reader.ts
│   │   │   ├── log-formatter.ts
│   │   │   ├── log-processor.ts
│   │   │   ├── log-types.ts
│   │   │   └── webdav-client-manager.ts
│   │   ├── ocapi
│   │   │   ├── code-versions-client.ts
│   │   │   ├── site-preferences-client.ts
│   │   │   └── system-objects-client.ts
│   │   ├── ocapi-client.ts
│   │   └── sfra-client.ts
│   ├── config
│   │   ├── configuration-factory.ts
│   │   └── dw-json-loader.ts
│   ├── core
│   │   ├── handlers
│   │   │   ├── abstract-log-tool-handler.ts
│   │   │   ├── base-handler.ts
│   │   │   ├── best-practices-handler.ts
│   │   │   ├── cartridge-handler.ts
│   │   │   ├── client-factory.ts
│   │   │   ├── code-version-handler.ts
│   │   │   ├── docs-handler.ts
│   │   │   ├── job-log-handler.ts
│   │   │   ├── job-log-tool-config.ts
│   │   │   ├── log-handler.ts
│   │   │   ├── log-tool-config.ts
│   │   │   ├── sfra-handler.ts
│   │   │   ├── system-object-handler.ts
│   │   │   └── validation-helpers.ts
│   │   ├── server.ts
│   │   └── tool-definitions.ts
│   ├── index.ts
│   ├── main.ts
│   ├── services
│   │   ├── file-system-service.ts
│   │   ├── index.ts
│   │   └── path-service.ts
│   ├── tool-configs
│   │   ├── best-practices-tool-config.ts
│   │   ├── cartridge-tool-config.ts
│   │   ├── code-version-tool-config.ts
│   │   ├── docs-tool-config.ts
│   │   ├── job-log-tool-config.ts
│   │   ├── log-tool-config.ts
│   │   ├── sfra-tool-config.ts
│   │   └── system-object-tool-config.ts
│   ├── types
│   │   └── types.ts
│   └── utils
│       ├── cache.ts
│       ├── job-log-tool-config.ts
│       ├── job-log-utils.ts
│       ├── log-cache.ts
│       ├── log-tool-config.ts
│       ├── log-tool-constants.ts
│       ├── log-tool-utils.ts
│       ├── logger.ts
│       ├── ocapi-url-builder.ts
│       ├── path-resolver.ts
│       ├── query-builder.ts
│       ├── utils.ts
│       └── validator.ts
├── tests
│   ├── __mocks__
│   │   ├── docs-client.ts
│   │   ├── src
│   │   │   └── clients
│   │   │       └── base
│   │   │           └── http-client.js
│   │   └── webdav.js
│   ├── base-handler.test.ts
│   ├── base-http-client.test.ts
│   ├── best-practices-handler.test.ts
│   ├── cache.test.ts
│   ├── cartridge-handler.test.ts
│   ├── class-content-parser.test.ts
│   ├── class-name-resolver.test.ts
│   ├── client-factory.test.ts
│   ├── code-version-handler.test.ts
│   ├── code-versions-client.test.ts
│   ├── config.test.ts
│   ├── configuration-factory.test.ts
│   ├── docs-handler.test.ts
│   ├── documentation-scanner.test.ts
│   ├── file-system-service.test.ts
│   ├── job-log-handler.test.ts
│   ├── job-log-utils.test.ts
│   ├── log-client.test.ts
│   ├── log-handler.test.ts
│   ├── log-processor.test.ts
│   ├── logger.test.ts
│   ├── mcp
│   │   ├── AGENTS.md
│   │   ├── node
│   │   │   ├── activate-code-version-advanced.full-mode.programmatic.test.js
│   │   │   ├── code-versions.full-mode.programmatic.test.js
│   │   │   ├── generate-cartridge-structure.docs-only.programmatic.test.js
│   │   │   ├── get-available-best-practice-guides.docs-only.programmatic.test.js
│   │   │   ├── get-available-sfra-documents.programmatic.test.js
│   │   │   ├── get-best-practice-guide.docs-only.programmatic.test.js
│   │   │   ├── get-hook-reference.docs-only.programmatic.test.js
│   │   │   ├── get-job-execution-summary.full-mode.programmatic.test.js
│   │   │   ├── get-job-log-entries.full-mode.programmatic.test.js
│   │   │   ├── get-latest-debug.full-mode.programmatic.test.js
│   │   │   ├── get-latest-error.full-mode.programmatic.test.js
│   │   │   ├── get-latest-info.full-mode.programmatic.test.js
│   │   │   ├── get-latest-job-log-files.full-mode.programmatic.test.js
│   │   │   ├── get-latest-warn.full-mode.programmatic.test.js
│   │   │   ├── get-log-file-contents.full-mode.programmatic.test.js
│   │   │   ├── get-sfcc-class-documentation.docs-only.programmatic.test.js
│   │   │   ├── get-sfcc-class-info.docs-only.programmatic.test.js
│   │   │   ├── get-sfra-categories.docs-only.programmatic.test.js
│   │   │   ├── get-sfra-document.programmatic.test.js
│   │   │   ├── get-sfra-documents-by-category.docs-only.programmatic.test.js
│   │   │   ├── get-system-object-definition.full-mode.programmatic.test.js
│   │   │   ├── get-system-object-definitions.docs-only.programmatic.test.js
│   │   │   ├── get-system-object-definitions.full-mode.programmatic.test.js
│   │   │   ├── list-log-files.full-mode.programmatic.test.js
│   │   │   ├── list-sfcc-classes.docs-only.programmatic.test.js
│   │   │   ├── search-best-practices.docs-only.programmatic.test.js
│   │   │   ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js
│   │   │   ├── search-job-logs-by-name.full-mode.programmatic.test.js
│   │   │   ├── search-job-logs.full-mode.programmatic.test.js
│   │   │   ├── search-logs.full-mode.programmatic.test.js
│   │   │   ├── search-sfcc-classes.docs-only.programmatic.test.js
│   │   │   ├── search-sfcc-methods.docs-only.programmatic.test.js
│   │   │   ├── search-sfra-documentation.docs-only.programmatic.test.js
│   │   │   ├── search-site-preferences.full-mode.programmatic.test.js
│   │   │   ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js
│   │   │   ├── search-system-object-attribute-groups.full-mode.programmatic.test.js
│   │   │   ├── summarize-logs.full-mode.programmatic.test.js
│   │   │   ├── tools.docs-only.programmatic.test.js
│   │   │   └── tools.full-mode.programmatic.test.js
│   │   ├── README.md
│   │   ├── test-fixtures
│   │   │   └── dw.json
│   │   └── yaml
│   │       ├── activate-code-version.docs-only.test.mcp.yml
│   │       ├── activate-code-version.full-mode.test.mcp.yml
│   │       ├── get_latest_error.test.mcp.yml
│   │       ├── get-available-best-practice-guides.docs-only.test.mcp.yml
│   │       ├── get-available-best-practice-guides.full-mode.test.mcp.yml
│   │       ├── get-available-sfra-documents.docs-only.test.mcp.yml
│   │       ├── get-available-sfra-documents.full-mode.test.mcp.yml
│   │       ├── get-best-practice-guide.docs-only.test.mcp.yml
│   │       ├── get-best-practice-guide.full-mode.test.mcp.yml
│   │       ├── get-code-versions.docs-only.test.mcp.yml
│   │       ├── get-code-versions.full-mode.test.mcp.yml
│   │       ├── get-hook-reference.docs-only.test.mcp.yml
│   │       ├── get-hook-reference.full-mode.test.mcp.yml
│   │       ├── get-job-execution-summary.full-mode.test.mcp.yml
│   │       ├── get-job-log-entries.full-mode.test.mcp.yml
│   │       ├── get-latest-debug.full-mode.test.mcp.yml
│   │       ├── get-latest-error.full-mode.test.mcp.yml
│   │       ├── get-latest-info.full-mode.test.mcp.yml
│   │       ├── get-latest-job-log-files.full-mode.test.mcp.yml
│   │       ├── get-latest-warn.full-mode.test.mcp.yml
│   │       ├── get-log-file-contents.full-mode.test.mcp.yml
│   │       ├── get-sfcc-class-documentation.docs-only.test.mcp.yml
│   │       ├── get-sfcc-class-documentation.full-mode.test.mcp.yml
│   │       ├── get-sfcc-class-info.docs-only.test.mcp.yml
│   │       ├── get-sfcc-class-info.full-mode.test.mcp.yml
│   │       ├── get-sfra-categories.docs-only.test.mcp.yml
│   │       ├── get-sfra-categories.full-mode.test.mcp.yml
│   │       ├── get-sfra-document.docs-only.test.mcp.yml
│   │       ├── get-sfra-document.full-mode.test.mcp.yml
│   │       ├── get-sfra-documents-by-category.docs-only.test.mcp.yml
│   │       ├── get-sfra-documents-by-category.full-mode.test.mcp.yml
│   │       ├── get-system-object-definition.docs-only.test.mcp.yml
│   │       ├── get-system-object-definition.full-mode.test.mcp.yml
│   │       ├── get-system-object-definitions.docs-only.test.mcp.yml
│   │       ├── get-system-object-definitions.full-mode.test.mcp.yml
│   │       ├── list-log-files.full-mode.test.mcp.yml
│   │       ├── list-sfcc-classes.docs-only.test.mcp.yml
│   │       ├── list-sfcc-classes.full-mode.test.mcp.yml
│   │       ├── search-best-practices.docs-only.test.mcp.yml
│   │       ├── search-best-practices.full-mode.test.mcp.yml
│   │       ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml
│   │       ├── search-custom-object-attribute-definitions.test.mcp.yml
│   │       ├── search-job-logs-by-name.full-mode.test.mcp.yml
│   │       ├── search-job-logs.full-mode.test.mcp.yml
│   │       ├── search-logs.full-mode.test.mcp.yml
│   │       ├── search-sfcc-classes.docs-only.test.mcp.yml
│   │       ├── search-sfcc-classes.full-mode.test.mcp.yml
│   │       ├── search-sfcc-methods.docs-only.test.mcp.yml
│   │       ├── search-sfcc-methods.full-mode.test.mcp.yml
│   │       ├── search-sfra-documentation.docs-only.test.mcp.yml
│   │       ├── search-sfra-documentation.full-mode.test.mcp.yml
│   │       ├── search-site-preferences.docs-only.test.mcp.yml
│   │       ├── search-site-preferences.full-mode.test.mcp.yml
│   │       ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml
│   │       ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml
│   │       ├── search-system-object-attribute-groups.docs-only.test.mcp.yml
│   │       ├── search-system-object-attribute-groups.full-mode.test.mcp.yml
│   │       ├── summarize-logs.full-mode.test.mcp.yml
│   │       ├── tools.docs-only.test.mcp.yml
│   │       └── tools.full-mode.test.mcp.yml
│   ├── oauth-token.test.ts
│   ├── ocapi-auth-client.test.ts
│   ├── ocapi-client.test.ts
│   ├── path-service.test.ts
│   ├── query-builder.test.ts
│   ├── referenced-types-extractor.test.ts
│   ├── servers
│   │   ├── sfcc-mock-server
│   │   │   ├── mock-data
│   │   │   │   └── ocapi
│   │   │   │       ├── code-versions.json
│   │   │   │       ├── custom-object-attributes-customapi.json
│   │   │   │       ├── custom-object-attributes-globalsettings.json
│   │   │   │       ├── custom-object-attributes-versionhistory.json
│   │   │   │       ├── site-preferences-ccv.json
│   │   │   │       ├── site-preferences-fastforward.json
│   │   │   │       ├── site-preferences-sfra.json
│   │   │   │       ├── site-preferences-storefront.json
│   │   │   │       ├── site-preferences-system.json
│   │   │   │       ├── system-object-attribute-groups-campaign.json
│   │   │   │       ├── system-object-attribute-groups-category.json
│   │   │   │       ├── system-object-attribute-groups-order.json
│   │   │   │       ├── system-object-attribute-groups-product.json
│   │   │   │       ├── system-object-attribute-groups-sitepreferences.json
│   │   │   │       ├── system-object-attributes-customeraddress.json
│   │   │   │       ├── system-object-attributes-product-expanded.json
│   │   │   │       ├── system-object-attributes-product.json
│   │   │   │       ├── system-object-definition-category.json
│   │   │   │       ├── system-object-definition-customer.json
│   │   │   │       ├── system-object-definition-customeraddress.json
│   │   │   │       ├── system-object-definition-order.json
│   │   │   │       ├── system-object-definition-product.json
│   │   │   │       ├── system-object-definitions-old.json
│   │   │   │       └── system-object-definitions.json
│   │   │   ├── package-lock.json
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── scripts
│   │   │   │   └── setup-logs.js
│   │   │   ├── server.js
│   │   │   └── src
│   │   │       ├── app.js
│   │   │       ├── config
│   │   │       │   └── server-config.js
│   │   │       ├── middleware
│   │   │       │   ├── auth.js
│   │   │       │   ├── cors.js
│   │   │       │   └── logging.js
│   │   │       ├── routes
│   │   │       │   ├── ocapi
│   │   │       │   │   ├── code-versions-handler.js
│   │   │       │   │   ├── oauth-handler.js
│   │   │       │   │   ├── ocapi-error-utils.js
│   │   │       │   │   ├── ocapi-utils.js
│   │   │       │   │   ├── site-preferences-handler.js
│   │   │       │   │   └── system-objects-handler.js
│   │   │       │   ├── ocapi.js
│   │   │       │   └── webdav.js
│   │   │       └── utils
│   │   │           ├── mock-data-loader.js
│   │   │           └── webdav-xml.js
│   │   └── sfcc-mock-server-manager.ts
│   ├── sfcc-mock-server.test.ts
│   ├── site-preferences-client.test.ts
│   ├── system-objects-client.test.ts
│   ├── utils.test.ts
│   ├── validation-helpers.test.ts
│   └── validator.test.ts
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/docs/best-practices/scapi_hooks.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Quick Guide: Salesforce B2C Commerce SCAPI Hooks for AI Agents
  2 | 
  3 | This guide provides essential best practices and code examples for implementing Salesforce Commerce API (SCAPI) hooks. It is designed to be a quick reference for development with AI code assistants.
  4 | 
  5 | **IMPORTANT**: Before implementing SCAPI hooks, consult the **Performance and Stability Best Practices** guide from this MCP server. Review the index-friendly APIs section and job development standards to ensure your hooks follow SFCC performance requirements and avoid database-intensive operations.
  6 | 
  7 | ## 1. Core Concepts
  8 | 
  9 | SCAPI hooks are server-side scripts that intercept SCAPI requests to add custom logic. They are used to augment, validate, or modify the behavior of existing API endpoints. For creating entirely new endpoints, use Custom APIs.
 10 | 
 11 | ### Hook Types & Execution Order
 12 | 
 13 | For any state-changing request (POST, PATCH, PUT, DELETE), hooks execute in a specific order:
 14 | 
 15 | - **`before<HTTP_Method>`**: Executes before core logic. Ideal for validation, preprocessing, and authorization.
 16 | - **`after<HTTP_Method>`**: Executes after core logic succeeds and the database transaction is committed. Use for business logic side effects, like calling an external system or triggering recalculations.
 17 | - **`modify<HTTP_Method>Response`**: Executes last, after the default JSON response is generated. Use only to format the final JSON payload sent to the client.
 18 | 
 19 | ### Transactional Integrity
 20 | 
 21 | A hook's ability to modify data depends on its transactional context.
 22 | 
 23 | | Hook Type | Transactional? | Can Modify Persistent Data? | Primary Purpose |
 24 | |-----------|---------------|----------------------------|-----------------|
 25 | | `before<HTTP_Method>` | Yes | Yes | Validation & Preprocessing |
 26 | | `after<HTTP_Method>` | Yes | Yes | Business Logic & Side Effects |
 27 | | `modifyResponse` | No | No | Formatting the JSON Response |
 28 | 
 29 | > **Note**: Attempting to modify persistent data (e.g., `basket.setCustomerEmail()`) in a `modifyResponse` hook will throw an ORM TransactionException.
 30 | 
 31 | ## 2. Registration
 32 | 
 33 | Hooks must be enabled in Business Manager (Administration > Global Preferences > Feature Switches) and registered in a custom cartridge via two files.
 34 | 
 35 | ### `package.json` (Cartridge Root)
 36 | 
 37 | This file points to your hooks configuration.
 38 | 
 39 | ```json
 40 | {
 41 |   "name": "int_scapi_hooks_extension",
 42 |   "hooks": "./cartridge/hooks.json"
 43 | }
 44 | ```
 45 | 
 46 | ### `hooks.json` (e.g., `/cartridge/hooks.json`)
 47 | 
 48 | This file maps the hook extension point name to your script file.
 49 | 
 50 | ```json
 51 | {
 52 |   "hooks": [
 53 |     {
 54 |       "name": "dw.ocapi.shop.basket.items.beforePOST",
 55 |       "script": "./basket/validateItems.js"
 56 |     },
 57 |     {
 58 |       "name": "dw.ocapi.shop.customer.modifyGETResponse",
 59 |       "script": "./customer/enrichResponse.js"
 60 |     },
 61 |     {
 62 |       "name": "dw.ocapi.shop.order.afterPOST",
 63 |       "script": "./order/notifyOms.js"
 64 |     }
 65 |   ]
 66 | }
 67 | ```
 68 | ### Recommended Cartridge Structure
 69 | 
 70 | Organize hook scripts by the resource they modify for better maintainability.
 71 | 
 72 | ```
 73 | /cartridge
 74 | ├── hooks.json
 75 | └──/scripts
 76 |    └──/hooks
 77 |       ├──/basket
 78 |       │  └── validateItems.js
 79 |       ├──/customer
 80 |       │  └── enrichResponse.js
 81 |       └──/order
 82 |          └── notifyOms.js
 83 | ```
 84 | 
 85 | ## 3. Core Implementation Patterns
 86 | 
 87 | ### Script Structure (CommonJS)
 88 | 
 89 | Hook scripts are CommonJS modules. The exported function name must match the hook's method name (e.g., `afterPOST`).
 90 | 
 91 | ```javascript
 92 | 'use strict';
 93 | var Status = require('dw/system/Status');
 94 | 
 95 | /**
 96 |  * @param {dw.order.Order} order - The newly created order object.
 97 |  * @returns {dw.system.Status | void}
 98 |  */
 99 | exports.afterPOST = function (order) {
100 |     // Custom logic here
101 |     return; // Return void for success to allow hook chain to continue
102 | };
103 | ```
104 | 
105 | ### Signaling Success vs. Failure (`dw.system.Status`)
106 | 
107 | Use the Status object to control the execution flow.
108 | 
109 | **Controlled Failure**: Halts execution and rolls back the transaction. Returns an HTTP 400 error with a fault document.
110 | 
111 | ```javascript
112 | return new Status(Status.ERROR, 'YOUR_ERROR_CODE', 'A descriptive error message.');
113 | ```
114 | 
115 | **Success (Allow Chain to Continue)**: For Shopper APIs, returning void is the best practice. It allows other hooks in the cartridge path to run.
116 | 
117 | ```javascript
118 | return;
119 | ```
120 | 
121 | **Success (Terminate Chain)**: Returning `Status.OK` signals success but stops any subsequent hooks for the same extension point from running.
122 | 
123 | ```javascript
124 | return new Status(Status.OK);
125 | ```
126 | 
127 | ## 4. Code Examples
128 | 
129 | ### Example 1: Custom Validation (beforePOST)
130 | 
131 | Reject adding a restricted product to the cart for non-wholesale customers.
132 | 
133 | **Hook**: `dw.ocapi.shop.basket.items.beforePOST`
134 | 
135 | ```javascript
136 | 'use strict';
137 | var Status = require('dw/system/Status');
138 | var ProductMgr = require('dw/catalog/ProductMgr');
139 | 
140 | exports.beforePOST = function (basket, items) {
141 |     var customer = basket.customer;
142 |     var isWholesaleCustomer = customer ? customer.isMemberOfCustomerGroup('Wholesale') : false;
143 | 
144 |     for (var i = 0; i < items.length; i++) {
145 |         var product = ProductMgr.getProduct(items[i].product_id);
146 |         if (product && product.custom.isRestricted && !isWholesaleCustomer) {
147 |             var errorMessage = 'Product ' + product.ID + ' is restricted.';
148 |             return new Status(Status.ERROR, 'ITEM_RESTRICTION', errorMessage);
149 |         }
150 |     }
151 |     return; // Success
152 | };
153 | ```
154 | 
155 | ### Example 2: Enriching a Response (modifyGETResponse)
156 | 
157 | Add a calculated `c_loyaltyTier` attribute to the customer GET response.
158 | 
159 | **Hook**: `dw.ocapi.shop.customer.modifyGETResponse`
160 | 
161 | ```javascript
162 | 'use strict';
163 | var Status = require('dw/system/Status');
164 | 
165 | exports.modifyGETResponse = function (customer, customerResponse) {
166 |     var loyaltyTier = 'Standard';
167 |     if (customer.isMemberOfCustomerGroup('GoldMembers')) {
168 |         loyaltyTier = 'Gold';
169 |     }
170 |     // Add a non-persistent attribute to the JSON response
171 |     customerResponse.c_loyaltyTier = loyaltyTier;
172 |     return new Status(Status.OK);
173 | };
174 | ```
175 | 
176 | ### Example 3: External Integration (afterPOST)
177 | 
178 | Notify an external Order Management System (OMS) after an order is created. The integration is wrapped in a try/catch to prevent an OMS failure from affecting the order creation status.
179 | 
180 | **Hook**: `dw.ocapi.shop.order.afterPOST`
181 | 
182 | ```javascript
183 | 'use strict';
184 | var Status = require('dw/system/Status');
185 | var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
186 | var Logger = require('dw/system/Logger').getLogger('OmsIntegration');
187 | 
188 | exports.afterPOST = function (order) {
189 |     try {
190 |         var omsService = LocalServiceRegistry.createService('oms.http.service', { /*... service config... */ });
191 |         var payload = { orderNo: order.getOrderNo(), total: order.getTotalGrossPrice().getValue() };
192 |         var result = omsService.call({ payload: payload });
193 | 
194 |         if (!result.isOk()) {
195 |             // Log the error for monitoring, but do NOT return Status.ERROR.
196 |             // The order is already created; returning an error here would be misleading.
197 |             Logger.error('Failed to notify OMS for order {0}. Error: {1}', order.getOrderNo(), result.getErrorMessage());
198 |         }
199 |     } catch (e) {
200 |         Logger.error('Exception notifying OMS for order {0}. Exception: {1}', order.getOrderNo(), e.toString());
201 |     }
202 |     // Always return OK because the primary operation (order creation) was successful.
203 |     return new Status(Status.OK);
204 | };
205 | ```
206 | 
207 | ## 5. Key Best Practices Checklist
208 | 
209 | ### Performance
210 | 
211 | - [ ] **DON'T** perform expensive API lookups inside a hook (e.g., `ProductMgr.getProduct()`).
212 | - [ ] **DO** be aware of caching. Hooks on cacheable GET endpoints only run on a cache miss.
213 | - [ ] **DO** use the Service Framework with aggressive timeouts and circuit breaker settings for all external calls.
214 | - [ ] **DO** use the Code Profiler to measure script performance before deploying to production.
215 | 
216 | ### Security
217 | 
218 | - [ ] **DO** treat all client input as untrusted. Sanitize and validate data in before hooks.
219 | - [ ] **DO** re-authorize resource ownership. For example, in a basket hook, verify `basket.customer.ID` matches the logged-in shopper's ID.
220 | - [ ] **DON'T** use hooks to bypass the platform's built-in security model or authentication.
221 | 
222 | ### Error Handling & Resilience
223 | 
224 | - [ ] **DO** wrap all hook logic in `try/catch` blocks to prevent unhandled exceptions.
225 | - [ ] **DO** use `dw.system.Logger` with custom categories and include the `request.requestID` for easy tracing in logs.
226 | - [ ] **BE AWARE** of the Hook Circuit Breaker. If a hook fails more than 50% of the time in its last 100 executions, it will be temporarily disabled (returning HTTP 503) to protect system stability.
227 | 
228 | ## 6. Comprehensive Hook Reference
229 | 
230 | This section provides a reference list of the available hook extension points for the SCAPI Shopper APIs, organized by resource.
231 | 
232 | ### Shopper Baskets API Hooks
233 | 
234 | | API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
235 | |------------------------------|---------------------|-------------------|
236 | | `POST /baskets` | `dw.ocapi.shop.basket.beforePOST_v2` | `beforePOST_v2(basketRequest : Basket) : dw.system.Status` |
237 | | `POST /baskets` | `dw.ocapi.shop.basket.afterPOST` | `afterPOST(basket : dw.order.Basket) : dw.system.Status` |
238 | | `POST /baskets` | `dw.ocapi.shop.basket.modifyPOSTResponse` | `modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket) : dw.system.Status` |
239 | | `GET /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforeGET` | `beforeGET(basketId : String) : dw.system.Status` |
240 | | `GET /baskets/{basket_id}` | `dw.ocapi.shop.basket.modifyGETResponse` | `modifyGETResponse(basket : dw.order.Basket, basketResponse : Basket) : dw.system.Status` |
241 | | `PATCH /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforePATCH` | `beforePATCH(basket : dw.order.Basket, basketInput : Basket) : dw.system.Status` |
242 | | `PATCH /baskets/{basket_id}` | `dw.ocapi.shop.basket.afterPATCH` | `afterPATCH(basket : dw.order.Basket, basketInput : Basket) : dw.system.Status` |
243 | | `PATCH /baskets/{basket_id}` | `dw.ocapi.shop.basket.modifyPATCHResponse` | `modifyPATCHResponse(basket : dw.order.Basket, basketResponse : Basket) : dw.system.Status` |
244 | | `DELETE /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforeDELETE` | `beforeDELETE(basket : dw.order.Basket) : dw.system.Status` |
245 | | `DELETE /baskets/{basket_id}` | `dw.ocapi.shop.basket.afterDELETE` | `afterDELETE(basketId : String) : dw.system.Status` |
246 | | `POST /baskets/{basket_id}/items` | `dw.ocapi.shop.basket.items.beforePOST` | `beforePOST(basket : dw.order.Basket, items : ProductItem) : dw.system.Status` |
247 | | `POST /baskets/{basket_id}/items` | `dw.ocapi.shop.basket.items.afterPOST` | `afterPOST(basket : dw.order.Basket, items : ProductItem) : dw.system.Status` |
248 | | `POST /baskets/{basket_id}/items` | `dw.ocapi.shop.basket.items.modifyPOSTResponse` | `modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket, productItems : ProductItem) : dw.system.Status` |
249 | | `POST /baskets/{basket_id}/coupons` | `dw.ocapi.shop.basket.coupon.beforePOST` | `beforePOST(basket : dw.order.Basket, couponItem : CouponItem) : dw.system.Status` |
250 | | `POST /baskets/{basket_id}/coupons` | `dw.ocapi.shop.basket.coupon.afterPOST` | `afterPOST(basket : dw.order.Basket, couponItem : CouponItem) : dw.system.Status` |
251 | | `POST /baskets/{basket_id}/coupons` | `dw.ocapi.shop.basket.coupon.modifyPOSTResponse` | `modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket, couponRequest : CouponItem) : dw.system.Status` |
252 | | `POST /baskets/{basket_id}/payment_instruments` | `dw.ocapi.shop.basket.payment_instrument.beforePOST` | `beforePOST(basket : dw.order.Basket, paymentInstrument : BasketPaymentInstrumentRequest) : dw.system.Status` |
253 | | `POST /baskets/{basket_id}/payment_instruments` | `dw.ocapi.shop.basket.payment_instrument.afterPOST` | `afterPOST(basket : dw.order.Basket, paymentInstrument : BasketPaymentInstrumentRequest) : dw.system.Status` |
254 | | `POST /baskets/{basket_id}/payment_instruments` | `dw.ocapi.shop.basket.payment_instrument.modifyPOSTResponse` | `modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket, paymentInstrumentRequest : BasketPaymentInstrumentRequest) : dw.system.Status` |
255 | | Various | `dw.ocapi.shop.basket.validateBasket` | `validateBasket(basketResponse : Basket, duringSubmit : Boolean) : dw.system.Status` |
256 | 
257 | ### Shopper Customers API Hooks
258 | 
259 | | API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
260 | |------------------------------|---------------------|-------------------|
261 | | `POST /customers` | `dw.ocapi.shop.customer.beforePOST` | `beforePOST(registration : CustomerRegistration) : dw.system.Status` |
262 | | `POST /customers` | `dw.ocapi.shop.customer.afterPOST` | `afterPOST(customer : dw.customer.Customer, registration : CustomerRegistration) : dw.system.Status` |
263 | | `POST /customers` | `dw.ocapi.shop.customer.modifyPOSTResponse` | `modifyPOSTResponse(customer : dw.customer.Customer, customerResponse : Customer) : dw.system.Status` |
264 | | `GET /customers/{customer_id}` | `dw.ocapi.shop.customer.beforeGET` | `beforeGET(customerId : String) : dw.system.Status` |
265 | | `GET /customers/{customer_id}` | `dw.ocapi.shop.customer.modifyGETResponse` | `modifyGETResponse(customer : dw.customer.Customer, customerResponse : Customer) : dw.system.Status` |
266 | | `PATCH /customers/{customer_id}` | `dw.ocapi.shop.customer.beforePATCH` | `beforePATCH(customer : dw.customer.Customer, customerInput : Customer) : dw.system.Status` |
267 | | `PATCH /customers/{customer_id}` | `dw.ocapi.shop.customer.afterPATCH` | `afterPATCH(customer : dw.customer.Customer, customerInput : Customer) : dw.system.Status` |
268 | | `PATCH /customers/{customer_id}` | `dw.ocapi.shop.customer.modifyPATCHResponse` | `modifyPATCHResponse(customer : dw.customer.Customer, customerResponse : Customer) : dw.system.Status` |
269 | | `POST /customers/auth` | `dw.ocapi.shop.auth.beforePOST` | `beforePOST(authorizationHeader : String, authRequestType : dw.value.EnumValue) : dw.system.Status` |
270 | | `POST /customers/auth` | `dw.ocapi.shop.auth.afterPOST` | `afterPOST(customer : dw.customer.Customer, authRequestType : dw.value.EnumValue) : dw.system.Status` |
271 | | `POST /customers/auth` | `dw.ocapi.shop.auth.modifyPOSTResponse` | `modifyPOSTResponse(customer : dw.customer.Customer, customerResponse : Customer, authRequestType : dw.value.EnumValue) : dw.system.Status` |
272 | | `PATCH /customers/{customer_id}/addresses/{address_name}` | `dw.ocapi.shop.customer.address.beforePATCH` | `beforePATCH(customer : dw.customer.Customer, addressName : String, customerAddress : CustomerAddress) : dw.system.Status` |
273 | | `PATCH /customers/{customer_id}/addresses/{address_name}` | `dw.ocapi.shop.customer.address.afterPATCH` | `afterPATCH(customer : dw.customer.Customer, addressName : String, customerAddress : CustomerAddress) : dw.system.Status` |
274 | 
275 | ### Shopper Orders API Hooks
276 | 
277 | | API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
278 | |------------------------------|---------------------|-------------------|
279 | | `POST /orders` | `dw.ocapi.shop.order.beforePOST` | `beforePOST(basket : dw.order.Basket) : dw.system.Status` |
280 | | `POST /orders` | `dw.ocapi.shop.order.afterPOST` | `afterPOST(order : dw.order.Order) : dw.system.Status` |
281 | | `POST /orders` | `dw.ocapi.shop.order.modifyPOSTResponse` | `modifyPOSTResponse(order : dw.order.Order, orderResponse : Order) : dw.system.Status` |
282 | | `GET /orders/{order_no}` | `dw.ocapi.shop.order.beforeGET` | `beforeGET(orderNo : String) : dw.system.Status` |
283 | | `GET /orders/{order_no}` | `dw.ocapi.shop.order.modifyGETResponse` | `modifyGETResponse(order : dw.order.Order, orderResponse : Order) : dw.system.Status` |
284 | | `PATCH /orders/{order_no}` | `dw.ocapi.shop.order.beforePATCH` | `beforePATCH(order : dw.order.Order, orderInput : Order) : dw.system.Status` |
285 | | `PATCH /orders/{order_no}` | `dw.ocapi.shop.order.afterPATCH` | `afterPATCH(order : dw.order.Order, orderInput : Order) : dw.system.Status` |
286 | | `PATCH /orders/{order_no}` | `dw.ocapi.shop.order.modifyPATCHResponse` | `modifyPATCHResponse(order : dw.order.Order, orderResponse : Order) : dw.system.Status` |
287 | 
288 | ### Other Key Shopper API Hooks
289 | 
290 | | API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
291 | |------------------------------|---------------------|-------------------|
292 | | `GET /products/{id}` | `dw.ocapi.shop.product.beforeGET` | `beforeGET(productId : String) : dw.system.Status` |
293 | | `GET /products/{id}` | `dw.ocapi.shop.product.modifyGETResponse` | `modifyGETResponse(scriptProduct : dw.catalog.Product, doc : Product) : dw.system.Status` |
294 | | `GET /product_search` | `dw.ocapi.shop.product_search.beforeGET` | `beforeGET() : dw.system.Status` |
295 | | `GET /product_search` | `dw.ocapi.shop.product_search.modifyGETResponse` | `modifyGETResponse(doc : ProductSearchResult) : dw.system.Status` |
296 | | `GET /categories/{id}` | `dw.ocapi.shop.category.beforeGET` | `beforeGET(categoryId : String) : dw.system.Status` |
297 | | `GET /categories/{id}` | `dw.ocapi.shop.category.modifyGETResponse` | `modifyGETResponse(scriptCategory : dw.catalog.Category, doc : Category) : dw.system.Status` |
298 | 
299 | ## Troubleshooting Hook Registration
300 | 
301 | **If SCAPI hooks are not executing after deployment:**
302 | 
303 | 1. **Verify Feature Switches**: Ensure hooks are enabled in Business Manager (Administration > Global Preferences > Feature Switches)
304 | 2. **Check Code Version**: If hooks still don't execute:
305 |    - **Check Available Versions**: Use MCP `get_code_versions` tool to see all code versions on the instance
306 |    - **Activate Different Version**: Use MCP `activate_code_version` tool to switch code versions
307 |    - **Alternative Manual Method**: Switch code versions in Business Manager (Administration > Site Development > Code Deployment > Activate)
308 | 3. **Verify Hook Registration**: Check logs for hook registration confirmations after version activation
309 | 4. **Test Hook Execution**: Make API calls to endpoints that should trigger your hooks and verify they execute
310 | 
311 | **Common Hook Issues:**
312 | - Hooks not triggering → Check feature switches and code version activation
313 | - Hook scripts not found → Verify file paths match registration in hooks.json
314 | - Runtime errors in hooks → Check logs for specific error messages during hook execution
315 | 
```

--------------------------------------------------------------------------------
/tests/mcp/node/get-sfra-categories.docs-only.programmatic.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { test, describe, before, after, beforeEach } from 'node:test';
  2 | import { strict as assert } from 'node:assert';
  3 | import { connect } from 'mcp-aegis';
  4 | 
  5 | describe('SFCC Dev MCP - get_sfra_categories Tool (docs-only mode)', () => {
  6 |   let client;
  7 | 
  8 |   before(async () => {
  9 |     client = await connect('./aegis.config.docs-only.json');
 10 |   });
 11 | 
 12 |   after(async () => {
 13 |     if (client?.connected) {
 14 |       await client.disconnect();
 15 |     }
 16 |   });
 17 | 
 18 |   beforeEach(() => {
 19 |     // Clear all buffers to prevent test interference
 20 |     client.clearAllBuffers();
 21 |   });
 22 | 
 23 |   describe('Basic Functionality', () => {
 24 |     test('should be available in tool list', async () => {
 25 |       const tools = await client.listTools();
 26 |       const toolNames = tools.map(tool => tool.name);
 27 |       assert.ok(toolNames.includes('get_sfra_categories'), 'get_sfra_categories should be available');
 28 |     });
 29 | 
 30 |     test('should have proper tool schema', async () => {
 31 |       const tools = await client.listTools();
 32 |       const tool = tools.find(t => t.name === 'get_sfra_categories');
 33 |       
 34 |       assert.ok(tool, 'Tool should exist');
 35 |       assert.equal(tool.name, 'get_sfra_categories');
 36 |       assert.ok(tool.description, 'Tool should have description');
 37 |       assert.ok(tool.description.includes('SFRA document categories'), 'Description should mention SFRA categories');
 38 |       assert.ok(tool.inputSchema, 'Tool should have input schema');
 39 |       assert.equal(tool.inputSchema.type, 'object');
 40 |     });
 41 | 
 42 |     test('should return valid MCP response structure', async () => {
 43 |       const result = await client.callTool('get_sfra_categories', {});
 44 |       
 45 |       assert.equal(result.isError, false, 'Should not be an error');
 46 |       assert.ok(result.content, 'Should have content');
 47 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
 48 |       assert.equal(result.content[0].type, 'text', 'Content should be text type');
 49 |       assert.ok(result.content[0].text, 'Should have text content');
 50 |     });
 51 |   });
 52 | 
 53 |   describe('Response Content Validation', () => {
 54 |     test('should return valid JSON array', async () => {
 55 |       const result = await client.callTool('get_sfra_categories', {});
 56 |       
 57 |       const responseText = result.content[0].text;
 58 |       assert.doesNotThrow(() => {
 59 |         const parsed = JSON.parse(responseText);
 60 |         assert.ok(Array.isArray(parsed), 'Response should be a JSON array');
 61 |       }, 'Response should be valid JSON');
 62 |     });
 63 | 
 64 |     test('should return exactly 7 categories', async () => {
 65 |       const result = await client.callTool('get_sfra_categories', {});
 66 |       
 67 |       const categories = JSON.parse(result.content[0].text);
 68 |       assert.equal(categories.length, 7, 'Should have exactly 7 categories');
 69 |     });
 70 | 
 71 |     test('should include all expected category names', async () => {
 72 |       const result = await client.callTool('get_sfra_categories', {});
 73 |       
 74 |       const categories = JSON.parse(result.content[0].text);
 75 |       const categoryNames = categories.map(cat => cat.category).sort();
 76 |       const expectedNames = ['core', 'customer', 'order', 'other', 'pricing', 'product', 'store'];
 77 |       
 78 |       assert.deepEqual(categoryNames, expectedNames, 'Should have all expected category names');
 79 |     });
 80 | 
 81 |     test('should have proper structure for each category', async () => {
 82 |       const result = await client.callTool('get_sfra_categories', {});
 83 |       
 84 |       const categories = JSON.parse(result.content[0].text);
 85 |       categories.forEach((category, index) => {
 86 |         assert.ok(category.category, `Category ${index} should have category field`);
 87 |         assert.ok(typeof category.category === 'string', `Category ${index} name should be string`);
 88 |         assert.ok(typeof category.count === 'number', `Category ${index} count should be number`);
 89 |         assert.ok(category.count > 0, `Category ${index} count should be positive`);
 90 |         assert.ok(category.description, `Category ${index} should have description`);
 91 |         assert.ok(typeof category.description === 'string', `Category ${index} description should be string`);
 92 |         assert.ok(category.description.length > 10, `Category ${index} description should be meaningful`);
 93 |       });
 94 |     });
 95 |   });
 96 | 
 97 |   describe('Specific Category Validation', () => {
 98 |     test('should have core category with expected properties', async () => {
 99 |       const result = await client.callTool('get_sfra_categories', {});
100 |       
101 |       const categories = JSON.parse(result.content[0].text);
102 |       const coreCategory = categories.find(cat => cat.category === 'core');
103 |       
104 |       assert.ok(coreCategory, 'Should have core category');
105 |       assert.equal(coreCategory.count, 5, 'Core category should have 5 documents');
106 |       assert.ok(coreCategory.description.includes('Core SFRA classes'), 'Core description should mention SFRA classes');
107 |       assert.ok(coreCategory.description.includes('Server'), 'Core description should mention Server');
108 |       assert.ok(coreCategory.description.includes('Request'), 'Core description should mention Request');
109 |       assert.ok(coreCategory.description.includes('Response'), 'Core description should mention Response');
110 |     });
111 | 
112 |     test('should have customer category with expected properties', async () => {
113 |       const result = await client.callTool('get_sfra_categories', {});
114 |       
115 |       const categories = JSON.parse(result.content[0].text);
116 |       const customerCategory = categories.find(cat => cat.category === 'customer');
117 |       
118 |       assert.ok(customerCategory, 'Should have customer category');
119 |       assert.equal(customerCategory.count, 2, 'Customer category should have 2 documents');
120 |       assert.ok(customerCategory.description.includes('Customer account'), 'Customer description should mention account');
121 |       assert.ok(customerCategory.description.includes('address'), 'Customer description should mention address');
122 |     });
123 | 
124 |     test('should have order category with expected properties', async () => {
125 |       const result = await client.callTool('get_sfra_categories', {});
126 |       
127 |       const categories = JSON.parse(result.content[0].text);
128 |       const orderCategory = categories.find(cat => cat.category === 'order');
129 |       
130 |       assert.ok(orderCategory, 'Should have order category');
131 |       assert.equal(orderCategory.count, 6, 'Order category should have 6 documents');
132 |       assert.ok(orderCategory.description.includes('Order'), 'Order description should mention Order');
133 |       assert.ok(orderCategory.description.includes('cart'), 'Order description should mention cart');
134 |       assert.ok(orderCategory.description.includes('billing'), 'Order description should mention billing');
135 |       assert.ok(orderCategory.description.includes('shipping'), 'Order description should mention shipping');
136 |     });
137 | 
138 |     test('should have product category with expected properties', async () => {
139 |       const result = await client.callTool('get_sfra_categories', {});
140 |       
141 |       const categories = JSON.parse(result.content[0].text);
142 |       const productCategory = categories.find(cat => cat.category === 'product');
143 |       
144 |       assert.ok(productCategory, 'Should have product category');
145 |       assert.equal(productCategory.count, 5, 'Product category should have 5 documents');
146 |       assert.ok(productCategory.description.includes('Product-related'), 'Product description should mention product-related');
147 |       assert.ok(productCategory.description.includes('models'), 'Product description should mention models');
148 |     });
149 | 
150 |     test('should have pricing category with expected properties', async () => {
151 |       const result = await client.callTool('get_sfra_categories', {});
152 |       
153 |       const categories = JSON.parse(result.content[0].text);
154 |       const pricingCategory = categories.find(cat => cat.category === 'pricing');
155 |       
156 |       assert.ok(pricingCategory, 'Should have pricing category');
157 |       assert.equal(pricingCategory.count, 3, 'Pricing category should have 3 documents');
158 |       assert.ok(pricingCategory.description.includes('Pricing'), 'Pricing description should mention Pricing');
159 |       assert.ok(pricingCategory.description.includes('discount'), 'Pricing description should mention discount');
160 |     });
161 | 
162 |     test('should have store category with expected properties', async () => {
163 |       const result = await client.callTool('get_sfra_categories', {});
164 |       
165 |       const categories = JSON.parse(result.content[0].text);
166 |       const storeCategory = categories.find(cat => cat.category === 'store');
167 |       
168 |       assert.ok(storeCategory, 'Should have store category');
169 |       assert.equal(storeCategory.count, 2, 'Store category should have 2 documents');
170 |       assert.ok(storeCategory.description.includes('Store'), 'Store description should mention Store');
171 |       assert.ok(storeCategory.description.includes('location'), 'Store description should mention location');
172 |     });
173 | 
174 |     test('should have other category with expected properties', async () => {
175 |       const result = await client.callTool('get_sfra_categories', {});
176 |       
177 |       const categories = JSON.parse(result.content[0].text);
178 |       const otherCategory = categories.find(cat => cat.category === 'other');
179 |       
180 |       assert.ok(otherCategory, 'Should have other category');
181 |       assert.equal(otherCategory.count, 3, 'Other category should have 3 documents');
182 |       assert.ok(otherCategory.description.includes('Other'), 'Other description should mention Other');
183 |       assert.ok(otherCategory.description.includes('models'), 'Other description should mention models');
184 |       assert.ok(otherCategory.description.includes('utilities'), 'Other description should mention utilities');
185 |     });
186 |   });
187 | 
188 |   describe('Total Count Validation', () => {
189 |     test('should have total of 26 documents across all categories', async () => {
190 |       const result = await client.callTool('get_sfra_categories', {});
191 |       
192 |       const categories = JSON.parse(result.content[0].text);
193 |       const totalCount = categories.reduce((sum, cat) => sum + cat.count, 0);
194 |       
195 |       assert.equal(totalCount, 26, 'Total documents across all categories should be 26');
196 |     });
197 | 
198 |     test('should have expected count distribution', async () => {
199 |       const result = await client.callTool('get_sfra_categories', {});
200 |       
201 |       const categories = JSON.parse(result.content[0].text);
202 |       const countMap = {};
203 |       categories.forEach(cat => {
204 |         countMap[cat.category] = cat.count;
205 |       });
206 | 
207 |       assert.equal(countMap.core, 5, 'Core should have 5 documents');
208 |       assert.equal(countMap.customer, 2, 'Customer should have 2 documents');
209 |       assert.equal(countMap.order, 6, 'Order should have 6 documents');
210 |       assert.equal(countMap.product, 5, 'Product should have 5 documents');
211 |       assert.equal(countMap.pricing, 3, 'Pricing should have 3 documents');
212 |       assert.equal(countMap.store, 2, 'Store should have 2 documents');
213 |       assert.equal(countMap.other, 3, 'Other should have 3 documents');
214 |     });
215 |   });
216 | 
217 |   describe('Error Handling and Edge Cases', () => {
218 |     test('should handle empty parameters', async () => {
219 |       const result = await client.callTool('get_sfra_categories', {});
220 |       
221 |       assert.equal(result.isError, false, 'Should handle empty parameters without error');
222 |       const categories = JSON.parse(result.content[0].text);
223 |       assert.equal(categories.length, 7, 'Should return all categories with empty parameters');
224 |     });
225 | 
226 |     test('should ignore invalid parameters', async () => {
227 |       const result = await client.callTool('get_sfra_categories', {
228 |         invalid: 'param',
229 |         another: 'value',
230 |         numeric: 123
231 |       });
232 |       
233 |       assert.equal(result.isError, false, 'Should ignore invalid parameters without error');
234 |       const categories = JSON.parse(result.content[0].text);
235 |       assert.equal(categories.length, 7, 'Should return all categories despite invalid parameters');
236 |     });
237 | 
238 |     test('should handle special parameter values', async () => {
239 |       const result = await client.callTool('get_sfra_categories', {
240 |         null_value: null,
241 |         empty_string: '',
242 |         zero: 0,
243 |         boolean: false
244 |       });
245 |       
246 |       assert.equal(result.isError, false, 'Should handle special parameter values without error');
247 |       const categories = JSON.parse(result.content[0].text);
248 |       assert.equal(categories.length, 7, 'Should return all categories with special parameter values');
249 |     });
250 |   });
251 | 
252 | 
253 |   describe('Content Quality and Consistency', () => {
254 |     test('should have consistent JSON formatting', async () => {
255 |       const result = await client.callTool('get_sfra_categories', {});
256 |       
257 |       const responseText = result.content[0].text;
258 |       
259 |       // Should be properly formatted JSON
260 |       assert.ok(responseText.startsWith('['), 'Response should start with array bracket');
261 |       assert.ok(responseText.endsWith(']'), 'Response should end with array bracket');
262 |       assert.ok(responseText.includes('{'), 'Response should contain objects');
263 |       assert.ok(responseText.includes('}'), 'Response should contain complete objects');
264 |       
265 |       // Should have proper field formatting
266 |       assert.ok(responseText.includes('"category":'), 'Should have category fields');
267 |       assert.ok(responseText.includes('"count":'), 'Should have count fields');
268 |       assert.ok(responseText.includes('"description":'), 'Should have description fields');
269 |     });
270 | 
271 |     test('should have alphabetically sorted categories', async () => {
272 |       const result = await client.callTool('get_sfra_categories', {});
273 |       
274 |       const categories = JSON.parse(result.content[0].text);
275 |       const categoryNames = categories.map(cat => cat.category);
276 |       const sortedNames = [...categoryNames].sort();
277 |       
278 |       assert.deepEqual(categoryNames, sortedNames, 'Categories should be alphabetically sorted');
279 |     });
280 | 
281 |     test('should have meaningful descriptions for all categories', async () => {
282 |       const result = await client.callTool('get_sfra_categories', {});
283 |       
284 |       const categories = JSON.parse(result.content[0].text);
285 |       
286 |       categories.forEach(category => {
287 |         assert.ok(category.description.length >= 20, `Description for ${category.category} should be substantial`);
288 |         assert.ok(category.description.includes('models') || category.description.includes('classes') || category.description.includes('utilities'), 
289 |           `Description for ${category.category} should mention relevant concepts`);
290 |         assert.ok(/^[A-Z]/.test(category.description), `Description for ${category.category} should start with capital letter`);
291 |         assert.ok(!category.description.endsWith('.'), `Description for ${category.category} should not end with period`);
292 |       });
293 |     });
294 | 
295 |     test('should have realistic count values', async () => {
296 |       const result = await client.callTool('get_sfra_categories', {});
297 |       
298 |       const categories = JSON.parse(result.content[0].text);
299 |       
300 |       categories.forEach(category => {
301 |         assert.ok(category.count >= 1, `Count for ${category.category} should be at least 1`);
302 |         assert.ok(category.count <= 10, `Count for ${category.category} should be reasonable (≤10)`);
303 |         assert.ok(Number.isInteger(category.count), `Count for ${category.category} should be an integer`);
304 |       });
305 |     });
306 |   });
307 | 
308 |   describe('Integration Validation', () => {
309 |     test('should provide categories useful for get_sfra_documents_by_category tool', async () => {
310 |       const result = await client.callTool('get_sfra_categories', {});
311 |       
312 |       const categories = JSON.parse(result.content[0].text);
313 |       const categoryNames = categories.map(cat => cat.category);
314 |       
315 |       // These categories should be usable with get_sfra_documents_by_category
316 |       const expectedCategoriesForFiltering = ['core', 'product', 'order', 'customer', 'pricing', 'store', 'other'];
317 |       expectedCategoriesForFiltering.forEach(expectedCategory => {
318 |         assert.ok(categoryNames.includes(expectedCategory), 
319 |           `Should include ${expectedCategory} category for document filtering`);
320 |       });
321 |     });
322 | 
323 |     test('should have categories that align with SFRA functional areas', async () => {
324 |       const result = await client.callTool('get_sfra_categories', {});
325 |       
326 |       const categories = JSON.parse(result.content[0].text);
327 |       
328 |       // Check that categories align with known SFRA functional areas
329 |       const coreCategory = categories.find(cat => cat.category === 'core');
330 |       const productCategory = categories.find(cat => cat.category === 'product');
331 |       const orderCategory = categories.find(cat => cat.category === 'order');
332 |       
333 |       assert.ok(coreCategory && coreCategory.count > 0, 'Should have core SFRA functionality');
334 |       assert.ok(productCategory && productCategory.count > 0, 'Should have product functionality');
335 |       assert.ok(orderCategory && orderCategory.count > 0, 'Should have order/cart functionality');
336 |     });
337 |   });
338 | 
339 |   describe('Robustness Testing', () => {
340 |     test('should handle rapid successive calls', async () => {
341 |       // Note: We execute sequentially to avoid MCP buffer conflicts
342 |       for (let i = 0; i < 3; i++) {
343 |         const result = await client.callTool('get_sfra_categories', {});
344 |         assert.equal(result.isError, false, `Rapid call ${i + 1} should succeed`);
345 |         
346 |         const categories = JSON.parse(result.content[0].text);
347 |         assert.equal(categories.length, 7, `Rapid call ${i + 1} should return correct number of categories`);
348 |       }
349 |     });
350 | 
351 |     test('should maintain state independence', async () => {
352 |       // First call
353 |       const result1 = await client.callTool('get_sfra_categories', {});
354 |       const categories1 = JSON.parse(result1.content[0].text);
355 |       
356 |       // Call with parameters (should ignore them)
357 |       const result2 = await client.callTool('get_sfra_categories', { someParam: 'value' });
358 |       const categories2 = JSON.parse(result2.content[0].text);
359 |       
360 |       // Third call without parameters
361 |       const result3 = await client.callTool('get_sfra_categories', {});
362 |       const categories3 = JSON.parse(result3.content[0].text);
363 |       
364 |       // All should be identical
365 |       assert.deepEqual(categories1, categories2, 'Results should be identical regardless of parameters');
366 |       assert.deepEqual(categories1, categories3, 'Results should be consistent across calls');
367 |     });
368 |   });
369 | });
370 | 
```

--------------------------------------------------------------------------------
/docs/dw_net/FTPClient.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Package: dw.net
  2 | 
  3 | # Class FTPClient
  4 | 
  5 | ## Inheritance Hierarchy
  6 | 
  7 | - Object
  8 |   - dw.net.FTPClient
  9 | 
 10 | ## Description
 11 | 
 12 | The FTPClient class supports the FTP commands CD, GET, PUT, DEL, MKDIR, RENAME, and LIST. The FTP connection is established using passive transfer mode (PASV). The transfer of files can be text or binary. Note: when this class is used with sensitive data, be careful in persisting sensitive information to disk. An example usage is as follows: var ftp : FTPClient = new dw.net.FTPClient(); ftp.connect("my.ftp-server.com", "username", "password"); var data : String = ftp.get("simple.txt"); ftp.disconnect(); The default connection timeout depends on the script context timeout and will be set to a maximum of 30 seconds (default script context timeout is 10 seconds within storefront requests and 15 minutes within jobs). IMPORTANT NOTE: Before you can make an outbound FTP connection, the FTP server IP address must be enabled for outbound traffic at the Commerce Cloud Digital firewall for your POD. Please file a support request to request a new firewall rule.
 13 | 
 14 | ## Constants
 15 | 
 16 | ### DEFAULT_GET_FILE_SIZE
 17 | 
 18 | **Type:** Number = 5242880
 19 | 
 20 | The default size for get() returning a File is 5MB
 21 | 
 22 | ### DEFAULT_GET_STRING_SIZE
 23 | 
 24 | **Type:** Number = 2097152
 25 | 
 26 | The default size for get() returning a String is 2MB
 27 | 
 28 | ### MAX_GET_FILE_SIZE
 29 | 
 30 | **Type:** Number = 209715200
 31 | 
 32 | The maximum size for get() returning a File is forty times the default size for getting a file. The largest file allowed is 200MB.
 33 | 
 34 | ### MAX_GET_STRING_SIZE
 35 | 
 36 | **Type:** Number = 10485760
 37 | 
 38 | The maximum size for get() returning a String is five times the default size for getting a String. The largest String allowed is 10MB.
 39 | 
 40 | ## Properties
 41 | 
 42 | ### connected
 43 | 
 44 | **Type:** boolean (Read Only)
 45 | 
 46 | Identifies if the FTP client is currently connected to the FTP server.
 47 | 
 48 | ### replyCode
 49 | 
 50 | **Type:** Number (Read Only)
 51 | 
 52 | The reply code from the last FTP action.
 53 | 
 54 | ### replyMessage
 55 | 
 56 | **Type:** String (Read Only)
 57 | 
 58 | The string message from the last FTP action.
 59 | 
 60 | ### timeout
 61 | 
 62 | **Type:** Number
 63 | 
 64 | The timeout for this client, in milliseconds.
 65 | 
 66 | ## Constructor Summary
 67 | 
 68 | FTPClient() Constructs the FTPClient instance.
 69 | 
 70 | ## Method Summary
 71 | 
 72 | ### cd
 73 | 
 74 | **Signature:** `cd(path : String) : boolean`
 75 | 
 76 | Changes the current directory on the remote server to the given path.
 77 | 
 78 | ### connect
 79 | 
 80 | **Signature:** `connect(host : String) : boolean`
 81 | 
 82 | Connects and logs on to an FTP Server as "anonymous" and returns a boolean indicating success or failure.
 83 | 
 84 | ### connect
 85 | 
 86 | **Signature:** `connect(host : String, user : String, password : String) : boolean`
 87 | 
 88 | Connects and logs on to an FTP server and returns a boolean indicating success or failure.
 89 | 
 90 | ### connect
 91 | 
 92 | **Signature:** `connect(host : String, port : Number) : boolean`
 93 | 
 94 | Connects and logs on to an FTP Server as "anonymous" and returns a boolean indicating success or failure.
 95 | 
 96 | ### connect
 97 | 
 98 | **Signature:** `connect(host : String, port : Number, user : String, password : String) : boolean`
 99 | 
100 | Connects and logs on to an FTP server and returns a boolean indicating success or failure.
101 | 
102 | ### del
103 | 
104 | **Signature:** `del(path : String) : boolean`
105 | 
106 | Deletes the remote file on the server identified by the path parameter.
107 | 
108 | ### disconnect
109 | 
110 | **Signature:** `disconnect() : void`
111 | 
112 | The method first logs the current user out from the server and then disconnects from the server.
113 | 
114 | ### get
115 | 
116 | **Signature:** `get(path : String) : String`
117 | 
118 | Reads the content of a remote file and returns it as a string using "ISO-8859-1" encoding to read it.
119 | 
120 | ### get
121 | 
122 | **Signature:** `get(path : String, encoding : String) : String`
123 | 
124 | Reads the content of a remote file and returns it as string using the passed encoding.
125 | 
126 | ### get
127 | 
128 | **Signature:** `get(path : String, maxGetSize : Number) : String`
129 | 
130 | Reads the content of a remote file and returns it as a string using "ISO-8859-1" encoding to read it.
131 | 
132 | ### get
133 | 
134 | **Signature:** `get(path : String, encoding : String, maxGetSize : Number) : String`
135 | 
136 | Reads the content of a remote file and returns it as a string using the specified encoding.
137 | 
138 | ### get
139 | 
140 | **Signature:** `get(path : String, encoding : String, file : File) : boolean`
141 | 
142 | Reads the content of a remote file and creates a local copy in the given file using the passed string encoding to read the file content and using the system standard encoding "UTF-8" to write the file.
143 | 
144 | ### get
145 | 
146 | **Signature:** `get(path : String, encoding : String, file : File, maxGetSize : Number) : boolean`
147 | 
148 | Reads the content of a remote file and creates a local copy in the given file using the passed string encoding to read the file content and using the system standard encoding "UTF-8" to write the file.
149 | 
150 | ### getBinary
151 | 
152 | **Signature:** `getBinary(path : String, file : File) : boolean`
153 | 
154 | Reads the content of a remote file and creates a local copy in the given file.
155 | 
156 | ### getBinary
157 | 
158 | **Signature:** `getBinary(path : String, file : File, maxGetSize : Number) : boolean`
159 | 
160 | Reads the content of a remote file and creates a local copy in the given file.
161 | 
162 | ### getConnected
163 | 
164 | **Signature:** `getConnected() : boolean`
165 | 
166 | Identifies if the FTP client is currently connected to the FTP server.
167 | 
168 | ### getReplyCode
169 | 
170 | **Signature:** `getReplyCode() : Number`
171 | 
172 | Returns the reply code from the last FTP action.
173 | 
174 | ### getReplyMessage
175 | 
176 | **Signature:** `getReplyMessage() : String`
177 | 
178 | Returns the string message from the last FTP action.
179 | 
180 | ### getTimeout
181 | 
182 | **Signature:** `getTimeout() : Number`
183 | 
184 | Returns the timeout for this client, in milliseconds.
185 | 
186 | ### list
187 | 
188 | **Signature:** `list() : FTPFileInfo[]`
189 | 
190 | Returns a list of FTPFileInfo objects containing information about the files in the current directory.
191 | 
192 | ### list
193 | 
194 | **Signature:** `list(path : String) : FTPFileInfo[]`
195 | 
196 | Returns a list of FTPFileInfo objects containing information about the files in the remote directory defined by the given path.
197 | 
198 | ### mkdir
199 | 
200 | **Signature:** `mkdir(path : String) : boolean`
201 | 
202 | Creates a directory
203 | 
204 | ### put
205 | 
206 | **Signature:** `put(path : String, content : String) : boolean`
207 | 
208 | Puts the specified content to the specified full path using "ISO-8859-1" encoding.
209 | 
210 | ### put
211 | 
212 | **Signature:** `put(path : String, content : String, encoding : String) : boolean`
213 | 
214 | Put the given content to a file on the given full path on the FTP server.
215 | 
216 | ### putBinary
217 | 
218 | **Signature:** `putBinary(path : String, file : File) : boolean`
219 | 
220 | Put the content of the given file into a file on the remote FTP server with the given full path.
221 | 
222 | ### removeDirectory
223 | 
224 | **Signature:** `removeDirectory(path : String) : boolean`
225 | 
226 | Deletes the remote directory on the server identified by the path parameter.
227 | 
228 | ### rename
229 | 
230 | **Signature:** `rename(from : String, to : String) : boolean`
231 | 
232 | Renames an existing file.
233 | 
234 | ### setTimeout
235 | 
236 | **Signature:** `setTimeout(timeoutMillis : Number) : void`
237 | 
238 | Sets the timeout for connections made with the FTP client to the given number of milliseconds.
239 | 
240 | ## Constructor Detail
241 | 
242 | ## Method Detail
243 | 
244 | ## Method Details
245 | 
246 | ### cd
247 | 
248 | **Signature:** `cd(path : String) : boolean`
249 | 
250 | **Description:** Changes the current directory on the remote server to the given path.
251 | 
252 | **Parameters:**
253 | 
254 | - `path`: the new current directory
255 | 
256 | **Returns:**
257 | 
258 | true if the directory change was okay
259 | 
260 | ---
261 | 
262 | ### connect
263 | 
264 | **Signature:** `connect(host : String) : boolean`
265 | 
266 | **Description:** Connects and logs on to an FTP Server as "anonymous" and returns a boolean indicating success or failure.
267 | 
268 | **Parameters:**
269 | 
270 | - `host`: Name of the FTP sever
271 | 
272 | **Returns:**
273 | 
274 | true when connection is successful, false otherwise.
275 | 
276 | ---
277 | 
278 | ### connect
279 | 
280 | **Signature:** `connect(host : String, user : String, password : String) : boolean`
281 | 
282 | **Description:** Connects and logs on to an FTP server and returns a boolean indicating success or failure.
283 | 
284 | **Parameters:**
285 | 
286 | - `host`: Name of the FTP sever
287 | - `user`: Username for the login
288 | - `password`: Password for the login
289 | 
290 | **Returns:**
291 | 
292 | true when connection is successful, false otherwise.
293 | 
294 | ---
295 | 
296 | ### connect
297 | 
298 | **Signature:** `connect(host : String, port : Number) : boolean`
299 | 
300 | **Description:** Connects and logs on to an FTP Server as "anonymous" and returns a boolean indicating success or failure.
301 | 
302 | **Parameters:**
303 | 
304 | - `host`: Name of the FTP sever
305 | - `port`: Port for FTP server
306 | 
307 | **Returns:**
308 | 
309 | true when connection is successful, false otherwise.
310 | 
311 | ---
312 | 
313 | ### connect
314 | 
315 | **Signature:** `connect(host : String, port : Number, user : String, password : String) : boolean`
316 | 
317 | **Description:** Connects and logs on to an FTP server and returns a boolean indicating success or failure.
318 | 
319 | **Parameters:**
320 | 
321 | - `host`: Name of the FTP sever
322 | - `port`: Port for FTP server
323 | - `user`: Username for the login
324 | - `password`: Password for the login
325 | 
326 | **Returns:**
327 | 
328 | true when connection is successful, false otherwise.
329 | 
330 | ---
331 | 
332 | ### del
333 | 
334 | **Signature:** `del(path : String) : boolean`
335 | 
336 | **Description:** Deletes the remote file on the server identified by the path parameter.
337 | 
338 | **Parameters:**
339 | 
340 | - `path`: the path to the file.
341 | 
342 | **Returns:**
343 | 
344 | true if the file was successfully deleted, false otherwise.
345 | 
346 | ---
347 | 
348 | ### disconnect
349 | 
350 | **Signature:** `disconnect() : void`
351 | 
352 | **Description:** The method first logs the current user out from the server and then disconnects from the server.
353 | 
354 | ---
355 | 
356 | ### get
357 | 
358 | **Signature:** `get(path : String) : String`
359 | 
360 | **Description:** Reads the content of a remote file and returns it as a string using "ISO-8859-1" encoding to read it. Read at most MAX_GET_STRING_SIZE bytes.
361 | 
362 | **Parameters:**
363 | 
364 | - `path`: remote path of the file to be read.
365 | 
366 | **Returns:**
367 | 
368 | the contents of the file or null if an error occurred while reading the file.
369 | 
370 | ---
371 | 
372 | ### get
373 | 
374 | **Signature:** `get(path : String, encoding : String) : String`
375 | 
376 | **Description:** Reads the content of a remote file and returns it as string using the passed encoding. Read at most MAX_GET_STRING_SIZE characters.
377 | 
378 | **Parameters:**
379 | 
380 | - `path`: remote path of the file to be read.
381 | - `encoding`: an ISO 8859 character encoding labeled as a string, e.g. "ISO-8859-1"
382 | 
383 | **Returns:**
384 | 
385 | the contents of the file or null if an error occurred while reading the file.
386 | 
387 | ---
388 | 
389 | ### get
390 | 
391 | **Signature:** `get(path : String, maxGetSize : Number) : String`
392 | 
393 | **Description:** Reads the content of a remote file and returns it as a string using "ISO-8859-1" encoding to read it. Read at most maxGetSize characters.
394 | 
395 | **Deprecated:**
396 | 
397 | The maxGetSize attribute is not supported anymore. Use the method get(String) instead.
398 | 
399 | **Parameters:**
400 | 
401 | - `path`: remote path of the file to be read.
402 | - `maxGetSize`: the maximum bytes fetched from the remote file.
403 | 
404 | **Returns:**
405 | 
406 | the contents of the file or null if an error occurred while reading the file.
407 | 
408 | ---
409 | 
410 | ### get
411 | 
412 | **Signature:** `get(path : String, encoding : String, maxGetSize : Number) : String`
413 | 
414 | **Description:** Reads the content of a remote file and returns it as a string using the specified encoding. Returns at most maxGetSize characters.
415 | 
416 | **Deprecated:**
417 | 
418 | The maxGetSize attribute is not supported anymore. Use the method get(String, String) instead.
419 | 
420 | **Parameters:**
421 | 
422 | - `path`: remote path of the file to be read.
423 | - `encoding`: the encoding to use.
424 | - `maxGetSize`: the maximum bytes fetched from the remote file.
425 | 
426 | **Returns:**
427 | 
428 | the contents of the file or null if an error occurred while reading the file.
429 | 
430 | ---
431 | 
432 | ### get
433 | 
434 | **Signature:** `get(path : String, encoding : String, file : File) : boolean`
435 | 
436 | **Description:** Reads the content of a remote file and creates a local copy in the given file using the passed string encoding to read the file content and using the system standard encoding "UTF-8" to write the file. Copies at most MAX_GET_FILE_SIZE bytes.
437 | 
438 | **Parameters:**
439 | 
440 | - `path`: remote path of the file to be read.
441 | - `encoding`: the encoding to use.
442 | - `file`: the local file name
443 | 
444 | **Returns:**
445 | 
446 | true if remote file is fetched and copied into local file.
447 | 
448 | ---
449 | 
450 | ### get
451 | 
452 | **Signature:** `get(path : String, encoding : String, file : File, maxGetSize : Number) : boolean`
453 | 
454 | **Description:** Reads the content of a remote file and creates a local copy in the given file using the passed string encoding to read the file content and using the system standard encoding "UTF-8" to write the file. Copies at most maxGetSize bytes.
455 | 
456 | **Deprecated:**
457 | 
458 | The maxGetSize attribute is not supported anymore. Use the method get(String, String, File) instead.
459 | 
460 | **Parameters:**
461 | 
462 | - `path`: remote path of the file to be read.
463 | - `encoding`: the encoding to use.
464 | - `file`: the local file name
465 | - `maxGetSize`: the maximum number of bytes to fetch
466 | 
467 | **Returns:**
468 | 
469 | true if remote file is fetched and copied into local file.
470 | 
471 | ---
472 | 
473 | ### getBinary
474 | 
475 | **Signature:** `getBinary(path : String, file : File) : boolean`
476 | 
477 | **Description:** Reads the content of a remote file and creates a local copy in the given file. Copies at most MAX_GET_FILE_SIZE bytes. The FTP transfer is done in Binary mode.
478 | 
479 | **Parameters:**
480 | 
481 | - `path`: remote path of the file to be read.
482 | - `file`: the local file name
483 | 
484 | **Returns:**
485 | 
486 | true if remote file is fetched and copied into local file.
487 | 
488 | ---
489 | 
490 | ### getBinary
491 | 
492 | **Signature:** `getBinary(path : String, file : File, maxGetSize : Number) : boolean`
493 | 
494 | **Description:** Reads the content of a remote file and creates a local copy in the given file. Copies at most maxGetSize bytes. The FTP transfer is done in Binary mode.
495 | 
496 | **Deprecated:**
497 | 
498 | The maxGetSize attribute is not supported anymore. Use the method getBinary(String, File) instead.
499 | 
500 | **Parameters:**
501 | 
502 | - `path`: remote path of the file to be read.
503 | - `file`: the local file name
504 | - `maxGetSize`: the maximum number of bytes to fetch
505 | 
506 | **Returns:**
507 | 
508 | true if remote file is fetched and copied into local file.
509 | 
510 | ---
511 | 
512 | ### getConnected
513 | 
514 | **Signature:** `getConnected() : boolean`
515 | 
516 | **Description:** Identifies if the FTP client is currently connected to the FTP server.
517 | 
518 | **Returns:**
519 | 
520 | true if the client is currently connected.
521 | 
522 | ---
523 | 
524 | ### getReplyCode
525 | 
526 | **Signature:** `getReplyCode() : Number`
527 | 
528 | **Description:** Returns the reply code from the last FTP action.
529 | 
530 | **Returns:**
531 | 
532 | the reply code from the last FTP action.
533 | 
534 | ---
535 | 
536 | ### getReplyMessage
537 | 
538 | **Signature:** `getReplyMessage() : String`
539 | 
540 | **Description:** Returns the string message from the last FTP action.
541 | 
542 | **Returns:**
543 | 
544 | the string message from the last FTP action.
545 | 
546 | ---
547 | 
548 | ### getTimeout
549 | 
550 | **Signature:** `getTimeout() : Number`
551 | 
552 | **Description:** Returns the timeout for this client, in milliseconds.
553 | 
554 | **Returns:**
555 | 
556 | the timeout in milliseconds
557 | 
558 | ---
559 | 
560 | ### list
561 | 
562 | **Signature:** `list() : FTPFileInfo[]`
563 | 
564 | **Description:** Returns a list of FTPFileInfo objects containing information about the files in the current directory.
565 | 
566 | **Returns:**
567 | 
568 | list of objects with remote file information.
569 | 
570 | ---
571 | 
572 | ### list
573 | 
574 | **Signature:** `list(path : String) : FTPFileInfo[]`
575 | 
576 | **Description:** Returns a list of FTPFileInfo objects containing information about the files in the remote directory defined by the given path.
577 | 
578 | **Parameters:**
579 | 
580 | - `path`: the remote path from which the file info is listed.
581 | 
582 | **Returns:**
583 | 
584 | list of objects with remote file information.
585 | 
586 | ---
587 | 
588 | ### mkdir
589 | 
590 | **Signature:** `mkdir(path : String) : boolean`
591 | 
592 | **Description:** Creates a directory
593 | 
594 | **Parameters:**
595 | 
596 | - `path`: the path to the directory to create.
597 | 
598 | **Returns:**
599 | 
600 | true if the directory was successfully created, false otherwise.
601 | 
602 | ---
603 | 
604 | ### put
605 | 
606 | **Signature:** `put(path : String, content : String) : boolean`
607 | 
608 | **Description:** Puts the specified content to the specified full path using "ISO-8859-1" encoding. The full path must include the path and the file name. If the content of a local file is to be uploaded, please use method putBinary(String, File) instead.
609 | 
610 | **Parameters:**
611 | 
612 | - `path`: full path on the remote FTP server where the file will be stored.
613 | - `content`: the content to put.
614 | 
615 | **Returns:**
616 | 
617 | true or false indicating success or failure.
618 | 
619 | ---
620 | 
621 | ### put
622 | 
623 | **Signature:** `put(path : String, content : String, encoding : String) : boolean`
624 | 
625 | **Description:** Put the given content to a file on the given full path on the FTP server. The full path must include the path and the file name. The transformation from String into binary data is done via the encoding provided with the method call. If the content of a local file is to be uploaded, please use method putBinary(String, File) instead.
626 | 
627 | **Parameters:**
628 | 
629 | - `path`: the full path on the remote FTP server where the file will be stored.
630 | - `content`: the content to put.
631 | - `encoding`: the encoding to use.
632 | 
633 | **Returns:**
634 | 
635 | true or false indicating success or failure.
636 | 
637 | ---
638 | 
639 | ### putBinary
640 | 
641 | **Signature:** `putBinary(path : String, file : File) : boolean`
642 | 
643 | **Description:** Put the content of the given file into a file on the remote FTP server with the given full path. The full path must include the path and the file name.
644 | 
645 | **Parameters:**
646 | 
647 | - `path`: the full path on the remote FTP server where the file will be stored.
648 | - `file`: the file on the local system, which content is send to the remote FTP server.
649 | 
650 | **Returns:**
651 | 
652 | true or false indicating success or failure.
653 | 
654 | ---
655 | 
656 | ### removeDirectory
657 | 
658 | **Signature:** `removeDirectory(path : String) : boolean`
659 | 
660 | **Description:** Deletes the remote directory on the server identified by the path parameter. In order to delete the directory successfully the directory needs to be empty, otherwise the removeDirectory() method will return false.
661 | 
662 | **Parameters:**
663 | 
664 | - `path`: the path to the directory.
665 | 
666 | **Returns:**
667 | 
668 | true if the directory was successfully deleted, false otherwise.
669 | 
670 | ---
671 | 
672 | ### rename
673 | 
674 | **Signature:** `rename(from : String, to : String) : boolean`
675 | 
676 | **Description:** Renames an existing file.
677 | 
678 | **Parameters:**
679 | 
680 | - `from`: the file that will be renamed.
681 | - `to`: the name of the new file.
682 | 
683 | **Returns:**
684 | 
685 | true if the file was successfully renamed, false otherwise.
686 | 
687 | ---
688 | 
689 | ### setTimeout
690 | 
691 | **Signature:** `setTimeout(timeoutMillis : Number) : void`
692 | 
693 | **Description:** Sets the timeout for connections made with the FTP client to the given number of milliseconds. If the given timeout is less than or equal to zero, the timeout is set to the same value as the script context timeout but will only be set to a maximum of 30 seconds. The maximum and default timeout depend on the script context timeout. The maximum timeout is set to a maximum of 2 minutes. The default timeout for a new client is set to a maximum of 30 seconds. This method can be called at any time, and will affect the next connection made with this client. It is not possible to set the timeout for an open connection.
694 | 
695 | **Parameters:**
696 | 
697 | - `timeoutMillis`: timeout, in milliseconds, up to a maximum of 2 minutes.
698 | 
699 | ---
```

--------------------------------------------------------------------------------
/tests/mcp/node/get-sfcc-class-documentation.docs-only.programmatic.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { test, describe, before, after, beforeEach } from 'node:test';
  2 | import { strict as assert } from 'node:assert';
  3 | import { connect } from 'mcp-aegis';
  4 | 
  5 | describe('get_sfcc_class_documentation Tool Programmatic Tests', () => {
  6 |   let client;
  7 | 
  8 |   before(async () => {
  9 |     client = await connect('./aegis.config.json');
 10 |   });
 11 | 
 12 |   after(async () => {
 13 |     if (client?.connected) {
 14 |       await client.disconnect();
 15 |     }
 16 |   });
 17 | 
 18 |   beforeEach(() => {
 19 |     // CRITICAL: Clear all buffers to prevent leaking into next tests
 20 |     client.clearAllBuffers(); // Recommended - comprehensive protection
 21 |   });
 22 | 
 23 |   describe('Valid Class Documentation Retrieval', () => {
 24 |     test('should retrieve documentation for dw.catalog.Product', async () => {
 25 |       const result = await client.callTool('get_sfcc_class_documentation', {
 26 |         className: 'dw.catalog.Product'
 27 |       });
 28 | 
 29 |       assert.equal(result.isError, false, 'Should have isError: false on success');
 30 |       assert.ok(result.content, 'Should have content');
 31 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
 32 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
 33 |       
 34 |       const documentation = result.content[0].text;
 35 |       assert.ok(documentation.includes('## Package: dw.catalog'), 'Should include package information');
 36 |       assert.ok(documentation.includes('# Class Product'), 'Should include class name');
 37 |       assert.ok(documentation.includes('## Inheritance Hierarchy'), 'Should include inheritance hierarchy');
 38 |       assert.ok(documentation.includes('## Description'), 'Should include description section');
 39 |       assert.ok(documentation.includes('## Properties'), 'Should include properties section');
 40 |       assert.ok(documentation.includes('## Method Summary'), 'Should include method summary');
 41 |     });
 42 | 
 43 |     test('should retrieve documentation for dw.system.Site', async () => {
 44 |       const result = await client.callTool('get_sfcc_class_documentation', {
 45 |         className: 'dw.system.Site'
 46 |       });
 47 | 
 48 |       assert.equal(result.isError, false, 'Should have isError: false on success');
 49 |       assert.ok(result.content, 'Should have content');
 50 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
 51 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
 52 |       
 53 |       const documentation = result.content[0].text;
 54 |       assert.ok(documentation.includes('dw.system'), 'Should include correct package');
 55 |       assert.ok(documentation.includes('Site'), 'Should include class name');
 56 |     });
 57 | 
 58 |     test('should retrieve documentation for dw.order.Order', async () => {
 59 |       const result = await client.callTool('get_sfcc_class_documentation', {
 60 |         className: 'dw.order.Order'
 61 |       });
 62 | 
 63 |       assert.equal(result.isError, false, 'Should have isError: false on success');
 64 |       assert.ok(result.content, 'Should have content');
 65 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
 66 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
 67 |       
 68 |       const documentation = result.content[0].text;
 69 |       assert.ok(documentation.includes('dw.order'), 'Should include correct package');
 70 |       assert.ok(documentation.includes('Order'), 'Should include class name');
 71 |     });
 72 | 
 73 |     test('should handle class names without package prefix', async () => {
 74 |       const result = await client.callTool('get_sfcc_class_documentation', {
 75 |         className: 'Product'
 76 |       });
 77 | 
 78 |       assert.equal(result.isError, false, 'Should have isError: false on success');
 79 |       assert.ok(result.content, 'Should have content');
 80 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
 81 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
 82 |       
 83 |       const documentation = result.content[0].text;
 84 |       assert.ok(documentation.includes('Product'), 'Should include class name');
 85 |     });
 86 |   });
 87 | 
 88 |   describe('Documentation Content Structure Validation', () => {
 89 |     test('should include all expected sections for a complex class', async () => {
 90 |       const result = await client.callTool('get_sfcc_class_documentation', {
 91 |         className: 'dw.catalog.Product'
 92 |       });
 93 | 
 94 |       assert.equal(result.isError, false, 'Should have isError: false on success');
 95 |       const documentation = result.content[0].text;
 96 | 
 97 |       // Check for main structural elements
 98 |       assert.ok(documentation.includes('## Package:'), 'Should include package section');
 99 |       assert.ok(documentation.includes('# Class'), 'Should include class header');
100 |       assert.ok(documentation.includes('## Inheritance Hierarchy'), 'Should include inheritance');
101 |       assert.ok(documentation.includes('## Description'), 'Should include description');
102 |       assert.ok(documentation.includes('## Properties'), 'Should include properties');
103 |       assert.ok(documentation.includes('## Method Summary'), 'Should include method summary');
104 |       assert.ok(documentation.includes('## Method Detail'), 'Should include method details');
105 | 
106 |       // Check for specific property examples
107 |       assert.ok(documentation.includes('### ID'), 'Should include ID property');
108 |       assert.ok(documentation.includes('### name'), 'Should include name property');
109 |       assert.ok(documentation.includes('### available'), 'Should include available property');
110 | 
111 |       // Check for method examples
112 |       assert.ok(documentation.includes('getID()'), 'Should include getID method');
113 |       assert.ok(documentation.includes('getName()'), 'Should include getName method');
114 |       assert.ok(documentation.includes('isAvailable()'), 'Should include isAvailable method');
115 | 
116 |       // Check for type information
117 |       assert.ok(documentation.includes('**Type:**'), 'Should include type information');
118 |       assert.ok(documentation.includes('**Signature:**'), 'Should include method signatures');
119 |       assert.ok(documentation.includes('**Returns:**'), 'Should include return information');
120 |     });
121 | 
122 |     test('should include proper markdown formatting', async () => {
123 |       const result = await client.callTool('get_sfcc_class_documentation', {
124 |         className: 'dw.catalog.Product'
125 |       });
126 | 
127 |       assert.equal(result.isError, false, 'Should have isError: false on success');
128 |       const documentation = result.content[0].text;
129 | 
130 |       // Check markdown formatting
131 |       assert.ok(documentation.includes('##'), 'Should use markdown headers');
132 |       assert.ok(documentation.includes('###'), 'Should use markdown subheaders');
133 |       assert.ok(documentation.includes('**'), 'Should use markdown bold formatting');
134 |       assert.ok(documentation.includes('`'), 'Should use markdown code formatting');
135 |       assert.ok(documentation.includes('\\n'), 'Should have escaped newlines in the documentation string');
136 |     });
137 | 
138 |     test('should include deprecation warnings when present', async () => {
139 |       const result = await client.callTool('get_sfcc_class_documentation', {
140 |         className: 'dw.catalog.Product'
141 |       });
142 | 
143 |       assert.equal(result.isError, false, 'Should have isError: false on success');
144 |       const documentation = result.content[0].text;
145 | 
146 |       // Product class has deprecated methods, should include deprecation info
147 |       if (documentation.includes('**Deprecated:**')) {
148 |         assert.ok(documentation.includes('**Deprecated:**'), 'Should mark deprecated items');
149 |       }
150 |     });
151 |   });
152 | 
153 |   describe('Error Handling', () => {
154 |     test('should handle non-existent class name gracefully', async () => {
155 |       const result = await client.callTool('get_sfcc_class_documentation', {
156 |         className: 'NonExistentClass'
157 |       });
158 | 
159 |       assert.equal(result.isError, true, 'Should be marked as error');
160 |       assert.ok(result.content, 'Should have content even for errors');
161 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
162 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
163 |       assert.ok(result.content[0].text.includes('not found'), 'Should indicate class not found');
164 |       assert.ok(result.content[0].text.includes('NonExistentClass'), 'Should include the requested class name');
165 |     });
166 | 
167 |     test('should handle invalid package.class format', async () => {
168 |       const result = await client.callTool('get_sfcc_class_documentation', {
169 |         className: 'invalid.package.InvalidClass'
170 |       });
171 | 
172 |       assert.equal(result.isError, true, 'Should be marked as error');
173 |       assert.ok(result.content, 'Should have content even for errors');
174 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
175 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
176 |       assert.ok(result.content[0].text.includes('not found'), 'Should indicate class not found');
177 |     });
178 | 
179 |     test('should handle empty class name', async () => {
180 |       const result = await client.callTool('get_sfcc_class_documentation', {
181 |         className: ''
182 |       });
183 | 
184 |       assert.equal(result.isError, true, 'Should be marked as error');
185 |       assert.ok(result.content, 'Should have content even for errors');
186 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
187 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
188 |       assert.ok(result.content[0].text.includes('non-empty string'), 'Should indicate className must be non-empty');
189 |     });
190 | 
191 |     test('should handle missing className parameter', async () => {
192 |       const result = await client.callTool('get_sfcc_class_documentation', {});
193 | 
194 |       assert.equal(result.isError, true, 'Should be marked as error');
195 |       assert.ok(result.content, 'Should have content even for errors');
196 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
197 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
198 |       assert.ok(result.content[0].text.includes('non-empty string'), 'Should indicate className is required');
199 |     });
200 | 
201 |     test('should handle null className parameter', async () => {
202 |       const result = await client.callTool('get_sfcc_class_documentation', {
203 |         className: null
204 |       });
205 | 
206 |       assert.equal(result.isError, true, 'Should be marked as error');
207 |       assert.ok(result.content, 'Should have content even for errors');
208 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
209 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
210 |       assert.ok(result.content[0].text.includes('non-empty string'), 'Should indicate className must be non-empty string');
211 |     });
212 | 
213 |     test('should handle whitespace-only className', async () => {
214 |       const result = await client.callTool('get_sfcc_class_documentation', {
215 |         className: '   '
216 |       });
217 | 
218 |       assert.equal(result.isError, true, 'Should be marked as error');
219 |       assert.ok(result.content, 'Should have content even for errors');
220 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
221 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
222 |       assert.ok(result.content[0].text.includes('non-empty string'), 'Should indicate className must be non-empty');
223 |     });
224 |   });
225 | 
226 | 
227 |   describe('Edge Cases and Special Characters', () => {
228 |     test('should handle class names with special characters gracefully', async () => {
229 |       const result = await client.callTool('get_sfcc_class_documentation', {
230 |         className: 'dw.catalog.Product$Special'
231 |       });
232 | 
233 |       // Should handle gracefully, either find documentation or return proper error
234 |       assert.ok(result.content, 'Should have content');
235 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
236 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
237 |       // Either success (isError undefined) or proper error (isError true)
238 |       assert.ok(result.isError === false || result.isError === true, 'Should have proper isError flag');
239 |     });
240 | 
241 |     test('should handle very long class names', async () => {
242 |       const longClassName = 'dw.catalog.' + 'A'.repeat(100);
243 |       const result = await client.callTool('get_sfcc_class_documentation', {
244 |         className: longClassName
245 |       });
246 | 
247 |       assert.equal(result.isError, true, 'Should be marked as error for non-existent long class');
248 |       assert.ok(result.content, 'Should have content');
249 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
250 |       assert.equal(result.content[0].type, 'text', 'Content should be of type text');
251 |     });
252 | 
253 |     test('should handle case sensitivity correctly', async () => {
254 |       // Test different case variations
255 |       const variations = [
256 |         'dw.catalog.product',  // lowercase
257 |         'DW.CATALOG.PRODUCT',  // uppercase
258 |         'dw.Catalog.Product'   // mixed case
259 |       ];
260 | 
261 |       for (const className of variations) {
262 |         const result = await client.callTool('get_sfcc_class_documentation', {
263 |           className
264 |         });
265 | 
266 |         // Should handle gracefully, likely return error for incorrect case
267 |         assert.ok(result.content, `Should have content for ${className}`);
268 |         assert.equal(result.content.length, 1, `Should have exactly one content item for ${className}`);
269 |         assert.equal(result.content[0].type, 'text', `Content should be of type text for ${className}`);
270 |         // Either success (isError undefined) or error (isError true)
271 |         assert.ok(result.isError === false || result.isError === true, 
272 |           `Should have proper isError flag for ${className}`);
273 |       }
274 |     });
275 |   });
276 | 
277 |   describe('Documentation Quality and Completeness', () => {
278 |     test('should provide comprehensive documentation for core classes', async () => {
279 |       const coreClasses = [
280 |         'dw.catalog.Product',
281 |         'dw.order.Order',
282 |         'dw.customer.Customer',
283 |         'dw.system.Site'
284 |       ];
285 | 
286 |       for (const className of coreClasses) {
287 |         const result = await client.callTool('get_sfcc_class_documentation', {
288 |           className
289 |         });
290 | 
291 |         assert.equal(result.isError, false, `Should not have isError property on success for ${className}`);
292 |         
293 |         const documentation = result.content[0].text;
294 |         assert.ok(documentation.length > 1000, `Documentation for ${className} should be comprehensive`);
295 |         assert.ok(documentation.includes('## Description'), `Should include description for ${className}`);
296 |         assert.ok(documentation.includes('## Properties') || documentation.includes('## Method Summary'), 
297 |           `Should include properties or methods for ${className}`);
298 |       }
299 |     });
300 | 
301 |     test('should include method signatures and return types', async () => {
302 |       const result = await client.callTool('get_sfcc_class_documentation', {
303 |         className: 'dw.catalog.Product'
304 |       });
305 | 
306 |       assert.equal(result.isError, false, 'Should have isError: false on success');
307 |       
308 |       const documentation = result.content[0].text;
309 |       assert.ok(documentation.includes('**Signature:**'), 'Should include method signatures');
310 |       assert.ok(documentation.includes('**Returns:**'), 'Should include return type information');
311 |       assert.ok(documentation.includes('**Parameters:**'), 'Should include parameter information');
312 |       assert.ok(documentation.includes('**Description:**'), 'Should include method descriptions');
313 |     });
314 | 
315 |     test('should include inheritance information for classes with hierarchy', async () => {
316 |       const result = await client.callTool('get_sfcc_class_documentation', {
317 |         className: 'dw.catalog.Product'
318 |       });
319 | 
320 |       assert.equal(result.isError, false, 'Should have isError: false on success');
321 |       
322 |       const documentation = result.content[0].text;
323 |       assert.ok(documentation.includes('## Inheritance Hierarchy'), 'Should include inheritance hierarchy');
324 |       assert.ok(documentation.includes('Object'), 'Should show Object as base class');
325 |       assert.ok(documentation.includes('PersistentObject') || documentation.includes('ExtensibleObject'), 
326 |         'Should show intermediate classes in hierarchy');
327 |     });
328 |   });
329 | 
330 |   describe('Tool Response Format Consistency', () => {
331 |     test('should always return consistent response structure for success', async () => {
332 |       const result = await client.callTool('get_sfcc_class_documentation', {
333 |         className: 'dw.catalog.Product'
334 |       });
335 | 
336 |       // Validate response structure
337 |       assert.ok(result, 'Should return a result object');
338 |       assert.ok(result.content, 'Should have content property');
339 |       assert.ok(Array.isArray(result.content), 'Content should be an array');
340 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
341 |       assert.equal(result.content[0].type, 'text', 'Content type should be text');
342 |       assert.ok(typeof result.content[0].text === 'string', 'Content text should be string');
343 |       assert.equal(result.isError, false, 'isError should be false for success');
344 |     });
345 | 
346 |     test('should always return consistent response structure for errors', async () => {
347 |       const result = await client.callTool('get_sfcc_class_documentation', {
348 |         className: 'NonExistentClass'
349 |       });
350 | 
351 |       // Validate response structure
352 |       assert.ok(result, 'Should return a result object');
353 |       assert.ok(result.content, 'Should have content property');
354 |       assert.ok(Array.isArray(result.content), 'Content should be an array');
355 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
356 |       assert.equal(result.content[0].type, 'text', 'Content type should be text');
357 |       assert.ok(typeof result.content[0].text === 'string', 'Content text should be string');
358 |       assert.equal(result.isError, true, 'isError should be true for errors');
359 |     });
360 | 
361 |     test('should include isError: false property when successful', async () => {
362 |       const result = await client.callTool('get_sfcc_class_documentation', {
363 |         className: 'dw.catalog.Product'
364 |       });
365 | 
366 |       // isError should now always be included for consistency
367 |       assert.equal(result.isError, false, 'isError should be false for successful operations');
368 |       assert.ok(Object.prototype.hasOwnProperty.call(result, 'isError'), 'Should have isError property for all responses');
369 |     });
370 |   });
371 | });
372 | 
```

--------------------------------------------------------------------------------
/tests/mcp/yaml/get-available-sfra-documents.full-mode.test.mcp.yml:
--------------------------------------------------------------------------------

```yaml
  1 | description: "Docs-only: get_available_sfra_documents tool tests"
  2 | config: ./aegis.config.with-dw.json
  3 | 
  4 | # We first list tools to ensure the tool exists in full-mode mode, then call it.
  5 | tests:
  6 |   - it: "should have get_available_sfra_documents in tools list"
  7 |     request:
  8 |       jsonrpc: "2.0"
  9 |       id: "list-sfra-docs-1"
 10 |       method: "tools/list"
 11 |       params: {}
 12 |     expect:
 13 |       response:
 14 |         jsonrpc: "2.0"
 15 |         id: "list-sfra-docs-1"
 16 |         result:
 17 |           tools:
 18 |             match:arrayElements:
 19 |               match:partial:
 20 |                 name: "match:type:string"
 21 |           match:extractField: "tools.*.name"
 22 |           value: "match:arrayContains:get_available_sfra_documents"
 23 | 
 24 |   - it: "should return an array JSON string of sfra documents in text content"
 25 |     request:
 26 |       jsonrpc: "2.0"
 27 |       id: "sfra-docs-1"
 28 |       method: "tools/call"
 29 |       params:
 30 |         name: "get_available_sfra_documents"
 31 |         arguments: {}
 32 |     expect:
 33 |       response:
 34 |         jsonrpc: "2.0"
 35 |         id: "sfra-docs-1"
 36 |         result:
 37 |           isError: false
 38 |           content:
 39 |             match:arrayElements:
 40 |               match:partial:
 41 |                 type: "text"
 42 |                 text: "match:regex:\\[[\\s\\S]*querystring[\\s\\S]*server[\\s\\S]*cart[\\s\\S]*stores[\\s\\S]*\\]" # array JSON includes expected doc names
 43 |       stderr: toBeEmpty
 44 | 
 45 |   - it: "should include core category documents (querystring, server, request, response, render)"
 46 |     request:
 47 |       jsonrpc: "2.0"
 48 |       id: "sfra-docs-2"
 49 |       method: "tools/call"
 50 |       params:
 51 |         name: "get_available_sfra_documents"
 52 |         arguments: {}
 53 |     expect:
 54 |       response:
 55 |         jsonrpc: "2.0"
 56 |         id: "sfra-docs-2"
 57 |         result:
 58 |           isError: false
 59 |           content:
 60 |             match:arrayElements:
 61 |               match:partial:
 62 |                 type: "text"
 63 |                 text: "match:contains:querystring"
 64 |       stderr: toBeEmpty
 65 | 
 66 |   - it: "should contain product and store model documents (product-full, product-tile, store, stores)"
 67 |     request:
 68 |       jsonrpc: "2.0"
 69 |       id: "sfra-docs-3"
 70 |       method: "tools/call"
 71 |       params:
 72 |         name: "get_available_sfra_documents"
 73 |         arguments: {}
 74 |     expect:
 75 |       response:
 76 |         jsonrpc: "2.0"
 77 |         id: "sfra-docs-3"
 78 |         result:
 79 |           isError: false
 80 |           content:
 81 |             match:arrayElements:
 82 |               match:partial:
 83 |                 type: "text"
 84 |                 text: "match:contains:product-full"
 85 |       stderr: toBeEmpty
 86 | 
 87 |   - it: "should not return error when called without arguments (empty object)"
 88 |     request:
 89 |       jsonrpc: "2.0"
 90 |       id: "sfra-docs-4"
 91 |       method: "tools/call"
 92 |       params:
 93 |         name: "get_available_sfra_documents"
 94 |         arguments: {}
 95 |     expect:
 96 |       response:
 97 |         jsonrpc: "2.0"
 98 |         id: "sfra-docs-4"
 99 |         result:
100 |           isError: false
101 |           content: "match:type:array"
102 |       stderr: toBeEmpty
103 | 
104 |   - it: "should include pricing documents (price-default, price-range, price-tiered)"
105 |     request:
106 |       jsonrpc: "2.0"
107 |       id: "sfra-docs-5"
108 |       method: "tools/call"
109 |       params:
110 |         name: "get_available_sfra_documents"
111 |         arguments: {}
112 |     expect:
113 |       response:
114 |         jsonrpc: "2.0"
115 |         id: "sfra-docs-5"
116 |         result:
117 |           isError: false
118 |           content:
119 |             match:arrayElements:
120 |               match:partial:
121 |                 text: "match:contains:price-default"
122 |       stderr: toBeEmpty
123 | 
124 |   - it: "should tolerate extraneous empty arguments object (idempotent behavior)"
125 |     request:
126 |       jsonrpc: "2.0"
127 |       id: "sfra-docs-6"
128 |       method: "tools/call"
129 |       params:
130 |         name: "get_available_sfra_documents"
131 |         arguments: {}
132 |     expect:
133 |       response:
134 |         jsonrpc: "2.0"
135 |         id: "sfra-docs-6"
136 |         result:
137 |           match:partial:
138 |             isError: false
139 |       stderr: toBeEmpty
140 | 
141 |   - it: "should respond within acceptable performance threshold"
142 |     request:
143 |       jsonrpc: "2.0"
144 |       id: "sfra-docs-7"
145 |       method: "tools/call"
146 |       params:
147 |         name: "get_available_sfra_documents"
148 |         arguments: {}
149 |     expect:
150 |       response:
151 |         jsonrpc: "2.0"
152 |         id: "sfra-docs-7"
153 |         result:
154 |           match:partial:
155 |             isError: false
156 |       performance:
157 |         maxResponseTime: "800ms" # docs listing should be fast but allow CI variance
158 |       stderr: toBeEmpty
159 | 
160 |   - it: "should include multiple distinct categories (core, order, product, pricing, store)"
161 |     request:
162 |       jsonrpc: "2.0"
163 |       id: "sfra-docs-8"
164 |       method: "tools/call"
165 |       params:
166 |         name: "get_available_sfra_documents"
167 |         arguments: {}
168 |     expect:
169 |       response:
170 |         jsonrpc: "2.0"
171 |         id: "sfra-docs-8"
172 |         result:
173 |           isError: false
174 |           content:
175 |             match:arrayElements:
176 |               match:partial:
177 |                 text: "match:regex:(core|order|product|pricing|store)"
178 |       stderr: toBeEmpty
179 | 
180 |   - it: "should expose at least 18 documents (count via regex on JSON array)"
181 |     request:
182 |       jsonrpc: "2.0"
183 |       id: "sfra-docs-9"
184 |       method: "tools/call"
185 |       params:
186 |         name: "get_available_sfra_documents"
187 |         arguments: {}
188 |     expect:
189 |       response:
190 |         jsonrpc: "2.0"
191 |         id: "sfra-docs-9"
192 |         result:
193 |           isError: false
194 |           content:
195 |             match:arrayElements:
196 |               match:partial:
197 |                 # Require at least 18 occurrences of the JSON key "name" (non-greedy across intervening content)
198 |                 # NOTE: The prior insanely large quantifier still matched because the engine satisfied the pattern once; quantifier applies to group occurrences.
199 |                 # This pattern enforces a realistic lower bound of 18.
200 |                 text: "match:regex:(?:\\\"name\\\"[\\s\\S]*?){18,}"
201 |       stderr: toBeEmpty
202 | 
203 |   # Individual presence tests replacing aggregated lookahead test for better failure diagnostics
204 |   # Core documents
205 |   - it: "should include doc name: server"
206 |     request:
207 |       jsonrpc: "2.0"
208 |       id: "sfra-docs-9b-server"
209 |       method: "tools/call"
210 |       params:
211 |         name: "get_available_sfra_documents"
212 |         arguments: {}
213 |     expect:
214 |       response:
215 |         jsonrpc: "2.0"
216 |         id: "sfra-docs-9b-server"
217 |         result:
218 |           isError: false
219 |           content:
220 |             match:arrayElements:
221 |               match:partial:
222 |                 text: "match:contains:server"
223 |       stderr: toBeEmpty
224 | 
225 |   - it: "should include doc name: request"
226 |     request:
227 |       jsonrpc: "2.0"
228 |       id: "sfra-docs-9b-request"
229 |       method: "tools/call"
230 |       params:
231 |         name: "get_available_sfra_documents"
232 |         arguments: {}
233 |     expect:
234 |       response:
235 |         jsonrpc: "2.0"
236 |         id: "sfra-docs-9b-request"
237 |         result:
238 |           isError: false
239 |           content:
240 |             match:arrayElements:
241 |               match:partial:
242 |                 text: "match:contains:request"
243 |       stderr: toBeEmpty
244 | 
245 |   - it: "should include doc name: response"
246 |     request:
247 |       jsonrpc: "2.0"
248 |       id: "sfra-docs-9b-response"
249 |       method: "tools/call"
250 |       params:
251 |         name: "get_available_sfra_documents"
252 |         arguments: {}
253 |     expect:
254 |       response:
255 |         jsonrpc: "2.0"
256 |         id: "sfra-docs-9b-response"
257 |         result:
258 |           isError: false
259 |           content:
260 |             match:arrayElements:
261 |               match:partial:
262 |                 text: "match:contains:response"
263 |       stderr: toBeEmpty
264 | 
265 |   - it: "should include doc name: querystring"
266 |     request:
267 |       jsonrpc: "2.0"
268 |       id: "sfra-docs-9b-querystring"
269 |       method: "tools/call"
270 |       params:
271 |         name: "get_available_sfra_documents"
272 |         arguments: {}
273 |     expect:
274 |       response:
275 |         jsonrpc: "2.0"
276 |         id: "sfra-docs-9b-querystring"
277 |         result:
278 |           isError: false
279 |           content:
280 |             match:arrayElements:
281 |               match:partial:
282 |                 text: "match:contains:querystring"
283 |       stderr: toBeEmpty
284 | 
285 |   - it: "should include doc name: render"
286 |     request:
287 |       jsonrpc: "2.0"
288 |       id: "sfra-docs-9b-render"
289 |       method: "tools/call"
290 |       params:
291 |         name: "get_available_sfra_documents"
292 |         arguments: {}
293 |     expect:
294 |       response:
295 |         jsonrpc: "2.0"
296 |         id: "sfra-docs-9b-render"
297 |         result:
298 |           isError: false
299 |           content:
300 |             match:arrayElements:
301 |               match:partial:
302 |                 text: "match:contains:render"
303 |       stderr: toBeEmpty
304 | 
305 |   # Functional / model documents
306 |   - it: "should include doc name: cart"
307 |     request:
308 |       jsonrpc: "2.0"
309 |       id: "sfra-docs-9b-cart"
310 |       method: "tools/call"
311 |       params:
312 |         name: "get_available_sfra_documents"
313 |         arguments: {}
314 |     expect:
315 |       response:
316 |         jsonrpc: "2.0"
317 |         id: "sfra-docs-9b-cart"
318 |         result:
319 |           isError: false
320 |           content:
321 |             match:arrayElements:
322 |               match:partial:
323 |                 text: "match:contains:cart"
324 |       stderr: toBeEmpty
325 | 
326 |   - it: "should include doc name: product-full"
327 |     request:
328 |       jsonrpc: "2.0"
329 |       id: "sfra-docs-9b-product-full"
330 |       method: "tools/call"
331 |       params:
332 |         name: "get_available_sfra_documents"
333 |         arguments: {}
334 |     expect:
335 |       response:
336 |         jsonrpc: "2.0"
337 |         id: "sfra-docs-9b-product-full"
338 |         result:
339 |           isError: false
340 |           content:
341 |             match:arrayElements:
342 |               match:partial:
343 |                 text: "match:contains:product-full"
344 |       stderr: toBeEmpty
345 | 
346 |   - it: "should include doc name: product-tile"
347 |     request:
348 |       jsonrpc: "2.0"
349 |       id: "sfra-docs-9b-product-tile"
350 |       method: "tools/call"
351 |       params:
352 |         name: "get_available_sfra_documents"
353 |         arguments: {}
354 |     expect:
355 |       response:
356 |         jsonrpc: "2.0"
357 |         id: "sfra-docs-9b-product-tile"
358 |         result:
359 |           isError: false
360 |           content:
361 |             match:arrayElements:
362 |               match:partial:
363 |                 text: "match:contains:product-tile"
364 |       stderr: toBeEmpty
365 | 
366 |   # Pricing documents
367 |   - it: "should include doc name: price-default"
368 |     request:
369 |       jsonrpc: "2.0"
370 |       id: "sfra-docs-9b-price-default"
371 |       method: "tools/call"
372 |       params:
373 |         name: "get_available_sfra_documents"
374 |         arguments: {}
375 |     expect:
376 |       response:
377 |         jsonrpc: "2.0"
378 |         id: "sfra-docs-9b-price-default"
379 |         result:
380 |           isError: false
381 |           content:
382 |             match:arrayElements:
383 |               match:partial:
384 |                 text: "match:contains:price-default"
385 |       stderr: toBeEmpty
386 | 
387 |   - it: "should include doc name: price-range"
388 |     request:
389 |       jsonrpc: "2.0"
390 |       id: "sfra-docs-9b-price-range"
391 |       method: "tools/call"
392 |       params:
393 |         name: "get_available_sfra_documents"
394 |         arguments: {}
395 |     expect:
396 |       response:
397 |         jsonrpc: "2.0"
398 |         id: "sfra-docs-9b-price-range"
399 |         result:
400 |           isError: false
401 |           content:
402 |             match:arrayElements:
403 |               match:partial:
404 |                 text: "match:contains:price-range"
405 |       stderr: toBeEmpty
406 | 
407 |   - it: "should include doc name: price-tiered"
408 |     request:
409 |       jsonrpc: "2.0"
410 |       id: "sfra-docs-9b-price-tiered"
411 |       method: "tools/call"
412 |       params:
413 |         name: "get_available_sfra_documents"
414 |         arguments: {}
415 |     expect:
416 |       response:
417 |         jsonrpc: "2.0"
418 |         id: "sfra-docs-9b-price-tiered"
419 |         result:
420 |           isError: false
421 |           content:
422 |             match:arrayElements:
423 |               match:partial:
424 |                 text: "match:contains:price-tiered"
425 |       stderr: toBeEmpty
426 | 
427 |   # Store documents
428 |   - it: "should include doc name: store"
429 |     request:
430 |       jsonrpc: "2.0"
431 |       id: "sfra-docs-9b-store"
432 |       method: "tools/call"
433 |       params:
434 |         name: "get_available_sfra_documents"
435 |         arguments: {}
436 |     expect:
437 |       response:
438 |         jsonrpc: "2.0"
439 |         id: "sfra-docs-9b-store"
440 |         result:
441 |           isError: false
442 |           content:
443 |             match:arrayElements:
444 |               match:partial:
445 |                 text: "match:contains:store"
446 |       stderr: toBeEmpty
447 | 
448 |   - it: "should include doc name: stores"
449 |     request:
450 |       jsonrpc: "2.0"
451 |       id: "sfra-docs-9b-stores"
452 |       method: "tools/call"
453 |       params:
454 |         name: "get_available_sfra_documents"
455 |         arguments: {}
456 |     expect:
457 |       response:
458 |         jsonrpc: "2.0"
459 |         id: "sfra-docs-9b-stores"
460 |         result:
461 |           isError: false
462 |           content:
463 |             match:arrayElements:
464 |               match:partial:
465 |                 text: "match:contains:stores"
466 |       stderr: toBeEmpty
467 | 
468 |   # Customer/account related documents
469 |   - it: "should include doc name: account"
470 |     request:
471 |       jsonrpc: "2.0"
472 |       id: "sfra-docs-9b-account"
473 |       method: "tools/call"
474 |       params:
475 |         name: "get_available_sfra_documents"
476 |         arguments: {}
477 |     expect:
478 |       response:
479 |         jsonrpc: "2.0"
480 |         id: "sfra-docs-9b-account"
481 |         result:
482 |           isError: false
483 |           content:
484 |             match:arrayElements:
485 |               match:partial:
486 |                 text: "match:contains:account"
487 |       stderr: toBeEmpty
488 | 
489 |   - it: "should include doc name: billing"
490 |     request:
491 |       jsonrpc: "2.0"
492 |       id: "sfra-docs-9b-billing"
493 |       method: "tools/call"
494 |       params:
495 |         name: "get_available_sfra_documents"
496 |         arguments: {}
497 |     expect:
498 |       response:
499 |         jsonrpc: "2.0"
500 |         id: "sfra-docs-9b-billing"
501 |         result:
502 |           isError: false
503 |           content:
504 |             match:arrayElements:
505 |               match:partial:
506 |                 text: "match:contains:billing"
507 |       stderr: toBeEmpty
508 | 
509 |   - it: "should include doc name: shipping"
510 |     request:
511 |       jsonrpc: "2.0"
512 |       id: "sfra-docs-9b-shipping"
513 |       method: "tools/call"
514 |       params:
515 |         name: "get_available_sfra_documents"
516 |         arguments: {}
517 |     expect:
518 |       response:
519 |         jsonrpc: "2.0"
520 |         id: "sfra-docs-9b-shipping"
521 |         result:
522 |           isError: false
523 |           content:
524 |             match:arrayElements:
525 |               match:partial:
526 |                 text: "match:contains:shipping"
527 |       stderr: toBeEmpty
528 | 
529 |   - it: "should include doc name: address"
530 |     request:
531 |       jsonrpc: "2.0"
532 |       id: "sfra-docs-9b-address"
533 |       method: "tools/call"
534 |       params:
535 |         name: "get_available_sfra_documents"
536 |         arguments: {}
537 |     expect:
538 |       response:
539 |         jsonrpc: "2.0"
540 |         id: "sfra-docs-9b-address"
541 |         result:
542 |           isError: false
543 |           content:
544 |             match:arrayElements:
545 |               match:partial:
546 |                 text: "match:contains:address"
547 |       stderr: toBeEmpty
548 | 
549 |   - it: "should include doc name: locale"
550 |     request:
551 |       jsonrpc: "2.0"
552 |       id: "sfra-docs-9b-locale"
553 |       method: "tools/call"
554 |       params:
555 |         name: "get_available_sfra_documents"
556 |         arguments: {}
557 |     expect:
558 |       response:
559 |         jsonrpc: "2.0"
560 |         id: "sfra-docs-9b-locale"
561 |         result:
562 |           isError: false
563 |           content:
564 |             match:arrayElements:
565 |               match:partial:
566 |                 text: "match:contains:locale"
567 |       stderr: toBeEmpty
568 | 
569 |   - it: "should return JSON-RPC method not found error for invalid method name"
570 |     request:
571 |       jsonrpc: "2.0"
572 |       id: "sfra-docs-error-1"
573 |       method: "tools/call_WRONG" # invalid base method to trigger JSON-RPC error
574 |       params:
575 |         name: "get_available_sfra_documents"
576 |         arguments: {}
577 |     expect:
578 |       response:
579 |         jsonrpc: "2.0"
580 |         id: "sfra-docs-error-1"
581 |         error:
582 |           code: "match:type:number"
583 |           message: "match:contains:Method"
584 | 
585 |   - it: "should include required keys in each document object at least once (name,title,category,filename)"
586 |     request:
587 |       jsonrpc: "2.0"
588 |       id: "sfra-docs-ext-1"
589 |       method: "tools/call"
590 |       params:
591 |         name: "get_available_sfra_documents"
592 |         arguments: {}
593 |     expect:
594 |       response:
595 |         jsonrpc: "2.0"
596 |         id: "sfra-docs-ext-1"
597 |         result:
598 |           isError: false
599 |           content:
600 |             match:arrayElements:
601 |               match:partial:
602 |                 # Newline-safe pattern ensuring all keys appear at least once in any order across pretty-printed JSON
603 |                 text: "match:regex:[\\s\\S]*\"name\"[\\s\\S]*\"title\"[\\s\\S]*\"category\"[\\s\\S]*\"filename\"[\\s\\S]*"
604 |       stderr: toBeEmpty
605 | 
606 |   - it: "should list multiple product model documents (at least 3 occurrences of 'product-')"
607 |     request:
608 |       jsonrpc: "2.0"
609 |       id: "sfra-docs-ext-2"
610 |       method: "tools/call"
611 |       params:
612 |         name: "get_available_sfra_documents"
613 |         arguments: {}
614 |     expect:
615 |       response:
616 |         jsonrpc: "2.0"
617 |         id: "sfra-docs-ext-2"
618 |         result:
619 |           isError: false
620 |           content:
621 |             match:arrayElements:
622 |               match:partial:
623 |                 # Use a broad regex ensuring at least three product- tokens appear anywhere
624 |                 text: "match:regex:(?:product-)[\\s\\S]*(?:product-)[\\s\\S]*(?:product-)"
625 |       stderr: toBeEmpty
626 | 
627 |   - it: "should have filenames ending with .md for multiple entries"
628 |     request:
629 |       jsonrpc: "2.0"
630 |       id: "sfra-docs-ext-4"
631 |       method: "tools/call"
632 |       params:
633 |         name: "get_available_sfra_documents"
634 |         arguments: {}
635 |     expect:
636 |       response:
637 |         jsonrpc: "2.0"
638 |         id: "sfra-docs-ext-4"
639 |         result:
640 |           isError: false
641 |           content:
642 |             match:arrayElements:
643 |               match:partial:
644 |                 text: "match:regex:\"filename\"\\s*:\\s*\"[a-z0-9\\-]+\\.md\""
645 |       stderr: toBeEmpty
646 | 
647 |   - it: "should respond faster than previous performance spec (tighten to 600ms)"
648 |     request:
649 |       jsonrpc: "2.0"
650 |       id: "sfra-docs-ext-5"
651 |       method: "tools/call"
652 |       params:
653 |         name: "get_available_sfra_documents"
654 |         arguments: {}
655 |     expect:
656 |       response:
657 |         jsonrpc: "2.0"
658 |         id: "sfra-docs-ext-5"
659 |         result:
660 |           match:partial:
661 |             isError: false
662 |       performance:
663 |         maxResponseTime: "600ms"
664 |       stderr: toBeEmpty
665 | 
666 |   - it: "should ignore unknown extraneous parameter without failing"
667 |     request:
668 |       jsonrpc: "2.0"
669 |       id: "sfra-docs-ext-6"
670 |       method: "tools/call"
671 |       params:
672 |         name: "get_available_sfra_documents"
673 |         arguments:
674 |           bogus: true
675 |     expect:
676 |       response:
677 |         jsonrpc: "2.0"
678 |         id: "sfra-docs-ext-6"
679 |         result:
680 |           match:partial:
681 |             isError: false
682 |       stderr: toBeEmpty
683 | 
684 | 
685 | 
```

--------------------------------------------------------------------------------
/tests/mcp/yaml/get-available-sfra-documents.docs-only.test.mcp.yml:
--------------------------------------------------------------------------------

```yaml
  1 | description: "Docs-only: get_available_sfra_documents tool tests"
  2 | config: ./aegis.config.docs-only.json
  3 | 
  4 | # We first list tools to ensure the tool exists in docs-only mode, then call it.
  5 | tests:
  6 |   - it: "should have get_available_sfra_documents in tools list"
  7 |     request:
  8 |       jsonrpc: "2.0"
  9 |       id: "list-sfra-docs-1"
 10 |       method: "tools/list"
 11 |       params: {}
 12 |     expect:
 13 |       response:
 14 |         jsonrpc: "2.0"
 15 |         id: "list-sfra-docs-1"
 16 |         result:
 17 |           tools:
 18 |             match:arrayElements:
 19 |               match:partial:
 20 |                 name: "match:type:string"
 21 |           match:extractField: "tools.*.name"
 22 |           value: "match:arrayContains:get_available_sfra_documents"
 23 | 
 24 |   - it: "should return an array JSON string of sfra documents in text content"
 25 |     request:
 26 |       jsonrpc: "2.0"
 27 |       id: "sfra-docs-1"
 28 |       method: "tools/call"
 29 |       params:
 30 |         name: "get_available_sfra_documents"
 31 |         arguments: {}
 32 |     expect:
 33 |       response:
 34 |         jsonrpc: "2.0"
 35 |         id: "sfra-docs-1"
 36 |         result:
 37 |           isError: false
 38 |           content:
 39 |             match:arrayElements:
 40 |               match:partial:
 41 |                 type: "text"
 42 |                 text: "match:regex:\\[[\\s\\S]*querystring[\\s\\S]*server[\\s\\S]*cart[\\s\\S]*stores[\\s\\S]*\\]" # array JSON includes expected doc names
 43 |       stderr: toBeEmpty
 44 | 
 45 |   - it: "should include core category documents (querystring, server, request, response, render)"
 46 |     request:
 47 |       jsonrpc: "2.0"
 48 |       id: "sfra-docs-2"
 49 |       method: "tools/call"
 50 |       params:
 51 |         name: "get_available_sfra_documents"
 52 |         arguments: {}
 53 |     expect:
 54 |       response:
 55 |         jsonrpc: "2.0"
 56 |         id: "sfra-docs-2"
 57 |         result:
 58 |           isError: false
 59 |           content:
 60 |             match:arrayElements:
 61 |               match:partial:
 62 |                 type: "text"
 63 |                 text: "match:contains:querystring"
 64 |       stderr: toBeEmpty
 65 | 
 66 |   - it: "should contain product and store model documents (product-full, product-tile, store, stores)"
 67 |     request:
 68 |       jsonrpc: "2.0"
 69 |       id: "sfra-docs-3"
 70 |       method: "tools/call"
 71 |       params:
 72 |         name: "get_available_sfra_documents"
 73 |         arguments: {}
 74 |     expect:
 75 |       response:
 76 |         jsonrpc: "2.0"
 77 |         id: "sfra-docs-3"
 78 |         result:
 79 |           isError: false
 80 |           content:
 81 |             match:arrayElements:
 82 |               match:partial:
 83 |                 type: "text"
 84 |                 text: "match:contains:product-full"
 85 |       stderr: toBeEmpty
 86 | 
 87 |   - it: "should not return error when called without arguments (empty object)"
 88 |     request:
 89 |       jsonrpc: "2.0"
 90 |       id: "sfra-docs-4"
 91 |       method: "tools/call"
 92 |       params:
 93 |         name: "get_available_sfra_documents"
 94 |         arguments: {}
 95 |     expect:
 96 |       response:
 97 |         jsonrpc: "2.0"
 98 |         id: "sfra-docs-4"
 99 |         result:
100 |           isError: false
101 |           content: "match:type:array"
102 |       stderr: toBeEmpty
103 | 
104 |   - it: "should include pricing documents (price-default, price-range, price-tiered)"
105 |     request:
106 |       jsonrpc: "2.0"
107 |       id: "sfra-docs-5"
108 |       method: "tools/call"
109 |       params:
110 |         name: "get_available_sfra_documents"
111 |         arguments: {}
112 |     expect:
113 |       response:
114 |         jsonrpc: "2.0"
115 |         id: "sfra-docs-5"
116 |         result:
117 |           isError: false
118 |           content:
119 |             match:arrayElements:
120 |               match:partial:
121 |                 text: "match:contains:price-default"
122 |       stderr: toBeEmpty
123 | 
124 |   - it: "should tolerate extraneous empty arguments object (idempotent behavior)"
125 |     request:
126 |       jsonrpc: "2.0"
127 |       id: "sfra-docs-6"
128 |       method: "tools/call"
129 |       params:
130 |         name: "get_available_sfra_documents"
131 |         arguments: {}
132 |     expect:
133 |       response:
134 |         jsonrpc: "2.0"
135 |         id: "sfra-docs-6"
136 |         result:
137 |           match:partial:
138 |             isError: false
139 |       stderr: toBeEmpty
140 | 
141 |   - it: "should respond within acceptable performance threshold"
142 |     request:
143 |       jsonrpc: "2.0"
144 |       id: "sfra-docs-7"
145 |       method: "tools/call"
146 |       params:
147 |         name: "get_available_sfra_documents"
148 |         arguments: {}
149 |     expect:
150 |       response:
151 |         jsonrpc: "2.0"
152 |         id: "sfra-docs-7"
153 |         result:
154 |           match:partial:
155 |             isError: false
156 |       performance:
157 |         maxResponseTime: "800ms" # docs listing should be fast but allow CI variance
158 |       stderr: toBeEmpty
159 | 
160 |   - it: "should include multiple distinct categories (core, order, product, pricing, store)"
161 |     request:
162 |       jsonrpc: "2.0"
163 |       id: "sfra-docs-8"
164 |       method: "tools/call"
165 |       params:
166 |         name: "get_available_sfra_documents"
167 |         arguments: {}
168 |     expect:
169 |       response:
170 |         jsonrpc: "2.0"
171 |         id: "sfra-docs-8"
172 |         result:
173 |           isError: false
174 |           content:
175 |             match:arrayElements:
176 |               match:partial:
177 |                 text: "match:regex:(core|order|product|pricing|store)"
178 |       stderr: toBeEmpty
179 | 
180 |   - it: "should expose at least 18 documents (count via regex on JSON array)"
181 |     request:
182 |       jsonrpc: "2.0"
183 |       id: "sfra-docs-9"
184 |       method: "tools/call"
185 |       params:
186 |         name: "get_available_sfra_documents"
187 |         arguments: {}
188 |     expect:
189 |       response:
190 |         jsonrpc: "2.0"
191 |         id: "sfra-docs-9"
192 |         result:
193 |           isError: false
194 |           content:
195 |             match:arrayElements:
196 |               match:partial:
197 |                 # Require at least 18 occurrences of the JSON key "name" (non-greedy across intervening content)
198 |                 # NOTE: The prior insanely large quantifier still matched because the engine satisfied the pattern once; quantifier applies to group occurrences.
199 |                 # This pattern enforces a realistic lower bound of 18.
200 |                 text: "match:regex:(?:\\\"name\\\"[\\s\\S]*?){18,}"
201 |       stderr: toBeEmpty
202 | 
203 |   # Individual presence tests replacing aggregated lookahead test for better failure diagnostics
204 |   # Core documents
205 |   - it: "should include doc name: server"
206 |     request:
207 |       jsonrpc: "2.0"
208 |       id: "sfra-docs-9b-server"
209 |       method: "tools/call"
210 |       params:
211 |         name: "get_available_sfra_documents"
212 |         arguments: {}
213 |     expect:
214 |       response:
215 |         jsonrpc: "2.0"
216 |         id: "sfra-docs-9b-server"
217 |         result:
218 |           isError: false
219 |           content:
220 |             match:arrayElements:
221 |               match:partial:
222 |                 text: "match:contains:server"
223 |       stderr: toBeEmpty
224 | 
225 |   - it: "should include doc name: request"
226 |     request:
227 |       jsonrpc: "2.0"
228 |       id: "sfra-docs-9b-request"
229 |       method: "tools/call"
230 |       params:
231 |         name: "get_available_sfra_documents"
232 |         arguments: {}
233 |     expect:
234 |       response:
235 |         jsonrpc: "2.0"
236 |         id: "sfra-docs-9b-request"
237 |         result:
238 |           isError: false
239 |           content:
240 |             match:arrayElements:
241 |               match:partial:
242 |                 text: "match:contains:request"
243 |       stderr: toBeEmpty
244 | 
245 |   - it: "should include doc name: response"
246 |     request:
247 |       jsonrpc: "2.0"
248 |       id: "sfra-docs-9b-response"
249 |       method: "tools/call"
250 |       params:
251 |         name: "get_available_sfra_documents"
252 |         arguments: {}
253 |     expect:
254 |       response:
255 |         jsonrpc: "2.0"
256 |         id: "sfra-docs-9b-response"
257 |         result:
258 |           isError: false
259 |           content:
260 |             match:arrayElements:
261 |               match:partial:
262 |                 text: "match:contains:response"
263 |       stderr: toBeEmpty
264 | 
265 |   - it: "should include doc name: querystring"
266 |     request:
267 |       jsonrpc: "2.0"
268 |       id: "sfra-docs-9b-querystring"
269 |       method: "tools/call"
270 |       params:
271 |         name: "get_available_sfra_documents"
272 |         arguments: {}
273 |     expect:
274 |       response:
275 |         jsonrpc: "2.0"
276 |         id: "sfra-docs-9b-querystring"
277 |         result:
278 |           isError: false
279 |           content:
280 |             match:arrayElements:
281 |               match:partial:
282 |                 text: "match:contains:querystring"
283 |       stderr: toBeEmpty
284 | 
285 |   - it: "should include doc name: render"
286 |     request:
287 |       jsonrpc: "2.0"
288 |       id: "sfra-docs-9b-render"
289 |       method: "tools/call"
290 |       params:
291 |         name: "get_available_sfra_documents"
292 |         arguments: {}
293 |     expect:
294 |       response:
295 |         jsonrpc: "2.0"
296 |         id: "sfra-docs-9b-render"
297 |         result:
298 |           isError: false
299 |           content:
300 |             match:arrayElements:
301 |               match:partial:
302 |                 text: "match:contains:render"
303 |       stderr: toBeEmpty
304 | 
305 |   # Functional / model documents
306 |   - it: "should include doc name: cart"
307 |     request:
308 |       jsonrpc: "2.0"
309 |       id: "sfra-docs-9b-cart"
310 |       method: "tools/call"
311 |       params:
312 |         name: "get_available_sfra_documents"
313 |         arguments: {}
314 |     expect:
315 |       response:
316 |         jsonrpc: "2.0"
317 |         id: "sfra-docs-9b-cart"
318 |         result:
319 |           isError: false
320 |           content:
321 |             match:arrayElements:
322 |               match:partial:
323 |                 text: "match:contains:cart"
324 |       stderr: toBeEmpty
325 | 
326 |   - it: "should include doc name: product-full"
327 |     request:
328 |       jsonrpc: "2.0"
329 |       id: "sfra-docs-9b-product-full"
330 |       method: "tools/call"
331 |       params:
332 |         name: "get_available_sfra_documents"
333 |         arguments: {}
334 |     expect:
335 |       response:
336 |         jsonrpc: "2.0"
337 |         id: "sfra-docs-9b-product-full"
338 |         result:
339 |           isError: false
340 |           content:
341 |             match:arrayElements:
342 |               match:partial:
343 |                 text: "match:contains:product-full"
344 |       stderr: toBeEmpty
345 | 
346 |   - it: "should include doc name: product-tile"
347 |     request:
348 |       jsonrpc: "2.0"
349 |       id: "sfra-docs-9b-product-tile"
350 |       method: "tools/call"
351 |       params:
352 |         name: "get_available_sfra_documents"
353 |         arguments: {}
354 |     expect:
355 |       response:
356 |         jsonrpc: "2.0"
357 |         id: "sfra-docs-9b-product-tile"
358 |         result:
359 |           isError: false
360 |           content:
361 |             match:arrayElements:
362 |               match:partial:
363 |                 text: "match:contains:product-tile"
364 |       stderr: toBeEmpty
365 | 
366 |   # Pricing documents
367 |   - it: "should include doc name: price-default"
368 |     request:
369 |       jsonrpc: "2.0"
370 |       id: "sfra-docs-9b-price-default"
371 |       method: "tools/call"
372 |       params:
373 |         name: "get_available_sfra_documents"
374 |         arguments: {}
375 |     expect:
376 |       response:
377 |         jsonrpc: "2.0"
378 |         id: "sfra-docs-9b-price-default"
379 |         result:
380 |           isError: false
381 |           content:
382 |             match:arrayElements:
383 |               match:partial:
384 |                 text: "match:contains:price-default"
385 |       stderr: toBeEmpty
386 | 
387 |   - it: "should include doc name: price-range"
388 |     request:
389 |       jsonrpc: "2.0"
390 |       id: "sfra-docs-9b-price-range"
391 |       method: "tools/call"
392 |       params:
393 |         name: "get_available_sfra_documents"
394 |         arguments: {}
395 |     expect:
396 |       response:
397 |         jsonrpc: "2.0"
398 |         id: "sfra-docs-9b-price-range"
399 |         result:
400 |           isError: false
401 |           content:
402 |             match:arrayElements:
403 |               match:partial:
404 |                 text: "match:contains:price-range"
405 |       stderr: toBeEmpty
406 | 
407 |   - it: "should include doc name: price-tiered"
408 |     request:
409 |       jsonrpc: "2.0"
410 |       id: "sfra-docs-9b-price-tiered"
411 |       method: "tools/call"
412 |       params:
413 |         name: "get_available_sfra_documents"
414 |         arguments: {}
415 |     expect:
416 |       response:
417 |         jsonrpc: "2.0"
418 |         id: "sfra-docs-9b-price-tiered"
419 |         result:
420 |           isError: false
421 |           content:
422 |             match:arrayElements:
423 |               match:partial:
424 |                 text: "match:contains:price-tiered"
425 |       stderr: toBeEmpty
426 | 
427 |   # Store documents
428 |   - it: "should include doc name: store"
429 |     request:
430 |       jsonrpc: "2.0"
431 |       id: "sfra-docs-9b-store"
432 |       method: "tools/call"
433 |       params:
434 |         name: "get_available_sfra_documents"
435 |         arguments: {}
436 |     expect:
437 |       response:
438 |         jsonrpc: "2.0"
439 |         id: "sfra-docs-9b-store"
440 |         result:
441 |           isError: false
442 |           content:
443 |             match:arrayElements:
444 |               match:partial:
445 |                 text: "match:contains:store"
446 |       stderr: toBeEmpty
447 | 
448 |   - it: "should include doc name: stores"
449 |     request:
450 |       jsonrpc: "2.0"
451 |       id: "sfra-docs-9b-stores"
452 |       method: "tools/call"
453 |       params:
454 |         name: "get_available_sfra_documents"
455 |         arguments: {}
456 |     expect:
457 |       response:
458 |         jsonrpc: "2.0"
459 |         id: "sfra-docs-9b-stores"
460 |         result:
461 |           isError: false
462 |           content:
463 |             match:arrayElements:
464 |               match:partial:
465 |                 text: "match:contains:stores"
466 |       stderr: toBeEmpty
467 | 
468 |   # Customer/account related documents
469 |   - it: "should include doc name: account"
470 |     request:
471 |       jsonrpc: "2.0"
472 |       id: "sfra-docs-9b-account"
473 |       method: "tools/call"
474 |       params:
475 |         name: "get_available_sfra_documents"
476 |         arguments: {}
477 |     expect:
478 |       response:
479 |         jsonrpc: "2.0"
480 |         id: "sfra-docs-9b-account"
481 |         result:
482 |           isError: false
483 |           content:
484 |             match:arrayElements:
485 |               match:partial:
486 |                 text: "match:contains:account"
487 |       stderr: toBeEmpty
488 | 
489 |   - it: "should include doc name: billing"
490 |     request:
491 |       jsonrpc: "2.0"
492 |       id: "sfra-docs-9b-billing"
493 |       method: "tools/call"
494 |       params:
495 |         name: "get_available_sfra_documents"
496 |         arguments: {}
497 |     expect:
498 |       response:
499 |         jsonrpc: "2.0"
500 |         id: "sfra-docs-9b-billing"
501 |         result:
502 |           isError: false
503 |           content:
504 |             match:arrayElements:
505 |               match:partial:
506 |                 text: "match:contains:billing"
507 |       stderr: toBeEmpty
508 | 
509 |   - it: "should include doc name: shipping"
510 |     request:
511 |       jsonrpc: "2.0"
512 |       id: "sfra-docs-9b-shipping"
513 |       method: "tools/call"
514 |       params:
515 |         name: "get_available_sfra_documents"
516 |         arguments: {}
517 |     expect:
518 |       response:
519 |         jsonrpc: "2.0"
520 |         id: "sfra-docs-9b-shipping"
521 |         result:
522 |           isError: false
523 |           content:
524 |             match:arrayElements:
525 |               match:partial:
526 |                 text: "match:contains:shipping"
527 |       stderr: toBeEmpty
528 | 
529 |   - it: "should include doc name: address"
530 |     request:
531 |       jsonrpc: "2.0"
532 |       id: "sfra-docs-9b-address"
533 |       method: "tools/call"
534 |       params:
535 |         name: "get_available_sfra_documents"
536 |         arguments: {}
537 |     expect:
538 |       response:
539 |         jsonrpc: "2.0"
540 |         id: "sfra-docs-9b-address"
541 |         result:
542 |           isError: false
543 |           content:
544 |             match:arrayElements:
545 |               match:partial:
546 |                 text: "match:contains:address"
547 |       stderr: toBeEmpty
548 | 
549 |   - it: "should include doc name: locale"
550 |     request:
551 |       jsonrpc: "2.0"
552 |       id: "sfra-docs-9b-locale"
553 |       method: "tools/call"
554 |       params:
555 |         name: "get_available_sfra_documents"
556 |         arguments: {}
557 |     expect:
558 |       response:
559 |         jsonrpc: "2.0"
560 |         id: "sfra-docs-9b-locale"
561 |         result:
562 |           isError: false
563 |           content:
564 |             match:arrayElements:
565 |               match:partial:
566 |                 text: "match:contains:locale"
567 |       stderr: toBeEmpty
568 | 
569 |   - it: "should return JSON-RPC method not found error for invalid method name"
570 |     request:
571 |       jsonrpc: "2.0"
572 |       id: "sfra-docs-error-1"
573 |       method: "tools/call_WRONG" # invalid base method to trigger JSON-RPC error
574 |       params:
575 |         name: "get_available_sfra_documents"
576 |         arguments: {}
577 |     expect:
578 |       response:
579 |         jsonrpc: "2.0"
580 |         id: "sfra-docs-error-1"
581 |         error:
582 |           code: "match:type:number"
583 |           message: "match:contains:Method"
584 | 
585 |   - it: "should include required keys in each document object at least once (name,title,category,filename)"
586 |     request:
587 |       jsonrpc: "2.0"
588 |       id: "sfra-docs-ext-1"
589 |       method: "tools/call"
590 |       params:
591 |         name: "get_available_sfra_documents"
592 |         arguments: {}
593 |     expect:
594 |       response:
595 |         jsonrpc: "2.0"
596 |         id: "sfra-docs-ext-1"
597 |         result:
598 |           isError: false
599 |           content:
600 |             match:arrayElements:
601 |               match:partial:
602 |                 # Newline-safe pattern ensuring all keys appear at least once in any order across pretty-printed JSON
603 |                 text: "match:regex:[\\s\\S]*\"name\"[\\s\\S]*\"title\"[\\s\\S]*\"category\"[\\s\\S]*\"filename\"[\\s\\S]*"
604 |       stderr: toBeEmpty
605 | 
606 |   - it: "should list multiple product model documents (at least 3 occurrences of 'product-')"
607 |     request:
608 |       jsonrpc: "2.0"
609 |       id: "sfra-docs-ext-2"
610 |       method: "tools/call"
611 |       params:
612 |         name: "get_available_sfra_documents"
613 |         arguments: {}
614 |     expect:
615 |       response:
616 |         jsonrpc: "2.0"
617 |         id: "sfra-docs-ext-2"
618 |         result:
619 |           isError: false
620 |           content:
621 |             match:arrayElements:
622 |               match:partial:
623 |                 # Use a broad regex ensuring at least three product- tokens appear anywhere
624 |                 text: "match:regex:(?:product-)[\\s\\S]*(?:product-)[\\s\\S]*(?:product-)"
625 |       stderr: toBeEmpty
626 | 
627 |   - it: "should have filenames ending with .md for multiple entries"
628 |     request:
629 |       jsonrpc: "2.0"
630 |       id: "sfra-docs-ext-4"
631 |       method: "tools/call"
632 |       params:
633 |         name: "get_available_sfra_documents"
634 |         arguments: {}
635 |     expect:
636 |       response:
637 |         jsonrpc: "2.0"
638 |         id: "sfra-docs-ext-4"
639 |         result:
640 |           isError: false
641 |           content:
642 |             match:arrayElements:
643 |               match:partial:
644 |                 text: "match:regex:\"filename\"\\s*:\\s*\"[a-z0-9\\-]+\\.md\""
645 |       stderr: toBeEmpty
646 | 
647 |   - it: "should respond faster than previous performance spec (tighten to 600ms)"
648 |     request:
649 |       jsonrpc: "2.0"
650 |       id: "sfra-docs-ext-5"
651 |       method: "tools/call"
652 |       params:
653 |         name: "get_available_sfra_documents"
654 |         arguments: {}
655 |     expect:
656 |       response:
657 |         jsonrpc: "2.0"
658 |         id: "sfra-docs-ext-5"
659 |         result:
660 |           match:partial:
661 |             isError: false
662 |       performance:
663 |         maxResponseTime: "600ms"
664 |       stderr: toBeEmpty
665 | 
666 |   - it: "should ignore unknown extraneous parameter without failing"
667 |     request:
668 |       jsonrpc: "2.0"
669 |       id: "sfra-docs-ext-6"
670 |       method: "tools/call"
671 |       params:
672 |         name: "get_available_sfra_documents"
673 |         arguments:
674 |           bogus: true
675 |     expect:
676 |       response:
677 |         jsonrpc: "2.0"
678 |         id: "sfra-docs-ext-6"
679 |         result:
680 |           match:partial:
681 |             isError: false
682 |       stderr: toBeEmpty
683 | 
684 | 
685 | 
```

--------------------------------------------------------------------------------
/src/clients/sfra-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * SFCC SFRA Documentation Client
  3 |  *
  4 |  * Provides access to SFRA (Storefront Reference Architecture) documentation including
  5 |  * core classes like Server, Request, Response, QueryString, render module, and comprehensive
  6 |  * model documentation for account, cart, products, pricing, billing, shipping, and more.
  7 |  */
  8 | 
  9 | import * as fs from 'fs/promises';
 10 | import * as path from 'path';
 11 | import { PathResolver } from '../utils/path-resolver.js';
 12 | import { CacheManager } from '../utils/cache.js';
 13 | import { Logger } from '../utils/logger.js';
 14 | 
 15 | export interface SFRADocument {
 16 |   title: string;
 17 |   description: string;
 18 |   sections: string[];
 19 |   content: string;
 20 |   type: 'class' | 'module' | 'model';
 21 |   category: 'core' | 'product' | 'order' | 'customer' | 'pricing' | 'store' | 'other';
 22 |   properties?: string[];
 23 |   methods?: string[];
 24 |   filename: string;
 25 |   lastModified?: Date;
 26 | }
 27 | 
 28 | export interface SFRADocumentSummary {
 29 |   name: string;
 30 |   title: string;
 31 |   description: string;
 32 |   type: string;
 33 |   category: string;
 34 |   filename: string;
 35 | }
 36 | 
 37 | // Document categorization rules
 38 | const CATEGORY_MAPPINGS: Record<string, string> = {
 39 |   // Core SFRA classes and modules
 40 |   'server': 'core',
 41 |   'request': 'core',
 42 |   'response': 'core',
 43 |   'querystring': 'core',
 44 |   'render': 'core',
 45 | 
 46 |   // Product-related models
 47 |   'product-full': 'product',
 48 |   'product-bundle': 'product',
 49 |   'product-tile': 'product',
 50 |   'product-search': 'product',
 51 |   'product-line-items': 'product',
 52 | 
 53 |   // Pricing models
 54 |   'price-default': 'pricing',
 55 |   'price-range': 'pricing',
 56 |   'price-tiered': 'pricing',
 57 | 
 58 |   // Order and cart models
 59 |   'cart': 'order',
 60 |   'order': 'order',
 61 |   'billing': 'order',
 62 |   'shipping': 'order',
 63 |   'payment': 'order',
 64 |   'totals': 'order',
 65 | 
 66 |   // Customer models
 67 |   'account': 'customer',
 68 |   'address': 'customer',
 69 | 
 70 |   // Store models
 71 |   'store': 'store',
 72 |   'stores': 'store',
 73 | 
 74 |   // Other models
 75 |   'categories': 'other',
 76 |   'content': 'other',
 77 |   'locale': 'other',
 78 | };
 79 | 
 80 | /**
 81 |  * Enhanced client for accessing SFRA documentation with dynamic discovery
 82 |  */
 83 | export class SFRAClient {
 84 |   private cache: CacheManager;
 85 |   private docsPath: string;
 86 |   private documentsCache: Map<string, SFRADocument> = new Map();
 87 |   private lastScanTime: number = 0;
 88 |   private static readonly SCAN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
 89 |   private logger: Logger;
 90 | 
 91 |   constructor() {
 92 |     this.cache = new CacheManager();
 93 |     this.docsPath = PathResolver.getSFRADocsPath();
 94 |     this.logger = Logger.getChildLogger('SFRAClient');
 95 |   }
 96 | 
 97 |   /**
 98 |    * Dynamically discover all available SFRA documentation files
 99 |    */
100 |   async getAvailableDocuments(): Promise<SFRADocumentSummary[]> {
101 |     const cacheKey = 'sfra:available-documents-v2';
102 |     const cached = this.cache.getSearchResults(cacheKey);
103 | 
104 |     // Check if we need to rescan the filesystem
105 |     const now = Date.now();
106 |     if (cached && (now - this.lastScanTime) < SFRAClient.SCAN_CACHE_TTL) {
107 |       return cached;
108 |     }
109 | 
110 |     try {
111 |       const files = await fs.readdir(this.docsPath);
112 |       const mdFiles = files.filter(file =>
113 |         file.endsWith('.md') &&
114 |         file !== 'README.md' &&
115 |         !file.startsWith('.'),
116 |       );
117 | 
118 |       const documents: SFRADocumentSummary[] = [];
119 | 
120 |       for (const filename of mdFiles) {
121 |         try {
122 |           const documentName = path.basename(filename, '.md');
123 |           const document = await this.getSFRADocumentMetadata(documentName);
124 | 
125 |           if (document) {
126 |             documents.push({
127 |               name: documentName,
128 |               title: document.title,
129 |               description: document.description,
130 |               type: document.type,
131 |               category: document.category,
132 |               filename: document.filename,
133 |             });
134 |           }
135 |         } catch (error) {
136 |           this.logger.error(`Error processing SFRA document ${filename}:`, error);
137 |           // Continue processing other files
138 |         }
139 |       }
140 | 
141 |       // Sort documents by category and then by name
142 |       documents.sort((a, b) => {
143 |         if (a.category !== b.category) {
144 |           // Prioritize core documents
145 |           if (a.category === 'core') {return -1;}
146 |           if (b.category === 'core') {return 1;}
147 |           return a.category.localeCompare(b.category);
148 |         }
149 |         return a.name.localeCompare(b.name);
150 |       });
151 | 
152 |       this.cache.setSearchResults(cacheKey, documents);
153 |       this.lastScanTime = now;
154 | 
155 |       return documents;
156 |     } catch (error) {
157 |       this.logger.error('Error scanning SFRA documents directory:', error);
158 |       return [];
159 |     }
160 |   }
161 | 
162 |   /**
163 |    * Get lightweight metadata for a document without loading full content
164 |    */
165 |   private async getSFRADocumentMetadata(documentName: string): Promise<SFRADocument | null> {
166 |     // Normalize document name for consistent caching and lookup
167 |     const normalizedDocumentName = documentName.toLowerCase();
168 | 
169 |     // Check if we already have this document cached
170 |     if (this.documentsCache.has(normalizedDocumentName)) {
171 |       return this.documentsCache.get(normalizedDocumentName)!;
172 |     }
173 | 
174 |     try {
175 |       const filePath = await this.validateAndConstructPath(documentName);
176 |       const stats = await fs.stat(filePath);
177 | 
178 |       // Check if we have a cached version that's still valid
179 |       const cacheKey = `sfra:metadata:${normalizedDocumentName}`;
180 |       const cached = this.cache.getFileContent(cacheKey);
181 |       if (cached) {
182 |         const cachedData = JSON.parse(cached);
183 |         if (cachedData.lastModified && new Date(cachedData.lastModified) >= stats.mtime) {
184 |           return cachedData;
185 |         }
186 |       }
187 | 
188 |       // Read only the first part of the file to extract metadata
189 |       const content = await fs.readFile(filePath, 'utf-8');
190 |       const lines = content.split('\n');
191 | 
192 |       // Extract title
193 |       const titleLine = lines.find(line => line.startsWith('#'));
194 |       const title = titleLine?.replace(/^#+\s*/, '').trim() ?? this.formatDocumentName(normalizedDocumentName);
195 | 
196 |       // Determine type based on title and content
197 |       const type = this.determineDocumentType(title, content);
198 | 
199 |       // Determine category - use normalized name for consistent mapping
200 |       const category = (CATEGORY_MAPPINGS[normalizedDocumentName] || 'other') as SFRADocument['category'];
201 | 
202 |       // Extract description (first substantial paragraph after title)
203 |       const description = this.extractDescription(lines, title);
204 | 
205 |       // Extract sections (## headers)
206 |       const sections = lines
207 |         .filter(line => line.startsWith('##'))
208 |         .map(line => line.replace(/^##\s*/, '').trim())
209 |         .filter(section => section.length > 0);
210 | 
211 |       const document: SFRADocument = {
212 |         title,
213 |         description,
214 |         sections,
215 |         content, // Keep full content for now, optimize later if needed
216 |         type,
217 |         category,
218 |         filename: `${normalizedDocumentName}.md`,
219 |         lastModified: stats.mtime,
220 |         ...(type === 'class' || type === 'model' ? {
221 |           properties: this.extractProperties(lines),
222 |           methods: this.extractMethods(lines),
223 |         } : {}),
224 |       };
225 | 
226 |       // Cache the metadata using normalized name
227 |       this.cache.setFileContent(cacheKey, JSON.stringify(document));
228 |       this.documentsCache.set(normalizedDocumentName, document);
229 | 
230 |       return document;
231 |     } catch (error) {
232 |       this.logger.error(`Error loading SFRA document metadata ${normalizedDocumentName}:`, error);
233 |       return null;
234 |     }
235 |   }
236 | 
237 |   /**
238 |    * Get a specific SFRA document with full content
239 |    */
240 |   async getSFRADocument(documentName: string): Promise<SFRADocument | null> {
241 |     // Normalize document name for consistent lookup
242 |     const normalizedDocumentName = documentName.toLowerCase();
243 | 
244 |     // First try to get from metadata cache
245 |     const metadata = await this.getSFRADocumentMetadata(documentName);
246 |     if (!metadata) {
247 |       return null;
248 |     }
249 | 
250 |     // If the content is already loaded, return it
251 |     if (metadata.content?.trim()) {
252 |       return metadata;
253 |     }
254 | 
255 |     // Otherwise, load the full content
256 |     try {
257 |       const filePath = await this.validateAndConstructPath(documentName);
258 |       const content = await fs.readFile(filePath, 'utf-8');
259 | 
260 |       const fullDocument: SFRADocument = {
261 |         ...metadata,
262 |         content,
263 |       };
264 | 
265 |       // Update cache using normalized name
266 |       this.documentsCache.set(normalizedDocumentName, fullDocument);
267 |       return fullDocument;
268 |     } catch (error) {
269 |       this.logger.error(`Error loading full SFRA document ${normalizedDocumentName}:`, error);
270 |       return metadata; // Return metadata even if content loading failed
271 |     }
272 |   }
273 | 
274 |   /**
275 |    * Enhanced search across all SFRA documentation with better categorization
276 |    */
277 |   async searchSFRADocumentation(query: string): Promise<Array<{
278 |     document: string;
279 |     title: string;
280 |     category: string;
281 |     type: string;
282 |     relevanceScore: number;
283 |     matches: Array<{section: string; content: string; lineNumber: number}>;
284 |   }>> {
285 |     const cacheKey = `sfra:search:${query.toLowerCase()}`;
286 |     const cached = this.cache.getSearchResults(cacheKey);
287 |     if (cached) {return cached;}
288 | 
289 |     const documents = await this.getAvailableDocuments();
290 |     const results = [];
291 |     const queryLower = query.toLowerCase();
292 |     const queryWords = queryLower.split(/\s+/).filter(word => word.length > 1);
293 | 
294 |     for (const doc of documents) {
295 |       const documentContent = await this.getSFRADocument(doc.name);
296 |       if (!documentContent) {continue;}
297 | 
298 |       const matches = [];
299 |       const lines = documentContent.content.split('\n');
300 |       let currentSection = '';
301 |       let relevanceScore = 0;
302 | 
303 |       // Calculate relevance score based on title and description matches
304 |       if (doc.title.toLowerCase().includes(queryLower)) {
305 |         relevanceScore += 10;
306 |       }
307 |       if (doc.description.toLowerCase().includes(queryLower)) {
308 |         relevanceScore += 5;
309 |       }
310 | 
311 |       // Search through content
312 |       for (let i = 0; i < lines.length; i++) {
313 |         const line = lines[i];
314 |         const lineLower = line.toLowerCase();
315 | 
316 |         if (line.startsWith('##')) {
317 |           currentSection = line.replace(/^##\s*/, '').trim();
318 |         }
319 | 
320 |         // Check for query matches
321 |         let matchFound = false;
322 |         let lineRelevance = 0;
323 | 
324 |         if (lineLower.includes(queryLower)) {
325 |           matchFound = true;
326 |           lineRelevance += 3;
327 |         } else {
328 |           // Check for partial matches with query words
329 |           const wordMatches = queryWords.filter(word => lineLower.includes(word));
330 |           if (wordMatches.length > 0) {
331 |             matchFound = true;
332 |             lineRelevance += wordMatches.length;
333 |           }
334 |         }
335 | 
336 |         if (matchFound) {
337 |           // Get context around the match
338 |           const contextStart = Math.max(0, i - 2);
339 |           const contextEnd = Math.min(lines.length, i + 3);
340 |           const context = lines.slice(contextStart, contextEnd)
341 |             .map((contextLine, idx) => {
342 |               const actualLineNumber = contextStart + idx;
343 |               return actualLineNumber === i ? `>>> ${contextLine}` : contextLine;
344 |             })
345 |             .join('\n');
346 | 
347 |           matches.push({
348 |             section: currentSection || 'Introduction',
349 |             content: context,
350 |             lineNumber: i + 1,
351 |           });
352 | 
353 |           relevanceScore += lineRelevance;
354 |         }
355 |       }
356 | 
357 |       if (matches.length > 0) {
358 |         results.push({
359 |           document: doc.name,
360 |           title: doc.title,
361 |           category: doc.category,
362 |           type: doc.type,
363 |           relevanceScore,
364 |           matches,
365 |         });
366 |       }
367 |     }
368 | 
369 |     // Sort by relevance score (highest first)
370 |     results.sort((a, b) => b.relevanceScore - a.relevanceScore);
371 | 
372 |     this.cache.setSearchResults(cacheKey, results);
373 |     return results;
374 |   }
375 | 
376 |   /**
377 |    * Get documents by category
378 |    */
379 |   async getDocumentsByCategory(category: string): Promise<SFRADocumentSummary[]> {
380 |     const allDocuments = await this.getAvailableDocuments();
381 |     return allDocuments.filter(doc => doc.category === category);
382 |   }
383 | 
384 |   /**
385 |    * Get all available categories
386 |    */
387 |   async getAvailableCategories(): Promise<Array<{category: string; count: number; description: string}>> {
388 |     const documents = await this.getAvailableDocuments();
389 |     const categoryMap = new Map<string, number>();
390 | 
391 |     documents.forEach(doc => {
392 |       categoryMap.set(doc.category, (categoryMap.get(doc.category) ?? 0) + 1);
393 |     });
394 | 
395 |     const categoryDescriptions = {
396 |       'core': 'Core SFRA classes and modules (Server, Request, Response, QueryString, render)',
397 |       'product': 'Product-related models and functionality',
398 |       'order': 'Order, cart, billing, shipping, and payment models',
399 |       'customer': 'Customer account and address models',
400 |       'pricing': 'Pricing and discount models',
401 |       'store': 'Store and location models',
402 |       'other': 'Other models and utilities',
403 |     };
404 | 
405 |     return Array.from(categoryMap.entries()).map(([category, count]) => ({
406 |       category,
407 |       count,
408 |       description: categoryDescriptions[category as keyof typeof categoryDescriptions] || 'Other documentation',
409 |     }));
410 |   }
411 | 
412 |   /**
413 |    * Enhanced path validation and construction
414 |    */
415 |   private async validateAndConstructPath(documentName: string): Promise<string> {
416 |     if (!documentName || typeof documentName !== 'string') {
417 |       throw new Error('Invalid document name: must be a non-empty string');
418 |     }
419 | 
420 |     if (documentName.includes('\0') || documentName.includes('\x00')) {
421 |       throw new Error('Invalid document name: contains null bytes');
422 |     }
423 | 
424 |     if (documentName.includes('..') || documentName.includes('/') || documentName.includes('\\')) {
425 |       throw new Error('Invalid document name: contains path traversal sequences');
426 |     }
427 | 
428 |     if (!/^[a-zA-Z0-9_-]+$/.test(documentName)) {
429 |       throw new Error('Invalid document name: contains invalid characters');
430 |     }
431 | 
432 |     // Normalize document name to lowercase for case-insensitive lookup
433 |     const normalizedDocumentName = documentName.toLowerCase();
434 |     const filePath = path.join(this.docsPath, `${normalizedDocumentName}.md`);
435 |     const resolvedPath = path.resolve(filePath);
436 |     const resolvedDocsPath = path.resolve(this.docsPath);
437 | 
438 |     if (!resolvedPath.startsWith(resolvedDocsPath)) {
439 |       throw new Error('Invalid document name: path outside allowed directory');
440 |     }
441 | 
442 |     if (!resolvedPath.toLowerCase().endsWith('.md')) {
443 |       throw new Error('Invalid document name: must reference a markdown file');
444 |     }
445 | 
446 |     return resolvedPath;
447 |   }
448 | 
449 |   /**
450 |    * Determine document type from title and content
451 |    */
452 |   private determineDocumentType(title: string, content: string): 'class' | 'module' | 'model' {
453 |     const titleLower = title.toLowerCase();
454 |     const contentLower = content.toLowerCase();
455 | 
456 |     if (titleLower.includes('class ')) {
457 |       return 'class';
458 |     }
459 | 
460 |     if (titleLower.includes('module ')) {
461 |       return 'module';
462 |     }
463 | 
464 |     if (titleLower.includes('model') || contentLower.includes('model') ||
465 |         contentLower.includes('constructor') || contentLower.includes('properties')) {
466 |       return 'model';
467 |     }
468 | 
469 |     return 'model'; // Default for most SFRA docs
470 |   }
471 | 
472 |   /**
473 |    * Extract description from document lines
474 |    */
475 |   private extractDescription(lines: string[], title: string): string {
476 |     const titleIndex = lines.findIndex(line => line.trim() === `# ${title}` || line.startsWith('#'));
477 |     if (titleIndex === -1) {
478 |       return 'No description available';
479 |     }
480 | 
481 |     // Look for overview section first
482 |     const overviewIndex = lines.findIndex((line, index) =>
483 |       index > titleIndex && line.toLowerCase().includes('## overview'),
484 |     );
485 | 
486 |     if (overviewIndex !== -1) {
487 |       // Get content under Overview section
488 |       let descriptionEnd = lines.findIndex((line, index) =>
489 |         index > overviewIndex + 1 && line.startsWith('##'),
490 |       );
491 | 
492 |       if (descriptionEnd === -1) {
493 |         descriptionEnd = Math.min(lines.length, overviewIndex + 10);
494 |       }
495 | 
496 |       const overviewContent = lines.slice(overviewIndex + 1, descriptionEnd)
497 |         .filter(line => line.trim() && !line.startsWith('#'))
498 |         .join(' ')
499 |         .trim();
500 | 
501 |       if (overviewContent) {
502 |         return overviewContent.substring(0, 300) + (overviewContent.length > 300 ? '...' : '');
503 |       }
504 |     }
505 | 
506 |     // Fallback to first paragraph after title
507 |     let descriptionStart = titleIndex + 1;
508 |     while (descriptionStart < lines.length && !lines[descriptionStart].trim()) {
509 |       descriptionStart++;
510 |     }
511 | 
512 |     const descriptionEnd = lines.findIndex((line, index) =>
513 |       index > descriptionStart && (line.startsWith('#') || line.trim() === ''));
514 | 
515 |     const description = lines
516 |       .slice(descriptionStart, descriptionEnd > -1 ? descriptionEnd : descriptionStart + 3)
517 |       .filter(line => line.trim() && !line.startsWith('#'))
518 |       .join(' ')
519 |       .trim();
520 | 
521 |     return description || 'No description available';
522 |   }
523 | 
524 |   /**
525 |    * Extract properties from document content
526 |    */
527 |   private extractProperties(lines: string[]): string[] {
528 |     const properties: string[] = [];
529 |     let inPropertiesSection = false;
530 | 
531 |     for (const line of lines) {
532 |       if (line.toLowerCase().includes('## properties') ||
533 |           line.toLowerCase().includes('## property')) {
534 |         inPropertiesSection = true;
535 |         continue;
536 |       }
537 | 
538 |       if (inPropertiesSection && line.startsWith('#') && !line.includes('properties')) {
539 |         break;
540 |       }
541 | 
542 |       if (inPropertiesSection && line.startsWith('### ')) {
543 |         const property = line.replace('### ', '').trim();
544 |         if (!properties.includes(property)) {
545 |           properties.push(property);
546 |         }
547 |       }
548 |     }
549 | 
550 |     return properties;
551 |   }
552 | 
553 |   /**
554 |    * Extract methods from document content
555 |    */
556 |   private extractMethods(lines: string[]): string[] {
557 |     const methods: string[] = [];
558 |     let inMethodSection = false;
559 | 
560 |     for (const line of lines) {
561 |       if (line.toLowerCase().includes('## method') ||
562 |           line.toLowerCase().includes('## function')) {
563 |         inMethodSection = true;
564 |         continue;
565 |       }
566 | 
567 |       if (inMethodSection && line.startsWith('#') &&
568 |           !line.toLowerCase().includes('method') &&
569 |           !line.toLowerCase().includes('function')) {
570 |         break;
571 |       }
572 | 
573 |       if (inMethodSection && line.startsWith('### ')) {
574 |         const method = line.replace('### ', '').trim();
575 |         if (!methods.includes(method)) {
576 |           methods.push(method);
577 |         }
578 |       }
579 |     }
580 | 
581 |     return methods;
582 |   }
583 | 
584 |   /**
585 |    * Format document name for display
586 |    */
587 |   private formatDocumentName(documentName: string): string {
588 |     return documentName
589 |       .split('-')
590 |       .map(word => word.charAt(0).toUpperCase() + word.slice(1))
591 |       .join(' ');
592 |   }
593 | 
594 |   /**
595 |    * Clear all caches
596 |    */
597 |   clearCache(): void {
598 |     this.cache.clearAll();
599 |     this.documentsCache.clear();
600 |     this.lastScanTime = 0;
601 |   }
602 | }
603 | 
```
Page 32/61FirstPrevNextLast