Content
# SmartGIL - Converse com seu banco de dados em linguagem natural
> Workshop prático: construindo um servidor MCP integrado a um banco de dados PostgreSQL
[](https://nodejs.org)
[](https://www.typescriptlang.org)
[](https://www.postgresql.org)
[](https://podman.io)
[](https://aistudio.google.com)
[](https://modelcontextprotocol.io)
Neste workshop, você vai construir do zero um servidor **MCP (Model Context Protocol)** que permite que modelos de linguagem como Claude respondam perguntas diretamente sobre os dados de um banco PostgreSQL. Em vez de escrever SQL manualmente, você faz uma pergunta em português - e o sistema encontra as tabelas certas, gera a query e retorna a resposta.
**Exemplo real:**
> _"Quais professores têm horário livre na quarta-feira à tarde?"_
>
> → SmartGIL encontra as tabelas certas, gera o SQL correto e responde em linguagem natural.
---
## Tecnologias utilizadas
Linguagens, frameworks e ferramentas utilizadas na construção do projeto.
| Tecnologia | Papel no projeto |
| ----------------------------- | ------------------------------------------------------------------------- |
| **TypeScript** | Linguagem principal do servidor MCP |
| **Express.js** | Servidor HTTP que expõe o protocolo MCP via SSE |
| **PostgreSQL** | Banco de dados relacional onde os dados ficam armazenados |
| **ChromaDB** | Banco vetorial - armazena o schema indexado para busca semântica |
| **Google Gemini API** | Gera embeddings do schema via `gemini-embedding-2` (grátis, sem instalar) |
| **Podman** | Runtime de containers (substituto ao Docker, sem daemon root) |
| **@modelcontextprotocol/sdk** | SDK oficial MCP da Anthropic |
| **Zod** | Validação e tipagem de schemas em tempo de execução |
---
## Onde Aplicar
Este projeto pode ser aplicado em diversas situações do mundo real:
- **Análise de dados interna** - perguntas em linguagem natural sobre bancos de dados corporativos
- **Suporte e helpdesk** - deixar que o suporte consulte o banco sem conhecer SQL
- **Exploração de dados** - cientistas e analistas descobrem o que há no banco sem documentação prévia
- **Automação com IA** - agentes que buscam, analisam e relatam dados autonomamente
- **Prototipagem de produtos** - integrar LLMs a qualquer banco PostgreSQL existente em minutos
---
## Sumário
- [SmartGIL - Converse com seu banco de dados em linguagem natural](#smartgil---converse-com-seu-banco-de-dados-em-linguagem-natural)
- [Tecnologias utilizadas](#tecnologias-utilizadas)
- [Onde Aplicar](#onde-aplicar)
- [Sumário](#sumário)
- [Instalações](#instalações)
- [Podman (runtime de containers)](#podman-runtime-de-containers)
- [Google Gemini API (embeddings)](#google-gemini-api-embeddings)
- [Node.js 20+](#nodejs-20)
- [Claude Desktop (opcional, recomendado)](#claude-desktop-opcional-recomendado)
- [Recursos adicionais](#recursos-adicionais)
- [Por que IA + Banco de Dados?](#por-que-ia--banco-de-dados)
- [O que é MCP?](#-o-que-é-mcp)
- [O que é um banco de dados relacional?](#️-o-que-é-um-banco-de-dados-relacional)
- [O que são embeddings e busca vetorial?](#-o-que-são-embeddings-e-busca-vetorial)
- [O que são APIs de IA?](#-o-que-são-apis-de-ia)
- [O que vamos construir](#o-que-vamos-construir)
- [Ferramentas MCP que o servidor expõe](#ferramentas-mcp-que-o-servidor-expõe)
- [Fluxo completo de uma pergunta](#fluxo-completo-de-uma-pergunta)
- [Roadmap](#roadmap)
- [Etapa 1 - Configuração do Ambiente](#️-etapa-1---configuração-do-ambiente)
- [1.1 - Clone o projeto](#11---clone-o-projeto)
- [1.2 - Configure as variáveis de ambiente](#12---configure-as-variáveis-de-ambiente)
- [1.3 - Verifique o ambiente](#13---verifique-o-ambiente)
- [1.4 - Suba os serviços](#14---suba-os-serviços)
- [1.5 - Verifique que tudo está rodando](#15---verifique-que-tudo-está-rodando)
- [Etapa 2 - Explorando a Estrutura do Projeto](#️-etapa-2---explorando-a-estrutura-do-projeto)
- [Etapa 3 - O Banco de Dados](#-etapa-3---o-banco-de-dados)
- [3.1 - O schema acadêmico demo](#31---o-schema-acadêmico-demo)
- [3.2 - Lendo o schema via `information_schema`](#32---lendo-o-schema-via-information_schema)
- [Etapa 4 - ChromaDB e Embeddings Vetoriais](#-etapa-4---chromadb-e-embeddings-vetoriais)
- [4.1 - O cliente ChromaDB (`chroma-client.ts`)](#41---o-cliente-chromadb-chroma-clientts)
- [4.2 - Gerando embeddings com a API Gemini](#42---gerando-embeddings-com-a-api-gemini)
- [4.3 - Testando a API com um cliente HTTP](#43---testando-a-api-com-um-cliente-http)
- [4.4 - Indexando o schema no ChromaDB](#44---indexando-o-schema-no-chromadb)
- [4.5 - Buscando tabelas relevantes](#45---buscando-tabelas-relevantes)
- [Etapa 5 - O Servidor MCP](#️-etapa-5---o-servidor-mcp)
- [5.1 - Gerenciando a conexão PostgreSQL (`db-manager.ts`)](#51---gerenciando-a-conexão-postgresql-db-managerts)
- [5.2 - O servidor Express com SSE (`index.ts`)](#52---o-servidor-express-com-sse-indexts)
- [5.3 - Registrando as ferramentas (`server.ts`)](#53---registrando-as-ferramentas-serverts)
- [Etapa 6 - As Ferramentas MCP](#-etapa-6---as-ferramentas-mcp)
- [Ferramenta 1 - `conectar_banco`](#ferramenta-1---conectar_banco)
- [Ferramenta 2 - `buscar_informacoes`](#ferramenta-2---buscar_informacoes)
- [Ferramenta 3 - `executar_query`](#ferramenta-3---executar_query)
- [Ferramenta 4 - `listar_schema`](#ferramenta-4---listar_schema)
- [Etapa 7 - Containers com Podman](#-etapa-7---containers-com-podman)
- [7.1 - O Dockerfile](#71---o-dockerfile)
- [7.2 - O `compose.yml`](#72---o-composeyml)
- [7.3 - Comandos essenciais](#73---comandos-essenciais)
- [Etapa 8 - Integração e Testes](#-etapa-8---integração-e-testes)
- [8.1 - Verificação de saúde](#81---verificação-de-saúde)
- [8.2 - Configurando o Claude Desktop](#82---configurando-o-claude-desktop)
- [8.3 - Primeira sessão completa](#83---primeira-sessão-completa)
- [Alternativa: Ollama (LLMs locais)](#-alternativa-ollama-llms-locais)
- [Boas Práticas](#boas-práticas)
- [Autores](#autores)
- [Licença](#licença)
---
## Instalações
Siga com precisão as orientações de configuração do ambiente para garantir que o projeto funcione corretamente durante o workshop.
### Podman (runtime de containers)
Podman 6.0+ é obrigatório - o comando `podman compose` vem embutido a partir desta versão.
| Sistema | Comando / Link |
| ----------------- | ---------------------------------------------------------------------- |
| **Ubuntu/Debian** | `sudo apt install podman` |
| **Fedora/RHEL** | `sudo dnf install podman` |
| **macOS** | `brew install podman` → `podman machine init` → `podman machine start` |
| **Windows** | [Podman Desktop](https://podman-desktop.io) |
Verifique a instalação:
```bash
podman --version # deve ser 6.0+
podman compose version
```
### Google Gemini API (embeddings)
O SmartGIL usa a API do Google Gemini para gerar os embeddings do schema do banco. É gratuita, não exige cartão de crédito e leva menos de 2 minutos para configurar.
**Passo 1 - Acesse o Google AI Studio:**
Abra [aistudio.google.com](https://aistudio.google.com) e faça login com uma conta Google.
**Passo 2 - Crie sua API Key:**
1. Clique em **"Get API key"** no menu lateral esquerdo
2. Clique em **"Create API key"**
3. Escolha um projeto Google Cloud existente (ou crie um novo - é gratuito)
4. Copie a chave gerada (começa com `AQ.` — esse é o formato atual do Google AI Studio)
**Passo 3 - Adicione a chave ao projeto:**
```bash
# Copie o arquivo de exemplo
cp .env.example .env
# Edite o .env e substitua "sua_chave_aqui" pela sua chave real
# GEMINI_API_KEY=AIzaSy...
```
> **Free tier do Gemini:** 1.500 requisições por dia e 100 por minuto para embeddings - mais do que suficiente para este workshop e projetos reais de pequeno porte.
### Node.js 20+
Necessário apenas para o cliente CLI (`ollama-client`).
Baixe em [nodejs.org](https://nodejs.org) ou use um gerenciador de versões como `nvm`:
```bash
nvm install 20
nvm use 20
node --version # deve ser v20+
```
### Claude Desktop (opcional, recomendado)
Para uma experiência completa de chat com interface gráfica. Baixe em [claude.ai/download](https://claude.ai/download).
### Recursos adicionais
- **[DBeaver](https://dbeaver.io)** - cliente SQL para explorar o banco de dados visualmente (opcional)
- **[Reqly](https://github.com/reqly-labs/reqly)** ou **[Insomnia](https://insomnia.rest)** - para testar os endpoints HTTP do servidor e da API Gemini (opcional, mas muito útil)
---
## Por que IA + Banco de Dados?
Imagine que você é analista em uma universidade e precisa responder perguntas sobre dados acadêmicos diariamente. Para cada pergunta, você precisaria:
1. **Conhecer a estrutura** - saber que existem tabelas `turmas`, `professores`, `horarios`
2. **Escrever o SQL** - com JOINs corretos, filtros, agregações
3. **Interpretar o resultado** - transformar linhas em informação útil
Com MCP + LLM, você simplesmente pergunta em português. O sistema faz todo o resto.
### 🔌 O que é MCP?
**MCP (Model Context Protocol)** é um protocolo aberto criado pela Anthropic que padroniza como modelos de linguagem se comunicam com ferramentas e fontes de dados externas.
Antes do MCP, cada integração era proprietária e incompatível. Com MCP, existe um contrato comum:
```
┌─────────────────────────────┐
│ Cliente MCP │
│ (Claude Desktop, etc.) │
└────────────┬────────────────┘
│ protocolo padronizado (SSE/HTTP)
┌────────────▼────────────────┐
│ Servidor MCP │ ← o que vamos construir
│ (nosso mcp-server) │
└────────────┬────────────────┘
│
┌──────▼──────┐
│ Ferramentas │
│ (banco, API)│
└─────────────┘
```
O servidor MCP expõe **ferramentas** - funções com nome e parâmetros que o LLM pode chamar. Quando você pergunta algo, o LLM decide automaticamente qual ferramenta chamar e com quais parâmetros.
### 🗄️ O que é um banco de dados relacional?
Um banco de dados relacional organiza informações em **tabelas** - estruturas com linhas (registros) e colunas (atributos). As tabelas se relacionam entre si através de chaves:
```
professores
┌────┬──────────────┬─────────────────┐
│ id │ nome │ departamento_id │
├────┼──────────────┼─────────────────┤
│ 1 │ Ana Lima │ 1 │──┐
│ 2 │ Bruno Costa │ 1 │ │
└────┴──────────────┴─────────────────┘ │
│ FK (chave estrangeira)
departamentos │
┌────┬──────────────────┐ │
│ id │ nome │ │
├────┼──────────────────┤ │
│ 1 │ Computação │◄───────────────┘
│ 2 │ Matemática │
└────┴──────────────────┘
```
**Schema** é a descrição completa dessa estrutura - quais tabelas existem, quais colunas cada uma tem, quais são os tipos e como elas se relacionam. O SmartGIL lê esse schema e o torna "buscável" pelo LLM.
**SQL (Structured Query Language)** é a linguagem para consultar bancos relacionais:
```sql
-- "Quais professores são do departamento de Computação?"
SELECT p.nome
FROM professores p
JOIN departamentos d ON d.id = p.departamento_id
WHERE d.nome = 'Computação e Matemática';
```
### 🔍 O que são embeddings e busca vetorial?
Um **embedding** é uma representação numérica de um texto - um vetor com centenas de números. Textos com significado similar ficam próximos no espaço vetorial:
```
"professores disponíveis na quarta" → [0.23, -0.41, 0.87, 0.12, ...]
"docentes com horário livre quarta" → [0.21, -0.39, 0.85, 0.14, ...] ← próximo!
"número de matrículas em turmas" → [-0.12, 0.67, -0.23, 0.55, ...] ← distante
```
O **ChromaDB** é um banco de dados vetorial - armazena os embeddings de cada tabela do schema e permite encontrar quais tabelas são relevantes para uma pergunta, mesmo que as palavras não sejam idênticas.
```
Pergunta: "professores livres na quarta"
↓ gera embedding da pergunta (via API Gemini)
ChromaDB compara com embeddings das tabelas
↓ encontra as mais próximas
Retorna: tabelas "professores" e "disponibilidade_professor"
```
### 🤖 O que são APIs de IA?
Uma **API de IA** (Application Programming Interface) é um serviço na nuvem que expõe capacidades de inteligência artificial via chamadas HTTP - sem precisar instalar nada localmente, sem GPU, sem gerenciar modelos.
Você envia um texto por HTTP e recebe de volta um resultado processado por um modelo de linguagem rodando em servidores especializados.
```
Seu servidor Internet Servidores Google
┌──────────┐ HTTP POST ┌─────────────────────────────┐
│ SmartGIL │ ──────────► │ Gemini API │
│ │ │ modelo: gemini-embedding-2 │
│ │ ◄────────── │ → retorna vetor [3072 nums] │
└──────────┘ JSON resp └─────────────────────────────┘
```
**Por que usar uma API de IA em vez de rodar localmente?**
| Aspecto | API de nuvem (Gemini) | Local (Ollama) |
| -------------------- | ------------------------------------------ | ------------------------------------- |
| **Instalação** | Nenhuma - só uma chave de API | ~274 MB para baixar o modelo |
| **Hardware** | Qualquer computador | Requer RAM/GPU adequados |
| **Velocidade** | Muito rápida (Google usa TPUs) | Depende do hardware local |
| **Custo** | Grátis até 1.500 req/dia | Gratuito após instalar |
| **Privacidade** | Dados passam pela Google | Tudo local, nenhum dado externo |
| **Offline** | Requer internet | Funciona sem internet |
> **Para este workshop**, usamos a API Gemini por ser mais simples e funcionar em qualquer ambiente - não depende do hardware disponível.
>
> **O Ollama também é uma excelente opção** para quem precisa de privacidade total ou trabalhar offline. Veja a seção [Alternativa: Ollama (LLMs locais)](#-alternativa-ollama-llms-locais) ao final deste documento.
---
## O que vamos construir
```
┌────────────────────────────────────────────────────────────────┐
│ Sua máquina │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Claude Desktop │ │ ollama-client (CLI) │ │
│ │ (interface MCP) │ │ Node.js + Ollama │ │
│ └────────┬─────────┘ └──────────┬───────────────┘ │
│ │ │ MCP/SSE │
└───────────┼───────────────────────────────┼───────────────────┘
│ HTTP porta 3001 │
┌───────────▼───────────────────────────────▼───────────────────┐
│ Podman Compose │
│ │
│ ┌─────────────────────────────┐ ┌────────────────────────┐ │
│ │ mcp-server (porta 3001) │ │ ChromaDB (porta 8000) │ │
│ │ Express + SSE │◄──│ schema indexado │ │
│ │ TypeScript │ │ busca vetorial │ │
│ └──────────────┬──────────────┘ └────────────────────────┘ │
│ │ pg (node-postgres) ↑ embeddings │
└─────────────────┼─────────────────────────────────────────────┘
│ TCP/IP ┌──────────────────────┐
┌─────────▼────────────┐ │ Google Gemini API │
│ PostgreSQL │ │ gemini-embedding-2 │
│ postgres-demo:5432 │ │ (nuvem - grátis) │
│ (banco de dados) │ └──────────────────────┘
└──────────────────────┘
```
### Ferramentas MCP que o servidor expõe
| Ferramenta | O que faz |
| -------------------- | ------------------------------------------------------------------- |
| `conectar_banco` | Conecta ao PostgreSQL, lê o schema inteiro e indexa no ChromaDB |
| `buscar_informacoes` | Busca semanticamente quais tabelas são relevantes para uma pergunta |
| `executar_query` | Executa SQL no banco e retorna os resultados formatados |
| `listar_schema` | Mostra o schema completo (tabelas, colunas, FK) para validação |
### Fluxo completo de uma pergunta
```
1. Você: "Quais professores têm horário livre na quarta à tarde?"
2. LLM chama: buscar_informacoes("professores horário quarta tarde")
→ API Gemini gera embedding da consulta
→ ChromaDB retorna: disponibilidade_professor, professores
3. LLM chama: executar_query("SELECT p.nome FROM professores p
JOIN disponibilidade_professor d ON d.professor_id = p.id
WHERE d.dia_semana = 'quarta' AND d.disponivel = true
AND d.hora_inicio <= '14:00' AND d.hora_fim >= '18:00'")
4. Resultado → LLM responde em linguagem natural
```
---
## Roadmap
### ⚙️ Etapa 1 - Configuração do Ambiente
Antes de escrever qualquer código, vamos garantir que todas as ferramentas estão instaladas e funcionando corretamente.
#### 1.1 - Clone o projeto
```bash
git clone https://github.com/claudiogpt/SmartGIU.git SmartGIL
cd SmartGIL
```
#### 1.2 - Configure as variáveis de ambiente
```bash
cp .env.example .env
```
Abra o `.env` e substitua `sua_chave_aqui` pela sua API Key do Gemini:
```env
# API Key do Google Gemini - obtenha em https://aistudio.google.com
GEMINI_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# SSL para bancos remotos (false para o banco demo local)
DB_SSL=false
```
> **Segurança:** Nunca compartilhe sua API Key publicamente. O `.gitignore` já ignora o arquivo `.env` - ela não vai para o repositório.
#### 1.3 - Verifique o ambiente
```bash
bash scripts/setup-podman.sh
```
O script verifica Podman, Podman Compose e a presença da `GEMINI_API_KEY` no `.env`. Corrija qualquer item marcado com `[XX]` antes de continuar.
#### 1.4 - Suba os serviços
```bash
# Serviços principais + banco de dados demo
podman compose --profile demo up -d
```
Na primeira execução o Podman baixa as imagens e compila o servidor - aguarde ~2 minutos.
#### 1.5 - Verifique que tudo está rodando
```bash
# Health check do servidor MCP
curl http://localhost:3001/health
# Esperado: {"status":"ok","service":"smartgiu-mcp-server"}
# Health check do ChromaDB
curl http://localhost:8000/api/v1/heartbeat
# Esperado: {"nanosecond heartbeat":...}
# Status de todos os containers
podman compose ps
```
---
### 🏗️ Etapa 2 - Explorando a Estrutura do Projeto
Antes de modificar qualquer arquivo, entenda o que já existe e por quê.
```
SmartGIL/
├── compose.yml ← orquestra os 3 containers (chromadb, mcp-server, postgres-demo)
├── .env / .env.example ← variáveis de ambiente (GEMINI_API_KEY, DB_SSL)
├── scripts/
│ └── setup-podman.sh ← script de verificação do ambiente
│
├── mcp-server/ ← o coração do projeto (TypeScript)
│ ├── Dockerfile ← como construir a imagem do servidor
│ ├── package.json ← dependências: express, @mcp/sdk, pg, zod
│ ├── tsconfig.json ← TypeScript com NodeNext modules
│ └── src/
│ ├── index.ts ← servidor Express + endpoints SSE/messages/health
│ ├── server.ts ← instância McpServer + registro das 4 ferramentas
│ ├── db-manager.ts ← singleton pg.Pool - uma conexão ativa por processo
│ ├── chroma-client.ts ← cliente REST para ChromaDB + chamadas de embedding ao Gemini
│ └── tools/
│ ├── conectar-banco.ts ← ferramenta 1: conecta + indexa schema
│ ├── buscar-informacoes.ts ← ferramenta 2: busca semântica no ChromaDB
│ ├── executar-query.ts ← ferramenta 3: executa SQL no banco
│ └── listar-schema.ts ← ferramenta 4: dump completo do schema
│
├── ollama-client/ ← cliente CLI de chat (usa Ollama como LLM - opcional)
│ └── src/index.ts
│
└── demo-db/
└── init.sql ← schema acadêmico com 6 tabelas e dados de exemplo
```
**Dependências principais do `mcp-server/package.json`:**
```json
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.x", // protocolo MCP
"express": "^4.x", // servidor HTTP
"pg": "^8.x", // driver PostgreSQL
"zod": "^3.x", // validação de schemas
"cors": "^2.x" // CORS para o Claude Desktop
},
"devDependencies": {
"tsx": "^4.x", // executa TypeScript diretamente
"typescript": "^5.x"
}
}
```
> **Nota:** a API Gemini é acessada via `fetch` nativo do Node.js - sem nenhum pacote npm adicional.
> **Nota sobre extensões `.js` em imports:** O projeto usa `"module": "NodeNext"` no `tsconfig.json`. Isso exige que imports internos usem extensão `.js` mesmo sendo arquivos `.ts`:
>
> ```typescript
> import { getPool } from "./db-manager.js"; // correto ✅
> import { getPool } from "./db-manager"; // erro ❌
> ```
---
### 📊 Etapa 3 - O Banco de Dados
#### 3.1 - O schema acadêmico demo
O projeto inclui um banco PostgreSQL de exemplo que simula um sistema de gestão universitária. Veja o arquivo `demo-db/init.sql`:
```sql
-- Departamentos da universidade
CREATE TABLE departamentos (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
codigo VARCHAR(10) UNIQUE NOT NULL
);
-- Professores vinculados a departamentos
CREATE TABLE professores (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
departamento_id INTEGER REFERENCES departamentos(id),
regime VARCHAR(20) NOT NULL DEFAULT '40h' -- 20h | 40h | DE
);
-- Disciplinas ofertadas por departamento
CREATE TABLE disciplinas (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
codigo VARCHAR(20) UNIQUE NOT NULL,
carga_horaria INTEGER NOT NULL, -- horas/semana
departamento_id INTEGER REFERENCES departamentos(id)
);
-- Turmas de cada disciplina no semestre
CREATE TABLE turmas (
id SERIAL PRIMARY KEY,
disciplina_id INTEGER NOT NULL REFERENCES disciplinas(id),
professor_id INTEGER REFERENCES professores(id), -- pode ser NULL
semestre VARCHAR(10) NOT NULL, -- ex: 2025.1
codigo_turma VARCHAR(10) NOT NULL,
vagas INTEGER DEFAULT 40,
alunos_matriculados INTEGER DEFAULT 0
);
-- Horários das turmas (dia + sala + horário)
CREATE TABLE horarios (
id SERIAL PRIMARY KEY,
turma_id INTEGER NOT NULL REFERENCES turmas(id),
dia_semana VARCHAR(15) NOT NULL, -- segunda | terca | quarta | quinta | sexta
hora_inicio TIME NOT NULL,
hora_fim TIME NOT NULL,
sala VARCHAR(20)
);
-- Disponibilidade declarada dos professores
CREATE TABLE disponibilidade_professor (
id SERIAL PRIMARY KEY,
professor_id INTEGER NOT NULL REFERENCES professores(id),
dia_semana VARCHAR(15) NOT NULL,
hora_inicio TIME NOT NULL,
hora_fim TIME NOT NULL,
disponivel BOOLEAN NOT NULL DEFAULT TRUE,
observacao VARCHAR(200)
);
```
O banco já vem populado com:
- **3 departamentos** (Computação, Ciências Humanas, Engenharia)
- **10 professores** em diferentes regimes
- **8 disciplinas** com carga horária
- **15 turmas** no semestre 2025.1
- **30+ registros** de horários e disponibilidade
#### 3.2 - Lendo o schema via `information_schema`
O PostgreSQL expõe a estrutura de qualquer banco através de um schema especial chamado `information_schema`. É assim que o `conectar_banco` descobre todas as tabelas automaticamente:
```sql
-- Lista todas as tabelas do usuário no schema public
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
ORDER BY table_name;
-- Lista todas as colunas com seus tipos
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
-- Lista todas as relações de chave estrangeira (FK)
SELECT
kcu.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public';
```
Essas três queries juntas dão ao sistema tudo que ele precisa para descrever qualquer banco PostgreSQL.
---
### 🔍 Etapa 4 - ChromaDB e Embeddings Vetoriais
#### 4.1 - O cliente ChromaDB (`chroma-client.ts`)
O arquivo `mcp-server/src/chroma-client.ts` implementa toda a comunicação com o ChromaDB e com a API Gemini - usando apenas `fetch` puro, sem pacotes adicionais:
```typescript
// Configuração via variáveis de ambiente
const CHROMA_URL = process.env.CHROMADB_URL ?? "http://chromadb:8000";
const GEMINI_API_KEY = process.env.GEMINI_API_KEY ?? "";
const EMBED_MODEL = "gemini-embedding-2";
const COLLECTION_NAME = "db_schema";
const GEMINI_EMBED_URL = `https://generativelanguage.googleapis.com/v1/models/${EMBED_MODEL}:embedContent`;
```
#### 4.2 - Gerando embeddings com a API Gemini
Para transformar textos em vetores numéricos, fazemos uma chamada HTTP à API Gemini. O endpoint `embedContent` processa um texto por vez — para indexar várias tabelas de uma vez, chamamos em paralelo com `Promise.all`:
```typescript
async function getEmbedding(text: string): Promise<number[]> {
const res = await fetch(GEMINI_EMBED_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": GEMINI_API_KEY,
},
body: JSON.stringify({
model: "models/gemini-embedding-2",
content: { parts: [{ text }] },
}),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Gemini embedding error (${res.status}): ${body}`);
}
const data = await res.json();
return data.embedding.values;
// cada vetor tem 3072 dimensões
}
async function getEmbeddingsBatch(texts: string[]): Promise<number[][]> {
// Chama embedContent em paralelo para cada texto
return Promise.all(texts.map((text) => getEmbedding(text)));
}
```
**Formato da requisição `embedContent`:**
```
POST https://generativelanguage.googleapis.com/v1/models/gemini-embedding-2:embedContent
x-goog-api-key: SUA_CHAVE
Content-Type: application/json
{
"model": "models/gemini-embedding-2",
"content": { "parts": [{ "text": "Tabela: professores..." }] }
}
```
**Resposta:**
```json
{
"embedding": {
"values": [0.021, -0.043, 0.187, ...]
}
}
```
#### 4.3 - Testando a API com um cliente HTTP
Antes de integrar ao código, é muito útil testar a API Gemini diretamente com um cliente HTTP. Isso confirma que sua chave funciona e ajuda a entender o formato de resposta.
**Opção A - curl (terminal):**
```bash
# Substitua SUA_CHAVE_AQUI pela sua GEMINI_API_KEY (formato AQ....)
curl -s \
"https://generativelanguage.googleapis.com/v1/models/gemini-embedding-2:embedContent" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: SUA_CHAVE_AQUI" \
-d '{
"model": "models/gemini-embedding-2",
"content": {
"parts": [{"text": "professores disponíveis na quarta-feira"}]
}
}'
```
O resultado deve ser um JSON com um campo `embedding.values` contendo 3072 números.
**Opção B - Reqly (cliente HTTP com interface visual):**
[Reqly](https://github.com/reqly-labs/reqly) é um cliente HTTP moderno e gratuito. Configure assim:
1. **Método:** `POST`
2. **URL:** `https://generativelanguage.googleapis.com/v1/models/gemini-embedding-2:embedContent`
3. **Headers:**
- `Content-Type: application/json`
- `x-goog-api-key: SUA_CHAVE_AQUI`
4. **Body (JSON):**
```json
{
"model": "models/gemini-embedding-2",
"content": {
"parts": [{ "text": "professores disponíveis na quarta-feira" }]
}
}
```
5. Clique em **Send** e observe a resposta com `embedding.values`.
> **Dica:** Se receber erro 404, verifique o nome do modelo (`gemini-embedding-2`) e que a chave está no header `x-goog-api-key`, não na URL.
#### 4.4 - Indexando o schema no ChromaDB
Quando `conectar_banco` é chamada, cada tabela vira um documento textual que é convertido em embedding e armazenado:
```typescript
export async function indexDocuments(
docs: Array<{ id: string; text: string; metadata: Record<string, string> }>,
): Promise<void> {
const collection = await getOrCreateCollection();
// Envia todos os documentos de uma vez para o Gemini (até 100 por batch)
const embeddings = await getEmbeddingsBatch(docs.map((d) => d.text));
// Armazena documentos + embeddings no ChromaDB
await fetch(`${CHROMA_URL}/api/v1/collections/${collection.id}/upsert`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ids: docs.map((d) => d.id),
embeddings,
documents: docs.map((d) => d.text),
metadatas: docs.map((d) => d.metadata),
}),
});
}
```
O texto que representa cada tabela é gerado assim no `conectar-banco.ts`:
```
Tabela: disponibilidade_professor
Colunas:
id: integer (obrigatório)
professor_id: integer (obrigatório)
dia_semana: character varying (obrigatório)
hora_inicio: time without time zone (obrigatório)
hora_fim: time without time zone (obrigatório)
disponivel: boolean (obrigatório)
observacao: character varying
Relações (FK):
professor_id → professores.id
```
#### 4.5 - Buscando tabelas relevantes
Quando `buscar_informacoes` é chamada com uma pergunta, o mesmo processo acontece com o texto da pergunta:
```typescript
export async function search(
queryText: string,
nResults = 5,
): Promise<Array<{ text: string; metadata: Record<string, string> }>> {
const collection = await getOrCreateCollection();
// 1. Converte a pergunta em embedding via API Gemini
const [queryEmbedding] = await getEmbeddingsBatch([queryText]);
// 2. Busca os documentos mais próximos no espaço vetorial
const res = await fetch(
`${CHROMA_URL}/api/v1/collections/${collection.id}/query`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query_embeddings: [queryEmbedding],
n_results: Math.min(nResults, 10),
include: ["documents", "metadatas", "distances"],
}),
},
);
const data = await res.json();
// 3. Retorna documentos ordenados por relevância semântica
return data.documents[0]
.map((text: string | null, i: number) => ({
text: text ?? "",
metadata: data.metadatas[0][i] ?? {},
}))
.filter((r: { text: string }) => r.text !== "");
}
```
---
### ⚙️ Etapa 5 - O Servidor MCP
#### 5.1 - Gerenciando a conexão PostgreSQL (`db-manager.ts`)
O `db-manager.ts` mantém um singleton com a conexão ativa:
```typescript
import pg from "pg";
const { Pool } = pg;
let pool: pg.Pool | null = null;
let connectionInfo: ConnectionConfig | null = null;
export async function connectDb(config: ConnectionConfig): Promise<void> {
// Fecha conexão anterior se existir
if (pool) {
await pool.end().catch(() => {});
pool = null;
}
const sslConfig =
process.env.DB_SSL === "false" ? false : { rejectUnauthorized: false };
pool = new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
ssl: sslConfig,
connectionTimeoutMillis: 10_000,
idleTimeoutMillis: 30_000,
max: 5, // máximo de 5 conexões simultâneas no pool
});
// Testa a conexão imediatamente
const client = await pool.connect();
try {
await client.query("SELECT 1");
} finally {
client.release();
}
connectionInfo = config;
}
export function isConnected(): boolean {
return pool !== null;
}
export function getPool(): pg.Pool {
if (!pool)
throw new Error("Banco não conectado. Use conectar_banco primeiro.");
return pool;
}
```
> **Decisão de design:** As credenciais ficam apenas em memória durante a execução do container. Cada reinício exige nova chamada ao `conectar_banco`. Isso é intencional - evita persistir senhas em disco.
#### 5.2 - O servidor Express com SSE (`index.ts`)
O arquivo `index.ts` configura o servidor HTTP que expõe o protocolo MCP:
```typescript
import express from "express";
import cors from "cors";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { createMcpServer } from "./server.js";
const app = express();
app.use(cors());
app.use(express.json());
const { server, transports } = createMcpServer();
// Health check - confirma que o servidor está ativo
app.get("/health", (_req, res) => {
res.json({ status: "ok", service: "smartgiu-mcp-server" });
});
// Endpoint SSE - cada cliente MCP conecta aqui e mantém a stream aberta
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
res.on("close", () => {
transports.delete(transport.sessionId); // limpa quando cliente desconecta
});
await server.connect(transport);
});
// Endpoint de mensagens - o cliente envia tool calls aqui
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
return res
.status(404)
.json({ error: `Sessão não encontrada: ${sessionId}` });
}
await transport.handlePostMessage(req, res, req.body);
});
app.listen(3001, () => {
console.log("SmartGIL MCP Server rodando na porta 3001");
});
```
**Por que SSE (Server-Sent Events)?** O Claude Desktop usa SSE como transporte MCP - o cliente abre uma conexão HTTP persistente e o servidor envia eventos de volta. É simples, funciona em qualquer HTTP e não requer WebSockets.
#### 5.3 - Registrando as ferramentas (`server.ts`)
O `server.ts` cria a instância MCP e registra as quatro ferramentas com seus schemas Zod:
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// importa os handlers de cada ferramenta...
export function createMcpServer() {
const server = new McpServer({ name: "smartgil", version: "1.0.0" });
const transports = new Map<string, SSEServerTransport>();
server.tool(
"conectar_banco",
"Conecta ao PostgreSQL e indexa o schema no ChromaDB para buscas semânticas.",
{
host: z.string().describe("Hostname ou IP do banco"),
port: z.number().int().default(5432).describe("Porta TCP (padrão: 5432)"),
database: z.string().describe("Nome do banco de dados"),
usuario: z.string().describe("Usuário PostgreSQL"),
senha: z.string().describe("Senha do usuário"),
},
conectarBanco,
);
server.tool(
"buscar_informacoes",
"Busca semanticamente quais tabelas são relevantes para uma pergunta.",
{
consulta: z.string().describe("Pergunta em linguagem natural"),
n_resultados: z.number().int().min(1).max(10).default(5),
},
buscarInformacoes,
);
server.tool("executar_query", "...", { sql: z.string() }, executarQuery);
server.tool("listar_schema", "...", {}, listarSchema);
return { server, transports };
}
```
O segundo argumento do `server.tool()` é a **descrição** - é o que o LLM lê para decidir qual ferramenta chamar. Uma boa descrição é fundamental.
---
### 🔧 Etapa 6 - As Ferramentas MCP
Cada ferramenta é um arquivo independente em `src/tools/`. Todas seguem o mesmo contrato de retorno: `{ content: [{ type: 'text', text: string }] }`.
#### Ferramenta 1 - `conectar_banco`
Conecta ao banco, lê o schema via `information_schema` e indexa no ChromaDB:
```typescript
export async function conectarBanco(input: {
host: string; port: number; database: string; usuario: string; senha: string;
}) {
try {
// 1. Conecta ao PostgreSQL
await connectDb({ host: input.host, port: input.port,
database: input.database, user: input.usuario,
password: input.senha });
// 2. Lê tabelas, colunas e FKs em paralelo
const [tablesRes, columnsRes, fkRes] = await Promise.all([
query(`SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`),
query(`SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position`),
query(`SELECT kcu.table_name, kcu.column_name,
ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`),
]);
// 3. Monta documentos e indexa no ChromaDB (embeddings via API Gemini)
await resetCollection();
const docs = tablesRes.rows.map(t => ({
id: `table_${t.table_name}`,
text: formatTableDescription(t.table_name, columnsRes.rows, fkRes.rows),
metadata: { table_name: t.table_name, ... },
}));
await indexDocuments(docs);
// 4. Retorna resumo para o LLM
return { content: [{ type: 'text' as const, text: `✅ Conectado. ${docs.length} tabelas indexadas.` }] };
} catch (err) {
return { content: [{ type: 'text' as const, text: `❌ Erro: ${err}` }], isError: true };
}
}
```
#### Ferramenta 2 - `buscar_informacoes`
Faz busca vetorial no ChromaDB com o texto da pergunta:
```typescript
export async function buscarInformacoes(input: {
consulta: string;
n_resultados: number;
}) {
if (!isConnected()) {
return {
content: [
{ type: "text" as const, text: "⚠️ Use conectar_banco primeiro." },
],
isError: true,
};
}
const results = await search(input.consulta, input.n_resultados);
const formatted = results
.map(
(r, i) =>
`### Resultado ${i + 1} - ${r.metadata.table_name}\n\n${r.text}`,
)
.join("\n\n---\n\n");
return {
content: [
{ type: "text" as const, text: `🔍 Tabelas relevantes:\n\n${formatted}` },
],
};
}
```
#### Ferramenta 3 - `executar_query`
Executa o SQL gerado pelo LLM e formata o resultado como tabela Markdown:
```typescript
export async function executarQuery(input: { sql: string }) {
if (!isConnected()) {
return {
content: [
{ type: "text" as const, text: "⚠️ Use conectar_banco primeiro." },
],
isError: true,
};
}
try {
const result = await query(input.sql.trim());
if (result.rows.length === 0) {
return {
content: [
{
type: "text" as const,
text: "✅ Query executada - nenhuma linha retornada.",
},
],
};
}
// Formata resultado como tabela Markdown
const columns = Object.keys(result.rows[0]);
const header = `| ${columns.join(" | ")} |`;
const separator = `| ${columns.map(() => "---").join(" | ")} |`;
const rows = result.rows
.map(
(row) =>
`| ${columns.map((col) => String(row[col] ?? "")).join(" | ")} |`,
)
.join("\n");
return {
content: [
{
type: "text" as const,
text: `✅ ${result.rowCount} linha(s)\n\n${header}\n${separator}\n${rows}`,
},
],
};
} catch (err) {
return {
content: [{ type: "text" as const, text: `❌ Erro SQL: ${err}` }],
isError: true,
};
}
}
```
#### Ferramenta 4 - `listar_schema`
Retorna o schema completo formatado com todos os tipos e FK:
```typescript
export async function listarSchema(_input: Record<string, unknown>) {
if (!isConnected()) {
return {
content: [
{ type: "text" as const, text: "⚠️ Use conectar_banco primeiro." },
],
isError: true,
};
}
// Lê tabelas + colunas + FKs do information_schema
// Agrupa por tabela e formata como:
// ## professores
// id integer NOT NULL
// nome character varying NOT NULL
// ↳ departamento_id → departamentos(id)
const schema = /* ... agrupamento e formatação ... */ "";
return {
content: [{ type: "text" as const, text: `# Schema\n\n${schema}` }],
};
}
```
---
### 🐳 Etapa 7 - Containers com Podman
#### 7.1 - O Dockerfile
O `mcp-server/Dockerfile` define como construir a imagem do servidor:
```dockerfile
FROM node:20-alpine
# curl é necessário para o healthcheck interno
RUN apk add --no-cache curl
WORKDIR /app
# Copia lockfile primeiro - aproveita cache de camadas do Podman
COPY package*.json ./
RUN npm ci # instala exatamente o que está no package-lock.json
COPY tsconfig.json ./
COPY src ./src
EXPOSE 3001
# Verifica se o servidor está respondendo a cada 15s
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1
# Executa TypeScript diretamente (sem build prévio)
CMD ["node_modules/.bin/tsx", "src/index.ts"]
```
**Por que `npm ci` e não `npm install`?**
- `npm ci` instala _exatamente_ o que está no `package-lock.json` - build reprodutível
- Mais rápido (não resolve conflitos de versão)
- Falha se o lockfile não existir ou estiver desatualizado (sinal de problema)
> **Regra importante**: Sempre execute `npm install` localmente para gerar o `package-lock.json` _antes_ de modificar o Dockerfile. O `npm ci` requer o lockfile.
#### 7.2 - O `compose.yml`
O `compose.yml` orquestra os três serviços e define como eles se comunicam:
```yaml
services:
# Banco vetorial para o schema indexado
chromadb:
image: chromadb/chroma:0.6.3
ports:
- "8000:8000" # 8000 do host → 8000 do container
volumes:
- chroma_data:/chroma/chroma # persiste dados entre reinícios
environment:
- ANONYMIZED_TELEMETRY=False
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s # aguarda 30s antes de começar a verificar
# Servidor MCP - só sobe quando ChromaDB estiver healthy
mcp-server:
build: ./mcp-server # constrói a partir do Dockerfile
ports:
- "3001:3001"
environment:
- PORT=3001
- CHROMADB_URL=http://chromadb:8000 # DNS interno do Podman
- GEMINI_API_KEY=${GEMINI_API_KEY} # passa a chave do .env para o container
- DB_SSL=${DB_SSL:-false}
depends_on:
chromadb:
condition: service_healthy # aguarda ChromaDB estar healthy
restart: unless-stopped
# Banco demo (opcional - ativado com --profile demo)
postgres-demo:
image: postgres:16-alpine
profiles:
- demo
ports:
- "5433:5432" # 5433 no host, 5432 interno (porta padrão PG)
environment:
- POSTGRES_DB=demo_giu
- POSTGRES_USER=demo
- POSTGRES_PASSWORD=demo123
volumes:
- ./demo-db/init.sql:/docker-entrypoint-initdb.d/init.sql
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U demo -d demo_giu"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
volumes:
chroma_data: # dados do ChromaDB
postgres_data: # dados do PostgreSQL demo
```
**Conceitos-chave do Compose:**
| Conceito | O que faz |
| ---------------------------------------- | ------------------------------------------------------- |
| `ports: "5433:5432"` | Mapeia porta `5433` do host para `5432` do container |
| `volumes:` | Persiste dados entre reinícios (sem isso tudo se perde) |
| `depends_on: condition: service_healthy` | Garante ordem de inicialização |
| `profiles: [demo]` | Serviço opcional - só sobe com `--profile demo` |
| `restart: unless-stopped` | Reinicia automaticamente em caso de falha |
#### 7.3 - Comandos essenciais
```bash
# Sobe todos os serviços (em background)
podman compose up -d
# Sobe com banco demo
podman compose --profile demo up -d
# Vê status e saúde de cada container
podman compose ps
# Logs em tempo real de um serviço
podman compose logs -f mcp-server
podman compose logs -f chromadb
# Reconstrói a imagem após mudanças no código
podman compose build
podman compose up -d
# Para os containers (mantém os volumes/dados)
podman compose down
# Para e apaga todos os volumes (dados perdidos)
podman compose down -v
```
---
### 🤖 Etapa 8 - Integração e Testes
Com tudo rodando, hora de usar o sistema completo.
#### 8.1 - Verificação de saúde
```bash
# Confirme que os serviços estão prontos
curl http://localhost:3001/health
# → {"status":"ok","service":"smartgiu-mcp-server"}
curl http://localhost:8000/api/v1/heartbeat
# → {"nanosecond heartbeat":1234567890}
podman compose ps
# Todos os serviços devem estar healthy
```
#### 8.2 - Configurando o Claude Desktop
Adicione ao arquivo de configuração do Claude Desktop:
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
**Linux:** `~/.config/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"smartgil": {
"type": "sse",
"url": "http://localhost:3001/sse"
}
}
}
```
Reinicie o Claude Desktop completamente. O ícone de ferramentas (🔧) deve aparecer na interface - clique nele para ver as 4 ferramentas listadas.
#### 8.3 - Primeira sessão completa
**Passo 1 - Conecte ao banco demo:**
```
Conecte no banco de dados: host postgres-demo, porta 5432,
database demo_giu, usuário demo, senha demo123
```
> Por que `postgres-demo` e não `localhost`? O mcp-server roda _dentro_ de um container. Para ele, `localhost` é o próprio container. O banco demo é outro container, acessível pelo nome do serviço `postgres-demo`.
**Passo 2 - Explore o schema:**
- _"Quais tabelas existem e o que cada uma armazena?"_
- _"Mostre o schema completo"_
**Passo 3 - Faça perguntas sobre os dados:**
- _"Quais professores estão com horário livre na quarta-feira à tarde?"_
- _"Qual turma está mais próxima da capacidade máxima?"_
- _"Quantas turmas cada professor está ministrando em 2025.1?"_
- _"Existe alguma turma sem professor alocado?"_
---
## 🦙 Alternativa: Ollama (LLMs locais)
O [Ollama](https://ollama.com) é uma ferramenta incrível que permite rodar **modelos de linguagem completamente offline** - no seu próprio computador, sem enviar dados para a nuvem, sem custo por token e sem depender de internet.
> **Por que não usamos no workshop?** No ambiente do evento, não foi possível instalar o Ollama nas máquinas disponíveis. Para workshops presenciais e ambientes corporativos com restrições de rede, a API Gemini (apenas uma chave de texto) é mais simples de configurar.
>
> **Para uso em produção ou projetos pessoais**, o Ollama é uma excelente escolha, especialmente se privacidade de dados for um requisito.
### Vantagens do Ollama
- **Privacidade total** - nenhum dado sai do seu computador
- **Funciona offline** - sem dependência de internet
- **Sem limite de requisições** - rode quantas queries quiser
- **Múltiplos modelos** - embeddings, chat, code, etc.
### Como substituir a API Gemini pelo Ollama
**1. Instale o Ollama e os modelos:**
```bash
# Instale em: https://ollama.com
# Modelo de embeddings (~274 MB) - equivalente ao gemini-embedding-2
ollama pull nomic-embed-text
# Modelo de chat para o ollama-client (~8.9 GB)
ollama pull gemma4
# Verifique os modelos instalados
ollama list
# Confirme que o servidor está rodando
curl http://localhost:11434/api/tags
```
**2. Altere o `mcp-server/src/chroma-client.ts`:**
Substitua a função `getEmbeddingsBatch` por uma chamada ao Ollama:
```typescript
// Versão Ollama (substitui a versão Gemini)
const OLLAMA_HOST =
process.env.OLLAMA_HOST ?? "http://host.containers.internal:11434";
const EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
async function getEmbedding(text: string): Promise<number[]> {
const res = await fetch(`${OLLAMA_HOST}/api/embeddings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: EMBED_MODEL, prompt: text }),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Ollama embedding error (${res.status}): ${body}`);
}
const data = (await res.json()) as { embedding: number[] };
return data.embedding; // 768 dimensões com nomic-embed-text (diferente das 3072 do gemini-embedding-2)
}
```
**3. Atualize o `.env`:**
```env
# Substitua GEMINI_API_KEY por estas variáveis
OLLAMA_HOST=http://host.containers.internal:11434
EMBED_MODEL=nomic-embed-text
DB_SSL=false
```
**4. Atualize o `compose.yml`:**
```yaml
environment:
- PORT=3001
- CHROMADB_URL=http://chromadb:8000
- OLLAMA_HOST=${OLLAMA_HOST:-http://host.containers.internal:11434}
- EMBED_MODEL=${EMBED_MODEL:-nomic-embed-text}
- DB_SSL=${DB_SSL:-false}
```
> **Por que `host.containers.internal`?** O mcp-server roda dentro de um container Podman. Para acessar o Ollama que roda no seu computador (host), usamos esse hostname especial que o Podman resolve automaticamente - sem configuração extra.
**5. Usando o `ollama-client` (chat via terminal):**
O projeto inclui um cliente de chat completo que usa o Ollama como LLM:
```bash
cd ollama-client
npm install
npm start
```
Saída esperada:
```
🧠 SmartGIU - Converse com seu banco de dados
MCP Server: http://localhost:3001/sse
Ollama: http://localhost:11434 modelo: gemma4
✅ MCP conectado - ferramentas: conectar_banco, buscar_informacoes, executar_query, listar_schema
💬 Digite sua pergunta (ou "sair" para encerrar):
Você: conecte no banco postgres-demo porta 5432 banco demo_giu usuário demo senha demo123
SmartGIL: 🔧 conectar_banco... ✓
✅ Conectado a "demo_giu" em postgres-demo:5432 - 6 tabelas indexadas.
Você: quais professores têm horário livre na quarta-feira à tarde?
SmartGIL: 🔧 buscar_informacoes... ✓
🔧 executar_query... ✓
Professores disponíveis na quarta-feira à tarde:
• Ana Lima (Computação)
• Felipe Nunes (Engenharia) - 14h às 18h
```
Para usar um modelo diferente:
```bash
OLLAMA_MODEL=gemma3 npm start # gemma3 usa modo XML automaticamente
OLLAMA_USE_PROMPTING=true npm start # força modo XML mesmo com modelos que suportam tools
```
---
## Boas Práticas
### Segurança
- **API Key no .env**: A `GEMINI_API_KEY` fica no `.env`, que é ignorado pelo `.gitignore`. Nunca comite a chave no repositório
- **Credenciais do banco em memória**: As senhas nunca são persistidas em disco - ficam apenas no processo do `mcp-server` durante a execução. Cada reinício exige nova autenticação via `conectar_banco`
- **SSL**: Use `DB_SSL=true` para conexões a bancos remotos em produção. O SmartGIL aceita certificados auto-assinados (`rejectUnauthorized: false`) para facilitar a configuração
- **Permissões mínimas**: O usuário PostgreSQL deveria ter apenas `SELECT` em produção - nunca `DROP`, `TRUNCATE` ou `INSERT` a não ser que a ferramenta precise
### Containers e Build
- **Lockfile obrigatório**: Sempre execute `npm install` localmente para atualizar o `package-lock.json` _antes_ de modificar o `Dockerfile`. O `npm ci` falha sem lockfile atualizado
- **Rebuild após mudanças**: Após modificar o código, execute `podman compose build` e depois `podman compose up -d`
- **Healthchecks**: O endpoint correto do ChromaDB é `/api/v1/heartbeat`. Nunca use `/api/v2` - não existe na imagem estável e causa `unhealthy` indefinidamente
### Desenvolvimento
- **Extensões `.js`**: Com `"module": "NodeNext"` no TypeScript, todos os imports internos devem usar `.js` mesmo sendo arquivos `.ts`
- **Hot reload local**: Para desenvolver sem containers, use `npm run dev` no `mcp-server` - o `tsx` recompila automaticamente a cada mudança
- **Logs**: Use `podman compose logs -f mcp-server` para debugar em tempo real
### Diagnóstico rápido
```bash
# Status completo do ambiente
podman compose ps
# Saúde dos serviços
curl http://localhost:3001/health
curl http://localhost:8000/api/v1/heartbeat
# Reiniciar tudo do zero (apaga dados)
podman compose down -v
podman compose --profile demo up -d
```
---
## 🧑🏻💻 Autores
| [<img loading="lazy" src="https://avatars.githubusercontent.com/u/96635074?v=4" width=115><br><sub>Artur Bomtempo</sub>](https://github.com/ArturColen) | [<img loading="lazy" src="https://avatars.githubusercontent.com/u/122758073?v=4" width=115><br><sub>Cláudio Augusto</sub>](https://github.com/claudiogpt) |
| :---: | :---: |
---
## Licença
Este projeto está licenciado sob a [MIT License](https://opensource.org/licenses/MIT).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available