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 | ```