2025, Nov 12 18:03

Подключаем MCP‑сервер на Google Cloud Run к LLM‑агенту через streamable HTTP и Bearer токен

Разверните MCP‑сервер на Cloud Run и подключите его к LLM‑агенту: ID‑токен, streamable HTTP и Bearer в заголовке для вызова инструментов по запросу клиента.

Когда вы переносите MCP‑сервер из локального процесса stdio на управляемую HTTP‑точку на Google Cloud Run, ваш LLM‑агент внезапно теряет простой доступ к этим инструментам. Да, прямой вызов инструмента по HTTP работает, но смысл в другом: чтобы агент сам находил и запускал инструменты по свободной формулировке запроса. Не хватает звена — MCP‑клиента, который умеет работать по streamable HTTP и передавать токен идентичности в заголовке Authorization.

Базовый вариант: MCP сервер на Cloud Run

MCP‑сервер публикует один инструмент для сложения и принимает запросы через транспорт streamable HTTP. Он запускается как контейнер на Cloud Run.

import asyncio
import os
from fastmcp import FastMCP, Context
svc = FastMCP("MCP Server on Cloud Run")
@svc.tool()
'''Use when two integers need addition; supply both inputs as parameters'''
async def sum_two(x: int, y: int, meta: Context) -> int:
    await meta.debug(f"[sum_two] {x}+{y}")
    out = x + y
    await meta.debug(f"result={out}")
    return out
if __name__ == "__main__":
    asyncio.run(
        svc.run_async(
            transport="streamable-http",
            host="0.0.0.0",
            port=os.getenv("PORT", 8080),
        )
    )

Прямой вызов из Python‑клиента работает, но агент не умеет планировать

Простой клиент может обратиться к серверу, получить ID‑токен для URL Cloud Run и вызвать инструмент по имени. Это подтверждает, что развертывание и аутентификация в порядке, но не дает LLM самому решать, когда вызывать инструмент, опираясь на произвольную подсказку.

from fastmcp import Client
import asyncio
import google.oauth2.id_token
import google.auth.transport.requests
import os
import sys
argv = sys.argv
if len(argv) != 3:
    sys.stderr.write(f"Usage: python {argv[0]} <a> <b>\n")
    sys.exit(1)
arg_a = argv[1]
arg_b = argv[2]
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'C:\\Path\\to\\file.json'
base_url = "https://mcp-server-url-from-cloud-run"
req = google.auth.transport.requests.Request()
id_tok = google.oauth2.id_token.fetch_id_token(req, base_url)
cfg = {
    "mcpServers": {
        "cloud-run":{
            "transport": "streamable-http",
            "url": f"{base_url}/mcp/",
            "headers": {
                "Authorization": "Bearer token",
            },
            "auth": id_tok,
        }
    }
}
cli = Client(cfg)
async def main():
    async with cli:
        print("Connected")
        a_val = int(arg_a)
        b_val = int(arg_b)
        res = await cli.call_tool(
            name="sum_two",
            arguments={"x": a_val, "y": b_val},
        )
        print(res)
if __name__ == "__main__":
    asyncio.run(main())

Что на самом деле мешает агенту

Локальные окружения часто используют stdio, подключая MCP‑сервер напрямую к среде агента — так инструменты автоматически обнаруживаются и задействуются в ходе рассуждений. Но как только сервер оказывается за HTTP‑эндпоинтом на Cloud Run, на стороне хоста нужен MCP‑клиент, который сможет подключиться по streamable HTTP и прикрепить токен идентичности Google в заголовке Authorization. Без этого фреймворк агента не видит удалённый реестр инструментов и не может пройти аутентификацию, поэтому и не планирует вызовы инструментов из естественного языка.

Рабочий подход: MultiServerMCPClient с заголовком Bearer token

Решение — создать MCP‑клиент с нативной поддержкой HTTP‑транспорта и пользовательских заголовков, а затем передать токен идентичности Cloud Run в виде Authorization: Bearer. Это сохраняет текущее развертывание на Cloud Run и избавляет от локальных прокси.

Ключевым стало использование MultiServerMCPClient для подключения к MCP‑серверу и передача токена Auth в заголовке.

import asyncio
import os
import google.oauth2.id_token
import google.auth.transport.requests
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
os.environ["OPENAI_API_KEY"] = "OpenAI_API_Key"
agent_llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0,
    max_tokens=None,
    timeout=None,
    max_retries=2,
)
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'C:\\Path\\To\\file.json'
service_url = "https://mcp-server-url"
http_req = google.auth.transport.requests.Request()
bearer_jwt = google.oauth2.id_token.fetch_id_token(http_req, service_url)
remote_cfg = {
    "cloud-run": {
        "transport": "streamable_http",
        "url": f"{service_url}/mcp/",
        "headers": {
            "Authorization": "Bearer " + bearer_jwt,
        }
    }
}
mcp_pool = MultiServerMCPClient(remote_cfg)
async def drive():
    toolset = await mcp_pool.get_tools()
    prompt_text = "What is 4 + 8"
    orchestrator = create_react_agent(agent_llm, toolset)
    agent_result = await orchestrator.ainvoke({"messages": prompt_text})
    last_ai_reply = None
    for msg in agent_result['messages']:
        if "AIMessage" in str(type(msg)):
            last_ai_reply = msg
    print(last_ai_reply.content)
if __name__ == "__main__":
    asyncio.run(drive())

Так агент обнаруживает удалённый MCP‑инструмент и вызывает его, когда подсказка подразумевает арифметику. На выходе печатается ожидаемый результат, например: 4+8=12.

Почему это важно

Инструменты приносят пользу тогда, когда LLM сам, исходя из неструктурированного ввода, решает, что вызов нужен, и направляет запрос к нужной возможности. Публикация MCP‑сервера через Cloud Run не меняет ваши инструменты — меняется способ подключения и аутентификации агента. Как только клиент умеет работать по streamable HTTP и передавать bearer‑токен в Authorization, вы возвращаете ту же бесшовную оркестрацию инструментов, что и локально, без немасштабируемых прокси.

Выводы

Оставьте MCP‑сервер на Cloud Run как есть — сосредоточьтесь на клиенте. Используйте клиент с поддержкой streamable HTTP и пользовательских заголовков. Получите токен идентичности Google из JSON‑ключа, на который ссылается переменная окружения GOOGLE_APPLICATION_CREDENTIALS, для URL Cloud Run и прикрепите его как Authorization: Bearer. После этого агент сможет загрузить удалённый реестр инструментов и вызывать их прямо из запросов на естественном языке.