diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8d2d20b..1161b33 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -6,6 +6,7 @@ on: - "*.*.*" branches: - actions* + jobs: build_linux: name: Build wheels on ubuntu-latest ${{ matrix.arch }} ${{ matrix.python }} @@ -76,3 +77,31 @@ jobs: with: files: | ./wheelhouse/** + build_mcp_server: + name: Build and Test MCP Server Container + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 # Use the latest version of setup-python + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' # Option + - name: Set up Docker + uses: docker/setup-docker-action@v4 + - name: Build MCP Server Container + run: docker build -f mcp_server/Dockerfile.mcp -t kstar-planner:v0.0 . + - name: Run MCP Server Container + run: docker run -p 8000:8000 --name kstar-planner -d kstar-planner:v0.0 + - name: Install Test dependencies + run: | + pip install -r mcp_server/requirements.txt + pip install -r mcp_server/requirements_test.txt + - name: Test MCP Server Container + env: + MCP_SERVER_URL: http://localhost:8000/mcp + run: python -m pytest mcp_server/tests/test_container.py \ No newline at end of file diff --git a/mcp/Dockerfile.mcp b/mcp/Dockerfile.mcp deleted file mode 100644 index 53f102f..0000000 --- a/mcp/Dockerfile.mcp +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.11 - -RUN apt-get update && apt-get install -y cmake make g++ -COPY mcp /src/mcp -WORKDIR /src -RUN pip install -r mcp/requirements.txt - -EXPOSE 8000 -CMD ["fastmcp", "run", "mcp/server.py:mcp", "--transport", "http", "--port", "8000", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/mcp_server/.vscode/settings.json b/mcp_server/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/mcp_server/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/mcp_server/Dockerfile.mcp b/mcp_server/Dockerfile.mcp new file mode 100644 index 0000000..7c578e6 --- /dev/null +++ b/mcp_server/Dockerfile.mcp @@ -0,0 +1,9 @@ +FROM python:3.11 + +RUN apt-get update && apt-get install -y cmake make g++ +COPY mcp_server /src/mcp_server +WORKDIR /src +RUN pip install -r mcp_server/requirements.txt + +EXPOSE 8000 +CMD ["fastmcp", "run", "mcp_server/server.py:mcp", "--transport", "http", "--port", "8000", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/mcp/README.md b/mcp_server/README.md similarity index 90% rename from mcp/README.md rename to mcp_server/README.md index 405e855..e0c6f4c 100644 --- a/mcp/README.md +++ b/mcp_server/README.md @@ -10,10 +10,10 @@ K* MCP Server provides a containerized deployment of Top-K and Top-Q planners fr To build the Docker image, navigate to the project root directory (where the `pyproject.toml` is located) and execute the following command: ```bash -docker build -f mcp/Dockerfile.mcp -t kstar-planner:v0.0 . +docker build -f mcp_server/Dockerfile.mcp -t kstar-planner:v0.0 . ``` -* `-f mcp/Dockerfile.mcp`: Specifies the Dockerfile to use for building the image. +* `-f mcp_server/Dockerfile.mcp`: Specifies the Dockerfile to use for building the image. * `-t kstar-planner:v0.0`: Tags the image as `kstar-planner` with the version `v0.0`. You can replace `v0.0` with your desired version tag. * `.`: Indicates that the build context is the current directory. diff --git a/mcp/__init__.py b/mcp_server/__init__.py similarity index 100% rename from mcp/__init__.py rename to mcp_server/__init__.py diff --git a/mcp/data/__init__.py b/mcp_server/data/__init__.py similarity index 100% rename from mcp/data/__init__.py rename to mcp_server/data/__init__.py diff --git a/mcp/data/descriptions/__init__.py b/mcp_server/data/descriptions/__init__.py similarity index 100% rename from mcp/data/descriptions/__init__.py rename to mcp_server/data/descriptions/__init__.py diff --git a/mcp/data/descriptions/tool_descriptions.py b/mcp_server/data/descriptions/tool_descriptions.py similarity index 100% rename from mcp/data/descriptions/tool_descriptions.py rename to mcp_server/data/descriptions/tool_descriptions.py diff --git a/mcp/data_models/__init__.py b/mcp_server/data_models/__init__.py similarity index 100% rename from mcp/data_models/__init__.py rename to mcp_server/data_models/__init__.py diff --git a/mcp/data_models/tool_data_models.py b/mcp_server/data_models/tool_data_models.py similarity index 100% rename from mcp/data_models/tool_data_models.py rename to mcp_server/data_models/tool_data_models.py diff --git a/mcp/helpers/__init__.py b/mcp_server/helpers/__init__.py similarity index 100% rename from mcp/helpers/__init__.py rename to mcp_server/helpers/__init__.py diff --git a/mcp/helpers/file_helper.py b/mcp_server/helpers/file_helper.py similarity index 100% rename from mcp/helpers/file_helper.py rename to mcp_server/helpers/file_helper.py diff --git a/mcp/helpers/planner_helper.py b/mcp_server/helpers/planner_helper.py similarity index 100% rename from mcp/helpers/planner_helper.py rename to mcp_server/helpers/planner_helper.py diff --git a/mcp/requirements.txt b/mcp_server/requirements.txt similarity index 100% rename from mcp/requirements.txt rename to mcp_server/requirements.txt diff --git a/mcp_server/requirements_test.txt b/mcp_server/requirements_test.txt new file mode 100644 index 0000000..9c556c2 --- /dev/null +++ b/mcp_server/requirements_test.txt @@ -0,0 +1,3 @@ +pytest +pytest-asyncio +mcp[cli] \ No newline at end of file diff --git a/mcp/server.py b/mcp_server/server.py similarity index 100% rename from mcp/server.py rename to mcp_server/server.py diff --git a/mcp_server/tests/__init__.py b/mcp_server/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcp_server/tests/data/__init__.py b/mcp_server/tests/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcp_server/tests/data/pddl/__init__.py b/mcp_server/tests/data/pddl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcp_server/tests/data/pddl/pddl_example.py b/mcp_server/tests/data/pddl/pddl_example.py new file mode 100644 index 0000000..1252640 --- /dev/null +++ b/mcp_server/tests/data/pddl/pddl_example.py @@ -0,0 +1,56 @@ +domain = """ +(define (domain blocksworld) + (:requirements :strips :typing) + (:types block) + + (:predicates + (on ?x ?y - block) ; Block ?x is on top of block ?y + (ontable ?x - block) ; Block ?x is on the table + (clear ?x - block) ; Nothing is on top of block ?x + (handempty) ; The robot arm is empty + (holding ?x - block) ; The robot arm is holding block ?x + ) + + (:action pick-up + :parameters (?x - block) + :precondition (and (clear ?x) (ontable ?x) (handempty)) + :effect (and (not (ontable ?x)) (not (clear ?x)) (not (handempty)) (holding ?x)) + ) + + (:action put-down + :parameters (?x - block) + :precondition (holding ?x) + :effect (and (not (holding ?x)) (clear ?x) (handempty) (ontable ?x)) + ) + + (:action stack + :parameters (?x ?y - block) + :precondition (and (holding ?x) (clear ?y)) + :effect (and (not (holding ?x)) (not (clear ?y)) (clear ?x) (handempty) (on ?x ?y)) + ) + + (:action unstack + :parameters (?x ?y - block) + :precondition (and (on ?x ?y) (clear ?x) (handempty)) + :effect (and (not (on ?x ?y)) (not (clear ?x)) (clear ?y) (holding ?x) (not (handempty))) + ) +) +""" + +problem = """ +(define (problem p01) + (:domain blocksworld) + (:objects A B - block) ; Two blocks, A and B + (:init + (ontable A) ; Block A is on the table + (on B A) ; Block B is on top of block A + (clear B) ; Nothing is on top of B + (handempty) ; The arm is empty + ) + (:goal (and + (on A B) ; Block A should be on block B + (clear A) ; Block A should be clear + (ontable B) ; Block B should be on the table + )) +) +""" diff --git a/mcp_server/tests/test_container.py b/mcp_server/tests/test_container.py new file mode 100644 index 0000000..c81b0f0 --- /dev/null +++ b/mcp_server/tests/test_container.py @@ -0,0 +1,32 @@ +import os +import pytest +from mcp_server.tests.data.pddl.pddl_example import domain, problem +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport + + +def get_client(server_url) -> Client: + transport = StreamableHttpTransport(url=server_url) + return Client(transport) + +class TestMcpContainer: + @pytest.mark.skipif( + "MCP_SERVER_URL" not in os.environ, reason="Requires MCP_SERVER_URL to be set" + ) + @pytest.mark.asyncio + async def test_planner_tool_t(self) -> None: + client = get_client(os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp")) + + async with client: + tool_list = await client.list_tools() + assert tool_list is not None + + payload = await client.call_tool( + "KstarPlannerUnorderedTopQ", + {"domain": domain, "problem": problem}, + ) + assert payload is not None + assert len(payload.structured_content["plans"]) == 1 + optimal_plan = payload.structured_content["plans"][0] + assert len(optimal_plan["actions"]) == 4 + assert optimal_plan["cost"] == 4