Commit 7e8f3b33 authored by Kostis Trantzas's avatar Kostis Trantzas
Browse files

Merge remote-tracking branch 'origin/2026Q2_RC'

parents a718beae 36f36b72
Loading
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -3,6 +3,6 @@ MAINTAINER osl.etsi.org

RUN mkdir /opt/osl

COPY target/org.etsi.osl.mcp.server-1.1.0-exec.jar /opt/osl
CMD ["java", "-jar", "/opt/osl/org.etsi.osl.mcp.server-1.1.0-exec.jar"]
COPY target/org.etsi.osl.mcp.server-1.2.0-exec.jar /opt/osl
CMD ["java", "-jar", "/opt/osl/org.etsi.osl.mcp.server-1.2.0-exec.jar"]
+59 −16
Original line number Diff line number Diff line
@@ -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

@@ -65,13 +76,13 @@ spring:
        stdio: false
  
  activemq:
    brokerUrl: tcp://portal.openslice.eu:61616
    brokerUrl: tcp://localhost:61616
    
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://portal.openslice.eu/auth/realms/openslice
          issuer-uri: https://localhost/auth/realms/openslice
```

## Installation & Usage
@@ -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

@@ -117,7 +128,7 @@ for Claude:
      "command": "npx",
      "args": [
	"mcp-remote@latest", 
	"http://portal.openslice.eu:13015/sse", 
	"http://localhost:13015/sse", 
	"--allow-http"]
    }
	
@@ -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 <token>"}

## Dependencies

- Spring Boot 3.4.5
+1 −1
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@
	<parent>
		<groupId>org.etsi.osl</groupId>
		<artifactId>org.etsi.osl.main</artifactId>
		<version>2025Q4</version>
		<version>2026Q2</version>
		<relativePath>../org.etsi.osl.main</relativePath>
	</parent>

+0 −22
Original line number Diff line number Diff line
@@ -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<McpServerFeatures.SyncResourceSpecification> myResources() {
    logger.info("calling myResources()");
+148 −150
Original line number Diff line number Diff line
@@ -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,50 +15,46 @@ 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<Catalog> serviceCatalogs = aCatalogClient.retrieveProductCatalogs();

    
 
    // Filter and get result as JSON string
    try {

      String[] tokens = { "id", "name", "description", "@type" };
      JsonNode filtered = JsonMassage.filterJsonByTokens(serviceCatalogs, tokens);
      return filtered;
@@ -68,21 +63,18 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(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) {
    
  @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<Category> productCategories = aCatalogClient.retrieveProductCategoriesDetailsOfCatalog(catalogName);

    // Filter and get result as JSON string
    try {

      String[] tokens = { "id", "name", "description", "@type" };
      JsonNode filtered = JsonMassage.filterJsonByTokens(productCategories, tokens);
      return filtered;
@@ -91,20 +83,18 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(serviceCatalogs);
    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<ProductOffering> productCategories = aCatalogClient.retrieveProductOfferingsByCategoryId(categoryId);

    // Filter and get result as JSON string
    try {

      String[] tokens = { "id", "name", "description" };
      JsonNode filtered = JsonMassage.filterJsonByTokens(productCategories, tokens);
      return filtered;
@@ -113,24 +103,19 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(serviceCatalogs);
    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) {
    
  @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" };
      String[] tokens = { "id", "name", "description", "isBundle", "@type", "configurable", "valueType" };
      JsonNode filtered = JsonMassage.filterJsonByTokens(spec, tokens);
      return filtered;
    } catch (Exception e) {
@@ -138,24 +123,20 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(serviceCatalogs);
    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) {
    
  @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" };
      String[] tokens = { "id", "name", "description", "isBundle", "@type", "configurable", "valueType",
          "productNumber", "brand" };
      JsonNode filtered = JsonMassage.filterJsonByTokens(spec, tokens);
      return filtered;
    } catch (Exception e) {
@@ -163,15 +144,14 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(serviceCatalogs);
    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<String> searchStrings) {
      McpSyncRequestContext context,
      @McpToolParam(description = "A list of search strings", required = true) List<String> searchStrings) {

    // Send logging notification
    context.info("Processing data: " + searchStrings);
@@ -201,8 +181,8 @@ public class ProductCatalogTools {

    // Filter and get result as JSON string
    try {

      String[] tokens = {"productOfferingId", "productName", "productDescription", "isBundle", "@type", "isBundle", "categoryName" };
      String[] tokens = { "productOfferingId", "productName", "productDescription", "isBundle", "@type",
          "categoryName" };
      JsonNode filtered = JsonMassage.filterJsonByTokens(spec, tokens);
      return filtered;
    } catch (Exception e) {
@@ -210,17 +190,26 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(serviceCatalogs);
    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<String, String> characteristics) {
  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<String, Map<String, String>> offeringsWithCharacteristics) {

    var authentication = SecurityContextHolder.getContext().getAuthentication();
    var username = authentication.getName();

    logger.info("createProductOrder {} {} {} {}", productOfferingId, startDate, endDate, characteristics.toString());
    logger.info("createProductOrder {} {} {} {}", username, startDate, endDate,
        offeringsWithCharacteristics.keySet().toString());

    ProductOrderCreate ponew = new ProductOrderCreate();

@@ -233,7 +222,6 @@ public class ProductCatalogTools {
    }
    ponew.setRequestedStartDate(sDate);


    if (endDate == null) {
      eDate = OffsetDateTime.now().plusDays(1);
    } else {
@@ -246,64 +234,78 @@ public class ProductCatalogTools {

    if (ponew.getRelatedParty() == null) {
      RelatedParty rp = new RelatedParty();
        rp.setName("MCP");
      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);
    }


    ProductOfferingRef prodOffRef = new ProductOfferingRef();
    prodOffRef.setId( productOfferingId );
    for (Map.Entry<String, Map<String, String>> entry : offeringsWithCharacteristics.entrySet()) {
      String productOfferingId = entry.getKey();
      Map<String, String> characteristics = entry.getValue();

      ProductOrderItem poi = new ProductOrderItem();
    poi.action(OrderItemActionType.ADD).productOffering(prodOffRef);
      poi.action(OrderItemActionType.ADD);

      ProductOffering productOffering = aCatalogClient.retrieveProductOffering(productOfferingId);

    ponew.getProductOrderItem().add(poi);
    
      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 servChar = new Characteristic();
        servChar.setUuid(null);
        servChar.setName(charKey);
        servChar.setValue( new Any( characteristics.get(charKey)  ));

          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 po = aCatalogClient.createProductOrder(ponew);

    ProductOrder so = aCatalogClient.createProductOrder(ponew);
    

    return "Product Order created with id :" + so.getId() ;
        
    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: " + po.getId();
  }

  @Tool(description = "Provide details for a product order given a Product Order id. "
  @McpTool(description = "Provide details for a product order given a Product Order id. "
      + "Focus attention to: "
      + "- the state of the product order"
      + "- 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"   };
      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) {
@@ -311,12 +313,8 @@ public class ProductCatalogTools {
    }

    ObjectMapper mapper = new ObjectMapper();
    //return mapper.writeValueAsString(serviceCatalogs);
    JsonNode rootNode = mapper.valueToTree(so);
    return rootNode;
  }


  

}
Loading