From 2cbd155e991b6177a47eb75eedc0a36956775e1b Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Tue, 29 Apr 2025 11:21:18 -0500 Subject: [PATCH 01/10] Add SSE demo notebook to PR --- examples/python mcp_server_client_demo.ipynb | 184 +++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 examples/python mcp_server_client_demo.ipynb diff --git a/examples/python mcp_server_client_demo.ipynb b/examples/python mcp_server_client_demo.ipynb new file mode 100644 index 00000000..acbef82c --- /dev/null +++ b/examples/python mcp_server_client_demo.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "8f0351de-69f9-40c3-9298-5d6321f33a1d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: flask in c:\\users\\vinis\\anaconda3\\lib\\site-packages (2.2.5)\n", + "Requirement already satisfied: requests in c:\\users\\vinis\\anaconda3\\lib\\site-packages (2.31.0)\n", + "Requirement already satisfied: Werkzeug>=2.2.2 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (2.2.3)\n", + "Requirement already satisfied: Jinja2>=3.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (3.1.3)\n", + "Requirement already satisfied: itsdangerous>=2.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (2.0.1)\n", + "Requirement already satisfied: click>=8.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (8.1.7)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (2.0.4)\n", + "Requirement already satisfied: idna<4,>=2.5 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (3.4)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (2.0.7)\n", + "Requirement already satisfied: certifi>=2017.4.17 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (2024.8.30)\n", + "Requirement already satisfied: colorama in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from click>=8.0->flask) (0.4.6)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from Jinja2>=3.0->flask) (2.1.3)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install flask requests\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "564f36a1-de43-4eb8-b0f4-b3e259f980c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " * Serving Flask app '__main__'\n", + " * Debug mode: off\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.\n", + " * Running on http://127.0.0.1:5000\n", + "Press CTRL+C to quit\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "āœ… Server started at http://127.0.0.1:5000\n", + "\n", + "šŸ“„ Received request: {'tool': 'example_tool', 'input': {'message': 'Hello via SSE!'}}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "127.0.0.1 - - [28/Apr/2025 17:31:39] \"POST /mcp HTTP/1.1\" 200 -\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "šŸš€ Connecting to SSE Server...\n", + "\n", + "data: {\"message\": \"Update 1\"}\n", + ": keep-alive\n", + "data: {\"message\": \"Update 2\"}\n", + "\n", + "šŸŽÆ Successfully received all SSE events!\n" + ] + } + ], + "source": [ + "import threading\n", + "import time\n", + "from flask import Flask, Response, request\n", + "import requests\n", + "\n", + "# --- 1. SETUP FLASK MOCK SERVER ---\n", + "\n", + "app = Flask(__name__)\n", + "\n", + "@app.route('/mcp', methods=['POST'])\n", + "def mcp_handler():\n", + " data = request.json\n", + " print(\"\\nšŸ“„ Received request:\", data)\n", + "\n", + " def stream():\n", + " for i in range(3):\n", + " time.sleep(1)\n", + " yield f\"data: {{\\\"message\\\": \\\"Update {i+1}\\\"}}\\n\\n\"\n", + " yield \": keep-alive\\n\\n\" # Force immediate flush\n", + "\n", + " return Response(stream(), mimetype='text/event-stream')\n", + "\n", + "@app.route('/')\n", + "def home():\n", + " return \"šŸ  MCP Mock Server Running!\"\n", + "\n", + "def start_server():\n", + " app.run(port=5000)\n", + "\n", + "# --- 2. START SERVER IN BACKGROUND THREAD ---\n", + "server_thread = threading.Thread(target=start_server)\n", + "server_thread.daemon = True\n", + "server_thread.start()\n", + "\n", + "# --- 3. WAIT FOR SERVER TO BE READY ---\n", + "time.sleep(3) # Give Flask server a few seconds to boot\n", + "\n", + "print(\"\\nāœ… Server started at http://127.0.0.1:5000\")\n", + "\n", + "# --- 4. CLIENT CODE: CONNECT TO SERVER ---\n", + "\n", + "try:\n", + " SERVER_URL = \"http://127.0.0.1:5000\"\n", + "\n", + " response = requests.post(f\"{SERVER_URL}/mcp\", stream=True, json={\n", + " \"tool\": \"example_tool\",\n", + " \"input\": {\"message\": \"Hello via SSE!\"}\n", + " })\n", + "\n", + " print(\"\\nšŸš€ Connecting to SSE Server...\\n\")\n", + " event_count = 0\n", + " for line in response.iter_lines():\n", + " if line:\n", + " decoded_line = line.decode('utf-8')\n", + " print(decoded_line)\n", + " event_count += 1\n", + " if event_count >= 3:\n", + " break # Stop after 3 messages\n", + "\n", + " print(\"\\nšŸŽÆ Successfully received all SSE events!\")\n", + "\n", + "except Exception as e:\n", + " print(\"āŒ Connection failed:\", e)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f9497e8-4354-44e7-b79a-105091ee479b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 6e222b9585e7350ee6f76b283161d682c03cca05 Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 12:51:22 -0500 Subject: [PATCH 02/10] Refactor test for SSE server to be type-safe and CI-compliant --- tests/test_sse_client_server.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_sse_client_server.py diff --git a/tests/test_sse_client_server.py b/tests/test_sse_client_server.py new file mode 100644 index 00000000..cb1b6627 --- /dev/null +++ b/tests/test_sse_client_server.py @@ -0,0 +1,53 @@ +import asyncio +from typing import List + +from fastapi import FastAPI +from starlette.responses import StreamingResponse +import uvicorn +from threading import Thread +import httpx + +from mcp.client.sse import aconnect_sse + + +app = FastAPI() + + +@app.get("/sse") +async def sse_endpoint() -> StreamingResponse: + async def event_stream() -> asyncio.AsyncGenerator[str, None]: + for i in range(3): + yield f"data: Hello {i+1}\n\n" + await asyncio.sleep(0.1) + return StreamingResponse(event_stream(), media_type="text/event-stream") + + +def run_mock_server() -> None: + uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") + + +async def test_aconnect_sse_server_response() -> None: + server_thread = Thread(target=run_mock_server, daemon=True) + server_thread.start() + await asyncio.sleep(1) + + messages: List[str] = [] + + async with httpx.AsyncClient() as client: + async with aconnect_sse(client, "GET", "http://127.0.0.1:8012/sse") as event_source: + async for event in event_source.aiter_sse(): + if event.data: + print("Event received:", event.data) + messages.append(event.data) + if len(messages) == 3: + break + + assert messages == ["Hello 1", "Hello 2", "Hello 3"] + print("\nāœ… Test passed! SSE connection via aconnect_sse worked correctly.") + + + + + + + From 09988a5d54704da0ab6cfbe29aa0cd891b0e32e0 Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 13:12:54 -0500 Subject: [PATCH 03/10] Replace SSE test with Pyright/ruff-compliant version --- tests/test_sse_client_server.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/test_sse_client_server.py b/tests/test_sse_client_server.py index cb1b6627..45675bd2 100644 --- a/tests/test_sse_client_server.py +++ b/tests/test_sse_client_server.py @@ -1,31 +1,26 @@ import asyncio -from typing import List +from typing import AsyncGenerator, List from fastapi import FastAPI from starlette.responses import StreamingResponse import uvicorn from threading import Thread import httpx - from mcp.client.sse import aconnect_sse - app = FastAPI() - @app.get("/sse") async def sse_endpoint() -> StreamingResponse: - async def event_stream() -> asyncio.AsyncGenerator[str, None]: + async def event_stream() -> AsyncGenerator[str, None]: for i in range(3): yield f"data: Hello {i+1}\n\n" await asyncio.sleep(0.1) return StreamingResponse(event_stream(), media_type="text/event-stream") - def run_mock_server() -> None: uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") - async def test_aconnect_sse_server_response() -> None: server_thread = Thread(target=run_mock_server, daemon=True) server_thread.start() @@ -44,10 +39,3 @@ async def test_aconnect_sse_server_response() -> None: assert messages == ["Hello 1", "Hello 2", "Hello 3"] print("\nāœ… Test passed! SSE connection via aconnect_sse worked correctly.") - - - - - - - From 1bae08b81230bc36d70fd91c3dfe710f8bbd254a Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 13:32:35 -0500 Subject: [PATCH 04/10] Add cleaned SSE client-server test --- tests/test_sse_client_server_cleaned.py | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_sse_client_server_cleaned.py diff --git a/tests/test_sse_client_server_cleaned.py b/tests/test_sse_client_server_cleaned.py new file mode 100644 index 00000000..fae7c8ac --- /dev/null +++ b/tests/test_sse_client_server_cleaned.py @@ -0,0 +1,43 @@ +import asyncio +from typing import AsyncGenerator, List + +from fastapi import FastAPI +from starlette.responses import StreamingResponse +import uvicorn +from threading import Thread +import httpx +from mcp.client.sse import aconnect_sse + +# Required packages: fastapi, uvicorn, httpx, httpx-sse, sse-starlette, anyio + +app = FastAPI() + +@app.get("/sse") +async def sse_endpoint() -> StreamingResponse: + async def event_stream() -> AsyncGenerator[str, None]: + for i in range(3): + yield f"data: Hello {i+1}\n\n" + await asyncio.sleep(0.1) + return StreamingResponse(event_stream(), media_type="text/event-stream") + +def run_mock_server() -> None: + uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") + +async def test_aconnect_sse_server_response() -> None: + server_thread = Thread(target=run_mock_server, daemon=True) + server_thread.start() + await asyncio.sleep(1) + + messages: List[str] = [] + + async with httpx.AsyncClient() as client: + async with aconnect_sse(client, "GET", "http://127.0.0.1:8012/sse") as event_source: + async for event in event_source.aiter_sse(): + if event.data: + print("Event received:", event.data) + messages.append(event.data) + if len(messages) == 3: + break + + assert messages == ["Hello 1", "Hello 2", "Hello 3"] + print("\nāœ… Test passed! SSE connection via aconnect_sse worked correctly.") From 90cbc66fcca930dbfc621a09e699592393569875 Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 14:29:25 -0500 Subject: [PATCH 05/10] Add final clean SSE client-server test --- tests/test_sse_client_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sse_client_server.py b/tests/test_sse_client_server.py index 45675bd2..506bc717 100644 --- a/tests/test_sse_client_server.py +++ b/tests/test_sse_client_server.py @@ -38,4 +38,4 @@ async def test_aconnect_sse_server_response() -> None: break assert messages == ["Hello 1", "Hello 2", "Hello 3"] - print("\nāœ… Test passed! SSE connection via aconnect_sse worked correctly.") + print("\n Test passed! SSE connection via aconnect_sse worked correctly.") From ce10f776cbdf9c0fd9f15f614209c71684ffe78c Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 14:38:15 -0500 Subject: [PATCH 06/10] Add clean SSE client-server test --- tests/test_sse_client_server.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_sse_client_server.py b/tests/test_sse_client_server.py index 506bc717..d7d85681 100644 --- a/tests/test_sse_client_server.py +++ b/tests/test_sse_client_server.py @@ -1,13 +1,3 @@ -import asyncio -from typing import AsyncGenerator, List - -from fastapi import FastAPI -from starlette.responses import StreamingResponse -import uvicorn -from threading import Thread -import httpx -from mcp.client.sse import aconnect_sse - app = FastAPI() @app.get("/sse") From be5143e8fcb66b7363ab80ee5893caaeee06fd22 Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 14:41:59 -0500 Subject: [PATCH 07/10] Add clean SSE client-server test --- tests/requirements-dev.txt | 6 ++++ tests/test.yml | 32 ++++++++++++++++++ tests/test_sse_client_server.py | 24 ++++++++++--- tests/test_sse_client_server_cleaned.py | 2 +- tests/test_sse_client_server_plain.py | 45 +++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 tests/requirements-dev.txt create mode 100644 tests/test.yml create mode 100644 tests/test_sse_client_server_plain.py diff --git a/tests/requirements-dev.txt b/tests/requirements-dev.txt new file mode 100644 index 00000000..882c6cb1 --- /dev/null +++ b/tests/requirements-dev.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn +httpx +httpx-sse +sse-starlette +anyio diff --git a/tests/test.yml b/tests/test.yml new file mode 100644 index 00000000..106a4f02 --- /dev/null +++ b/tests/test.yml @@ -0,0 +1,32 @@ +name: Run Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install main project dependencies + run: | + pip install -r requirements.txt || true + + - name: Install dev dependencies + run: | + pip install -r requirements-dev.txt + + - name: Run standalone SSE client-server test + run: | + python tests/test_sse_client_server_plain.py diff --git a/tests/test_sse_client_server.py b/tests/test_sse_client_server.py index d7d85681..ff169adc 100644 --- a/tests/test_sse_client_server.py +++ b/tests/test_sse_client_server.py @@ -1,23 +1,31 @@ +import asyncio +from typing import AsyncGenerator, List +from fastapi import FastAPI +from starlette.responses import StreamingResponse +import uvicorn +from threading import Thread +import httpx +from mcp.client.sse import aconnect_sse + app = FastAPI() @app.get("/sse") async def sse_endpoint() -> StreamingResponse: async def event_stream() -> AsyncGenerator[str, None]: for i in range(3): - yield f"data: Hello {i+1}\n\n" + yield f"data: Hello {i+1}\\n\\n" await asyncio.sleep(0.1) return StreamingResponse(event_stream(), media_type="text/event-stream") def run_mock_server() -> None: uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") -async def test_aconnect_sse_server_response() -> None: +async def run_sse_test() -> None: server_thread = Thread(target=run_mock_server, daemon=True) server_thread.start() await asyncio.sleep(1) messages: List[str] = [] - async with httpx.AsyncClient() as client: async with aconnect_sse(client, "GET", "http://127.0.0.1:8012/sse") as event_source: async for event in event_source.aiter_sse(): @@ -27,5 +35,11 @@ async def test_aconnect_sse_server_response() -> None: if len(messages) == 3: break - assert messages == ["Hello 1", "Hello 2", "Hello 3"] - print("\n Test passed! SSE connection via aconnect_sse worked correctly.") + if messages == ["Hello 1", "Hello 2", "Hello 3"]: + print("\\n Test passed!") + else: + print("\\n Test failed:", messages) + +if __name__ == "__main__": + asyncio.run(run_sse_test()) + diff --git a/tests/test_sse_client_server_cleaned.py b/tests/test_sse_client_server_cleaned.py index fae7c8ac..fdc0c879 100644 --- a/tests/test_sse_client_server_cleaned.py +++ b/tests/test_sse_client_server_cleaned.py @@ -40,4 +40,4 @@ async def test_aconnect_sse_server_response() -> None: break assert messages == ["Hello 1", "Hello 2", "Hello 3"] - print("\nāœ… Test passed! SSE connection via aconnect_sse worked correctly.") + print("\n Test passed! SSE connection via aconnect_sse worked correctly.") diff --git a/tests/test_sse_client_server_plain.py b/tests/test_sse_client_server_plain.py new file mode 100644 index 00000000..e7982d49 --- /dev/null +++ b/tests/test_sse_client_server_plain.py @@ -0,0 +1,45 @@ +import asyncio +from typing import AsyncGenerator, List + +from fastapi import FastAPI +from starlette.responses import StreamingResponse +import uvicorn +from threading import Thread +import httpx +from mcp.client.sse import aconnect_sse + +app = FastAPI() + +@app.get("/sse") +async def sse_endpoint() -> StreamingResponse: + async def event_stream() -> AsyncGenerator[str, None]: + for i in range(3): + yield f"data: Hello {i+1}\n\n" + await asyncio.sleep(0.1) + return StreamingResponse(event_stream(), media_type="text/event-stream") + +def run_mock_server() -> None: + uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") + +async def run_sse_test() -> None: + server_thread = Thread(target=run_mock_server, daemon=True) + server_thread.start() + await asyncio.sleep(1) + + messages: List[str] = [] + async with httpx.AsyncClient() as client: + async with aconnect_sse(client, "GET", "http://127.0.0.1:8012/sse") as event_source: + async for event in event_source.aiter_sse(): + if event.data: + print("Event received:", event.data) + messages.append(event.data) + if len(messages) == 3: + break + + if messages == ["Hello 1", "Hello 2", "Hello 3"]: + print("Test passed!") + else: + print("Test failed:", messages) + +if __name__ == "__main__": + asyncio.run(run_sse_test()) From 35917e365fa14c220ae4868ff75f3181ef812fe1 Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 11:42:23 -0500 Subject: [PATCH 08/10] Remove demo notebook from PR --- examples/python mcp_server_client_demo.ipynb | 184 ------------------- 1 file changed, 184 deletions(-) delete mode 100644 examples/python mcp_server_client_demo.ipynb diff --git a/examples/python mcp_server_client_demo.ipynb b/examples/python mcp_server_client_demo.ipynb deleted file mode 100644 index acbef82c..00000000 --- a/examples/python mcp_server_client_demo.ipynb +++ /dev/null @@ -1,184 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "id": "8f0351de-69f9-40c3-9298-5d6321f33a1d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: flask in c:\\users\\vinis\\anaconda3\\lib\\site-packages (2.2.5)\n", - "Requirement already satisfied: requests in c:\\users\\vinis\\anaconda3\\lib\\site-packages (2.31.0)\n", - "Requirement already satisfied: Werkzeug>=2.2.2 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (2.2.3)\n", - "Requirement already satisfied: Jinja2>=3.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (3.1.3)\n", - "Requirement already satisfied: itsdangerous>=2.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (2.0.1)\n", - "Requirement already satisfied: click>=8.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from flask) (8.1.7)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (2.0.4)\n", - "Requirement already satisfied: idna<4,>=2.5 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (3.4)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (2.0.7)\n", - "Requirement already satisfied: certifi>=2017.4.17 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from requests) (2024.8.30)\n", - "Requirement already satisfied: colorama in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from click>=8.0->flask) (0.4.6)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in c:\\users\\vinis\\anaconda3\\lib\\site-packages (from Jinja2>=3.0->flask) (2.1.3)\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "pip install flask requests\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "564f36a1-de43-4eb8-b0f4-b3e259f980c2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " * Serving Flask app '__main__'\n", - " * Debug mode: off\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.\n", - " * Running on http://127.0.0.1:5000\n", - "Press CTRL+C to quit\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "āœ… Server started at http://127.0.0.1:5000\n", - "\n", - "šŸ“„ Received request: {'tool': 'example_tool', 'input': {'message': 'Hello via SSE!'}}\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "127.0.0.1 - - [28/Apr/2025 17:31:39] \"POST /mcp HTTP/1.1\" 200 -\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "šŸš€ Connecting to SSE Server...\n", - "\n", - "data: {\"message\": \"Update 1\"}\n", - ": keep-alive\n", - "data: {\"message\": \"Update 2\"}\n", - "\n", - "šŸŽÆ Successfully received all SSE events!\n" - ] - } - ], - "source": [ - "import threading\n", - "import time\n", - "from flask import Flask, Response, request\n", - "import requests\n", - "\n", - "# --- 1. SETUP FLASK MOCK SERVER ---\n", - "\n", - "app = Flask(__name__)\n", - "\n", - "@app.route('/mcp', methods=['POST'])\n", - "def mcp_handler():\n", - " data = request.json\n", - " print(\"\\nšŸ“„ Received request:\", data)\n", - "\n", - " def stream():\n", - " for i in range(3):\n", - " time.sleep(1)\n", - " yield f\"data: {{\\\"message\\\": \\\"Update {i+1}\\\"}}\\n\\n\"\n", - " yield \": keep-alive\\n\\n\" # Force immediate flush\n", - "\n", - " return Response(stream(), mimetype='text/event-stream')\n", - "\n", - "@app.route('/')\n", - "def home():\n", - " return \"šŸ  MCP Mock Server Running!\"\n", - "\n", - "def start_server():\n", - " app.run(port=5000)\n", - "\n", - "# --- 2. START SERVER IN BACKGROUND THREAD ---\n", - "server_thread = threading.Thread(target=start_server)\n", - "server_thread.daemon = True\n", - "server_thread.start()\n", - "\n", - "# --- 3. WAIT FOR SERVER TO BE READY ---\n", - "time.sleep(3) # Give Flask server a few seconds to boot\n", - "\n", - "print(\"\\nāœ… Server started at http://127.0.0.1:5000\")\n", - "\n", - "# --- 4. CLIENT CODE: CONNECT TO SERVER ---\n", - "\n", - "try:\n", - " SERVER_URL = \"http://127.0.0.1:5000\"\n", - "\n", - " response = requests.post(f\"{SERVER_URL}/mcp\", stream=True, json={\n", - " \"tool\": \"example_tool\",\n", - " \"input\": {\"message\": \"Hello via SSE!\"}\n", - " })\n", - "\n", - " print(\"\\nšŸš€ Connecting to SSE Server...\\n\")\n", - " event_count = 0\n", - " for line in response.iter_lines():\n", - " if line:\n", - " decoded_line = line.decode('utf-8')\n", - " print(decoded_line)\n", - " event_count += 1\n", - " if event_count >= 3:\n", - " break # Stop after 3 messages\n", - "\n", - " print(\"\\nšŸŽÆ Successfully received all SSE events!\")\n", - "\n", - "except Exception as e:\n", - " print(\"āŒ Connection failed:\", e)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9f9497e8-4354-44e7-b79a-105091ee479b", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.10" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 0420e5bd747d291edf0c03ca8f99a44e0e660fc7 Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 19:59:20 -0500 Subject: [PATCH 09/10] Restore final SSE test and CI workflow for GitHub Actions --- .github/workflows/test.yml | 30 ++++++++++++++++ tests/test_sse_client_server_hardened.py | 44 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 tests/test_sse_client_server_hardened.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..04bffce8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Run SSE Test + +on: + push: + branches: + - sse-test-final + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/requirements-dev.txt + + - name: Run tests + run: | + pytest tests/test_sse_client_server_hardened.py diff --git a/tests/test_sse_client_server_hardened.py b/tests/test_sse_client_server_hardened.py new file mode 100644 index 00000000..18a932cf --- /dev/null +++ b/tests/test_sse_client_server_hardened.py @@ -0,0 +1,44 @@ +import asyncio +from typing import AsyncGenerator, List + +from fastapi import FastAPI +from starlette.responses import StreamingResponse +import uvicorn +from threading import Thread +import httpx +from mcp.client.sse import aconnect_sse + +app = FastAPI() + +@app.get("/sse") +async def sse_endpoint() -> StreamingResponse: + async def event_stream() -> AsyncGenerator[str, None]: + for i in range(3): + yield f"data: Hello {i + 1}\n\n" + await asyncio.sleep(0.1) + return StreamingResponse(event_stream(), media_type="text/event-stream") + +def run_mock_server() -> None: + uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") + +async def run_test() -> None: + server_thread = Thread(target=run_mock_server, daemon=True) + server_thread.start() + await asyncio.sleep(1) + + messages: List[str] = [] + + async with httpx.AsyncClient() as client: + async with aconnect_sse(client, "GET", "http://127.0.0.1:8012/sse") as event_source: + async for event in event_source.aiter_sse(): + if event.data: + print("Event received:", event.data) + messages.append(event.data) + if len(messages) == 3: + break + + assert messages == ["Hello 1", "Hello 2", "Hello 3"] + print("\nTest passed! SSE connection via aconnect_sse worked correctly.") + +def test_aconnect_sse_server_response() -> None: + asyncio.run(run_test()) From 179102a1fa4c69c6376e80eb237bd1b3692595aa Mon Sep 17 00:00:00 2001 From: Vinisha Projects Date: Sun, 4 May 2025 20:07:06 -0500 Subject: [PATCH 10/10] Final SSE test and CI workflow setup --- .github/workflows/test.yml | 15 ++++----------- tests/requirements-dev.txt | 5 ++--- tests/test_sse_client_server_hardened.py | 19 +++++++++---------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04bffce8..8833cbd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,30 +1,23 @@ name: Run SSE Test on: - push: - branches: - - sse-test-final pull_request: - branches: - - main jobs: test: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r tests/requirements-dev.txt - - name: Run tests - run: | - pytest tests/test_sse_client_server_hardened.py + - name: Run test file + run: pytest tests/test_sse_client_server_hardened.py \ No newline at end of file diff --git a/tests/requirements-dev.txt b/tests/requirements-dev.txt index 882c6cb1..43720b1a 100644 --- a/tests/requirements-dev.txt +++ b/tests/requirements-dev.txt @@ -1,6 +1,5 @@ fastapi -uvicorn httpx -httpx-sse +uvicorn sse-starlette -anyio +anyio \ No newline at end of file diff --git a/tests/test_sse_client_server_hardened.py b/tests/test_sse_client_server_hardened.py index 18a932cf..356c41bb 100644 --- a/tests/test_sse_client_server_hardened.py +++ b/tests/test_sse_client_server_hardened.py @@ -1,5 +1,5 @@ import asyncio -from typing import AsyncGenerator, List +from typing import AsyncGenerator from fastapi import FastAPI from starlette.responses import StreamingResponse @@ -8,37 +8,36 @@ import httpx from mcp.client.sse import aconnect_sse + app = FastAPI() + @app.get("/sse") async def sse_endpoint() -> StreamingResponse: async def event_stream() -> AsyncGenerator[str, None]: for i in range(3): - yield f"data: Hello {i + 1}\n\n" + yield f"data: Hello {i+1}\n\n" await asyncio.sleep(0.1) return StreamingResponse(event_stream(), media_type="text/event-stream") + def run_mock_server() -> None: uvicorn.run(app, host="127.0.0.1", port=8012, log_level="warning") -async def run_test() -> None: + +async def test_aconnect_sse_server_response() -> None: server_thread = Thread(target=run_mock_server, daemon=True) server_thread.start() await asyncio.sleep(1) - messages: List[str] = [] + messages = [] async with httpx.AsyncClient() as client: async with aconnect_sse(client, "GET", "http://127.0.0.1:8012/sse") as event_source: async for event in event_source.aiter_sse(): if event.data: - print("Event received:", event.data) messages.append(event.data) if len(messages) == 3: break - assert messages == ["Hello 1", "Hello 2", "Hello 3"] - print("\nTest passed! SSE connection via aconnect_sse worked correctly.") - -def test_aconnect_sse_server_response() -> None: - asyncio.run(run_test()) + assert messages == ["Hello 1", "Hello 2", "Hello 3"] \ No newline at end of file