mirror of
				https://github.com/open-webui/mcpo
				synced 2025-06-26 18:26:58 +00:00 
			
		
		
		
	Merge pull request #42 from open-webui/dev
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Release / release (push) Has been cancelled
				
			
		
			
				
	
				Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Has been cancelled
				
			
		
			
				
	
				Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Has been cancelled
				
			
		
			
				
	
				Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Release / release (push) Has been cancelled
				
			Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Has been cancelled
				
			Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Has been cancelled
				
			Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
				
			0.0.9
This commit is contained in:
		
						commit
						8c8a35e07e
					
				
							
								
								
									
										165
									
								
								.github/workflows/docker-build.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								.github/workflows/docker-build.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | ||||
| name: Create and publish Docker images with specific build args | ||||
| 
 | ||||
| on: | ||||
|     workflow_dispatch: | ||||
|     push: | ||||
|         branches: | ||||
|             - main | ||||
|             - dev | ||||
|         tags: | ||||
|             - v* | ||||
| 
 | ||||
| env: | ||||
|     REGISTRY: ghcr.io | ||||
| 
 | ||||
| jobs: | ||||
|     build-main-image: | ||||
|         runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} | ||||
|         permissions: | ||||
|             contents: read | ||||
|             packages: write | ||||
|         strategy: | ||||
|             fail-fast: false | ||||
|             matrix: | ||||
|                 platform: | ||||
|                     - linux/amd64 | ||||
|                     - linux/arm64 | ||||
| 
 | ||||
|         steps: | ||||
|             # GitHub Packages requires the entire repository name to be in lowercase | ||||
|             # although the repository owner has a lowercase username, this prevents some people from running actions after forking | ||||
|             - name: Set repository and image name to lowercase | ||||
|               run: | | ||||
|                   echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} | ||||
|                   echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} | ||||
|               env: | ||||
|                   IMAGE_NAME: "${{ github.repository }}" | ||||
| 
 | ||||
|             - name: Prepare | ||||
|               run: | | ||||
|                   platform=${{ matrix.platform }} | ||||
|                   echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV | ||||
| 
 | ||||
|             - name: Checkout repository | ||||
|               uses: actions/checkout@v4 | ||||
| 
 | ||||
|             - name: Set up QEMU | ||||
|               uses: docker/setup-qemu-action@v3 | ||||
| 
 | ||||
|             - name: Set up Docker Buildx | ||||
|               uses: docker/setup-buildx-action@v3 | ||||
| 
 | ||||
|             - name: Log in to the Container registry | ||||
|               uses: docker/login-action@v3 | ||||
|               with: | ||||
|                   registry: ${{ env.REGISTRY }} | ||||
|                   username: ${{ github.actor }} | ||||
|                   password: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|             - name: Extract metadata for Docker images (default latest tag) | ||||
|               id: meta | ||||
|               uses: docker/metadata-action@v5 | ||||
|               with: | ||||
|                   images: ${{ env.FULL_IMAGE_NAME }} | ||||
|                   tags: | | ||||
|                       type=ref,event=branch | ||||
|                       type=ref,event=tag | ||||
|                       type=sha,prefix=git- | ||||
|                       type=semver,pattern={{version}} | ||||
|                       type=semver,pattern={{major}}.{{minor}} | ||||
|                   flavor: | | ||||
|                       latest=${{ github.ref == 'refs/heads/main' }} | ||||
| 
 | ||||
|             - name: Extract metadata for Docker cache | ||||
|               id: cache-meta | ||||
|               uses: docker/metadata-action@v5 | ||||
|               with: | ||||
|                   images: ${{ env.FULL_IMAGE_NAME }} | ||||
|                   tags: | | ||||
|                       type=ref,event=branch | ||||
|                       ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} | ||||
|                   flavor: | | ||||
|                       prefix=cache-${{ matrix.platform }}- | ||||
|                       latest=false | ||||
| 
 | ||||
