Loading mcp_module/README.md +15 −6 Changes for mcp_module/README.md: 15 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -33,17 +33,26 @@ mcp_module/ | Tool | File | Description | | --- | --- | --- | | `create_qod_session_oai` | `tools/qod.py` | Creates a CAMARA QoD session | | `get_qod_session_oai` | `tools/qod.py` | Retrieves QoD session details | | `delete_qod_session_oai` | `tools/qod.py` | Deletes an existing QoD session | | `get_app_definitions` | `tools/edge_application.py` | Lists available edge app definitions | | `register_app_definition` | `tools/edge_application.py` | Registers an app definition from package type, image path, and port | | `register_app_definition` | `tools/edge_application.py` | Registers an application manifest with the Edge Cloud Provider | | `get_app_definition` | `tools/edge_application.py` | Retrieves one app definition by id | | `delete_app_definition` | `tools/edge_application.py` | Deletes one app definition by id | | `get_edge_cloud_zones` | `tools/edge_application.py` | Lists edge cloud zones | | `instantiate_app` | `tools/edge_application.py` | Instantiates an app to one or more edge cloud zones | | `get_edge_cloud_zones` | `tools/edge_application.py` | Lists edge cloud zones, optionally filtered by region and status | | `instantiate_app` | `tools/edge_application.py` | Instantiates an app in a single edge cloud zone | | `get_app_instances` | `tools/edge_application.py` | Lists instantiated/deployed applications | | `delete_app_instance` | `tools/edge_application.py` | Deletes one app instance by id | | `create_app_deployment` | `tools/edge_application.py` | Requests application deployment across one or more edge cloud zones | | `get_app_deployments` | `tools/edge_application.py` | Lists application deployments | | `delete_app_deployment` | `tools/edge_application.py` | Requests termination of an application deployment by id | | `create_qod_session_oai` | `tools/qod.py` | Creates a CAMARA QoD session | | `get_qod_session_oai` | `tools/qod.py` | Retrieves QoD session details | | `delete_qod_session_oai` | `tools/qod.py` | Deletes an existing QoD session | | `create_traffic_influence` | `tools/traffic_influence.py` | Creates traffic influence for an application | | `create_traffic_influence_device` | `tools/traffic_influence.py` | Creates traffic influence for one device, identified by its IPv4 address | | `get_traffic_influences` | `tools/traffic_influence.py` | Lists traffic influence resources, optionally filtered by app id | | `get_traffic_influence` | `tools/traffic_influence.py` | Retrieves one traffic influence resource by id | | `delete_traffic_influence` | `tools/traffic_influence.py` | Requests deletion of a traffic influence resource by id | | `retrieve_location` | `tools/location.py` | Retrieves the location of a device | --- Loading mcp_module/app.py +4 −1 Changes for mcp_module/app.py: 4 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ from mcp_module.tools.qod import ( ) from mcp_module.tools.traffic_influence import ( create_traffic_influence, create_traffic_influence_device, delete_traffic_influence, get_traffic_influence, get_traffic_influences, Loading Loading @@ -62,6 +63,7 @@ SAFE_TOOLS: list[Callable[..., Any]] = [ create_app_deployment, delete_app_deployment, create_traffic_influence, create_traffic_influence_device, get_traffic_influences, get_traffic_influence, delete_traffic_influence, Loading @@ -69,7 +71,8 @@ SAFE_TOOLS: list[Callable[..., Any]] = [ ] for tool in SAFE_TOOLS: mcp.tool()(tool) mcp.add_tool(tool) mcp_app = mcp.http_app(path="") Loading mcp_module/models/traffic_influence_models.py +9 −0 Changes for mcp_module/models/traffic_influence_models.py: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ from uuid import UUID from pydantic import AwareDatetime, BaseModel, Field, field_validator from mcp_module.models.qod_models import DeviceInput class SourceTrafficFiltersInput(BaseModel): sourcePort: int | None = Field(default=None, ge=0, le=65535) Loading Loading @@ -59,3 +61,10 @@ class CreateTrafficInfluenceInput(BaseModel): sourceTrafficFilters: SourceTrafficFiltersInput | None = None destinationTrafficFilters: DestinationTrafficFiltersInput | None = None subscriptionRequest: SubscriptionRequestInput | None = None class CreateTrafficInfluenceDeviceInput(CreateTrafficInfluenceInput): device: DeviceInput | None = Field( default=None, description="The device, identified by ipv4Address. OEG requires it.", ) mcp_module/tools/traffic_influence.py +52 −0 Changes for mcp_module/tools/traffic_influence.py: 52 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ import logging import httpx from mcp_module.models.traffic_influence_models import ( CreateTrafficInfluenceDeviceInput, CreateTrafficInfluenceInput, ) from mcp_module.tools.oeg_errors import raise_for_oeg_status Loading Loading @@ -67,6 +68,57 @@ async def create_traffic_influence( return data async def create_traffic_influence_device( inp: CreateTrafficInfluenceDeviceInput, xCorrelator: str | None = None, ) -> dict: """ Create device-specific traffic influence toward local application instances. Identify the device by its IPv4 address. OEG cannot infer the device from the authorization token and refuses other identifiers. Optionally target an instance, zone, or region and supply traffic filters or a notification subscription. Reuse the idempotency key only when retrying the same creation. Creation does not imply that traffic influence is already active. """ payload = inp.model_dump(mode="json", exclude_none=True, exclude={"idempotencyKey"}) url = f"{get_traffic_influence_service_url()}/traffic-influence-devices" headers = get_traffic_influence_headers(xCorrelator) headers["content-type"] = "application/json" if inp.idempotencyKey is not None: headers["Idempotency-Key"] = inp.idempotencyKey logger.debug("Creating traffic influence at: %s", url) try: async with httpx.AsyncClient(trust_env=True) as client: response = await client.post( url, headers=headers, json=payload, timeout=15, ) raise_for_oeg_status(response) except httpx.HTTPError as e: logger.error("Failed to create traffic influence: %s", e) raise try: data = response.json() except ValueError as e: logger.error("Invalid JSON in traffic influence response: %s", e) raise if not isinstance(data, dict): raise TypeError("Expected a traffic influence object from OEG") return data async def get_traffic_influences( appId: str | None = None, xCorrelator: str | None = None, Loading tests/mcp_module/test_tool_requests.py +41 −2 Changes for tests/mcp_module/test_tool_requests.py: 41 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -11,7 +11,10 @@ from fastmcp.exceptions import ToolError from mcp_module.models import edge_application_models as em from mcp_module.models.location_models import RetrieveLocationInput from mcp_module.models.qod_models import CreateQoDSessionOAIInput, GetQoDSessionInput from mcp_module.models.traffic_influence_models import CreateTrafficInfluenceInput from mcp_module.models.traffic_influence_models import ( CreateTrafficInfluenceDeviceInput, CreateTrafficInfluenceInput, ) from mcp_module.security import authorization from mcp_module.tools import edge_application as edge from mcp_module.tools import location, qod Loading Loading @@ -42,6 +45,7 @@ SESSION = { "duration": 60, "devicePorts": {"ranges": [{"from": 80, "to": 90}]}, } DEVICE_IPV4 = {"ipv4Address": {"publicAddress": "198.51.100.1", "publicPort": 443}} @dataclass Loading Loading @@ -218,6 +222,23 @@ def cases(): status=201, idempotency=True, ), Case( traffic, "create_traffic_influence_device", { "inp": CreateTrafficInfluenceDeviceInput( apiConsumerId="consumer", appId=APP, device=DEVICE_IPV4, idempotencyKey="retry-1", ), }, "POST", "/traffic-influence/v0.10/traffic-influence-devices", {"apiConsumerId": "consumer", "appId": APP, "device": DEVICE_IPV4}, status=201, idempotency=True, ), Case( traffic, "get_traffic_influences", Loading Loading @@ -382,4 +403,22 @@ def test_cases_cover_all_registered_tools(): and node.target.id == "SAFE_TOOLS" ) assert {node.id for node in registered} == {case.name for case in CASES} assert len(CASES) == 19 assert len(CASES) == 20 def test_device_traffic_influence_without_idempotency_key(mock_oeg): body = {"apiConsumerId": "consumer", "appId": APP, "device": DEVICE_IPV4} def respond(request): assert request.url.path == "/traffic-influence/v0.10/traffic-influence-devices" assert json.loads(request.content) == body assert "idempotency-key" not in request.headers return httpx.Response(201, json={"trafficInfluenceID": APP, "state": "ordered"}) mock_oeg(respond) result = asyncio.run( traffic.create_traffic_influence_device( CreateTrafficInfluenceDeviceInput(**body) ) ) assert result == {"trafficInfluenceID": APP, "state": "ordered"} Loading
mcp_module/README.md +15 −6 Changes for mcp_module/README.md: 15 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -33,17 +33,26 @@ mcp_module/ | Tool | File | Description | | --- | --- | --- | | `create_qod_session_oai` | `tools/qod.py` | Creates a CAMARA QoD session | | `get_qod_session_oai` | `tools/qod.py` | Retrieves QoD session details | | `delete_qod_session_oai` | `tools/qod.py` | Deletes an existing QoD session | | `get_app_definitions` | `tools/edge_application.py` | Lists available edge app definitions | | `register_app_definition` | `tools/edge_application.py` | Registers an app definition from package type, image path, and port | | `register_app_definition` | `tools/edge_application.py` | Registers an application manifest with the Edge Cloud Provider | | `get_app_definition` | `tools/edge_application.py` | Retrieves one app definition by id | | `delete_app_definition` | `tools/edge_application.py` | Deletes one app definition by id | | `get_edge_cloud_zones` | `tools/edge_application.py` | Lists edge cloud zones | | `instantiate_app` | `tools/edge_application.py` | Instantiates an app to one or more edge cloud zones | | `get_edge_cloud_zones` | `tools/edge_application.py` | Lists edge cloud zones, optionally filtered by region and status | | `instantiate_app` | `tools/edge_application.py` | Instantiates an app in a single edge cloud zone | | `get_app_instances` | `tools/edge_application.py` | Lists instantiated/deployed applications | | `delete_app_instance` | `tools/edge_application.py` | Deletes one app instance by id | | `create_app_deployment` | `tools/edge_application.py` | Requests application deployment across one or more edge cloud zones | | `get_app_deployments` | `tools/edge_application.py` | Lists application deployments | | `delete_app_deployment` | `tools/edge_application.py` | Requests termination of an application deployment by id | | `create_qod_session_oai` | `tools/qod.py` | Creates a CAMARA QoD session | | `get_qod_session_oai` | `tools/qod.py` | Retrieves QoD session details | | `delete_qod_session_oai` | `tools/qod.py` | Deletes an existing QoD session | | `create_traffic_influence` | `tools/traffic_influence.py` | Creates traffic influence for an application | | `create_traffic_influence_device` | `tools/traffic_influence.py` | Creates traffic influence for one device, identified by its IPv4 address | | `get_traffic_influences` | `tools/traffic_influence.py` | Lists traffic influence resources, optionally filtered by app id | | `get_traffic_influence` | `tools/traffic_influence.py` | Retrieves one traffic influence resource by id | | `delete_traffic_influence` | `tools/traffic_influence.py` | Requests deletion of a traffic influence resource by id | | `retrieve_location` | `tools/location.py` | Retrieves the location of a device | --- Loading
mcp_module/app.py +4 −1 Changes for mcp_module/app.py: 4 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ from mcp_module.tools.qod import ( ) from mcp_module.tools.traffic_influence import ( create_traffic_influence, create_traffic_influence_device, delete_traffic_influence, get_traffic_influence, get_traffic_influences, Loading Loading @@ -62,6 +63,7 @@ SAFE_TOOLS: list[Callable[..., Any]] = [ create_app_deployment, delete_app_deployment, create_traffic_influence, create_traffic_influence_device, get_traffic_influences, get_traffic_influence, delete_traffic_influence, Loading @@ -69,7 +71,8 @@ SAFE_TOOLS: list[Callable[..., Any]] = [ ] for tool in SAFE_TOOLS: mcp.tool()(tool) mcp.add_tool(tool) mcp_app = mcp.http_app(path="") Loading
mcp_module/models/traffic_influence_models.py +9 −0 Changes for mcp_module/models/traffic_influence_models.py: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ from uuid import UUID from pydantic import AwareDatetime, BaseModel, Field, field_validator from mcp_module.models.qod_models import DeviceInput class SourceTrafficFiltersInput(BaseModel): sourcePort: int | None = Field(default=None, ge=0, le=65535) Loading Loading @@ -59,3 +61,10 @@ class CreateTrafficInfluenceInput(BaseModel): sourceTrafficFilters: SourceTrafficFiltersInput | None = None destinationTrafficFilters: DestinationTrafficFiltersInput | None = None subscriptionRequest: SubscriptionRequestInput | None = None class CreateTrafficInfluenceDeviceInput(CreateTrafficInfluenceInput): device: DeviceInput | None = Field( default=None, description="The device, identified by ipv4Address. OEG requires it.", )
mcp_module/tools/traffic_influence.py +52 −0 Changes for mcp_module/tools/traffic_influence.py: 52 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ import logging import httpx from mcp_module.models.traffic_influence_models import ( CreateTrafficInfluenceDeviceInput, CreateTrafficInfluenceInput, ) from mcp_module.tools.oeg_errors import raise_for_oeg_status Loading Loading @@ -67,6 +68,57 @@ async def create_traffic_influence( return data async def create_traffic_influence_device( inp: CreateTrafficInfluenceDeviceInput, xCorrelator: str | None = None, ) -> dict: """ Create device-specific traffic influence toward local application instances. Identify the device by its IPv4 address. OEG cannot infer the device from the authorization token and refuses other identifiers. Optionally target an instance, zone, or region and supply traffic filters or a notification subscription. Reuse the idempotency key only when retrying the same creation. Creation does not imply that traffic influence is already active. """ payload = inp.model_dump(mode="json", exclude_none=True, exclude={"idempotencyKey"}) url = f"{get_traffic_influence_service_url()}/traffic-influence-devices" headers = get_traffic_influence_headers(xCorrelator) headers["content-type"] = "application/json" if inp.idempotencyKey is not None: headers["Idempotency-Key"] = inp.idempotencyKey logger.debug("Creating traffic influence at: %s", url) try: async with httpx.AsyncClient(trust_env=True) as client: response = await client.post( url, headers=headers, json=payload, timeout=15, ) raise_for_oeg_status(response) except httpx.HTTPError as e: logger.error("Failed to create traffic influence: %s", e) raise try: data = response.json() except ValueError as e: logger.error("Invalid JSON in traffic influence response: %s", e) raise if not isinstance(data, dict): raise TypeError("Expected a traffic influence object from OEG") return data async def get_traffic_influences( appId: str | None = None, xCorrelator: str | None = None, Loading
tests/mcp_module/test_tool_requests.py +41 −2 Changes for tests/mcp_module/test_tool_requests.py: 41 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -11,7 +11,10 @@ from fastmcp.exceptions import ToolError from mcp_module.models import edge_application_models as em from mcp_module.models.location_models import RetrieveLocationInput from mcp_module.models.qod_models import CreateQoDSessionOAIInput, GetQoDSessionInput from mcp_module.models.traffic_influence_models import CreateTrafficInfluenceInput from mcp_module.models.traffic_influence_models import ( CreateTrafficInfluenceDeviceInput, CreateTrafficInfluenceInput, ) from mcp_module.security import authorization from mcp_module.tools import edge_application as edge from mcp_module.tools import location, qod Loading Loading @@ -42,6 +45,7 @@ SESSION = { "duration": 60, "devicePorts": {"ranges": [{"from": 80, "to": 90}]}, } DEVICE_IPV4 = {"ipv4Address": {"publicAddress": "198.51.100.1", "publicPort": 443}} @dataclass Loading Loading @@ -218,6 +222,23 @@ def cases(): status=201, idempotency=True, ), Case( traffic, "create_traffic_influence_device", { "inp": CreateTrafficInfluenceDeviceInput( apiConsumerId="consumer", appId=APP, device=DEVICE_IPV4, idempotencyKey="retry-1", ), }, "POST", "/traffic-influence/v0.10/traffic-influence-devices", {"apiConsumerId": "consumer", "appId": APP, "device": DEVICE_IPV4}, status=201, idempotency=True, ), Case( traffic, "get_traffic_influences", Loading Loading @@ -382,4 +403,22 @@ def test_cases_cover_all_registered_tools(): and node.target.id == "SAFE_TOOLS" ) assert {node.id for node in registered} == {case.name for case in CASES} assert len(CASES) == 19 assert len(CASES) == 20 def test_device_traffic_influence_without_idempotency_key(mock_oeg): body = {"apiConsumerId": "consumer", "appId": APP, "device": DEVICE_IPV4} def respond(request): assert request.url.path == "/traffic-influence/v0.10/traffic-influence-devices" assert json.loads(request.content) == body assert "idempotency-key" not in request.headers return httpx.Response(201, json={"trafficInfluenceID": APP, "state": "ordered"}) mock_oeg(respond) result = asyncio.run( traffic.create_traffic_influence_device( CreateTrafficInfluenceDeviceInput(**body) ) ) assert result == {"trafficInfluenceID": APP, "state": "ordered"}