pythonhttpsgoogle-cloud-runmodel-context-protocol

How to call a python MCP tool hosted on Google Cloud Run


I have deployed a python script for mcp server in a docker container on Google Cloud Run. Below is a sample script

import asyncio
import logging
import os

from fastmcp import FastMCP 

logger = logging.getLogger(__name__)
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)

mcp = FastMCP("MCP Server on Cloud Run")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Use this to add two numbers together.
    
    Args:
        a: The first number.
        b: The second number.
    
    Returns:
        The sum of the two numbers.
    """
    logger.info(f">>> Tool: 'add' called with numbers '{a}' and '{b}'")
    return a + b

if __name__ == "__main__":
    logger.info(f" MCP server started on port {os.getenv('PORT', 8080)}")
    # Could also use 'sse' transport, host="0.0.0.0" required for Cloud Run.
    asyncio.run(
        mcp.run_async(
            transport="streamable-http", 
            host="0.0.0.0", 
            port=os.getenv("PORT", 8080),
        )
    ) 

I have put this in docker and deployed the image to CloudRun and got the https endpoint for calling a streamable https request. The Cloud Run service showing the deployment

I have created a Service account with Cloud Run Invoker permission and generated a json key. But when I try to access the service from python I am getting 403 unauthorised error. I used the below code to try to call the mcp server.

import os
import json
import requests
import google.oauth2.id_token
import google.auth.transport.requests

def runCloudFunction():
    os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'path\to\file.json'
    request = google.auth.transport.requests.Request()
    audience = 'https://cloud_run_service_url'
    TOKEN = google.oauth2.id_token.fetch_id_token(request, audience)
    print(TOKEN)
    r=requests.post(
        audience+'/mcp',
        headers={'Authorization':"Bearer "+TOKEN, 'Content-Type':'application/json'})
    print(r.status_code)

if __name__ == "__main__":
    runCloudFunction()    

The above code is printing the token but returning status 403 for the request to the service. I do not want to remove authentication from the service since that will make it insecure.So , I have selected the Require Authentication option. Security Settings for Cloud Service

I checked that public access is enabled for the Cloud Service in Networking Settings. Networking Settings

Will be really grateful if someone can let me know what I missed. I am not aware of the body to pass to the service to call a particular python function/mcp-tool. WIll be helpful if someone can guide on that as well. Thank you in advance.


Solution

  • The following works for me.

    In reverse order:

    export GOOGLE_APPLICATION_CREDENTIALS=${PWD}/tester.json
    export CLOUD_RUN_URL=$(\
      gcloud run services describe ${NAME} \
      --region=${REGION} \
      --project=${PROJECT} \
      --format="value(status.url)")
    
    uv run add.py 25 17 # add(25,17)
    

    Yields:

    Connected
    [TextContent(type='text', text='42', annotations=None)]
    

    With: pyproject.toml

    [project]
    name = "79685701"
    version = "0.0.1"
    description = "Stackoverflow: 79685701"
    readme = "README.md"
    requires-python = ">=3.13"
    dependencies = [
        "fastmcp>=2.9.2",
        "google-auth>=2.40.3",
        "requests>=2.32.4"
    ]
    

    main.py:

    import asyncio
    import os
    
    from fastmcp import FastMCP, Context
    
    mcp = FastMCP("MCP Server on Cloud Run")
    
    @mcp.tool()
    async def add(a: int, b: int, ctx: Context) -> int:
        await ctx.debug(f"[add] {a}+{b}")
        result = a+b
        await ctx.debug(f"result={result}")
        return result
    
    
    if __name__ == "__main__":
        asyncio.run(
            mcp.run_async(
                transport="streamable-http", 
                host="0.0.0.0", 
                port=os.getenv("PORT", 8080),
            )
        )
    

    add.py:

    from fastmcp import Client
    
    import asyncio
    import google.oauth2.id_token
    import google.auth.transport.requests
    import os
    import sys
    
    args = sys.argv
    if len(args) != 3:
        sys.stderr.write(f"Usage: python {args[0]} <a> <b>\n")
        sys.exit(1)
    
    a = args[1]
    b = args[2]
    
    audience = os.getenv("CLOUD_RUN_URL")
    
    request = google.auth.transport.requests.Request()
    token = google.oauth2.id_token.fetch_id_token(request, audience)
    
    config = {
        "mcpServers": {
            "cloud-run":{
                "transport": "http",
                "url": f"{audience}/mcp/",
                "headers": {
                    "Authorization": "Bearer token",
                },
                "auth": token,
            }
        }
    }
    
    client = Client(config)
    
    
    async def run():
        async with client:
            print("Connected")
            result = await client.call_tool(
                name="add",
                arguments={"a":a, "b":b},
            )
            print(result)
    
    
    if __name__ == "__main__":
        asyncio.run(run())
    

    Dockerfile:

    # FastMCP Application Dockerfile
    FROM docker.io/python:3.13-slim
    
    RUN apt-get update && \
        apt-get install -y --no-install-recommends curl ca-certificates
    
    ADD https://astral.sh/uv/install.sh /uv-installer.sh
    
    RUN sh /uv-installer.sh && \
        rm /uv-installer.sh
    
    ENV PATH="/root/.local/bin:${PATH}"
    
    WORKDIR /app
    
    COPY main.py main.py
    COPY pyproject.toml pyproject.toml
    COPY uv.lock uv.lock
    
    RUN uv sync --locked
    
    EXPOSE 8080
    
    ENTRYPOINT ["uv", "run","/app/main.py"]
    

    And:

    BILLING="..."
    PROJECT="..."
    
    NAME="fastmcp"
    
    REGION="..."
    
    ACCOUNT="tester"
    EMAIL=${ACCOUNT}@${PROJECT}.iam.gserviceaccount.com
    
    gcloud iam service-accounts create ${ACCOUNT} \
    --project=${PROJECT}
    
    gcloud iam service-accounts keys create ${PWD}/${ACCOUNT}.json \
     --iam-account=${EMAIL} \
     --project=${PROJECT}
    
    gcloud projects add-iam-policy-binding ${PROJECT} \
    --member=serviceAccount:${EMAIL} \
    --role=roles/run.invoker
    
    gcloud auth print-access-token \
    | podman login ${REGION}-docker.pkg.dev \
      --username=oauth2accesstoken \
      --password-stdin
    
    REPO="cloud-run-source-deploy"
    VERS="0.0.1"
    IMAGE=${REGION}-docker.pkg.dev/${PROJECT}/${REPO}/${NAME}:${VERS}
    
    podman build \
    --tag=${IMAGE} \
    --file=${PWD}/Dockerfile \
    ${PWD}
    
    podman push ${IMAGE}
    
    gcloud run deploy ${NAME} \
    --image=${IMAGE} \
    --region=${REGION} \
    --project=${PROJECT} \
    --no-allow-unauthenticated