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. После этого агент сможет загрузить удалённый реестр инструментов и вызывать их прямо из запросов на естественном языке.