From 86e24491482ae7938a12828c49b58c8d83af2cef Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 20 Jun 2025 10:30:08 +0200 Subject: [PATCH 1/6] run server --- llama_deploy/apiserver/deployment.py | 8 ++++++++ llama_deploy/apiserver/server.py | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/llama_deploy/apiserver/deployment.py b/llama_deploy/apiserver/deployment.py index bf4c1536..97447f14 100644 --- a/llama_deploy/apiserver/deployment.py +++ b/llama_deploy/apiserver/deployment.py @@ -13,6 +13,8 @@ from typing import Any, Tuple, Type from dotenv import dotenv_values +from fastmcp import FastMCP +from fastmcp.server.http import StarletteWithLifespan from workflows import Context, Workflow from workflows.handler import WorkflowHandler @@ -395,6 +397,8 @@ def __init__(self, max_deployments: int = 10) -> None: self._last_control_plane_port = 8002 self._simple_message_queue_server: asyncio.Task | None = None self._serving = False + self._mcp = FastMCP("LlamaDeploy") + self._mcp_app = self._mcp.http_app(path="/") @property def deployment_names(self) -> list[str]: @@ -407,6 +411,10 @@ def deployments_path(self) -> Path: raise ValueError("Deployments path not set") return self._deployments_path + @property + def mcp_app(self) -> StarletteWithLifespan: + return self._mcp_app + def set_deployments_path(self, path: Path | None) -> None: self._deployments_path = ( path or Path(tempfile.gettempdir()) / "llama_deploy" / "deployments" diff --git a/llama_deploy/apiserver/server.py b/llama_deploy/apiserver/server.py index 3bfa3f9b..efa1d843 100644 --- a/llama_deploy/apiserver/server.py +++ b/llama_deploy/apiserver/server.py @@ -19,6 +19,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]: apiserver_state.state("starting") manager.set_deployments_path(settings.deployments_path) + app.mount("/mcp", manager.mcp_app) + t = asyncio.create_task(manager.serve()) await asyncio.sleep(0) @@ -39,7 +41,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, Any]: logger.error(f"Failed to deploy {yaml_file}: {str(e)}") apiserver_state.state("running") - yield + async with manager.mcp_app.lifespan(app): + yield t.cancel() From db1ac58a612c2c6d1d19e5f57f91ab80fb96bfd4 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 20 Jun 2025 11:16:03 +0200 Subject: [PATCH 2/6] mount workflow services into the MCP server --- llama_deploy/apiserver/deployment.py | 38 +++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/llama_deploy/apiserver/deployment.py b/llama_deploy/apiserver/deployment.py index 97447f14..09e92c27 100644 --- a/llama_deploy/apiserver/deployment.py +++ b/llama_deploy/apiserver/deployment.py @@ -13,9 +13,12 @@ from typing import Any, Tuple, Type from dotenv import dotenv_values +from fastmcp import Context as MCPContext from fastmcp import FastMCP from fastmcp.server.http import StarletteWithLifespan +from pydantic import BaseModel from workflows import Context, Workflow +from workflows.events import Event, StartEvent, StopEvent from workflows.handler import WorkflowHandler from llama_deploy.apiserver.source_managers.base import SyncPolicy @@ -47,6 +50,7 @@ def __init__( config: DeploymentConfig, base_path: Path, deployment_path: Path, + mcp: FastMCP, local: bool = False, ) -> None: """Creates a Deployment instance. @@ -59,6 +63,7 @@ def __init__( self._local = local self._name = config.name self._base_path = base_path + self._mcp = mcp # If not local, isolate the deployment in a folder with the same name to avoid conflicts self._deployment_path = ( deployment_path if local else deployment_path / config.name @@ -236,7 +241,9 @@ def _load_services(self, config: DeploymentConfig) -> dict[str, Workflow]: sys.path.append(str(pythonpath)) module = importlib.import_module(module_name) - workflow_services[service_id] = getattr(module, workflow_name) + workflow = getattr(module, workflow_name) + workflow_services[service_id] = workflow + self._make_mcp_service(workflow, service_id) service_state.labels(self._name, service_id).state("ready") @@ -369,6 +376,34 @@ def _install_dependencies(service_config: Service, source_root: Path) -> None: msg = f"Unable to install service dependencies using command '{e.cmd}': {e.stderr}" raise DeploymentError(msg) from None + def _make_mcp_service( + self, workflow: Workflow, service_name: str, description: str = "" + ) -> None: + # Dynamically get the start event class -- this is a bit of a hack + StartEventT = workflow._start_event_class + if StartEventT is StartEvent: + raise ValueError("Must declare a custom StartEvent class in your workflow.") + + @self._mcp.tool(name=service_name, description=description) + async def _workflow_tool(run_args: StartEventT, context: MCPContext) -> Any: # type:ignore + # Handle edge cases where the start event is an Event or a BaseModel + # If the workflow does not have a custom StartEvent class, then we need to handle the event differently + if isinstance(run_args, Event) and StartEventT != StartEvent: + handler = workflow.run(start_event=run_args) + elif isinstance(run_args, BaseModel): + handler = workflow.run(**run_args.model_dump()) # type: ignore + elif isinstance(run_args, dict): + start_event = StartEventT.model_validate(run_args) + handler = workflow.run(start_event=start_event) + else: + raise ValueError(f"Invalid start event type: {type(run_args)}") + + async for event in handler.stream_events(): + if not isinstance(event, StopEvent): + await context.log(level="info", message=event.model_dump_json()) + + return await handler + class Manager: """The Manager orchestrates deployments and their runtime. @@ -476,6 +511,7 @@ async def deploy( base_path=Path(base_path), deployment_path=self.deployments_path, local=local, + mcp=self._mcp, ) self._deployments[config.name] = deployment await deployment.start() From 0165ea25c8ac0a4f86b9daf653597f87684f4fc3 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 20 Jun 2025 13:21:09 +0200 Subject: [PATCH 3/6] add example --- examples/mcp_server/README.md | 61 ++++++++++++++++++++++++++++ examples/mcp_server/deployment.yaml | 12 ++++++ examples/mcp_server/src/workflow.py | 53 ++++++++++++++++++++++++ llama_deploy/apiserver/deployment.py | 6 +-- 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 examples/mcp_server/README.md create mode 100644 examples/mcp_server/deployment.yaml create mode 100644 examples/mcp_server/src/workflow.py diff --git a/examples/mcp_server/README.md b/examples/mcp_server/README.md new file mode 100644 index 00000000..101aafa6 --- /dev/null +++ b/examples/mcp_server/README.md @@ -0,0 +1,61 @@ +# LlamaDeploy MCP Server Example + +This example demonstrates how to deploy a LlamaDeploy workflow as an MCP (Model Context Protocol) remote server that +can be integrated with Claude Desktop or other MCP-compatible clients. + +## Overview + +The example includes a **StatusWorkflow** that checks the operational status of popular web services including GitHub, +Reddit, Cloudflare, Vercel, OpenAI, Linear, and Discord. The workflow fetches status information from each service's +status page API and returns the current operational status. + +## Files + +- `src/workflow.py` - Contains the StatusWorkflow implementation +- `deployment.yaml` - LlamaDeploy configuration file +- `README.md` - This documentation + +## Quick Start + +### 1. Launch the LlamaDeploy Server + +```bash +llamactl serve deployment.yaml +``` + +This will start the server and make the workflow available as an MCP tool at `http://localhost:4501/mcp`. + +### 2. Configure Claude Desktop + +Add the following configuration to your Claude Desktop settings to connect to the MCP server: + +```json +{ + "mcpServers": { + "LlamaDeploy": { + "command": "npx", + "args": ["mcp-remote", "http://localhost:4501/mcp"] + } + } +} +``` + +### 3. Use in Claude Desktop + +Once configured, you can ask Claude to check service statuses: +- "What's the status of GitHub?" +- "Check if OpenAI services are operational" +- "Is Discord working?" + +## How It Works + +1. The `StatusWorkflow` class defines a workflow with a single step that checks service status +2. The workflow accepts a service name and queries the corresponding status page API +3. LlamaDeploy serves this workflow as an MCP tool that can be called by Claude Desktop +4. The MCP integration allows Claude to invoke the workflow and return results in natural conversation + +## Requirements + +- LlamaDeploy installed and configured +- Node.js (for the `mcp-remote` package) +- Internet connection to access status page APIs diff --git a/examples/mcp_server/deployment.yaml b/examples/mcp_server/deployment.yaml new file mode 100644 index 00000000..aafce455 --- /dev/null +++ b/examples/mcp_server/deployment.yaml @@ -0,0 +1,12 @@ +name: StatuspageTool + +services: + status_workflow: + name: Statuspage + source: + type: local + name: src + python-dependencies: + - requests + + path: src/workflow:statuspage_workflow diff --git a/examples/mcp_server/src/workflow.py b/examples/mcp_server/src/workflow.py new file mode 100644 index 00000000..3a772000 --- /dev/null +++ b/examples/mcp_server/src/workflow.py @@ -0,0 +1,53 @@ +import asyncio + +import requests +from workflows import Workflow, step +from workflows.events import StartEvent, StopEvent + +SERVICES = { + "github": "https://kctbh9vrtdwd.statuspage.io/api/v2/status.json", + "reddit": "https://reddit.statuspage.io/api/v2/status.json", + "cloudflare": "https://yh6f0r4529hb.statuspage.io/api/v2/status.json", + "vercel": "https://www.vercel-status.com/api/v2/status.json", + "openai": "https://status.openai.com/api/v2/status.json", + "linear": "https://linearstatus.com/api/v2/status.json", + "discord": "https://discordstatus.com/api/v2/status.json", +} + + +class InputService(StartEvent): + service_name: str + + +class StatusWorkflow(Workflow): + """Reports the operational status for notable public services. + + This workflow can report status for major services like OpenAI, Github and Discord + """ + + @step + async def check_status(self, ev: InputService) -> StopEvent: + try: + res = requests.get(SERVICES[ev.service_name.lower()], timeout=5) + res.raise_for_status() + data = res.json() + status = data["status"]["description"] + return StopEvent(status) + except Exception as e: + return StopEvent(f"{ev.service_name}: ERROR - {e}") + + +statuspage_workflow = StatusWorkflow() + + +async def main(): + print("🔍 Checking service statuses...\n") + for name in SERVICES: + print( + name, + await statuspage_workflow.run(start_event=InputService(service_name=name)), + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/llama_deploy/apiserver/deployment.py b/llama_deploy/apiserver/deployment.py index 09e92c27..8ecefb6a 100644 --- a/llama_deploy/apiserver/deployment.py +++ b/llama_deploy/apiserver/deployment.py @@ -376,15 +376,13 @@ def _install_dependencies(service_config: Service, source_root: Path) -> None: msg = f"Unable to install service dependencies using command '{e.cmd}': {e.stderr}" raise DeploymentError(msg) from None - def _make_mcp_service( - self, workflow: Workflow, service_name: str, description: str = "" - ) -> None: + def _make_mcp_service(self, workflow: Workflow, service_name: str) -> None: # Dynamically get the start event class -- this is a bit of a hack StartEventT = workflow._start_event_class if StartEventT is StartEvent: raise ValueError("Must declare a custom StartEvent class in your workflow.") - @self._mcp.tool(name=service_name, description=description) + @self._mcp.tool(name=service_name, description=workflow.__doc__) async def _workflow_tool(run_args: StartEventT, context: MCPContext) -> Any: # type:ignore # Handle edge cases where the start event is an Event or a BaseModel # If the workflow does not have a custom StartEvent class, then we need to handle the event differently From c303d041646422be137589b7b4292df9665e84f9 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 20 Jun 2025 13:30:40 +0200 Subject: [PATCH 4/6] fix tests --- llama_deploy/apiserver/deployment.py | 8 ++- tests/apiserver/test_deployment.py | 92 +++++++++++++++++++++------- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/llama_deploy/apiserver/deployment.py b/llama_deploy/apiserver/deployment.py index 8ecefb6a..3d47072c 100644 --- a/llama_deploy/apiserver/deployment.py +++ b/llama_deploy/apiserver/deployment.py @@ -7,6 +7,7 @@ import subprocess import sys import tempfile +import warnings from asyncio.subprocess import Process from multiprocessing.pool import ThreadPool from pathlib import Path @@ -380,7 +381,12 @@ def _make_mcp_service(self, workflow: Workflow, service_name: str) -> None: # Dynamically get the start event class -- this is a bit of a hack StartEventT = workflow._start_event_class if StartEventT is StartEvent: - raise ValueError("Must declare a custom StartEvent class in your workflow.") + msg = ( + f"Cannot expose service {service_name} as an MCP tool: " + "workflow must declare a custom StartEvent class." + ) + warnings.warn(msg) + return @self._mcp.tool(name=service_name, description=workflow.__doc__) async def _workflow_tool(run_args: StartEventT, context: MCPContext) -> Any: # type:ignore diff --git a/tests/apiserver/test_deployment.py b/tests/apiserver/test_deployment.py index ddd56aa6..8fe6d1f8 100644 --- a/tests/apiserver/test_deployment.py +++ b/tests/apiserver/test_deployment.py @@ -9,6 +9,7 @@ from unittest import mock import pytest +from fastmcp import FastMCP from workflows import Context, Workflow from workflows.handler import WorkflowHandler @@ -53,7 +54,9 @@ def test_deployment_ctor(data_path: Path, mock_importlib: Any, tmp_path: Path) - config = DeploymentConfig.from_yaml(data_path / "git_service.yaml") with mock.patch("llama_deploy.apiserver.deployment.SOURCE_MANAGERS") as sm_dict: sm_dict["git"] = mock.MagicMock() - d = Deployment(config=config, base_path=data_path, deployment_path=tmp_path) + d = Deployment( + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() + ) sm_dict["git"].return_value.sync.assert_called_once() assert d.name == "TestDeployment" @@ -70,7 +73,9 @@ def test_deployment_ctor_missing_service_path(data_path: Path, tmp_path: Path) - with pytest.raises( ValueError, match="path field in service definition must be set" ): - Deployment(config=config, base_path=data_path, deployment_path=tmp_path) + Deployment( + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() + ) def test_deployment_ctor_skip_default_service( @@ -81,7 +86,9 @@ def test_deployment_ctor_skip_default_service( with mock.patch("llama_deploy.apiserver.deployment.SOURCE_MANAGERS") as sm_dict: sm_dict["git"] = mock.MagicMock() - d = Deployment(config=config, base_path=data_path, deployment_path=tmp_path) + d = Deployment( + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() + ) assert len(d._workflow_services) == 2 @@ -91,7 +98,9 @@ def test_deployment_ctor_invalid_default_service( config = DeploymentConfig.from_yaml(data_path / "local.yaml") config.default_service = "does-not-exist" - Deployment(config=config, base_path=data_path, deployment_path=tmp_path) + Deployment( + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() + ) assert ( "Service with id 'does-not-exist' does not exist, cannot set it as default." in caplog.text @@ -104,7 +113,9 @@ def test_deployment_ctor_default_service( config = DeploymentConfig.from_yaml(data_path / "local.yaml") config.default_service = "test-workflow" - d = Deployment(config=config, base_path=data_path, deployment_path=tmp_path) + d = Deployment( + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() + ) assert d.default_service == "test-workflow" @@ -513,7 +524,10 @@ async def test_start_sequence( deployment_config: DeploymentConfig, tmp_path: Path ) -> None: deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) deployment._start_ui_server = mock.AsyncMock() # type: ignore await deployment.start() @@ -526,7 +540,7 @@ async def test_start_with_services(data_path: Path, tmp_path: Path) -> None: config = DeploymentConfig.from_yaml(data_path / "git_service.yaml") with mock.patch("llama_deploy.apiserver.deployment.SOURCE_MANAGERS") as sm_dict: deployment = Deployment( - config=config, base_path=data_path, deployment_path=tmp_path + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() ) deployment._start_ui_server = mock.AsyncMock(return_value=[]) # type: ignore @@ -544,7 +558,7 @@ async def test_start_with_services_ui(data_path: Path, tmp_path: Path) -> None: config.ui = mock.MagicMock() with mock.patch("llama_deploy.apiserver.deployment.SOURCE_MANAGERS") as sm_dict: deployment = Deployment( - config=config, base_path=data_path, deployment_path=tmp_path + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() ) deployment._start_ui_server = mock.AsyncMock(return_value=[]) # type: ignore @@ -558,7 +572,7 @@ async def test_start_with_services_ui(data_path: Path, tmp_path: Path) -> None: async def test_start_ui_server_success(data_path: Path, tmp_path: Path) -> None: config = DeploymentConfig.from_yaml(data_path / "with_ui.yaml") deployment = Deployment( - config=config, base_path=data_path, deployment_path=tmp_path + config=config, base_path=data_path, deployment_path=tmp_path, mcp=FastMCP() ) # Mock the necessary components @@ -609,7 +623,10 @@ async def test_start_ui_server_missing_config( """Test that _start_ui_server raises appropriate error when UI config is missing.""" deployment_config.ui = None deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) with pytest.raises(ValueError, match="missing ui configuration settings"): @@ -623,7 +640,10 @@ async def test_merges_in_path_to_installation( """Test that _start_ui_server raises appropriate error when source is missing.""" deployment_config.ui = mock.MagicMock(source=None) deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) with pytest.raises(ValueError, match="source must be defined"): @@ -660,7 +680,10 @@ async def test_start_ui_server_uses_merge_sync_policy( mock_subprocess.return_value.pid = 1234 deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) await deployment._start_ui_server() @@ -689,7 +712,10 @@ async def test_run_workflow_without_session_without_kwargs( ) -> None: """Test run_workflow with no session_id and no run_kwargs.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow @@ -709,7 +735,10 @@ async def test_run_workflow_without_session_with_kwargs( ) -> None: """Test run_workflow with no session_id but with run_kwargs.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow @@ -730,7 +759,10 @@ async def test_run_workflow_with_session_id( ) -> None: """Test run_workflow with session_id.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow and context @@ -757,7 +789,10 @@ def test_run_workflow_no_wait_without_session_id( ) -> None: """Test run_workflow_no_wait without session_id.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow and handler @@ -796,7 +831,10 @@ def test_run_workflow_no_wait_with_session_id( ) -> None: """Test run_workflow_no_wait with existing session_id.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow, handler, and context @@ -837,7 +875,10 @@ def test_run_workflow_no_wait_empty_kwargs( ) -> None: """Test run_workflow_no_wait with empty run_kwargs.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow and handler @@ -871,7 +912,10 @@ async def test_run_workflow_service_not_found( ) -> None: """Test run_workflow raises KeyError when service not found.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) deployment._workflow_services = {} @@ -885,7 +929,10 @@ def test_run_workflow_no_wait_service_not_found( ) -> None: """Test run_workflow_no_wait raises KeyError when service not found.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) deployment._workflow_services = {} @@ -900,7 +947,10 @@ async def test_run_workflow_session_not_found( ) -> None: """Test run_workflow raises KeyError when session not found.""" deployment = Deployment( - config=deployment_config, base_path=Path(), deployment_path=tmp_path + config=deployment_config, + base_path=Path(), + deployment_path=tmp_path, + mcp=FastMCP(), ) # Mock workflow From 33b2752dc050c50f893ef7efa68b48e5207755f2 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Fri, 20 Jun 2025 13:35:12 +0200 Subject: [PATCH 5/6] remove leftover --- tests/apiserver/conftest.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/apiserver/conftest.py b/tests/apiserver/conftest.py index 8d2ddacf..61cc736a 100644 --- a/tests/apiserver/conftest.py +++ b/tests/apiserver/conftest.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Iterator +from typing import Iterator from unittest import mock import pytest @@ -8,8 +8,6 @@ from workflows.events import StartEvent, StopEvent from llama_deploy.apiserver.app import app -from llama_deploy.apiserver.deployment import Deployment -from llama_deploy.apiserver.deployment_config_parser import DeploymentConfig class SmallWorkflow(Workflow): @@ -33,14 +31,6 @@ def data_path() -> Path: return data_p.resolve() -@pytest.fixture -def mocked_deployment(data_path: Path, mock_importlib: Any) -> Iterator[Deployment]: - config = DeploymentConfig.from_yaml(data_path / "git_service.yaml") - with mock.patch("llama_deploy.apiserver.deployment.SOURCE_MANAGERS") as sm_dict: - sm_dict["git"] = mock.MagicMock() - yield Deployment(config=config, base_path=data_path, deployment_path=Path(".")) - - @pytest.fixture def http_client() -> TestClient: return TestClient(app) From b12b6101bbd8232038e11a18f9c2b01ef22316e2 Mon Sep 17 00:00:00 2001 From: Massimiliano Pippi Date: Mon, 23 Jun 2025 10:31:46 +0200 Subject: [PATCH 6/6] ignore coverage for wrapper --- llama_deploy/apiserver/deployment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/llama_deploy/apiserver/deployment.py b/llama_deploy/apiserver/deployment.py index 3d47072c..aa83ba33 100644 --- a/llama_deploy/apiserver/deployment.py +++ b/llama_deploy/apiserver/deployment.py @@ -388,7 +388,9 @@ def _make_mcp_service(self, workflow: Workflow, service_name: str) -> None: warnings.warn(msg) return - @self._mcp.tool(name=service_name, description=workflow.__doc__) + @self._mcp.tool( + name=service_name, description=workflow.__doc__ + ) # pragma: no cover async def _workflow_tool(run_args: StartEventT, context: MCPContext) -> Any: # type:ignore # Handle edge cases where the start event is an Event or a BaseModel # If the workflow does not have a custom StartEvent class, then we need to handle the event differently @@ -474,9 +476,7 @@ async def serve(self) -> None: # Waits indefinitely since `event` will never be set await event.wait() except asyncio.CancelledError: - if self._simple_message_queue_server is not None: - self._simple_message_queue_server.cancel() - await self._simple_message_queue_server + return async def deploy( self,