### What problem does this PR solve? Upgrade MCP SDK to v1.9.4 (latest). ### Type of change - [x] Refactoringtags/v0.20.0
| "$PY" "${MCP_SCRIPT_PATH}" \ | "$PY" "${MCP_SCRIPT_PATH}" \ | ||||
| --host="${MCP_HOST}" \ | --host="${MCP_HOST}" \ | ||||
| --port="${MCP_PORT}" \ | --port="${MCP_PORT}" \ | ||||
| --base_url="${MCP_BASE_URL}" \ | |||||
| --base-url="${MCP_BASE_URL}" \ | |||||
| --mode="${MCP_MODE}" \ | --mode="${MCP_MODE}" \ | ||||
| --api_key="${MCP_HOST_API_KEY}" & | |||||
| --api-key="${MCP_HOST_API_KEY}" & | |||||
| } | } | ||||
| # ----------------------------------------------------------------------------- | # ----------------------------------------------------------------------------- |
| ```bash | ```bash | ||||
| # Launch the MCP server to work in self-host mode, run either of the following | # 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: | # 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: | Where: | ||||
| - If launching from source, include the API key in the command. | - If launching from source, include the API key in the command. | ||||
| - If launching from Docker, update the API key in **docker/docker-compose.yml**. | - If launching from Docker, update the API key in **docker/docker-compose.yml**. | ||||
| - **Host mode**: | - **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). | |||||
| 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). |
| from contextlib import asynccontextmanager | from contextlib import asynccontextmanager | ||||
| from functools import wraps | from functools import wraps | ||||
| import click | |||||
| import requests | import requests | ||||
| from starlette.applications import Starlette | from starlette.applications import Starlette | ||||
| from starlette.middleware import Middleware | 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 starlette.routing import Mount, Route | ||||
| from strenum import StrEnum | from strenum import StrEnum | ||||
| async def handle_sse(request): | async def handle_sse(request): | ||||
| async with sse.connect_sse(request.scope, request.receive, request._send) as streams: | 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)})) | 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(): | def create_starlette_app(): | ||||
| middleware = None | middleware = None | ||||
| if MODE == LaunchMode.HOST: | 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)] | middleware = [Middleware(AuthMiddleware)] | ||||
| return Starlette( | return Starlette( | ||||
| debug=True, | debug=True, | ||||
| routes=[ | routes=[ | ||||
| Route("/sse", endpoint=handle_sse), | |||||
| Route("/sse", endpoint=handle_sse, methods=["GET"]), | |||||
| Mount("/messages/", app=sse.handle_post_message), | Mount("/messages/", app=sse.handle_post_message), | ||||
| ], | ], | ||||
| middleware=middleware, | 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 os | ||||
| import uvicorn | import uvicorn | ||||
| load_dotenv() | 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://<host_address>") | |||||
| 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'") | |||||
| 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) | |||||
| 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) | |||||
| if MODE == "self-host" and not HOST_API_KEY: | |||||
| raise click.UsageError("--api-key is required when --mode is 'self-host'") | |||||
| print( | print( | ||||
| r""" | r""" | ||||
| | |\/| | | | |_) | \___ \| _| | |_) \ \ / /| _| | |_) | | | |\/| | | | |_) | \___ \| _| | |_) \ \ / /| _| | |_) | | ||||
| | | | | |___| __/ ___) | |___| _ < \ V / | |___| _ < | | | | | |___| __/ ___) | |___| _ < \ V / | |___| _ < | ||||
| |_| |_|\____|_| |____/|_____|_| \_\ \_/ |_____|_| \_\ | |_| |_|\____|_| |____/|_____|_| \_\ \_/ |_____|_| \_\ | ||||
| """, | |||||
| """, | |||||
| flush=True, | flush=True, | ||||
| ) | ) | ||||
| print(f"MCP launch mode: {MODE}", flush=True) | print(f"MCP launch mode: {MODE}", flush=True) | ||||
| host=HOST, | host=HOST, | ||||
| port=int(PORT), | 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() |
| "trio>=0.29.0", | "trio>=0.29.0", | ||||
| "langfuse>=2.60.0", | "langfuse>=2.60.0", | ||||
| "debugpy>=1.8.13", | "debugpy>=1.8.13", | ||||
| "mcp>=1.6.0", | |||||
| "mcp>=1.9.4", | |||||
| "opensearch-py==2.7.1", | "opensearch-py==2.7.1", | ||||
| "pluginlib==0.9.4", | "pluginlib==0.9.4", | ||||
| "click>=8.1.8", | |||||
| ] | ] | ||||
| [project.optional-dependencies] | [project.optional-dependencies] |
| [[package]] | [[package]] | ||||
| name = "mcp" | name = "mcp" | ||||
| version = "1.6.0" | |||||
| version = "1.9.4" | |||||
| source = { registry = "https://mirrors.aliyun.com/pypi/simple" } | source = { registry = "https://mirrors.aliyun.com/pypi/simple" } | ||||
| dependencies = [ | dependencies = [ | ||||
| { name = "anyio" }, | { name = "anyio" }, | ||||
| { name = "httpx-sse" }, | { name = "httpx-sse" }, | ||||
| { name = "pydantic" }, | { name = "pydantic" }, | ||||
| { name = "pydantic-settings" }, | { name = "pydantic-settings" }, | ||||
| { name = "python-multipart" }, | |||||
| { name = "sse-starlette" }, | { name = "sse-starlette" }, | ||||
| { name = "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 = [ | 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]] | [[package]] | ||||
| { url = "https://mirrors.aliyun.com/pypi/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" }, | { 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]] | [[package]] | ||||
| name = "python-pptx" | name = "python-pptx" | ||||
| version = "1.0.2" | version = "1.0.2" | ||||
| { name = "botocore" }, | { name = "botocore" }, | ||||
| { name = "cachetools" }, | { name = "cachetools" }, | ||||
| { name = "chardet" }, | { name = "chardet" }, | ||||
| { name = "click" }, | |||||
| { name = "cn2an" }, | { name = "cn2an" }, | ||||
| { name = "cohere" }, | { name = "cohere" }, | ||||
| { name = "crawl4ai" }, | { name = "crawl4ai" }, | ||||
| { name = "botocore", specifier = "==1.34.140" }, | { name = "botocore", specifier = "==1.34.140" }, | ||||
| { name = "cachetools", specifier = "==5.3.3" }, | { name = "cachetools", specifier = "==5.3.3" }, | ||||
| { name = "chardet", specifier = "==5.2.0" }, | { name = "chardet", specifier = "==5.2.0" }, | ||||
| { name = "click", specifier = ">=8.1.8" }, | |||||
| { name = "cn2an", specifier = "==0.5.22" }, | { name = "cn2an", specifier = "==0.5.22" }, | ||||
| { name = "cohere", specifier = "==5.6.2" }, | { name = "cohere", specifier = "==5.6.2" }, | ||||
| { name = "crawl4ai", specifier = "==0.3.8" }, | { name = "crawl4ai", specifier = "==0.3.8" }, | ||||
| { name = "langfuse", specifier = ">=2.60.0" }, | { name = "langfuse", specifier = ">=2.60.0" }, | ||||
| { name = "markdown", specifier = "==3.6" }, | { name = "markdown", specifier = "==3.6" }, | ||||
| { name = "markdown-to-json", specifier = "==2.1.1" }, | { 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 = "mini-racer", specifier = ">=0.12.4,<0.13.0" }, | ||||
| { name = "minio", specifier = "==7.2.4" }, | { name = "minio", specifier = "==7.2.4" }, | ||||
| { name = "mistralai", specifier = "==0.4.2" }, | { name = "mistralai", specifier = "==0.4.2" }, |