diff --git a/README.md b/README.md index 76a4157600899bee658b0e0e8208b5dbbdb67898..a21c4d8432d708bdcb81d78e6982f70dba6fa55f 100644 --- a/README.md +++ b/README.md @@ -15,24 +15,35 @@ The OpenSlice MCP Server is a Spring Boot application that bridges OpenSlice's s ### MCP Tools Available - **Service Catalog Management**: - - Browse service catalogs and categories - - Search for service specifications - - Get detailed service specification information - - Access resource specifications + - `getOSLServiceCatalogs`: List all published service catalogs + - `getOSLServiceCategories(catalogName)`: List categories in a catalog + - `getOSLServiceSpecsInCategory(categoryId)`: List service specification references in a category + - `getOSLServiceSpecificationByServiceSpecificationId(serviceSpecId)`: Get full details of a service specification + - `getOSLResourceSpecificationByResourceSpecificationId(resourceSpecI)`: Get full details of a resource specification + - `searchOSLServiceSpecifications(searchStrings)`: Search across all published service specifications - **Service Order Management**: - - Create service orders with customizable characteristics - - Track service order status and progress - - Retrieve service order details and history + - `createServiceOrder(serviceSpecId, startDate, endDate, characteristics)`: Create a service order with optional characteristics. Requires authorization. + - `getServiceOrder(serviceOrderId)`: Get full details and status of a service order - **Service Instance Management**: - - View service instances and their details - - Update service characteristics - - Monitor service status and supporting resources + - `getService(serviceId)`: Get details of a service instance (state, characteristics, supporting services/resources) + - `updateService(serviceId, characteristics)`: Update characteristics of a running service instance + +- **Product Catalog Management**: + - `getOSLProductCatalogs`: List all published product catalogs + - `getOSLProductCategories(catalogName)`: List categories in a product catalog + - `getOSLProductOfferingsInCategory(categoryId)`: List product offerings in a category + - `getOSLProductOfferingByProductOfferingId(productOfferingId)`: Get full details of a product offering + - `getOSLProductByProductSpecificationId(productSpecId)`: Get full details of a product specification + - `searchOSLProductOfferings(searchStrings)`: Search across all published product offerings + +- **Product Order Management**: + - `createProductOrder(startDate, endDate, offeringsWithCharacteristics)`: Create a product order for one or more offerings with per-offering characteristics. Requires authorization. + - `getProductOrder(productOrderId)`: Get full details and status of a product order - **Resource Management**: - - Access resource information and specifications - - Track resource status and characteristics + - `getResource(resourceId)`: Get details of a resource instance (status, characteristics) ### Authentication & Security @@ -101,7 +112,7 @@ The server will start on port 13015 and provide MCP endpoints for AI assistant i Once running, the server can be connected to Claude Code or other MCP-compatible clients: -1. The server exposes its MCP interface at: `http://localhost:13015/mcp/messages` +1. The server exposes its MCP interface (SSE) at: `http://localhost:13015/sse` 2. AI assistants can discover and use the available tools for OpenSlice operations 3. Authentication is required via JWT tokens from the configured OAuth2 provider @@ -127,6 +138,38 @@ for Claude: > Note: At the time of your download, the `mcp-remote@latest` tag may be incompatible with the committed OSL MCP Server, due to the growing ongoing research. If you experience connectivity issues with your MCP Client, please use `mcp-remote@0.1.18`, which is extensively tested and works. +### Typical Tool Workflow + +The intended call sequence for an AI agent interacting with OpenSlice via MCP: + +**Service ordering flow:** +1. `getOSLServiceCatalogs`: discover available catalogs +2. `getOSLServiceCategories(catalogName)`: list categories in a catalog +3. `getOSLServiceSpecsInCategory(categoryId)`: find specs in a category, or use `searchOSLServiceSpecifications(["keyword"])` to search directly +4. `getOSLServiceSpecificationByServiceSpecificationId(serviceSpecId)`: inspect characteristics and details before ordering +5. `createServiceOrder(serviceSpecId, startDate, endDate, characteristics)`: place the order +6. `getServiceOrder(serviceOrderId)`: poll for status and retrieve supporting service/resource IDs + +**Product ordering flow:** +1. `getOSLProductCatalogs`: discover available catalogs +2. `getOSLProductCategories(catalogName)`: list categories in a catalog +3. `getOSLProductOfferingsInCategory(categoryId)`: find offerings in a category, or use `searchOSLProductOfferings(["keyword"])` to search directly +4. `getOSLProductOfferingByProductOfferingId(productOfferingId)`: inspect configurable characteristics before ordering +5. `createProductOrder(startDate, endDate, offeringsWithCharacteristics)`: place the order +6. `getProductOrder(productOrderId)`: poll for status and retrieve supporting product IDs + +### Debugging with MCP Inspector + +To browse and test all available tools interactively: + +```bash +npx @modelcontextprotocol/inspector http://localhost:13015/sse +``` + +The Inspector UI opens at `http://localhost:5173` and lets you call any tool manually, inspect inputs/outputs, and verify authentication flows. +To use tools that require authorization, a Custom Header should be added as follows: +{"Authorization": "Bearer "} + ## Dependencies - Spring Boot 3.4.5 diff --git a/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java b/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java index 5808501beeefe89f9577585515e1ca2cb0aa785d..5c549dd81bf3a0f30de3ba863a8e36cad82bd4c9 100644 --- a/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java +++ b/src/main/java/org/etsi/osl/mcp/server/OSLMCPServerApplication.java @@ -35,31 +35,9 @@ public class OSLMCPServerApplication { SpringApplication.run(OSLMCPServerApplication.class, args); } -// @Bean -// public ToolCallbackProvider serviceTools( ServiceCatalogTools oslServices) { -// return MethodToolCallbackProvider.builder().toolObjects( oslServices ).build(); -// } -// - - - @Bean - public ToolCallbackProvider productTools( ProductCatalogTools oslProducts) { - return MethodToolCallbackProvider.builder().toolObjects( oslProducts ).build(); - } - - - public record TextInput(String input) { } -// @Bean -// public ToolCallback toUpperCase() { -// return FunctionToolCallback.builder("toUpperCase", (TextInput input) -> input.input().toUpperCase()) -// .inputType(TextInput.class) -// .description("Put the text to upper case") -// .build(); -// } - @Bean public List myResources() { logger.info("calling myResources()"); diff --git a/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java b/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java index d3ae3208e062d5fbe5f6ff66f8f65dbaf4e0cfec..8f1c409971d791c47275584dabfba244060904f1 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java +++ b/src/main/java/org/etsi/osl/mcp/server/ProductCatalogTools.java @@ -4,8 +4,7 @@ import java.io.IOException; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; + import org.etsi.osl.tmf.common.model.Any; import org.etsi.osl.tmf.common.model.service.Characteristic; import org.etsi.osl.tmf.common.model.service.Note; @@ -16,166 +15,147 @@ import org.etsi.osl.tmf.pcm620.model.ProductOfferingRef; import org.etsi.osl.tmf.pcm620.model.ProductSpecification; import org.etsi.osl.tmf.pcm620.model.ProductSpecificationRef; import org.etsi.osl.tmf.pim637.model.Product; +import org.etsi.osl.tmf.pim637.model.ProductRefOrValue; import org.etsi.osl.tmf.pim637.model.ProductUpdate; import org.etsi.osl.tmf.po622.model.OrderItemActionType; import org.etsi.osl.tmf.po622.model.ProductOrder; import org.etsi.osl.tmf.po622.model.ProductOrderCreate; import org.etsi.osl.tmf.po622.model.ProductOrderItem; -import org.etsi.osl.tmf.po622.model.ProductOrderStateType; import org.etsi.osl.tmf.prm669.model.RelatedParty; -import org.etsi.osl.tmf.ri639.model.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; import org.springaicommunity.mcp.context.McpSyncRequestContext; -import org.springframework.ai.tool.annotation.Tool; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** + * Product Catalog MCP Tools * * @author ctranoris */ @org.springframework.stereotype.Service public class ProductCatalogTools { - private static final Logger logger = LoggerFactory.getLogger(ProductCatalogTools.class); - - - //private final RestClient restClient; - + @Autowired ProductCatalogQClient aCatalogClient; - - @Tool(description = "Get a list of all published OSL OpenSlice product catalogs." + @McpTool(description = "Get a list of all published OSL OpenSlice product catalogs. " + "Each catalog contains product categories, that we can search individually to get the details and contents of each category.") public JsonNode getOSLProductCatalogs() { - logger.info("getOSLProductCatalogs"); List serviceCatalogs = aCatalogClient.retrieveProductCatalogs(); - - - + // Filter and get result as JSON string try { - - String[] tokens = {"id", "name", "description", "@type"}; - JsonNode filtered = JsonMassage.filterJsonByTokens( serviceCatalogs, tokens); + String[] tokens = { "id", "name", "description", "@type" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(serviceCatalogs, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( serviceCatalogs ); + JsonNode rootNode = mapper.valueToTree(serviceCatalogs); return rootNode; } - - @Tool(description = "Get OSL product categories in product catalog providing a catalog name") - public JsonNode getOSLProductCategories(String catalogName) { - - logger.info("getOSLProductCategories {}", catalogName); + @McpTool(description = "Get OSL product categories in product catalog providing a catalog name") + public JsonNode getOSLProductCategories( + @McpToolParam(description = "The product catalog name", required = true) String catalogName) { + logger.info("getOSLProductCategories {}", catalogName); List productCategories = aCatalogClient.retrieveProductCategoriesDetailsOfCatalog(catalogName); - + // Filter and get result as JSON string try { - - String[] tokens = {"id", "name", "description", "@type"}; - JsonNode filtered = JsonMassage.filterJsonByTokens( productCategories, tokens); + String[] tokens = { "id", "name", "description", "@type" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(productCategories, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( productCategories ); + JsonNode rootNode = mapper.valueToTree(productCategories); return rootNode; } - - @Tool(description = "Get a list of OSL product offerings in a product category, given a category ID") - public JsonNode getOSLProductOfferingsInCategory(String categoryId) { - + @McpTool(description = "Get a list of OSL product offerings in a product category, given a category ID") + public JsonNode getOSLProductOfferingsInCategory( + @McpToolParam(description = "The categoryId needed to search product offering references", required = true) String categoryId) { logger.info("getOSLProductOfferingsInCategory {}", categoryId); List productCategories = aCatalogClient.retrieveProductOfferingsByCategoryId(categoryId); + // Filter and get result as JSON string try { - - String[] tokens = {"id", "name", "description"}; - JsonNode filtered = JsonMassage.filterJsonByTokens( productCategories, tokens); + String[] tokens = { "id", "name", "description" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(productCategories, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( productCategories ); + JsonNode rootNode = mapper.valueToTree(productCategories); return rootNode; - - } - - @Tool(description = "Get all the details of an OSL product offering give a product offering Id") - public JsonNode getOSLProductOfferingByProductOfferingId(String productOfferingId) { - - logger.info("getOSLProductOfferingByProductOfferingId {}", productOfferingId); - ProductOffering spec = aCatalogClient.retrieveProductOffering(productOfferingId); + @McpTool(description = "Get all the details of an OSL product offering give a product offering Id") + public JsonNode getOSLProductOfferingByProductOfferingId( + @McpToolParam(description = "The product Offering Id needed to get product offering details", required = true) String productOfferingId) { + logger.info("getOSLProductOfferingByProductOfferingId {}", productOfferingId); + ProductOffering spec = aCatalogClient.retrieveProductOffering(productOfferingId); // Filter and get result as JSON string try { - - String[] tokens = {"id", "name", "description", "isBundle", "@type", "configurable", "valueType", "isBundle" }; - JsonNode filtered = JsonMassage.filterJsonByTokens( spec, tokens); + String[] tokens = { "id", "name", "description", "isBundle", "@type", "configurable", "valueType" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(spec, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( spec ); + JsonNode rootNode = mapper.valueToTree(spec); return rootNode; - } - - @Tool(description = "Get all the details of an OSL product specification give a product Specification Id") - public JsonNode getOSLProductByProductSpecificationId(String productSpecId) { - - logger.info("getOSLProductByProductSpecificationId {}", productSpecId); - ProductSpecification spec = aCatalogClient.retrieveProductSpec(productSpecId); + @McpTool(description = "Get all the details of an OSL product specification give a product Specification Id") + public JsonNode getOSLProductByProductSpecificationId( + @McpToolParam(description = "The product Specification Id needed to get product specification details", required = true) String productSpecId) { + logger.info("getOSLProductByProductSpecificationId {}", productSpecId); + ProductSpecification spec = aCatalogClient.retrieveProductSpec(productSpecId); // Filter and get result as JSON string try { - - String[] tokens = {"id", "name", "description", "isBundle", "@type", "configurable", "valueType", "isBundle", - "productNumber", "brand", "isBundle", "isBundle", "isBundle", "isBundle" }; - JsonNode filtered = JsonMassage.filterJsonByTokens( spec, tokens); + String[] tokens = { "id", "name", "description", "isBundle", "@type", "configurable", "valueType", + "productNumber", "brand" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(spec, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( spec ); + JsonNode rootNode = mapper.valueToTree(spec); return rootNode; } - @McpTool(description = "Search for OSL product Offerings that are published and available for product ordering in all categories") - public JsonNode searchOSLProductOfferings( - McpSyncRequestContext context,List searchStrings) { - + public JsonNode searchOSLProductOfferings( + McpSyncRequestContext context, + @McpToolParam(description = "A list of search strings", required = true) List searchStrings) { + // Send logging notification context.info("Processing data: " + searchStrings); - // Send progress notification (using convenient method) + // Send progress notification (using convenient method) context.progress(p -> p.progress(0.5).total(1.0).message("Processing...")); logger.info("searchOSLServiceSpecifications containing words: {}", searchStrings); @@ -197,126 +177,144 @@ public class ProductCatalogTools { } logger.info("Expanded search strings: {}", expandedSearchStrings); - List spec = aCatalogClient.searchProductOfferings( expandedSearchStrings ); + List spec = aCatalogClient.searchProductOfferings(expandedSearchStrings); // Filter and get result as JSON string try { - - String[] tokens = {"productOfferingId", "productName", "productDescription", "isBundle", "@type", "isBundle", "categoryName" }; - JsonNode filtered = JsonMassage.filterJsonByTokens( spec, tokens); + String[] tokens = { "productOfferingId", "productName", "productDescription", "isBundle", "@type", + "categoryName" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(spec, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( spec ); + JsonNode rootNode = mapper.valueToTree(spec); return rootNode; } - - @Tool(description = "Create a product order given a Product Offering id, the Start date an end date of the order. " - + "The user can provide also characteristics of product in the map with format key, value" + + @PreAuthorize("isAuthenticated()") + @McpTool(description = "Create a product order for one or more product offerings, with a common start and end date. " + + "Each product offering can have its own set of characteristics. " + "Date Time has the format YYYY-MM-DDTHH:mm:ss+00:00") - public String createProductOrder(String productOfferingId, String startDate, String endDate, Map characteristics) { - - logger.info("createProductOrder {} {} {} {}", productOfferingId, startDate, endDate, characteristics.toString()); + public String createProductOrder( + @McpToolParam(description = "The start date of the order", required = true) String startDate, + @McpToolParam(description = "The end date of the order", required = true) String endDate, + @McpToolParam(description = "A map of product offerings to order. " + + "Each key is a productOfferingId, and each value is a map of characteristic name to characteristic value for that offering. " + + "Example: {\"offering-id-123\": {\"speed\": \"100Mbps\", \"location\": \"Rome\"}}", required = true) Map> offeringsWithCharacteristics) { + + var authentication = SecurityContextHolder.getContext().getAuthentication(); + var username = authentication.getName(); + + logger.info("createProductOrder {} {} {} {}", username, startDate, endDate, + offeringsWithCharacteristics.keySet().toString()); - ProductOrderCreate ponew = new ProductOrderCreate(); + ProductOrderCreate ponew = new ProductOrderCreate(); OffsetDateTime sDate; OffsetDateTime eDate; if (startDate == null) { sDate = OffsetDateTime.now(); } else { - sDate = OffsetDateTime.parse(startDate); - } - ponew.setRequestedStartDate( sDate ); - + sDate = OffsetDateTime.parse(startDate); + } + ponew.setRequestedStartDate(sDate); if (endDate == null) { eDate = OffsetDateTime.now().plusDays(1); } else { - eDate = OffsetDateTime.parse(endDate); + eDate = OffsetDateTime.parse(endDate); } - ponew.setRequestedCompletionDate( eDate ); + ponew.setRequestedCompletionDate(eDate); ponew.setCategory("Automated order from MCP"); ponew.setDescription("Automatically from MCP "); - + if (ponew.getRelatedParty() == null) { - RelatedParty rp = new RelatedParty(); - rp.setName("MCP"); - rp.setRole("REQUESTER"); - ponew.addRelatedPartyItem(rp); + RelatedParty rp = new RelatedParty(); + rp.setName(username); + rp.setRole("REQUESTER"); + ponew.addRelatedPartyItem(rp); } if (ponew.getNote() == null) { - Note n = new Note(); - - n.setText( "Order created by MCP"); - - ponew.addNoteItem(n); + Note n = new Note(); + n.setText("Order created by MCP"); + ponew.addNoteItem(n); } - - - ProductOfferingRef prodOffRef = new ProductOfferingRef(); - prodOffRef.setId( productOfferingId ); - - ProductOrderItem poi = new ProductOrderItem(); - poi.action(OrderItemActionType.ADD).productOffering(prodOffRef); - - - ponew.getProductOrderItem().add(poi); - - - if (characteristics!=null) { - for (String charKey : characteristics.keySet()) { - Characteristic servChar = new Characteristic(); - servChar.setUuid(null); - servChar.setName(charKey); - servChar.setValue( new Any( characteristics.get(charKey) )); + for (Map.Entry> entry : offeringsWithCharacteristics.entrySet()) { + String productOfferingId = entry.getKey(); + Map characteristics = entry.getValue(); + + ProductOrderItem poi = new ProductOrderItem(); + poi.action(OrderItemActionType.ADD); + + ProductOffering productOffering = aCatalogClient.retrieveProductOffering(productOfferingId); + + ProductOfferingRef prodOffRef = new ProductOfferingRef(); + prodOffRef.setId(productOfferingId); + prodOffRef.setName(productOffering.getName()); + + ProductRefOrValue productRef = new ProductRefOrValue(); + if (productOffering.getProductSpecification() != null) { + ProductSpecificationRef aProductSpecificationRef = new ProductSpecificationRef(); + aProductSpecificationRef.setId(productOffering.getProductSpecification().getId()); + aProductSpecificationRef.setName(productOffering.getProductSpecification().getName()); + aProductSpecificationRef.setVersion(productOffering.getProductSpecification().getVersion()); + productRef.setProductSpecification(aProductSpecificationRef); } + + if (characteristics != null) { + for (String charKey : characteristics.keySet()) { + Characteristic prodChar = new Characteristic(); + prodChar.setUuid(null); + prodChar.setName(charKey); + prodChar.setValue(new Any(characteristics.get(charKey))); + productRef.addProductCharacteristicItem(prodChar); + } + } + + poi.productOffering(prodOffRef); + poi.setProduct(productRef); + ponew.getProductOrderItem().add(poi); } - - - ProductOrder so = aCatalogClient.createProductOrder(ponew); - + ProductOrder po = aCatalogClient.createProductOrder(ponew); + + if (po == null) { + logger.error("createProductOrder returned null - catalog service may be unavailable"); + return "Product Order submission failed: no response from catalog service. Please try again later."; + } - return "Product Order created with id :" + so.getId() ; - + return "Product Order created with id: " + po.getId(); } - - - @Tool(description = "Provide details for a product order given a Product Order id. " - + "Focus attention to:" - + "- the state of the product order" - + "- and each order item. Especially for each order item focus to the product and especially: status, characteristics and supporting products." + + @McpTool(description = "Provide details for a product order given a Product Order id. " + + "Focus attention to: " + + "- the state of the product order, " + + "- and each order item. Especially for each order item focus to the product and especially: status, characteristics and supporting products. " + "- For each supporting product we can retrieve more information by using the product id.") - public JsonNode getProductOrder(String productOrderId) { - - logger.info("productOrderId {} {} {} {}", productOrderId); + public JsonNode getProductOrder( + @McpToolParam(description = "The Product Order id", required = true) String productOrderId) { + logger.info("getProductOrder {}", productOrderId); ProductOrder so = aCatalogClient.retrieveProductOrder(productOrderId); + // Filter and get result as JSON string try { - - String[] tokens = {"id", "name", "description", "orderDate", "completionDate", - "expectedCompletionDate", "requestedCompletionDate" , "requestedStartDate" , "startDate" , "category" , "state" , "action" , "value" }; - JsonNode filtered = JsonMassage.filterJsonByTokens( so, tokens); + String[] tokens = { "id", "name", "description", "orderDate", "completionDate", "expectedCompletionDate", + "requestedCompletionDate", "requestedStartDate", "startDate", "category", "state", "action", "value" }; + JsonNode filtered = JsonMassage.filterJsonByTokens(so, tokens); return filtered; } catch (Exception e) { e.printStackTrace(); } - + ObjectMapper mapper = new ObjectMapper(); - //return mapper.writeValueAsString(serviceCatalogs); - JsonNode rootNode = mapper.valueToTree( so ); + JsonNode rootNode = mapper.valueToTree(so); return rootNode; } - - - } diff --git a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java index 10b27d945f7092ba2193bda25c2f167166348c40..da2249d491d98fa8143198a40b1cb37c8c02503f 100644 --- a/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java +++ b/src/main/java/org/etsi/osl/mcp/server/ServiceCatalogTools.java @@ -318,7 +318,11 @@ public class ServiceCatalogTools { ServiceOrder so = aCatalogClient.createServiceOrder(sonew); - + + if (so == null) { + logger.error("createServiceOrder returned null - catalog service may be unavailable"); + return "Service Order submission failed: no response from catalog service. Please try again later."; + } return "Service Order created with id :" + so.getId() ;