# Copyright 2026 Emcie Co Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os import tempfile from fastapi import status import httpx from lagom import Container from parlant.core.services.tools.mcp_service import DEFAULT_MCP_PORT, MCPToolServer from parlant.core.services.tools.plugins import tool from parlant.core.tools import ToolResult, ToolContext from parlant.core.services.tools.service_registry import ServiceRegistry from tests.test_utilities import ( SERVER_BASE_URL, run_mcp_server, run_openapi_server, run_service_server, ) async def test_that_sdk_service_is_created( async_client: httpx.AsyncClient, ) -> None: content = ( ( await async_client.put( "/services/my_sdk_service", json={ "kind": "sdk", "sdk": { "url": "https://example.com/sdk", }, }, ) ) .raise_for_status() .json() ) assert content["name"] == "my_sdk_service" assert content["kind"] == "sdk" assert content["url"] == "https://example.com/sdk" async def test_that_sdk_service_fails_to_create_due_to_url_not_starting_with_http_or_https( async_client: httpx.AsyncClient, ) -> None: response = await async_client.put( "/services/my_sdk_service", json={ "kind": "sdk", "sdk": { "url": "example.com/sdk", }, }, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT assert response.json()["detail"] == "Service URL is missing schema (http:// or https://)" async def test_that_openapi_service_is_created_with_url_source( async_client: httpx.AsyncClient, ) -> None: async with run_openapi_server() as server_info: url = f"{server_info.url}:{server_info.port}" source = f"{url}/openapi.json" response = await async_client.put( "/services/my_openapi_service", json={ "kind": "openapi", "openapi": { "url": url, "source": source, }, }, ) response.raise_for_status() content = response.json() assert content["name"] == "my_openapi_service" assert content["kind"] == "openapi" assert content["url"] == url async def test_that_openapi_service_is_created_with_file_source( async_client: httpx.AsyncClient, ) -> None: openapi_json = { "openapi": "3.0.0", "info": {"title": "TestAPI", "version": "1.0.0"}, "paths": { "/hello": { "get": { "summary": "Say Hello", "operationId": "print_hello__get", "responses": { "200": { "description": "Successful Response", "content": {"application/json": {"schema": {"type": "string"}}}, } }, } } }, } with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as tmp_file: json.dump(openapi_json, tmp_file) source = tmp_file.name response = await async_client.put( "/services/my_openapi_file_service", json={ "kind": "openapi", "openapi": { "url": SERVER_BASE_URL, "source": source, }, }, ) response.raise_for_status() content = response.json() assert content["name"] == "my_openapi_file_service" assert content["kind"] == "openapi" assert content["url"] == SERVER_BASE_URL os.remove(source) async def test_that_sdk_service_is_created_and_deleted( async_client: httpx.AsyncClient, ) -> None: _ = ( ( await async_client.put( "/services/my_sdk_service", json={ "kind": "sdk", "sdk": { "url": "https://example.com/sdk", }, }, ) ) .raise_for_status() .json() ) await async_client.delete("/services/my_sdk_service") response = await async_client.get("/services/my_sdk_service") assert response.status_code == status.HTTP_404_NOT_FOUND async def test_that_openapi_service_is_created_and_deleted( async_client: httpx.AsyncClient, ) -> None: async with run_openapi_server() as server_info: url = f"{server_info.url}:{server_info.port}" source = f"{url}/openapi.json" _ = ( await async_client.put( "/services/my_openapi_service", json={ "kind": "openapi", "openapi": { "url": url, "source": source, }, }, ) ).raise_for_status() await async_client.delete("/services/my_openapi_service") response = await async_client.get("/services/my_sdk_service") assert response.status_code == status.HTTP_404_NOT_FOUND async def test_that_services_can_be_listed( async_client: httpx.AsyncClient, ) -> None: assert (await async_client.get("/services")).raise_for_status().json() == [] _ = ( ( await async_client.put( "/services/my_sdk_service", json={ "kind": "sdk", "sdk": { "url": "https://example.com/sdk", }, }, ) ) .raise_for_status() .json() ) async with run_openapi_server() as server_info: url = f"{server_info.url}:{server_info.port}" source = f"{url}/openapi.json" response = await async_client.put( "/services/my_openapi_service", json={ "kind": "openapi", "openapi": { "url": url, "source": source, }, }, ) response.raise_for_status() async with MCPToolServer(tools=[], host="localhost"): _ = ( await async_client.put( "/services/my_mcp_service", json={ "kind": "mcp", "mcp": { "url": f"{SERVER_BASE_URL}:{DEFAULT_MCP_PORT}", }, }, ) ).raise_for_status() services = (await async_client.get("/services")).raise_for_status().json() assert len(services) == 3 sdk_service = next((p for p in services if p["name"] == "my_sdk_service"), None) assert sdk_service is not None assert sdk_service["kind"] == "sdk" assert sdk_service["url"] == "https://example.com/sdk" openapi_service = next((p for p in services if p["name"] == "my_openapi_service"), None) assert openapi_service is not None assert openapi_service["kind"] == "openapi" assert openapi_service["url"] == url mcp_service = next((p for p in services if p["name"] == "my_mcp_service"), None) assert mcp_service is not None assert mcp_service["kind"] == "mcp" assert mcp_service["url"] == f"{SERVER_BASE_URL}:{DEFAULT_MCP_PORT}" async def test_that_reading_an_existing_openapi_service_returns_its_metadata_and_tools( async_client: httpx.AsyncClient, container: Container, ) -> None: service_registry = container[ServiceRegistry] async with run_openapi_server() as server_info: url = f"{server_info.url}:{server_info.port}" source = f"{url}/openapi.json" await service_registry.update_tool_service( name="my_openapi_service", kind="openapi", url=url, source=source, ) service_data = ( (await async_client.get("/services/my_openapi_service")).raise_for_status().json() ) assert service_data["name"] == "my_openapi_service" assert service_data["kind"] == "openapi" assert service_data["url"] == url tools = service_data["tools"] assert len(tools) > 0 for t in tools: assert "name" in t assert "description" in t async def test_that_reading_an_existing_sdk_service_returns_its_metadata_and_tools( async_client: httpx.AsyncClient, container: Container, ) -> None: @tool def my_tool(context: ToolContext, arg_1: int, arg_2: int) -> ToolResult: return ToolResult(arg_1 + arg_2) @tool async def my_async_tool(context: ToolContext, message: str) -> ToolResult: return ToolResult(f"Echo: {message}") service_registry = container[ServiceRegistry] async with run_service_server([my_tool, my_async_tool]) as server: await service_registry.update_tool_service( name="my_sdk_service", kind="sdk", url=server.url, ) response = await async_client.get("/services/my_sdk_service") response.raise_for_status() service_data = response.json() assert service_data["name"] == "my_sdk_service" assert service_data["kind"] == "sdk" assert service_data["url"] == server.url tools_list = service_data["tools"] assert len(tools_list) == 2 assert any( t["name"] == my_tool.tool.name and t["description"] == my_tool.tool.description for t in tools_list ) assert any( t["name"] == my_async_tool.tool.name and t["description"] == my_async_tool.tool.description for t in tools_list ) async def test_that_mcp_service_is_created( async_client: httpx.AsyncClient, ) -> None: async with run_mcp_server(tools=[]) as server_info: url = f"{server_info.url}:{server_info.port}" content = ( ( await async_client.put( "/services/my_mcp_service", json={ "kind": "mcp", "mcp": { "url": url, }, }, ) ) .raise_for_status() .json() ) assert content["name"] == "my_mcp_service" assert content["kind"] == "mcp" assert content["url"] == url async def test_that_mcp_service_is_created_and_deleted( async_client: httpx.AsyncClient, ) -> None: async with run_mcp_server(tools=[]) as server_info: _ = ( ( await async_client.put( "/services/my_mcp_service", json={ "kind": "mcp", "mcp": { "url": f"{server_info.url}:{server_info.port}", }, }, ) ) .raise_for_status() .json() ) await async_client.delete("/services/my_mcp_service") response = await async_client.get("/services/my_mcp_service") assert response.status_code == status.HTTP_404_NOT_FOUND async def test_that_reading_an_existing_mcp_service_returns_its_metadata_and_tools( async_client: httpx.AsyncClient, container: Container, ) -> None: def my_tool(arg_1: int, arg_2: int) -> int: return arg_1 + arg_2 async def my_async_tool(message: str) -> str: return f"Echo: {message}" service_registry = container[ServiceRegistry] async with run_mcp_server(tools=[my_tool, my_async_tool]) as server_info: url = f"{server_info.url}:{server_info.port}" await service_registry.update_tool_service( name="my_mcp_service", kind="mcp", url=url, ) service_data = ( (await async_client.get("/services/my_mcp_service")).raise_for_status().json() ) assert service_data["name"] == "my_mcp_service" assert service_data["kind"] == "mcp" assert service_data["url"] == url tools = service_data["tools"] assert len(tools) == 2 for t in tools: assert "name" in t assert "description" in t