mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-07-09 14:04:19 +00:00
Ajustes na documentação e remanejamento dos folders
This commit is contained in:
22
.idea/workspace.xml
generated
22
.idea/workspace.xml
generated
@@ -5,8 +5,17 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders">
|
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.env.example" beforeDir="false" afterPath="$PROJECT_DIR$/.env.example" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/MANUAL_AGENT_PLATFORM_GATEWAYS.md" beforeDir="false" afterPath="$PROJECT_DIR$/MANUAL_AGENT_PLATFORM_GATEWAYS.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/README_en.md" beforeDir="false" afterPath="$PROJECT_DIR$/README_en.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/agent_gateway/config/backends.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/apps/agent_gateway/config/backends.yaml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/mcp_gateway/app/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/apps/mcp_gateway/app/main.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/apps/mcp_gateway/config/mcp_gateway.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/apps/mcp_gateway/config/mcp_gateway.yaml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/mcp/servers/retail_mcp_server/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/mcp/servers/retail_mcp_server/main.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/mcp/servers/telecom_mcp_server/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/mcp/servers/telecom_mcp_server/main.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/specs/SPEC-001-Architecture.md" beforeDir="false" afterPath="$PROJECT_DIR$/specs/SPEC-001-Architecture.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/templates/agent_template_backend/.env" beforeDir="false" afterPath="$PROJECT_DIR$/templates/agent_template_backend/.env" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/templates/agent_template_backend_day_zero/.env" beforeDir="false" afterPath="$PROJECT_DIR$/templates/agent_template_backend_day_zero/.env" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -31,10 +40,10 @@
|
|||||||
"accountId": "bd799ac0-7624-42b2-999a-b96e6e11f98a"
|
"accountId": "bd799ac0-7624-42b2-999a-b96e6e11f98a"
|
||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
<component name="KubernetesApiPersistence"><![CDATA[{}]]></component>
|
<component name="KubernetesApiPersistence">{}</component>
|
||||||
<component name="KubernetesApiProvider"><![CDATA[{
|
<component name="KubernetesApiProvider">{
|
||||||
"isMigrated": true
|
"isMigrated": true
|
||||||
}]]></component>
|
}</component>
|
||||||
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="PHP 8.1" />
|
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="PHP 8.1" />
|
||||||
<component name="ProjectCodeStyleSettingsMigration">
|
<component name="ProjectCodeStyleSettingsMigration">
|
||||||
<option name="version" value="2" />
|
<option name="version" value="2" />
|
||||||
@@ -82,6 +91,7 @@
|
|||||||
<updated>1781996088468</updated>
|
<updated>1781996088468</updated>
|
||||||
<workItem from="1781996091031" duration="1242000" />
|
<workItem from="1781996091031" duration="1242000" />
|
||||||
<workItem from="1782038604623" duration="6486000" />
|
<workItem from="1782038604623" duration="6486000" />
|
||||||
|
<workItem from="1782047166074" duration="27000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
|
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
|
|||||||
133
Documentacao/INVENTARIO_AGENT_GATEWAY_MCP_GATEWAY.md
Normal file
133
Documentacao/INVENTARIO_AGENT_GATEWAY_MCP_GATEWAY.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Inventário — Agent Gateway + MCP Gateway Overlay
|
||||||
|
|
||||||
|
Este inventário lista os arquivos incluídos no overlay `agent_platform_agent_gateway_mcp_gateway_overlay.zip`, indicando a área, o tipo de alteração e a finalidade de cada arquivo.
|
||||||
|
|
||||||
|
## Resumo
|
||||||
|
|
||||||
|
| Área | Quantidade |
|
||||||
|
|---|---:|
|
||||||
|
| Documentação | 1 |
|
||||||
|
| Agent Gateway | 10 |
|
||||||
|
| MCP Gateway | 5 |
|
||||||
|
| Agent Framework | 4 |
|
||||||
|
| Template Backend | 2 |
|
||||||
|
| MCP Server Mock | 2 |
|
||||||
|
| Deploy | 2 |
|
||||||
|
|
||||||
|
## Arquivos por área
|
||||||
|
|
||||||
|
### Documentação
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `README_AGENT_GATEWAY_AND_MCP_GATEWAY_EVOLUTION.md` | Novo / overlay | Documento principal do overlay. Explica a nova arquitetura sem AI Gateway separado, com Agent Gateway governando políticas/modelos e MCP Gateway separado para tools. |
|
||||||
|
|
||||||
|
### Agent Gateway
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `apps/agent_gateway/app/config/governance_loader.py` | Novo / overlay | Carrega o arquivo YAML de governança do Agent Gateway a partir de AGENT_GATEWAY_GOVERNANCE_CONFIG. |
|
||||||
|
| `apps/agent_gateway/app/governance/__init__.py` | Novo / overlay | Inicializa o pacote Python de governança do Agent Gateway. |
|
||||||
|
| `apps/agent_gateway/app/governance/audit.py` | Novo / overlay | Centraliza logging/auditoria das decisões de governança do Agent Gateway, com proteção simples para não logar mensagem completa. |
|
||||||
|
| `apps/agent_gateway/app/governance/evaluation_hooks.py` | Novo / overlay | Hooks antes e depois da chamada ao backend/runtime. Serve para amostragem, evaluator, scoring ou integração futura com Langfuse. |
|
||||||
|
| `apps/agent_gateway/app/governance/model_policies.py` | Novo / overlay | Resolve políticas de modelo/profile no Agent Gateway. Define qual provider/model/profile deve ser usado por operação, tenant e agente. |
|
||||||
|
| `apps/agent_gateway/app/governance/rate_limit.py` | Novo / overlay | Implementa rate limit em memória por tenant, agente e canal antes de encaminhar a requisição ao backend/runtime. |
|
||||||
|
| `apps/agent_gateway/app/governance/usage.py` | Novo / overlay | Hook para registrar uso de gateway, políticas aplicadas e respostas do backend. Pronto para plugar métricas, banco, Langfuse ou OTEL. |
|
||||||
|
| `apps/agent_gateway/app/governance_middleware.py` | Novo / overlay | Componente principal de governança do Agent Gateway. Aplica rate limit, resolve model_policy, gera headers/metadados e executa hooks antes/depois do backend. |
|
||||||
|
| `apps/agent_gateway/app/routes/governed_proxy_example.py` | Novo / overlay | Exemplo de rota governada para demonstrar como aplicar governança antes de encaminhar para o Agent Backend/Runtime. |
|
||||||
|
| `apps/agent_gateway/config/gateway_governance.yaml` | Novo / overlay | Configuração de governança do Agent Gateway: profiles, operation_profiles, providers permitidos, rate limits, headers propagados e evaluation hooks. |
|
||||||
|
|
||||||
|
### MCP Gateway
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `apps/mcp_gateway/Dockerfile` | Novo / overlay | Imagem Docker do MCP Gateway. |
|
||||||
|
| `apps/mcp_gateway/app/__init__.py` | Novo / overlay | Inicializa o pacote Python da aplicação MCP Gateway. |
|
||||||
|
| `apps/mcp_gateway/app/main.py` | Novo / overlay | Aplicação FastAPI do MCP Gateway. Expõe health, ready, catálogo de tools e endpoint de invoke com auth, autorização, mapping, cache, timeout e retry. |
|
||||||
|
| `apps/mcp_gateway/config/mcp_gateway.yaml` | Novo / overlay | Configuração central do MCP Gateway: MCP servers, tools, versões, cache, timeout, retry, autorização por agente/canal e mapping BusinessContext → parâmetros. |
|
||||||
|
| `apps/mcp_gateway/requirements.txt` | Novo / overlay | Dependências Python do MCP Gateway. |
|
||||||
|
|
||||||
|
### Agent Framework
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `libs/agent_framework/src/agent_framework/gateway_policy_context.py` | Novo / overlay | Helper no framework para o Runtime ler a política de modelo enviada pelo Agent Gateway em state['metadata']['model_policy']. |
|
||||||
|
| `libs/agent_framework/src/agent_framework/gateways/__init__.py` | Novo / overlay | Inicializa o pacote de clients de gateways no framework, exportando MCPGatewayClient. |
|
||||||
|
| `libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py` | Novo / overlay | Client assíncrono do framework para chamar o MCP Gateway: listar tools e executar tools. |
|
||||||
|
| `libs/agent_framework/src/agent_framework/runtime_mcp_gateway_adapter.py` | Novo / overlay | Mixin opcional para agentes/runtime chamarem tools via MCP Gateway e anexarem resultados em state['mcp_results']. |
|
||||||
|
|
||||||
|
### Template Backend
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `templates/agent_template_backend/app/mcp_gateway_client_factory.py` | Novo / overlay | Factory no template backend para construir MCPGatewayClient a partir de variáveis de ambiente. |
|
||||||
|
|
||||||
|
### MCP Server Mock
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `mcp/servers/mock_telecom_mcp/app.py` | Novo / overlay | Mock MCP Server com tools consultar_fatura e consultar_pagamentos para validar o MCP Gateway localmente. |
|
||||||
|
| `mcp/servers/mock_telecom_mcp/requirements.txt` | Novo / overlay | Dependências do mock MCP Server de telecom usado para testes locais. |
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `deploy/docker/docker-compose.mcp-gateway.yml` | Novo / overlay | Docker Compose para subir MCP Gateway e mock_telecom_mcp localmente. |
|
||||||
|
| `deploy/k8s/mcp-gateway.yaml` | Novo / overlay | Manifest Kubernetes de Deployment e Service do MCP Gateway. |
|
||||||
|
|
||||||
|
## Observações de integração
|
||||||
|
|
||||||
|
### Agent Gateway
|
||||||
|
|
||||||
|
Os arquivos em `apps/agent_gateway` não criam um novo serviço. Eles evoluem o Agent Gateway existente para atuar como gateway dedicado da plataforma, centralizando:
|
||||||
|
|
||||||
|
- políticas de modelo/profile;
|
||||||
|
- rate limit;
|
||||||
|
- auditoria;
|
||||||
|
- hooks de avaliação;
|
||||||
|
- propagação de metadados de governança para o Runtime.
|
||||||
|
|
||||||
|
A rota `governed_proxy_example.py` é um exemplo de integração. O handler real do `POST /gateway/message` deve aplicar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
governed_body, headers = governance.prepare_backend_request(body)
|
||||||
|
```
|
||||||
|
|
||||||
|
antes de chamar o backend/runtime, e:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return governance.process_backend_response(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
após receber a resposta.
|
||||||
|
|
||||||
|
### MCP Gateway
|
||||||
|
|
||||||
|
O MCP Gateway é um serviço separado. Ele centraliza:
|
||||||
|
|
||||||
|
- catálogo de tools;
|
||||||
|
- autorização por agente/canal;
|
||||||
|
- versionamento de tools;
|
||||||
|
- mapping de BusinessContext para parâmetros;
|
||||||
|
- cache;
|
||||||
|
- timeout;
|
||||||
|
- retry;
|
||||||
|
- auditoria simples.
|
||||||
|
|
||||||
|
### Runtime / Backend
|
||||||
|
|
||||||
|
O Runtime continua responsável por:
|
||||||
|
|
||||||
|
- LangGraph;
|
||||||
|
- estado;
|
||||||
|
- memória;
|
||||||
|
- checkpoints;
|
||||||
|
- fluxo;
|
||||||
|
- providers LLM existentes.
|
||||||
|
|
||||||
|
O Runtime passa a chamar tools via MCP Gateway usando `MCPGatewayClient` e/ou `MCPGatewayRuntimeMixin`.
|
||||||
|
|
||||||
|
### AI Gateway
|
||||||
|
|
||||||
|
Este overlay não cria `apps/ai_gateway`. A governança de modelo fica no Agent Gateway, e a execução LLM continua no Runtime/backend usando os providers já existentes.
|
||||||
@@ -0,0 +1,627 @@
|
|||||||
|
# Manual de Execução Local
|
||||||
|
## Agent Gateway + MCP Gateway + Agent Template Backend + Frontend
|
||||||
|
|
||||||
|
## 1. Arquitetura de execução
|
||||||
|
|
||||||
|
A arquitetura local fica assim:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend
|
||||||
|
porta 5173
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent Gateway
|
||||||
|
porta 9000
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent Template Backend / Agent Runtime
|
||||||
|
porta 8000
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
MCP Gateway
|
||||||
|
porta 8300
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
MCP Server / Mock Telecom MCP
|
||||||
|
porta 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
A governança de modelo, rate limit, auditoria e políticas ficam no **Agent Gateway**.
|
||||||
|
|
||||||
|
O **Agent Runtime / Agent Template Backend** continua responsável por:
|
||||||
|
|
||||||
|
- LangGraph;
|
||||||
|
- estado;
|
||||||
|
- memória;
|
||||||
|
- checkpoints;
|
||||||
|
- supervisor/router;
|
||||||
|
- guardrails;
|
||||||
|
- judges;
|
||||||
|
- chamada LLM via providers existentes;
|
||||||
|
- chamada de tools via MCP Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Portas
|
||||||
|
|
||||||
|
| Componente | Porta | URL |
|
||||||
|
|---|---:|---|
|
||||||
|
| Frontend | 5173 | `http://localhost:5173` |
|
||||||
|
| Agent Gateway | 9000 | `http://localhost:9000` |
|
||||||
|
| Agent Template Backend | 8000 | `http://localhost:8000` |
|
||||||
|
| MCP Gateway | 8300 | `http://localhost:8300` |
|
||||||
|
| MCP Server / Mock Telecom MCP | 8001 | `http://localhost:8001` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ordem recomendada para subir
|
||||||
|
|
||||||
|
Subir nesta ordem:
|
||||||
|
|
||||||
|
1. MCP Server / Mock Telecom MCP
|
||||||
|
2. MCP Gateway
|
||||||
|
3. Agent Template Backend
|
||||||
|
4. Agent Gateway
|
||||||
|
5. Frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Terminal 1 — MCP Server / Mock Telecom MCP
|
||||||
|
|
||||||
|
Se estiver usando o mock incluído no overlay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/mcp/servers/mock_telecom_mcp
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port 8001 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "mock_telecom_mcp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Terminal 2 — MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/apps/mcp_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar health:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar readiness:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
Listar tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8300/v1/tools | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Executar tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"ok": true,
|
||||||
|
"data": {
|
||||||
|
"invoice_id": "INV-001",
|
||||||
|
"msisdn": "11999999999",
|
||||||
|
"valor_total": 249.9,
|
||||||
|
"vencimento": "2026-06-10",
|
||||||
|
"status": "ABERTA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Terminal 3 — Agent Template Backend / Agent Runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/templates/agent_template_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
ou, se o seu backend estiver em outra pasta:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/templates/agent_template_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Ativar ambiente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Se ainda não existir `.venv`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurar variáveis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MCP_GATEWAY_ENABLED=true
|
||||||
|
export MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
export MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Se estiver usando OCI/OpenAI-compatible, manter também as variáveis já existentes do backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LLM_PROVIDER=oci_openai
|
||||||
|
export OCI_GENAI_API_KEY=<sua-chave>
|
||||||
|
```
|
||||||
|
|
||||||
|
ou, para mock:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LLM_PROVIDER=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
Subir backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar agentes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/agents | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Testar backend direto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"payload": {
|
||||||
|
"message": "Quero consultar minha fatura",
|
||||||
|
"session_id": "session-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Terminal 4 — Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/apps/agent_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Ativar ambiente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurar variáveis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Subir Agent Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Se a rota governada de exemplo estiver registrada no `app.main`, testar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:9000/gateway/message/governed \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"payload": {
|
||||||
|
"message": "Quero consultar minha fatura",
|
||||||
|
"session_id": "session-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"metadata": {
|
||||||
|
"operation": "agent.final_answer"
|
||||||
|
},
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Se a rota real for `/gateway/message`, testar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:9000/gateway/message \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"payload": {
|
||||||
|
"message": "Quero consultar minha fatura",
|
||||||
|
"session_id": "session-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"metadata": {
|
||||||
|
"operation": "agent.final_answer"
|
||||||
|
},
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Terminal 5 — Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/agent_frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
ou a pasta onde estiver o frontend.
|
||||||
|
|
||||||
|
Instalar dependências:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Subir:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Abrir:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurar no frontend:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Backend URL: http://localhost:9000
|
||||||
|
Agent: telecom_contas
|
||||||
|
Session ID: session-001
|
||||||
|
Customer Key: 11999999999
|
||||||
|
Contract Key: INV-001
|
||||||
|
```
|
||||||
|
|
||||||
|
O frontend deve chamar o **Agent Gateway** na porta 9000, não o MCP Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Fluxo final esperado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend 5173
|
||||||
|
↓
|
||||||
|
Agent Gateway 9000
|
||||||
|
↓
|
||||||
|
Agent Template Backend 8000
|
||||||
|
↓
|
||||||
|
MCP Gateway 8300
|
||||||
|
↓
|
||||||
|
Mock Telecom MCP 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Docker Compose para MCP Gateway + Mock MCP
|
||||||
|
|
||||||
|
Também é possível subir MCP Gateway + Mock MCP com Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci
|
||||||
|
|
||||||
|
docker compose -f deploy/docker/docker-compose.mcp-gateway.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso sobe:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MCP Gateway http://localhost:8300
|
||||||
|
Mock Telecom MCP http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois subir manualmente:
|
||||||
|
|
||||||
|
- Agent Template Backend na porta 8000;
|
||||||
|
- Agent Gateway na porta 9000;
|
||||||
|
- Frontend na porta 5173.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Checklist de validação
|
||||||
|
|
||||||
|
## MCP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. Erros comuns
|
||||||
|
|
||||||
|
## 12.1. Frontend chamando porta errada
|
||||||
|
|
||||||
|
Errado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend → http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Correto:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend → http://localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
Se você quiser testar sem Agent Gateway, pode apontar temporariamente para 8000. Mas no modelo final, o frontend deve usar o Agent Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.2. MCP Gateway sem MCP Server
|
||||||
|
|
||||||
|
Sintoma:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MCP server unavailable
|
||||||
|
```
|
||||||
|
|
||||||
|
Correção:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Se falhar, subir o mock MCP server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.3. Tool sem BusinessContext
|
||||||
|
|
||||||
|
Sintoma:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"missing_business_keys": ["customer_key", "contract_key"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correção:
|
||||||
|
|
||||||
|
enviar:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.4. Agent Gateway não encontra backend
|
||||||
|
|
||||||
|
Sintoma:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Connection refused http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Correção:
|
||||||
|
|
||||||
|
validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
e configurar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.5. Rota governada não registrada
|
||||||
|
|
||||||
|
Se `/gateway/message/governed` retornar 404, significa que o arquivo de exemplo ainda não foi incluído no `app.main`.
|
||||||
|
|
||||||
|
Nesse caso, use a rota real `/gateway/message` ou registre no `main.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.routes.governed_proxy_example import router as governed_router
|
||||||
|
|
||||||
|
app.include_router(governed_router)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Variáveis consolidadas
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
```env
|
||||||
|
DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Template Backend
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 14. Resumo rápido
|
||||||
|
|
||||||
|
Em cinco terminais:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1
|
||||||
|
cd mcp/servers/mock_telecom_mcp
|
||||||
|
source .venv/bin/activate
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port 8001 --reload
|
||||||
|
|
||||||
|
# Terminal 2
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
source .venv/bin/activate
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
|
||||||
|
# Terminal 3
|
||||||
|
cd templates/agent_template_backend
|
||||||
|
source .venv/bin/activate
|
||||||
|
export MCP_GATEWAY_ENABLED=true
|
||||||
|
export MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
# Terminal 4
|
||||||
|
cd apps/agent_gateway
|
||||||
|
source .venv/bin/activate
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
|
||||||
|
# Terminal 5
|
||||||
|
cd agent_frontend
|
||||||
|
npm install
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
```
|
||||||
93
Documentacao/MCP_GATEWAY_RUNBOOK.md
Normal file
93
Documentacao/MCP_GATEWAY_RUNBOOK.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# MCP Gateway Runbook
|
||||||
|
|
||||||
|
## Arquitetura corrigida
|
||||||
|
|
||||||
|
O backend/agente não deve chamar diretamente os MCP servers finais. O fluxo correto é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_template_backend / agent_framework
|
||||||
|
-> MCP Gateway Client
|
||||||
|
-> apps/mcp_gateway
|
||||||
|
-> mcp/servers/telecom_mcp_server ou mcp/servers/retail_mcp_server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subir localmente
|
||||||
|
|
||||||
|
A partir da raiz do projeto:
|
||||||
|
|
||||||
|
### Terminal 1 - Telecom MCP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/servers/telecom_mcp_server
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 2 - Retail MCP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/servers/retail_mcp_server
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 3 - MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 4 - Backend/agente
|
||||||
|
|
||||||
|
No `.env` do backend/agente ou do runtime que usa o `agent_framework`, habilite:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testes rápidos
|
||||||
|
|
||||||
|
### Health do gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lista de tools expostas pelo gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chamada de tool via gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"arguments": {
|
||||||
|
"msisdn": "11999999999",
|
||||||
|
"invoice_id": "INV-123"
|
||||||
|
},
|
||||||
|
"business_context": {},
|
||||||
|
"metadata": {"session_id": "local-test"}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Resposta esperada: `ok: true`, `data.invoice_id`, `data.msisdn`, `metadata.server: telecom`.
|
||||||
|
|
||||||
|
## O que foi corrigido
|
||||||
|
|
||||||
|
- `apps/mcp_gateway/config/mcp_gateway.yaml` agora aponta para os MCP servers reais nas portas `8100` e `8200`.
|
||||||
|
- O MCP Gateway agora suporta o contrato legado dos MCP servers: `POST /mcp/tools/call` com `{tool_name, arguments}`.
|
||||||
|
- O `agent_framework` ganhou flags `MCP_GATEWAY_ENABLED`, `MCP_GATEWAY_URL`, `MCP_GATEWAY_TOKEN`, `MCP_GATEWAY_AGENT_ID` e `MCP_GATEWAY_TENANT_ID`.
|
||||||
|
- O `MCPToolRouter` passa a chamar o MCP Gateway quando `MCP_GATEWAY_ENABLED=true`.
|
||||||
|
- `libs/agent_framework/config/mcp_servers.yaml` foi mantido como registry lógico/fallback, não como caminho principal quando o gateway está ativo.
|
||||||
129
Documentacao/README_AGENT_GATEWAY_AND_MCP_GATEWAY_EVOLUTION.md
Normal file
129
Documentacao/README_AGENT_GATEWAY_AND_MCP_GATEWAY_EVOLUTION.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Agent Platform OCI — Agent Gateway + MCP Gateway Evolution
|
||||||
|
|
||||||
|
Este overlay remove o conceito de `AI Gateway` separado.
|
||||||
|
|
||||||
|
## Arquitetura
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend
|
||||||
|
↓
|
||||||
|
Agent Gateway
|
||||||
|
├── governance
|
||||||
|
├── model policies
|
||||||
|
├── rate limit
|
||||||
|
├── audit
|
||||||
|
└── evaluation hooks
|
||||||
|
↓
|
||||||
|
Agent Backend / Runtime
|
||||||
|
├── LangGraph
|
||||||
|
├── state
|
||||||
|
├── memory
|
||||||
|
├── checkpoints
|
||||||
|
└── LLM providers via profiles existentes
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
MCP Servers
|
||||||
|
```
|
||||||
|
|
||||||
|
## O que entra no Agent Gateway
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/governance/
|
||||||
|
apps/agent_gateway/app/governance_middleware.py
|
||||||
|
apps/agent_gateway/app/routes/governed_proxy_example.py
|
||||||
|
apps/agent_gateway/config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## O que entra no MCP Gateway
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/
|
||||||
|
libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py
|
||||||
|
libs/agent_framework/src/agent_framework/runtime_mcp_gateway_adapter.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aplicar overlay
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unzip agent_platform_agent_gateway_mcp_gateway_overlay.zip -d /tmp/overlay
|
||||||
|
rsync -av /tmp/overlay/ ./
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subir MCP Gateway local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker/docker-compose.mcp-gateway.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Serviços:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MCP Gateway http://localhost:8300
|
||||||
|
Mock Telecom MCP http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testar MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Executar tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como plugar no Agent Gateway
|
||||||
|
|
||||||
|
No handler real do `POST /gateway/message`, antes de encaminhar ao backend/runtime:
|
||||||
|
|
||||||
|
```python
|
||||||
|
governed_body, headers = governance.prepare_backend_request(body)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ao receber resposta do backend:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return governance.process_backend_response(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
O arquivo abaixo mostra um exemplo completo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/routes/governed_proxy_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variáveis do Runtime
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
```
|
||||||
|
|
||||||
|
## Importante
|
||||||
|
|
||||||
|
Não existe `apps/ai_gateway`.
|
||||||
|
|
||||||
|
A governança de modelo fica no Agent Gateway como policy/metadados.
|
||||||
|
|
||||||
|
O Runtime continua usando os LLM providers existentes, podendo ler a política enviada pelo Gateway em:
|
||||||
|
|
||||||
|
```python
|
||||||
|
state["metadata"]["model_policy"]
|
||||||
|
```
|
||||||
@@ -45,14 +45,25 @@ class ToolResult(BaseModel):
|
|||||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryResult(BaseModel):
|
||||||
|
ok: bool
|
||||||
|
servers_scanned: int = 0
|
||||||
|
tools_discovered: int = 0
|
||||||
|
errors: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
tools: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> dict[str, Any]:
|
def load_config() -> dict[str, Any]:
|
||||||
path = Path(os.getenv("MCP_GATEWAY_CONFIG_PATH", "config/mcp_gateway.yaml"))
|
path = Path(os.getenv("MCP_GATEWAY_CONFIG_PATH", "config/mcp_gateway.yaml"))
|
||||||
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||||
|
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
static_tools: dict[str, dict[str, Any]] = dict(config.get("tools") or {})
|
||||||
|
discovered_tools: dict[str, dict[str, Any]] = {}
|
||||||
|
discovery_state: dict[str, Any] = {"last_sync": None, "errors": [], "tools": []}
|
||||||
cache: dict[str, tuple[float, Any]] = {}
|
cache: dict[str, tuple[float, Any]] = {}
|
||||||
app = FastAPI(title="Agent Platform OCI - MCP Gateway", version="1.0.0")
|
app = FastAPI(title="Agent Platform OCI - MCP Gateway", version="1.1.0")
|
||||||
|
|
||||||
|
|
||||||
def audit(name: str, payload: dict[str, Any]) -> None:
|
def audit(name: str, payload: dict[str, Any]) -> None:
|
||||||
@@ -70,6 +81,13 @@ def auth_check(authorization: str | None) -> None:
|
|||||||
raise HTTPException(status_code=403, detail="Invalid MCP Gateway token")
|
raise HTTPException(status_code=403, detail="Invalid MCP Gateway token")
|
||||||
|
|
||||||
|
|
||||||
|
def all_tools() -> dict[str, dict[str, Any]]:
|
||||||
|
merged = dict(discovered_tools)
|
||||||
|
# Static config wins over discovery so operators can override metadata safely.
|
||||||
|
merged.update(static_tools)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
def map_arguments(tool_name: str, args: dict[str, Any], bc: dict[str, Any]) -> dict[str, Any]:
|
def map_arguments(tool_name: str, args: dict[str, Any], bc: dict[str, Any]) -> dict[str, Any]:
|
||||||
result = dict(args or {})
|
result = dict(args or {})
|
||||||
for source, target in ((config.get("parameter_mapping") or {}).get(tool_name) or {}).items():
|
for source, target in ((config.get("parameter_mapping") or {}).get(tool_name) or {}).items():
|
||||||
@@ -100,16 +118,22 @@ async def post_with_retry(url: str, payload: dict[str, Any], timeout: int, retry
|
|||||||
raise RuntimeError(str(last_exc))
|
raise RuntimeError(str(last_exc))
|
||||||
|
|
||||||
|
|
||||||
def build_server_payload(tool_name: str, args: dict[str, Any], server: dict[str, Any], tool: dict[str, Any]) -> dict[str, Any]:
|
async def get_json(url: str, timeout: int) -> Any:
|
||||||
"""Builds the payload expected by the downstream MCP server.
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
|
resp = await client.get(url)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
The example servers under mcp/servers expose the framework legacy contract:
|
|
||||||
POST /mcp/tools/call {tool_name, arguments}. Some mocks expose direct
|
def build_server_payload(tool_name: str, args: dict[str, Any], server: dict[str, Any], tool: dict[str, Any]) -> dict[str, Any]:
|
||||||
tool endpoints that accept only the argument object. The gateway supports
|
"""Build the payload expected by the downstream MCP server.
|
||||||
both shapes through the server/tool config.
|
|
||||||
|
Supported protocols:
|
||||||
|
- legacy_http/framework_http: POST /mcp/tools/call with {tool_name, arguments}
|
||||||
|
- direct_http: POST to the configured endpoint with only the argument object
|
||||||
"""
|
"""
|
||||||
protocol = str(tool.get("protocol") or server.get("protocol") or "legacy_http")
|
protocol = str(tool.get("protocol") or server.get("protocol") or "legacy_http")
|
||||||
if protocol in {"legacy_http", "framework_http"}:
|
if protocol in {"legacy_http", "framework_http", "fastmcp_http"}:
|
||||||
return {"tool_name": tool_name, "arguments": args or {}}
|
return {"tool_name": tool_name, "arguments": args or {}}
|
||||||
return args or {}
|
return args or {}
|
||||||
|
|
||||||
@@ -121,36 +145,202 @@ def normalize_server_response(data: Any) -> tuple[bool, Any, str | None, dict[st
|
|||||||
return True, data, None, {}
|
return True, data, None, {}
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_name(raw: dict[str, Any]) -> str | None:
|
||||||
|
return raw.get("name") or raw.get("tool_name") or raw.get("id")
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_schema(raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
schema = raw.get("input_schema") or raw.get("inputSchema") or raw.get("schema") or {}
|
||||||
|
if isinstance(schema, dict):
|
||||||
|
return schema
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_tools_from_catalog(catalog: Any) -> list[dict[str, Any]]:
|
||||||
|
"""Normalize common MCP/FastMCP/custom catalog shapes.
|
||||||
|
|
||||||
|
Accepted examples:
|
||||||
|
- {"tools": [{"name": "x", "description": "...", "input_schema": {...}}]}
|
||||||
|
- [{"name": "x", "inputSchema": {...}}]
|
||||||
|
- {"server_id": "s", "capabilities": {"tools": [...]}}
|
||||||
|
- {"data": {"tools": [...]}}
|
||||||
|
"""
|
||||||
|
if isinstance(catalog, list):
|
||||||
|
return [x for x in catalog if isinstance(x, dict)]
|
||||||
|
if not isinstance(catalog, dict):
|
||||||
|
return []
|
||||||
|
if isinstance(catalog.get("tools"), list):
|
||||||
|
return [x for x in catalog["tools"] if isinstance(x, dict)]
|
||||||
|
data = catalog.get("data")
|
||||||
|
if isinstance(data, dict) and isinstance(data.get("tools"), list):
|
||||||
|
return [x for x in data["tools"] if isinstance(x, dict)]
|
||||||
|
caps = catalog.get("capabilities")
|
||||||
|
if isinstance(caps, dict) and isinstance(caps.get("tools"), list):
|
||||||
|
return [x for x in caps["tools"] if isinstance(x, dict)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_urls(server_id: str, server: dict[str, Any]) -> list[str]:
|
||||||
|
if server.get("manifest_url"):
|
||||||
|
return [str(server["manifest_url"])]
|
||||||
|
base = str(server.get("url", "")).rstrip("/")
|
||||||
|
if server.get("catalog_endpoint"):
|
||||||
|
return [f"{base}{server['catalog_endpoint']}"]
|
||||||
|
discovery = config.get("discovery") or {}
|
||||||
|
endpoints = server.get("discovery_endpoints") or discovery.get("default_catalog_endpoints") or [
|
||||||
|
"/.well-known/mcp-server.json",
|
||||||
|
"/manifest",
|
||||||
|
"/mcp/tools",
|
||||||
|
"/tools",
|
||||||
|
"/v1/tools",
|
||||||
|
]
|
||||||
|
return [f"{base}{endpoint}" for endpoint in endpoints]
|
||||||
|
|
||||||
|
|
||||||
|
def _endpoint_for_tool(server: dict[str, Any], raw_tool: dict[str, Any]) -> str:
|
||||||
|
if raw_tool.get("endpoint"):
|
||||||
|
return str(raw_tool["endpoint"])
|
||||||
|
protocol = str(raw_tool.get("protocol") or server.get("protocol") or "legacy_http")
|
||||||
|
if protocol in {"legacy_http", "framework_http", "fastmcp_http"}:
|
||||||
|
return str(server.get("invoke_endpoint") or "/tools/call")
|
||||||
|
name = _tool_name(raw_tool) or ""
|
||||||
|
return str(server.get("invoke_endpoint") or f"/tools/{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_discovered_tool(server_id: str, server: dict[str, Any], raw_tool: dict[str, Any]) -> tuple[str, dict[str, Any]] | None:
|
||||||
|
name = _tool_name(raw_tool)
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
discovery = config.get("discovery") or {}
|
||||||
|
defaults = discovery.get("tool_defaults") or {}
|
||||||
|
tool_cfg: dict[str, Any] = {
|
||||||
|
"version": str(raw_tool.get("version") or defaults.get("version") or "1.0.0"),
|
||||||
|
"server": server_id,
|
||||||
|
"endpoint": _endpoint_for_tool(server, raw_tool),
|
||||||
|
"protocol": raw_tool.get("protocol") or server.get("protocol") or defaults.get("protocol") or "legacy_http",
|
||||||
|
"enabled": bool(raw_tool.get("enabled", defaults.get("enabled", True))),
|
||||||
|
"idempotent": bool(raw_tool.get("idempotent", defaults.get("idempotent", True))),
|
||||||
|
"cache_ttl_seconds": int(raw_tool.get("cache_ttl_seconds", defaults.get("cache_ttl_seconds", 0)) or 0),
|
||||||
|
"timeout_seconds": int(raw_tool.get("timeout_seconds", server.get("timeout_seconds", defaults.get("timeout_seconds", 30))) or 30),
|
||||||
|
"retry": raw_tool.get("retry") or defaults.get("retry") or {"enabled": False},
|
||||||
|
"allowed_agents": raw_tool.get("allowed_agents") or defaults.get("allowed_agents") or [],
|
||||||
|
"allowed_channels": raw_tool.get("allowed_channels") or defaults.get("allowed_channels") or [],
|
||||||
|
"required_business_keys": raw_tool.get("required_business_keys") or defaults.get("required_business_keys") or [],
|
||||||
|
"description": raw_tool.get("description") or raw_tool.get("doc") or "",
|
||||||
|
"input_schema": _tool_schema(raw_tool),
|
||||||
|
"source": "discovery",
|
||||||
|
}
|
||||||
|
return name, tool_cfg
|
||||||
|
|
||||||
|
|
||||||
|
async def discover_server(server_id: str, server: dict[str, Any]) -> tuple[list[tuple[str, dict[str, Any]]], list[dict[str, Any]]]:
|
||||||
|
errors: list[dict[str, Any]] = []
|
||||||
|
if not server.get("enabled", True):
|
||||||
|
return [], []
|
||||||
|
if not server.get("discover", False):
|
||||||
|
return [], []
|
||||||
|
timeout = int((config.get("discovery") or {}).get("timeout_seconds", server.get("timeout_seconds", 10)) or 10)
|
||||||
|
for url in _catalog_urls(server_id, server):
|
||||||
|
try:
|
||||||
|
catalog = await get_json(url, timeout=timeout)
|
||||||
|
raw_tools = _extract_tools_from_catalog(catalog)
|
||||||
|
normalized = []
|
||||||
|
for raw in raw_tools:
|
||||||
|
item = normalize_discovered_tool(server_id, server, raw)
|
||||||
|
if item:
|
||||||
|
normalized.append(item)
|
||||||
|
if normalized:
|
||||||
|
return normalized, errors
|
||||||
|
errors.append({"server": server_id, "url": url, "error": "catalog returned no tools"})
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append({"server": server_id, "url": url, "error": str(exc)})
|
||||||
|
return [], errors
|
||||||
|
|
||||||
|
|
||||||
|
async def sync_discovery() -> DiscoveryResult:
|
||||||
|
discovered_tools.clear()
|
||||||
|
errors: list[dict[str, Any]] = []
|
||||||
|
tools_count = 0
|
||||||
|
scanned = 0
|
||||||
|
for server_id, server in (config.get("servers") or {}).items():
|
||||||
|
if not isinstance(server, dict) or not server.get("discover", False):
|
||||||
|
continue
|
||||||
|
scanned += 1
|
||||||
|
normalized, server_errors = await discover_server(server_id, server)
|
||||||
|
errors.extend(server_errors)
|
||||||
|
for name, tool_cfg in normalized:
|
||||||
|
discovered_tools[name] = tool_cfg
|
||||||
|
tools_count += 1
|
||||||
|
discovery_state.update({
|
||||||
|
"last_sync": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"errors": errors,
|
||||||
|
"tools": sorted(discovered_tools.keys()),
|
||||||
|
})
|
||||||
|
audit("mcp.discovery.completed", {"servers_scanned": scanned, "tools_discovered": tools_count, "errors": len(errors)})
|
||||||
|
return DiscoveryResult(ok=not errors, servers_scanned=scanned, tools_discovered=tools_count, errors=errors, tools=sorted(discovered_tools.keys()))
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_discovery() -> None:
|
||||||
|
discovery = config.get("discovery") or {}
|
||||||
|
if discovery.get("enabled", False) and discovery.get("sync_on_startup", True):
|
||||||
|
try:
|
||||||
|
await sync_discovery()
|
||||||
|
except Exception as exc:
|
||||||
|
audit("mcp.discovery.failed", {"error": str(exc)})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health():
|
async def health():
|
||||||
return {"status": "ok", "service": "mcp_gateway"}
|
return {"status": "ok", "service": "mcp_gateway", "version": "1.1.0"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/ready")
|
@app.get("/ready")
|
||||||
async def ready():
|
async def ready():
|
||||||
enabled_tools = [k for k, v in (config.get("tools") or {}).items() if v.get("enabled", True)]
|
enabled_tools = [k for k, v in all_tools().items() if v.get("enabled", True)]
|
||||||
return {"status": "ready", "tools": enabled_tools}
|
return {"status": "ready", "tools": enabled_tools, "discovery": discovery_state}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v1/tools")
|
@app.get("/v1/tools")
|
||||||
async def tools():
|
async def tools():
|
||||||
return {"tools": [{"name": name, **cfg} for name, cfg in (config.get("tools") or {}).items()]}
|
return {"tools": [{"name": name, **cfg} for name, cfg in all_tools().items()]}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v1/tools/{tool_name}")
|
@app.get("/v1/tools/{tool_name}")
|
||||||
async def tool_detail(tool_name: str):
|
async def tool_detail(tool_name: str):
|
||||||
tool = (config.get("tools") or {}).get(tool_name)
|
tool = all_tools().get(tool_name)
|
||||||
if not tool:
|
if not tool:
|
||||||
raise HTTPException(status_code=404, detail=f"Tool not found: {tool_name}")
|
raise HTTPException(status_code=404, detail=f"Tool not found: {tool_name}")
|
||||||
return {"name": tool_name, **tool}
|
return {"name": tool_name, **tool}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/v1/discovery/servers")
|
||||||
|
async def discovery_servers():
|
||||||
|
servers = []
|
||||||
|
for server_id, server in (config.get("servers") or {}).items():
|
||||||
|
servers.append({
|
||||||
|
"id": server_id,
|
||||||
|
"enabled": server.get("enabled", True),
|
||||||
|
"discover": server.get("discover", False),
|
||||||
|
"url": server.get("url"),
|
||||||
|
"manifest_url": server.get("manifest_url"),
|
||||||
|
"catalog_endpoint": server.get("catalog_endpoint"),
|
||||||
|
})
|
||||||
|
return {"servers": servers, "state": discovery_state}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v1/discovery/sync", response_model=DiscoveryResult)
|
||||||
|
async def discovery_sync(authorization: str | None = Header(default=None)):
|
||||||
|
auth_check(authorization)
|
||||||
|
return await sync_discovery()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/v1/tools/{tool_name}/invoke", response_model=ToolResult)
|
@app.post("/v1/tools/{tool_name}/invoke", response_model=ToolResult)
|
||||||
async def invoke(tool_name: str, invocation: ToolInvocation, authorization: str | None = Header(default=None)):
|
async def invoke(tool_name: str, invocation: ToolInvocation, authorization: str | None = Header(default=None)):
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
auth_check(authorization)
|
auth_check(authorization)
|
||||||
|
|
||||||
tool = (config.get("tools") or {}).get(tool_name)
|
tool = all_tools().get(tool_name)
|
||||||
if not tool or not tool.get("enabled", True):
|
if not tool or not tool.get("enabled", True):
|
||||||
raise HTTPException(status_code=404, detail=f"Tool not found or disabled: {tool_name}")
|
raise HTTPException(status_code=404, detail=f"Tool not found or disabled: {tool_name}")
|
||||||
|
|
||||||
@@ -217,7 +407,7 @@ async def invoke(tool_name: str, invocation: ToolInvocation, authorization: str
|
|||||||
error=error,
|
error=error,
|
||||||
cache={"hit": False, "key": ck, "ttl_seconds": ttl},
|
cache={"hit": False, "key": ck, "ttl_seconds": ttl},
|
||||||
latency_ms=latency_ms,
|
latency_ms=latency_ms,
|
||||||
metadata={"server": tool.get("server"), **server_metadata},
|
metadata={"server": tool.get("server"), "source": tool.get("source", "static"), **server_metadata},
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
latency_ms = int((time.perf_counter() - started) * 1000)
|
latency_ms = int((time.perf_counter() - started) * 1000)
|
||||||
@@ -228,5 +418,5 @@ async def invoke(tool_name: str, invocation: ToolInvocation, authorization: str
|
|||||||
ok=False,
|
ok=False,
|
||||||
error=str(exc),
|
error=str(exc),
|
||||||
latency_ms=latency_ms,
|
latency_ms=latency_ms,
|
||||||
metadata={"server": tool.get("server")},
|
metadata={"server": tool.get("server"), "source": tool.get("source", "static")},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,36 @@
|
|||||||
# Dedicated MCP Gateway configuration.
|
# Dedicated MCP Gateway configuration.
|
||||||
# The agent backend/framework calls this gateway; this gateway calls the final MCP servers.
|
# The agent backend/framework calls this gateway; this gateway calls the final MCP servers.
|
||||||
|
|
||||||
|
|
||||||
|
# Discovery allows the gateway to sync tool catalogs from registered MCP servers.
|
||||||
|
# Static tools below remain supported and override discovered tools with the same name.
|
||||||
|
discovery:
|
||||||
|
enabled: true
|
||||||
|
sync_on_startup: true
|
||||||
|
timeout_seconds: 10
|
||||||
|
default_catalog_endpoints:
|
||||||
|
- /.well-known/mcp-server.json
|
||||||
|
- /manifest
|
||||||
|
- /mcp/tools
|
||||||
|
- /tools/list
|
||||||
|
- /tools
|
||||||
|
- /v1/tools
|
||||||
|
tool_defaults:
|
||||||
|
version: 1.0.0
|
||||||
|
protocol: legacy_http
|
||||||
|
enabled: true
|
||||||
|
idempotent: true
|
||||||
|
cache_ttl_seconds: 300
|
||||||
|
timeout_seconds: 30
|
||||||
|
retry: {enabled: true, max_attempts: 2, backoff_ms: 250}
|
||||||
|
allowed_agents: []
|
||||||
|
allowed_channels: []
|
||||||
|
required_business_keys: []
|
||||||
|
|
||||||
servers:
|
servers:
|
||||||
telecom:
|
telecom:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
discover: false
|
||||||
protocol: legacy_http
|
protocol: legacy_http
|
||||||
transport: http
|
transport: http
|
||||||
# Local run: uvicorn mcp.servers.telecom_mcp_server.main:app --port 8100
|
# Local run: uvicorn mcp.servers.telecom_mcp_server.main:app --port 8100
|
||||||
@@ -12,6 +39,7 @@ servers:
|
|||||||
|
|
||||||
retail:
|
retail:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
discover: false
|
||||||
protocol: legacy_http
|
protocol: legacy_http
|
||||||
transport: http
|
transport: http
|
||||||
# Local run: uvicorn mcp.servers.retail_mcp_server.main:app --port 8200
|
# Local run: uvicorn mcp.servers.retail_mcp_server.main:app --port 8200
|
||||||
@@ -170,3 +198,19 @@ auth:
|
|||||||
static_tokens:
|
static_tokens:
|
||||||
runtime-local-token:
|
runtime-local-token:
|
||||||
agents: []
|
agents: []
|
||||||
|
|
||||||
|
|
||||||
|
# Example of a dynamically discovered MCP server.
|
||||||
|
# Uncomment and adjust the URL to plug a new server that exposes a catalog/manifest.
|
||||||
|
# servers:
|
||||||
|
# nf_items:
|
||||||
|
# enabled: true
|
||||||
|
# discover: true
|
||||||
|
# protocol: legacy_http
|
||||||
|
# transport: http
|
||||||
|
# url: http://localhost:8400/mcp
|
||||||
|
# # Optional. If omitted, the gateway tries the discovery.default_catalog_endpoints.
|
||||||
|
# # manifest_url: http://localhost:8400/.well-known/mcp-server.json
|
||||||
|
# # catalog_endpoint: /tools
|
||||||
|
# # invoke_endpoint: /tools/call
|
||||||
|
# timeout_seconds: 30
|
||||||
|
|||||||
156
docs/MCP_GATEWAY_DISCOVERY.md
Normal file
156
docs/MCP_GATEWAY_DISCOVERY.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# MCP Gateway — Server Discovery and Catalog Sync
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This evolution allows the MCP Gateway to discover tools from registered MCP Servers by reading a manifest or catalog endpoint.
|
||||||
|
|
||||||
|
The framework still points to a single MCP Gateway:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
```
|
||||||
|
|
||||||
|
The MCP Gateway can point to many MCP Servers:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Framework
|
||||||
|
-> MCP Gateway
|
||||||
|
-> telecom_mcp_server
|
||||||
|
-> retail_mcp_server
|
||||||
|
-> nf_items_mcp_server
|
||||||
|
-> any other MCP Server
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is automatic
|
||||||
|
|
||||||
|
After a server is registered in `apps/mcp_gateway/config/mcp_gateway.yaml` with `discover: true`, the gateway can:
|
||||||
|
|
||||||
|
- call its manifest/catalog endpoint;
|
||||||
|
- normalize the returned tool list;
|
||||||
|
- publish the tools in `GET /v1/tools`;
|
||||||
|
- execute the discovered tool through `POST /v1/tools/{tool_name}/invoke`.
|
||||||
|
|
||||||
|
## What is still explicit
|
||||||
|
|
||||||
|
The gateway does not scan the network or GitHub by itself. You still register the MCP Server endpoint in YAML.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
servers:
|
||||||
|
nf_items:
|
||||||
|
enabled: true
|
||||||
|
discover: true
|
||||||
|
protocol: legacy_http
|
||||||
|
transport: http
|
||||||
|
url: http://localhost:8400/mcp
|
||||||
|
catalog_endpoint: /tools
|
||||||
|
invoke_endpoint: /tools/call
|
||||||
|
timeout_seconds: 30
|
||||||
|
```
|
||||||
|
|
||||||
|
If `catalog_endpoint` is omitted, the gateway tries:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/.well-known/mcp-server.json
|
||||||
|
/manifest
|
||||||
|
/mcp/tools
|
||||||
|
/tools/list
|
||||||
|
/tools
|
||||||
|
/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected manifest/catalog formats
|
||||||
|
|
||||||
|
The gateway accepts common shapes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_id": "nf_items",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "buscar_notas_por_criterios",
|
||||||
|
"description": "Search invoice items by criteria.",
|
||||||
|
"input_schema": {
|
||||||
|
"cliente": "string",
|
||||||
|
"estado": "string",
|
||||||
|
"preco": "number",
|
||||||
|
"ean": "string",
|
||||||
|
"margem": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It also accepts:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"tools": [...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"data": {"tools": [...]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"capabilities": {"tools": [...]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## New endpoints
|
||||||
|
|
||||||
|
### List discovery servers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/v1/discovery/servers | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### Force catalog sync
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8300/v1/discovery/sync | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### List merged static + discovered tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/v1/tools | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Precedence rule
|
||||||
|
|
||||||
|
Static tools configured under `tools:` override discovered tools with the same name. This allows operations teams to override timeout, cache, allowed agents, required business keys, and endpoint behavior safely.
|
||||||
|
|
||||||
|
## Plugging a new MCP Server
|
||||||
|
|
||||||
|
1. Start the MCP Server.
|
||||||
|
2. Confirm that it exposes a catalog or manifest endpoint.
|
||||||
|
3. Add it under `servers:` in `mcp_gateway.yaml` with `discover: true`.
|
||||||
|
4. Restart the MCP Gateway or call `POST /v1/discovery/sync`.
|
||||||
|
5. Confirm the tool appears in `GET /v1/tools`.
|
||||||
|
6. Invoke the tool through the gateway.
|
||||||
|
|
||||||
|
## Example invocation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8300/v1/tools/buscar_notas_por_criterios/invoke \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "buscar_notas_por_criterios",
|
||||||
|
"arguments": {
|
||||||
|
"cliente": "CLIENTE-001",
|
||||||
|
"estado": "SP",
|
||||||
|
"preco": 100.0,
|
||||||
|
"ean": "7890000000000",
|
||||||
|
"margem": 0.05
|
||||||
|
},
|
||||||
|
"business_context": {
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
@@ -31,6 +31,19 @@ TOOLS = {
|
|||||||
async def health():
|
async def health():
|
||||||
return {"status": "ok", "server": "retail_mcp_server"}
|
return {"status": "ok", "server": "retail_mcp_server"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/.well-known/mcp-server.json")
|
||||||
|
async def well_known_manifest():
|
||||||
|
return {"server_id": "retail", "protocol": "legacy_http", "invoke_endpoint": "/tools/call", "tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
||||||
|
|
||||||
|
@app.get("/manifest")
|
||||||
|
async def manifest():
|
||||||
|
return await well_known_manifest()
|
||||||
|
|
||||||
|
@app.get("/mcp/tools")
|
||||||
|
async def tools_catalog():
|
||||||
|
return {"tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
||||||
|
|
||||||
@app.get("/mcp/tools/list")
|
@app.get("/mcp/tools/list")
|
||||||
async def list_tools():
|
async def list_tools():
|
||||||
return {"tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
return {"tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
||||||
|
|||||||
@@ -31,6 +31,19 @@ TOOLS = {
|
|||||||
async def health():
|
async def health():
|
||||||
return {"status": "ok", "server": "telecom_mcp_server"}
|
return {"status": "ok", "server": "telecom_mcp_server"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/.well-known/mcp-server.json")
|
||||||
|
async def well_known_manifest():
|
||||||
|
return {"server_id": "telecom", "protocol": "legacy_http", "invoke_endpoint": "/tools/call", "tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
||||||
|
|
||||||
|
@app.get("/manifest")
|
||||||
|
async def manifest():
|
||||||
|
return await well_known_manifest()
|
||||||
|
|
||||||
|
@app.get("/mcp/tools")
|
||||||
|
async def tools_catalog():
|
||||||
|
return {"tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
||||||
|
|
||||||
@app.get("/mcp/tools/list")
|
@app.get("/mcp/tools/list")
|
||||||
async def list_tools():
|
async def list_tools():
|
||||||
return {"tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
return {"tools": [{"name": name, **cfg} for name, cfg in TOOLS.items()]}
|
||||||
|
|||||||
Reference in New Issue
Block a user