|             - name: Build Docker image (latest) | ||||
|               uses: docker/build-push-action@v5 | ||||
|               id: build | ||||
|               with: | ||||
|                   context: . | ||||
|                   push: true | ||||
|                   platforms: ${{ matrix.platform }} | ||||
|                   labels: ${{ steps.meta.outputs.labels }} | ||||
|                   outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | ||||
|                   cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} | ||||
|                   cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max | ||||
|                   build-args: | | ||||
|                       BUILD_HASH=${{ github.sha }} | ||||
| 
 | ||||
|             - name: Export digest | ||||
|               run: | | ||||
|                   mkdir -p /tmp/digests | ||||
|                   digest="${{ steps.build.outputs.digest }}" | ||||
|                   touch "/tmp/digests/${digest#sha256:}" | ||||
| 
 | ||||
|             - name: Upload digest | ||||
|               uses: actions/upload-artifact@v4 | ||||
|               with: | ||||
|                   name: digests-main-${{ env.PLATFORM_PAIR }} | ||||
|                   path: /tmp/digests/* | ||||
|                   if-no-files-found: error | ||||
|                   retention-days: 1 | ||||
| 
 | ||||
|     merge-main-images: | ||||
|         runs-on: ubuntu-latest | ||||
|         needs: [build-main-image] | ||||
|         steps: | ||||
|             # GitHub Packages requires the entire repository name to be in lowercase | ||||
|             # although the repository owner has a lowercase username, this prevents some people from running actions after forking | ||||
|             - name: Set repository and image name to lowercase | ||||
|               run: | | ||||
|                   echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} | ||||
|                   echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} | ||||
|               env: | ||||
|                   IMAGE_NAME: "${{ github.repository }}" | ||||
| 
 | ||||
|             - name: Download digests | ||||
|               uses: actions/download-artifact@v4 | ||||
|               with: | ||||
|                   pattern: digests-main-* | ||||
|                   path: /tmp/digests | ||||
|                   merge-multiple: true | ||||
| 
 | ||||
|             - name: Set up Docker Buildx | ||||
|               uses: docker/setup-buildx-action@v3 | ||||
| 
 | ||||
|             - name: Log in to the Container registry | ||||
|               uses: docker/login-action@v3 | ||||
|               with: | ||||
|                   registry: ${{ env.REGISTRY }} | ||||
|                   username: ${{ github.actor }} | ||||
|                   password: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|             - name: Extract metadata for Docker images (default latest tag) | ||||
|               id: meta | ||||
|               uses: docker/metadata-action@v5 | ||||
|               with: | ||||
|                   images: ${{ env.FULL_IMAGE_NAME }} | ||||
|                   tags: | | ||||
|                       type=ref,event=branch | ||||
|                       type=ref,event=tag | ||||
|                       type=sha,prefix=git- | ||||
|                       type=semver,pattern={{version}} | ||||
|                       type=semver,pattern={{major}}.{{minor}} | ||||
|                   flavor: | | ||||
|                       latest=${{ github.ref == 'refs/heads/main' }} | ||||
| 
 | ||||
|             - name: Create manifest list and push | ||||
|               working-directory: /tmp/digests | ||||
|               run: | | ||||
|                   docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||
|                     $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) | ||||
| 
 | ||||
|             - name: Inspect image | ||||
|               run: | | ||||
|                   docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} | ||||
| @ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. | ||||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
| 
 | ||||
| ## [0.0.9] - 2025-04-06 | ||||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - 🧭 **Clearer Docs Navigation with Path Awareness**: Optimized the /docs and /[tool]/docs pages to clearly display full endpoint paths when using mcpo --config, making it obvious where each tool is hosted—no more guessing or confusion when running multiple tools under different routes. | ||||
| - 🛤️ **New --path-prefix Option for Precise Routing Control**: Introduced optional --path-prefix flag allowing you to customize the route prefix for all mounted tools—great for integrating mcpo into existing infrastructures, reverse proxies, or multi-service APIs without route collisions. | ||||
| - 🐳 **Official Dockerfile for Easy Deployment**: Added a first-party Dockerfile so you can containerize mcpo in seconds—perfect for deploying to production, shipping models with standardized dependencies, and running anywhere with a consistent environment. | ||||
| 
 | ||||
| ## [0.0.8] - 2025-04-03 | ||||
| 
 | ||||
| ### Added | ||||
|  | ||||
							
								
								
									
										42
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| FROM python:3.12-slim-bookworm | ||||
| 
 | ||||
| # Install uv (from official binary), nodejs, npm, and git | ||||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ | ||||
| 
 | ||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||||
|     git \ | ||||
|     curl \ | ||||
|     ca-certificates \ | ||||
|     && rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
| # Install Node.js and npm via NodeSource  | ||||
| RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ | ||||
|     && apt-get install -y nodejs \ | ||||
|     && rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
| # Confirm npm and node versions (optional debugging info) | ||||
| RUN node -v && npm -v | ||||
| 
 | ||||
| # Copy your mcpo source code (assuming in src/mcpo) | ||||
| COPY . /app | ||||
| WORKDIR /app | ||||
| 
 | ||||
| # Create virtual environment explicitly in known location | ||||
| ENV VIRTUAL_ENV=/app/.venv | ||||
| RUN uv venv "$VIRTUAL_ENV" | ||||
| ENV PATH="$VIRTUAL_ENV/bin:$PATH" | ||||
| 
 | ||||
| # Install mcpo (assuming pyproject.toml is properly configured) | ||||
| RUN uv pip install . && rm -rf ~/.cache | ||||
| 
 | ||||
| # Verify mcpo installed correctly | ||||
| RUN which mcpo | ||||
| 
 | ||||
| # Expose port (optional but common default) | ||||
| EXPOSE 8000 | ||||
| 
 | ||||
| # Entrypoint set for easy container invocation | ||||
| ENTRYPOINT ["mcpo"] | ||||
| 
 | ||||
| # Default help CMD (can override at runtime) | ||||
| CMD ["--help"] | ||||
| @ -40,6 +40,12 @@ pip install mcpo | ||||
| mcpo --port 8000 --api-key "top-secret" -- your_mcp_server_command | ||||
| ``` | ||||
| 
 | ||||
| You can also run mcpo via Docker with no installation: | ||||
| 
 | ||||
| ```bash | ||||
| docker run -p 8000:8000 ghcr.io/open-webui/mcpo:main --api-key "top-secret" -- your_mcp_server_command | ||||
| ``` | ||||
| 
 | ||||
| Example: | ||||
| 
 | ||||
| ```bash | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [project] | ||||
| name = "mcpo" | ||||
| version = "0.0.8" | ||||
| version = "0.0.9" | ||||
| description = "A simple, secure MCP-to-OpenAPI proxy server" | ||||
| authors = [ | ||||
|     { name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" } | ||||
| @ -12,6 +12,7 @@ dependencies = [ | ||||
|     "fastapi>=0.115.12", | ||||
|     "mcp>=1.6.0", | ||||
|     "passlib[bcrypt]>=1.7.4", | ||||
|     "pydantic>=2.11.1", | ||||
|     "pyjwt[crypto]>=2.10.1", | ||||
|     "typer>=0.15.2", | ||||
|     "uvicorn>=0.34.0", | ||||
|  | ||||
| @ -45,7 +45,10 @@ def main( | ||||
|         Optional[str], typer.Option("--ssl-certfile", "-t", help="SSL certfile") | ||||
|     ] = None, | ||||
|     ssl_keyfile: Annotated[ | ||||
|         Optional[str], typer.Option("--ssl-keyfile", "-k",  help="SSL keyfile") | ||||
|         Optional[str], typer.Option("--ssl-keyfile", "-k", help="SSL keyfile") | ||||
|     ] = None, | ||||
|     path_prefix: Annotated[ | ||||
|         Optional[str], typer.Option("--path-prefix", help="URL prefix") | ||||
|     ] = None, | ||||
| ): | ||||
|     server_command = None | ||||
| @ -81,6 +84,17 @@ def main( | ||||
|     for key, value in env_dict.items(): | ||||
|         os.environ[key] = value | ||||
| 
 | ||||
|     # Whatever the prefix is, make sure it starts and ends with a / | ||||
|     if path_prefix is None: | ||||
|         # Set default value | ||||
|         path_prefix = "/" | ||||
|     # if prefix doesn't end with a /, add it | ||||
|     if not path_prefix.endswith("/"): | ||||
|         path_prefix = f"{path_prefix}/" | ||||
|     # if prefix doesn't start with a /, add it | ||||
|     if not path_prefix.startswith("/"): | ||||
|         path_prefix = f"/{path_prefix}" | ||||
| 
 | ||||
|     # Run your async run function from mcpo.main | ||||
|     asyncio.run( | ||||
|         run( | ||||
| @ -95,6 +109,7 @@ def main( | ||||
|             server_command=server_command, | ||||
|             ssl_certfile=ssl_certfile, | ||||
|             ssl_keyfile=ssl_keyfile, | ||||
|             path_prefix=path_prefix, | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										132
									
								
								src/mcpo/main.py
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								src/mcpo/main.py
									
									
									
									
									
								
							| @ -1,18 +1,18 @@ | ||||
| from fastapi import FastAPI, Body, Depends | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| from starlette.routing import Mount | ||||
| from pydantic import create_model | ||||
| from contextlib import AsyncExitStack, asynccontextmanager | ||||
| 
 | ||||
| from mcp import ClientSession, StdioServerParameters, types | ||||
| from mcp.client.stdio import stdio_client | ||||
| 
 | ||||
| from typing import Dict, Any, Optional | ||||
| import uvicorn | ||||
| import json | ||||
| import os | ||||
| from contextlib import AsyncExitStack, asynccontextmanager | ||||
| from typing import Dict, Any, Optional | ||||
| 
 | ||||
| import uvicorn | ||||
| from fastapi import FastAPI, Body, Depends | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| from mcp import ClientSession, StdioServerParameters, types | ||||
| from mcp.client.stdio import stdio_client | ||||
| from mcp.types import CallToolResult | ||||
| 
 | ||||
| from mcpo.utils.auth import get_verify_api_key | ||||
| from pydantic import create_model | ||||
| from starlette.routing import Mount | ||||
| 
 | ||||
| 
 | ||||
| def get_python_type(param_type: str): | ||||
| @ -33,6 +33,27 @@ def get_python_type(param_type: str): | ||||
|     # Expand as needed. PRs welcome! | ||||
| 
 | ||||
| 
 | ||||
| def process_tool_response(result: CallToolResult) -> list: | ||||
|     """Universal response processor for all tool endpoints""" | ||||
|     response = [] | ||||
|     for content in result.content: | ||||
|         if isinstance(content, types.TextContent): | ||||
|             text = content.text | ||||
|             if isinstance(text, str): | ||||
|                 try: | ||||
|                     text = json.loads(text) | ||||
|                 except json.JSONDecodeError: | ||||
|                     pass | ||||
|             response.append(text) | ||||
|         elif isinstance(content, types.ImageContent): | ||||
|             image_data = f"data:{content.mimeType};base64,{content.data}" | ||||
|             response.append(image_data) | ||||
|         elif isinstance(content, types.EmbeddedResource): | ||||
|             # TODO: Handle embedded resources | ||||
|             response.append("Embedded resource not supported yet.") | ||||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| async def create_dynamic_endpoints(app: FastAPI, api_dependency=None): | ||||
|     session = app.state.session | ||||
|     if not session: | ||||
| @ -55,10 +76,11 @@ async def create_dynamic_endpoints(app: FastAPI, api_dependency=None): | ||||
|         endpoint_description = tool.description | ||||
|         schema = tool.inputSchema | ||||
| 
 | ||||
|         # Build Pydantic model | ||||
|         model_fields = {} | ||||
|         required_fields = schema.get("required", []) | ||||
|         for param_name, param_schema in schema["properties"].items(): | ||||
|         properties = schema.get("properties", {}) | ||||
| 
 | ||||
|         for param_name, param_schema in properties.items(): | ||||
|             param_type = param_schema.get("type", "string") | ||||
|             param_desc = param_schema.get("description", "") | ||||
|             python_type = get_python_type(param_type) | ||||
| @ -68,43 +90,41 @@ async def create_dynamic_endpoints(app: FastAPI, api_dependency=None): | ||||
|                 Body(default_value, description=param_desc), | ||||
|             ) | ||||
| 
 | ||||
|         FormModel = create_model(f"{endpoint_name}_form_model", **model_fields) | ||||
|         if model_fields: | ||||
|             FormModel = create_model(f"{endpoint_name}_form_model", **model_fields) | ||||
| 
 | ||||
|         def make_endpoint_func(endpoint_name: str, FormModel, session: ClientSession): | ||||
|             async def tool_endpoint(form_data: FormModel): | ||||
|                 args = form_data.model_dump(exclude_none=True) | ||||
|                 print(f"Calling {endpoint_name} with arguments:", args) | ||||
|                 result = await session.call_tool(endpoint_name, arguments=args) | ||||
|                 response = [] | ||||
|                 for content in result.content: | ||||
|                     if isinstance(content, types.TextContent): | ||||
|                         text = content.text | ||||
|                         if isinstance(text, str): | ||||
|                             try: | ||||
|                                 text = json.loads(text) | ||||
|                             except json.JSONDecodeError: | ||||
|                                 pass | ||||
|                         response.append(text) | ||||
|                     elif isinstance(content, types.ImageContent): | ||||
|                         image_data = content.data | ||||
|                         image_data = f"data:{content.mimeType};base64,{image_data}" | ||||
|                         response.append(image_data) | ||||
|                     elif isinstance(content, types.EmbeddedResource): | ||||
|                         # TODO: Handle embedded resources | ||||
|                         response.append("Embedded resource not supported yet.") | ||||
|             def make_endpoint_func( | ||||
|                 endpoint_name: str, FormModel, session: ClientSession | ||||
|             ):  # Parameterized endpoint | ||||
|                 async def tool(form_data: FormModel): | ||||
|                     args = form_data.model_dump(exclude_none=True) | ||||
|                     result = await session.call_tool(endpoint_name, arguments=args) | ||||
|                     return process_tool_response(result) | ||||
| 
 | ||||
|                 return response | ||||
|                 return tool | ||||
| 
 | ||||
|             return tool_endpoint | ||||
|             tool_handler = make_endpoint_func(endpoint_name, FormModel, session) | ||||
|         else: | ||||
| 
 | ||||
|         tool = make_endpoint_func(endpoint_name, FormModel, session) | ||||
|             def make_endpoint_func_no_args( | ||||
|                 endpoint_name: str, session: ClientSession | ||||
|             ):  # Parameterless endpoint | ||||
|                 async def tool():  # No parameters | ||||
|                     result = await session.call_tool( | ||||
|                         endpoint_name, arguments={} | ||||
|                     )  # Empty dict | ||||
|                     return process_tool_response(result)  # Same processor | ||||
| 
 | ||||
|                 return tool | ||||
| 
 | ||||
|             tool_handler = make_endpoint_func_no_args(endpoint_name, session) | ||||
| 
 | ||||
|         app.post( | ||||
|             f"/{endpoint_name}", | ||||
|             summary=endpoint_name.replace("_", " ").title(), | ||||
|             description=endpoint_description, | ||||
|             dependencies=[Depends(api_dependency)] if api_dependency else [], | ||||
|         )(tool) | ||||
|         )(tool_handler) | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| @ -145,7 +165,6 @@ async def run( | ||||
|     cors_allow_origins=["*"], | ||||
|     **kwargs, | ||||
| ): | ||||
| 
 | ||||
|     # Server API Key | ||||
|     api_dependency = get_verify_api_key(api_key) if api_key else None | ||||
| 
 | ||||
| @ -158,10 +177,16 @@ async def run( | ||||
|     ) | ||||
|     version = kwargs.get("version") or "1.0" | ||||
|     ssl_certfile = kwargs.get("ssl_certfile") | ||||
|     ssl_keyfile= kwargs.get("ssl_keyfile") | ||||
|     ssl_keyfile = kwargs.get("ssl_keyfile") | ||||
|     path_prefix = kwargs.get("path_prefix") or "/" | ||||
| 
 | ||||
|     main_app = FastAPI( | ||||
|         title=name, description=description, version=version, ssl_certfile=ssl_certfile, ssl_keyfile=ssl_keyfile, lifespan=lifespan | ||||
|         title=name, | ||||
|         description=description, | ||||
|         version=version, | ||||
|         ssl_certfile=ssl_certfile, | ||||
|         ssl_keyfile=ssl_keyfile, | ||||
|         lifespan=lifespan, | ||||
|     ) | ||||
| 
 | ||||
|     main_app.add_middleware( | ||||
| @ -183,14 +208,13 @@ async def run( | ||||
|         with open(config_path, "r") as f: | ||||
|             config_data = json.load(f) | ||||
|         mcp_servers = config_data.get("mcpServers", {}) | ||||
| 
 | ||||
|         if not mcp_servers: | ||||
|             raise ValueError("No 'mcpServers' found in config file.") | ||||
| 
 | ||||
|         main_app.description += "\n\n- **available tools**:" | ||||
|         for server_name, server_cfg in mcp_servers.items(): | ||||
|             sub_app = FastAPI( | ||||
|                 title=f"{server_name}", | ||||
|                 description=f"{server_name} MCP Server", | ||||
|                 description=f"{server_name} MCP Server\n\n- [back to tool list](http://{host}:{port}/docs)", | ||||
|                 version="1.0", | ||||
|                 lifespan=lifespan, | ||||
|             ) | ||||
| @ -208,13 +232,21 @@ async def run( | ||||
|             sub_app.state.env = {**os.environ, **server_cfg.get("env", {})} | ||||
| 
 | ||||
|             sub_app.state.api_dependency = api_dependency | ||||
| 
 | ||||
|             main_app.mount(f"/{server_name}", sub_app) | ||||
| 
 | ||||
|             main_app.mount(f"{path_prefix}{server_name}", sub_app) | ||||
|             main_app.description += ( | ||||
|                 f"\n    - [{server_name}](http://{host}:{port}/{server_name}/docs)" | ||||
|             ) | ||||
|     else: | ||||
|         raise ValueError("You must provide either server_command or config.") | ||||
| 
 | ||||
|     config = uvicorn.Config(app=main_app, host=host, port=port, ssl_certfile=ssl_certfile , ssl_keyfile=ssl_keyfile ,log_level="info") | ||||
|     config = uvicorn.Config( | ||||
|         app=main_app, | ||||
|         host=host, | ||||
|         port=port, | ||||
|         ssl_certfile=ssl_certfile, | ||||
|         ssl_keyfile=ssl_keyfile, | ||||
|         log_level="info", | ||||
|     ) | ||||
|     server = uvicorn.Server(config) | ||||
| 
 | ||||
|     await server.serve() | ||||
|  | ||||
							
								
								
									
										4
									
								
								uv.lock
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								uv.lock
									
									
									
									
									
								
							| @ -294,13 +294,14 @@ wheels = [ | ||||
| 
 | ||||
| [[package]] | ||||
| name = "mcpo" | ||||
| version = "0.0.6" | ||||
| version = "0.0.9" | ||||
| source = { editable = "." } | ||||
| dependencies = [ | ||||
|     { name = "click" }, | ||||
|     { name = "fastapi" }, | ||||
|     { name = "mcp" }, | ||||
|     { name = "passlib", extra = ["bcrypt"] }, | ||||
|     { name = "pydantic" }, | ||||
|     { name = "pyjwt", extra = ["crypto"] }, | ||||
|     { name = "typer" }, | ||||
|     { name = "uvicorn" }, | ||||
| @ -312,6 +313,7 @@ requires-dist = [ | ||||
|     { name = "fastapi", specifier = ">=0.115.12" }, | ||||
|     { name = "mcp", specifier = ">=1.6.0" }, | ||||
|     { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, | ||||
|     { name = "pydantic", specifier = ">=2.11.1" }, | ||||
|     { name = "pyjwt", extras = ["crypto"], specifier = ">=2.10.1" }, | ||||
|     { name = "typer", specifier = ">=0.15.2" }, | ||||
|     { name = "uvicorn", specifier = ">=0.34.0" }, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user