From 03656da4dd8549904a102fb833b98cead88354b8 Mon Sep 17 00:00:00 2001 From: Yongteng Lei Date: Mon, 23 Jun 2025 16:53:59 +0800 Subject: [PATCH] Refa: upgrade MCP SDK to v1.9.4 (#8421) ### What problem does this PR solve? Upgrade MCP SDK to v1.9.4 (latest). ### Type of change - [x] Refactoring --- docker/entrypoint.sh | 4 +- docs/develop/mcp/launch_mcp_server.md | 8 +- mcp/server/server.py | 119 ++++++++++++++------------ pyproject.toml | 3 +- uv.lock | 22 +++-- 5 files changed, 89 insertions(+), 67 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 53868daae..d4065c6fa 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -148,9 +148,9 @@ function start_mcp_server() { "$PY" "${MCP_SCRIPT_PATH}" \ --host="${MCP_HOST}" \ --port="${MCP_PORT}" \ - --base_url="${MCP_BASE_URL}" \ + --base-url="${MCP_BASE_URL}" \ --mode="${MCP_MODE}" \ - --api_key="${MCP_HOST_API_KEY}" & + --api-key="${MCP_HOST_API_KEY}" & } # ----------------------------------------------------------------------------- diff --git a/docs/develop/mcp/launch_mcp_server.md b/docs/develop/mcp/launch_mcp_server.md index a98939efb..8388ce56c 100644 --- a/docs/develop/mcp/launch_mcp_server.md +++ b/docs/develop/mcp/launch_mcp_server.md @@ -41,11 +41,11 @@ You can start an MCP server either from source code or via Docker. ```bash # Launch the MCP server to work in self-host mode, run either of the following -uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base_url=http://127.0.0.1:9380 --api_key=ragflow-xxxxx -# uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base_url=http://127.0.0.1:9380 --mode=self-host --api_key=ragflow-xxxxx +uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base-url=http://127.0.0.1:9380 --api-key=ragflow-xxxxx +# uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base-url=http://127.0.0.1:9380 --mode=self-host --api-key=ragflow-xxxxx # To launch the MCP server to work in host mode, run the following instead: -# uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base_url=http://127.0.0.1:9380 --mode=host +# uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base-url=http://127.0.0.1:9380 --mode=host ``` Where: @@ -194,4 +194,4 @@ The use of an API key depends on the operating mode of your MCP server. - If launching from source, include the API key in the command. - If launching from Docker, update the API key in **docker/docker-compose.yml**. - **Host mode**: - If your RAGFlow MCP server is working in host mode, include the API key in the `headers` of your client requests to authenticate your client with the RAGFlow server. An example is available [here](https://github.com/infiniflow/ragflow/blob/main/mcp/client/client.py). \ No newline at end of file + If your RAGFlow MCP server is working in host mode, include the API key in the `headers` of your client requests to authenticate your client with the RAGFlow server. An example is available [here](https://github.com/infiniflow/ragflow/blob/main/mcp/client/client.py). diff --git a/mcp/server/server.py b/mcp/server/server.py index 743cd16f9..899cbca49 100644 --- a/mcp/server/server.py +++ b/mcp/server/server.py @@ -19,11 +19,11 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager from functools import wraps +import click import requests from starlette.applications import Starlette from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response from starlette.routing import Mount, Route from strenum import StrEnum @@ -209,50 +209,64 @@ async def call_tool(name: str, arguments: dict, *, connector) -> list[types.Text async def handle_sse(request): async with sse.connect_sse(request.scope, request.receive, request._send) as streams: await app.run(streams[0], streams[1], app.create_initialization_options(experimental_capabilities={"headers": dict(request.headers)})) - - -class AuthMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request, call_next): - # Authentication is deferred, will be handled by RAGFlow core service. - if request.url.path.startswith("/sse") or request.url.path.startswith("/messages"): - token = None - - auth_header = request.headers.get("Authorization") - if auth_header and auth_header.startswith("Bearer "): - token = auth_header.removeprefix("Bearer ").strip() - elif request.headers.get("api_key"): - token = request.headers["api_key"] - - if not token: - return JSONResponse({"error": "Missing or invalid authorization header"}, status_code=401) - return await call_next(request) + return Response() def create_starlette_app(): middleware = None if MODE == LaunchMode.HOST: + from starlette.types import ASGIApp, Receive, Scope, Send + + class AuthMiddleware: + def __init__(self, app: ASGIApp): + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + path = scope["path"] + if path.startswith("/messages/") or path.startswith("/sse"): + headers = dict(scope["headers"]) + token = None + auth_header = headers.get(b"authorization") + if auth_header and auth_header.startswith(b"Bearer "): + token = auth_header.removeprefix(b"Bearer ").strip() + elif b"api_key" in headers: + token = headers[b"api_key"] + + if not token: + response = JSONResponse({"error": "Missing or invalid authorization header"}, status_code=401) + await response(scope, receive, send) + return + + await self.app(scope, receive, send) + middleware = [Middleware(AuthMiddleware)] return Starlette( debug=True, routes=[ - Route("/sse", endpoint=handle_sse), + Route("/sse", endpoint=handle_sse, methods=["GET"]), Mount("/messages/", app=sse.handle_post_message), ], middleware=middleware, ) -if __name__ == "__main__": - """ - Launch example: - self-host: - uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base_url=http://127.0.0.1:9380 --mode=self-host --api_key=ragflow-xxxxx - host: - uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base_url=http://127.0.0.1:9380 --mode=host - """ - - import argparse +@click.command() +@click.option("--base-url", type=str, default="http://127.0.0.1:9380", help="API base URL for RAGFlow backend") +@click.option("--host", type=str, default="127.0.0.1", help="Host to bind the RAGFlow MCP server") +@click.option("--port", type=int, default=9382, help="Port to bind the RAGFlow MCP server") +@click.option( + "--mode", + type=click.Choice(["self-host", "host"]), + default="self-host", + help=("Launch mode:\n self-host: run MCP for a single tenant (requires --api-key)\n host: multi-tenant mode, users must provide Authorization headers"), +) +@click.option("--api-key", type=str, default="", help="API key to use when in self-host mode") +def main(base_url, host, port, mode, api_key): import os import uvicorn @@ -260,31 +274,15 @@ if __name__ == "__main__": load_dotenv() - parser = argparse.ArgumentParser(description="RAGFlow MCP Server") - parser.add_argument("--base_url", type=str, default="http://127.0.0.1:9380", help="api_url: http://") - parser.add_argument("--host", type=str, default="127.0.0.1", help="RAGFlow MCP SERVER host") - parser.add_argument("--port", type=str, default="9382", help="RAGFlow MCP SERVER port") - parser.add_argument( - "--mode", - type=str, - default="self-host", - help="Launch mode options:\n" - " * self-host: Launches an MCP server to access a specific tenant space. The 'api_key' argument is required.\n" - " * host: Launches an MCP server that allows users to access their own spaces. Each request must include a Authorization header " - "indicating the user's identification.", - ) - parser.add_argument("--api_key", type=str, default="", help="RAGFlow MCP SERVER HOST API KEY") - args = parser.parse_args() - if args.mode not in ["self-host", "host"]: - parser.error("--mode is only accept 'self-host' or 'host'") - if args.mode == "self-host" and not args.api_key: - parser.error("--api_key is required when --mode is 'self-host'") + global BASE_URL, HOST, PORT, MODE, HOST_API_KEY + BASE_URL = os.environ.get("RAGFLOW_MCP_BASE_URL", base_url) + HOST = os.environ.get("RAGFLOW_MCP_HOST", host) + PORT = os.environ.get("RAGFLOW_MCP_PORT", str(port)) + MODE = os.environ.get("RAGFLOW_MCP_LAUNCH_MODE", mode) + HOST_API_KEY = os.environ.get("RAGFLOW_MCP_HOST_API_KEY", api_key) - BASE_URL = os.environ.get("RAGFLOW_MCP_BASE_URL", args.base_url) - HOST = os.environ.get("RAGFLOW_MCP_HOST", args.host) - PORT = os.environ.get("RAGFLOW_MCP_PORT", args.port) - MODE = os.environ.get("RAGFLOW_MCP_LAUNCH_MODE", args.mode) - HOST_API_KEY = os.environ.get("RAGFLOW_MCP_HOST_API_KEY", args.api_key) + if MODE == "self-host" and not HOST_API_KEY: + raise click.UsageError("--api-key is required when --mode is 'self-host'") print( r""" @@ -293,7 +291,7 @@ __ __ ____ ____ ____ _____ ______ _______ ____ | |\/| | | | |_) | \___ \| _| | |_) \ \ / /| _| | |_) | | | | | |___| __/ ___) | |___| _ < \ V / | |___| _ < |_| |_|\____|_| |____/|_____|_| \_\ \_/ |_____|_| \_\ - """, + """, flush=True, ) print(f"MCP launch mode: {MODE}", flush=True) @@ -306,3 +304,14 @@ __ __ ____ ____ ____ _____ ______ _______ ____ host=HOST, port=int(PORT), ) + + +if __name__ == "__main__": + """ + Launch example: + self-host: + uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base-url=http://127.0.0.1:9380 --mode=self-host --api-key=ragflow-xxxxx + host: + uv run mcp/server/server.py --host=127.0.0.1 --port=9382 --base-url=http://127.0.0.1:9380 --mode=host + """ + main() diff --git a/pyproject.toml b/pyproject.toml index ded458b98..7f2fa1777 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,9 +126,10 @@ dependencies = [ "trio>=0.29.0", "langfuse>=2.60.0", "debugpy>=1.8.13", - "mcp>=1.6.0", + "mcp>=1.9.4", "opensearch-py==2.7.1", "pluginlib==0.9.4", + "click>=8.1.8", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 5cf632156..cd5efce0e 100644 --- a/uv.lock +++ b/uv.lock @@ -3010,7 +3010,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.6.0" +version = "1.9.4" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "anyio" }, @@ -3018,13 +3018,14 @@ dependencies = [ { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-multipart" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0" }, ] [[package]] @@ -4748,6 +4749,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104" }, +] + [[package]] name = "python-pptx" version = "1.0.2" @@ -4868,6 +4878,7 @@ dependencies = [ { name = "botocore" }, { name = "cachetools" }, { name = "chardet" }, + { name = "click" }, { name = "cn2an" }, { name = "cohere" }, { name = "crawl4ai" }, @@ -5016,6 +5027,7 @@ requires-dist = [ { name = "botocore", specifier = "==1.34.140" }, { name = "cachetools", specifier = "==5.3.3" }, { name = "chardet", specifier = "==5.2.0" }, + { name = "click", specifier = ">=8.1.8" }, { name = "cn2an", specifier = "==0.5.22" }, { name = "cohere", specifier = "==5.6.2" }, { name = "crawl4ai", specifier = "==0.3.8" }, @@ -5054,7 +5066,7 @@ requires-dist = [ { name = "langfuse", specifier = ">=2.60.0" }, { name = "markdown", specifier = "==3.6" }, { name = "markdown-to-json", specifier = "==2.1.1" }, - { name = "mcp", specifier = ">=1.6.0" }, + { name = "mcp", specifier = ">=1.9.4" }, { name = "mini-racer", specifier = ">=0.12.4,<0.13.0" }, { name = "minio", specifier = "==7.2.4" }, { name = "mistralai", specifier = "==0.4.2" },