Skip to main content
Glama
README.md
# QGIS MCP Server

Plugin do QGIS Desktop que sobe um servidor MCP **dentro** da sessão do QGIS em
execução, expondo o projeto aberto do usuário a clientes MCP.

Não é um servidor standalone e não roda headless: as tools operam sobre o
projeto que está na tela, no estado em que ele se encontra. Uma camada criada
por geoprocessamento aparece no painel do usuário na hora.

## Requisitos

- QGIS Desktop 3.28 ou superior (Windows)
- `fastmcp` e `uvicorn` instalados **no Python embutido do QGIS**

O PyQGIS não é instalável via pip — ele vem acoplado ao build do QGIS. Por isso
as dependências vão para o Python do QGIS, não para o Python do sistema.

## Instalação

### 1. Dependências no Python do QGIS

Abra o **OSGeo4W Shell** (vem com o QGIS, procure no menu Iniciar) e rode:

```bat
python -m pip install fastmcp uvicorn
```

Se der erro de permissão, abra o shell como administrador. Para confirmar que
foi para o lugar certo:

```bat
python -c "import fastmcp, uvicorn; print(fastmcp.__version__)"
```

### 2. Copiar o plugin

O destino é a pasta de plugins do perfil do QGIS:

```
%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins
```

Ela só existe depois que o QGIS foi aberto pelo menos uma vez. Se você usa um
perfil diferente do padrão, troque `default` pelo nome dele — a lista está em
**Configurações → Perfis do usuário**.

No **PowerShell**, a partir da raiz do repositório:

```powershell
$dest = "$env:APPDATA\QGIS\QGIS3\profiles\default\python\plugins\qgis_mcp"
robocopy .\src\qgis_mcp $dest /MIR /XD __pycache__ /XF *.pyc
```

O `/MIR` espelha a pasta: arquivos apagados do repositório somem do destino
também. Sem isso, um módulo antigo continuaria sendo importado pelo QGIS. O
robocopy termina com código de saída 1 quando copiou algo — é sucesso, não erro.

Se preferir o Explorador de Arquivos: aperte **Win+R**, cole
`%APPDATA%\QGIS\QGIS3\profiles\default\python\plugins` e copie para lá a
pasta `src\qgis_mcp` do repositório. O nome da pasta no destino precisa
continuar sendo `qgis_mcp` — é ele que o QGIS importa.

### 3. Ativar no QGIS

**Complementos → Gerenciar e Instalar Complementos → Instalados** e marque
*QGIS MCP Server*.

### 4. Iniciar o servidor

**Complementos → MCP Server → Iniciar servidor MCP**. Uma mensagem confirma o
endereço; o padrão é `http://127.0.0.1:9876/mcp`.

O log fica em **Visualizar → Painéis → Mensagens do log**, aba *QGIS MCP*.

## Conectar o cliente

O transporte é HTTP, não stdio: o QGIS já está rodando, não há processo para o
cliente lançar. O cliente fica na mesma máquina do QGIS e aponta para a
loopback.

No PowerShell, para o Claude Code:

```powershell
claude mcp add --transport http qgis http://127.0.0.1:9876/mcp
```

Em clientes que se configuram por arquivo JSON, a entrada equivalente é:

```json
{
  "mcpServers": {
    "qgis": { "type": "http", "url": "http://127.0.0.1:9876/mcp" }
  }
}
```

Os nomes dos campos variam por cliente: o Antigravity, por exemplo, usa
`serverUrl` e não aceita `url` — veja *Conectar o Antigravity*, abaixo.

Para conectar de **outra máquina da rede local**, o servidor precisa ouvir em
todas as interfaces, não só na loopback. Defina a variável e reabra o QGIS —
`setx` só vale para processos abertos depois dela:

```powershell
setx QGIS_MCP_HOST 0.0.0.0
```

O Firewall do Windows vai pedir liberação da porta na primeira conexão. Só faça
isso em rede confiável: as tools dão acesso de leitura e escrita ao projeto, sem
autenticação. As variáveis disponíveis estão em `.env.example`.

## Conectar o Antigravity

O Antigravity roda na sua máquina, então alcança a loopback direto: não há
túnel, HTTPS nem domínio público no caminho. A URL é a mesma que o plugin
mostra ao iniciar, `http://127.0.0.1:9876/mcp`.

A ordem importa. O **QGIS precisa estar aberto e com o servidor iniciado** antes
de o Antigravity tentar conectar. Se você subir o servidor depois, não reinicie
o editor: use o botão de recarregar da própria tela de MCP.

### 1. Abrir a configuração de MCP

No painel do agente, clique em **…** no topo → **MCP Servers** → **Manage MCP
Servers**. Esse botão abre o `mcp_config.json` do perfil para edição — prefira
esse caminho a procurar o arquivo no disco, porque o local varia entre versões
do editor.

Se preferir editar à mão, o global fica em:

```
C:\Users\<seu-usuário>\.gemini\config\mcp_config.json
```

Em algumas versões o arquivo do perfil está em
`.gemini\antigravity\mcp_config.json`. Para valer só num projeto, use
`.agents\mcp_config.json` na raiz do workspace.

### 2. Declarar o servidor

```json
{
  "mcpServers": {
    "qgis": {
      "serverUrl": "http://127.0.0.1:9876/mcp"
    }
  }
}
```

O campo é **`serverUrl`**. O Antigravity não aceita `url` nem `httpUrl` para
conexões HTTP — com o nome errado o servidor não aparece na lista, e não há
mensagem dizendo por quê. Se o arquivo já tiver outros servidores, acrescente a
chave `"qgis"` dentro do `mcpServers` existente em vez de substituir o bloco.

Não é preciso `headers` nem credenciais: o plugin não autentica ninguém.

### 3. Recarregar e conferir

De volta à tela **MCP Servers**, clique no botão de recarregar. O servidor
`qgis` deve aparecer com as tools listadas. Do lado do QGIS, a aba *QGIS MCP* do
painel de log (**Visualizar → Painéis → Mensagens do log**) registra a conexão.

Teste com um pedido que só o seu projeto sabe responder — "liste as camadas do
projeto aberto no QGIS", ou "quantas feições tem a camada X". Se a resposta
trouxer o que está na sua tela, a ligação está de pé.

### Conferir o servidor antes de culpar o cliente

Se nada aparece, confirme primeiro que o plugin está ouvindo. No PowerShell:

```powershell
Test-NetConnection -ComputerName 127.0.0.1 -Port 9876
```

`TcpTestSucceeded : True` quer dizer que o servidor está no ar e o problema é de
configuração do cliente.

Abrir `http://127.0.0.1:9876/mcp` no navegador **não** é um teste válido: o
endereço responde `400 Bad Request: Missing session ID`. Isso é sinal de vida, e
não erro de instalação — o transporte MCP exige POST com sessão, coisa que o
navegador não faz.

### Sobre a exposição

Ouvindo em `127.0.0.1`, o servidor não está visível para a rede: só processos da
própria máquina o alcançam. Entre eles não há distinção — não há autenticação,
então qualquer programa local que conheça a porta tem o mesmo acesso ao projeto
que o Antigravity. É o modelo dos servidores MCP locais em geral, e é bem mais
contido que o túnel público exigido pelo Spark.

## Conectar o Gemini Spark

O Gemini do Google aceita servidores MCP de terceiros através do **Spark**. Na
web: **gemini.google.com → Configurações e ajuda → Apps conectados → Apps
personalizados para o Spark → Adicionar um app personalizado**, e informe a URL
do servidor MCP. No app móvel o caminho é o mesmo, pelo menu.

Requisitos do lado do Google, hoje: ter acesso ao Spark, usar uma **conta
pessoal** (contas de trabalho ou escola não funcionam), ter 18 anos ou mais,
estar nos EUA, com a interface em inglês e o *Keep Activity* ligado.

Há uma diferença de fundo em relação aos clientes de desktop: o Spark roda na
nuvem do Google, não na sua máquina. `http://127.0.0.1:9876/mcp` é inalcançável
para ele — a URL precisa ser pública e em HTTPS. O passo do ngrok abaixo, que
para qualquer outro cliente é opcional, aqui é o único caminho.

### Expor o servidor por HTTPS com o ngrok

O agente do ngrok roda na mesma máquina do QGIS e aponta para a loopback. Assim
o plugin continua ouvindo só em `127.0.0.1` — não é preciso mexer em
`QGIS_MCP_HOST`, e nada na sua rede local passa a enxergar o servidor: só o
túnel.

1. Instale e autentique o agente (o token sai do painel do ngrok):

   ```bat
   winget install ngrok.ngrok
   ngrok config add-authtoken SEU_TOKEN
   ```

2. Com o servidor MCP já iniciado no QGIS, abra o túnel:

   ```bat
   ngrok http 9876 --host-header=rewrite
   ```

   O `--host-header=rewrite` faz o ngrok reescrever o cabeçalho `Host` para
   `127.0.0.1:9876`. Sem isso, alguns servidores HTTP recusam a requisição por
   proteção contra DNS rebinding.

3. Copie a URL `Forwarding` que o ngrok mostra e acrescente `/mcp`:

   ```
   https://xxxx-xxx-xxx-xxx-xxx.ngrok-free.app/mcp
   ```

4. Cole essa URL no campo de app personalizado do Spark. Se o servidor não
   suportar Dynamic Client Registration — é o caso deste plugin, que não tem
   camada de autenticação —, o Spark oferece um bloco *Advanced features* para
   credenciais; deixe-o vazio.

5. Teste pedindo ao Gemini algo simples, como listar as camadas do projeto
   aberto. A aba *QGIS MCP* do painel de log mostra a chamada chegando.

No plano gratuito a URL muda a cada reinício do ngrok, e o app precisa ser
recadastrado no Spark. Um domínio reservado (plano pago) resolve isso.

### Antes de abrir o túnel

Uma URL pública do ngrok apontando para este servidor dá a **qualquer pessoa que
souber o endereço** o mesmo poder que as tools dão ao modelo: ler o projeto,
rodar geoprocessamento, sobrescrever arquivos, exportar dados. O plugin não
autentica ninguém.

Mitigações, em ordem de eficácia:

- Suba o túnel só enquanto estiver usando e encerre com `Ctrl+C` depois.
- Proteja com autenticação básica: `ngrok http 9876 --basic-auth "usuario:senha"`
  (a senha precisa de 8 caracteres ou mais) e informe as credenciais no bloco
  *Advanced features* do Spark. Se o Spark recusar as credenciais, volte à
  primeira mitigação.
- Trabalhe num projeto de teste, não no projeto de produção, enquanto avalia.

## As tools

35 tools em quatro módulos. Todas retornam `{"success": bool, ...}`; em caso de
erro, a mensagem carrega o contexto necessário para corrigir a chamada — campos
disponíveis, camadas existentes, mensagem do parser de expressões.

### Inspeção — somente leitura

| Tool | Para quê |
|---|---|
| `get_project_info` | panorama do projeto aberto |
| `list_layers` | camadas com IDs e metadados |
| `describe_layer` | CRS, extensão, provedor, geometria |
| `get_layer_fields` | esquema de atributos |
| `count_features` | contagem, com filtro opcional |
| `get_unique_values` | valores distintos de um campo |
| `get_field_statistics` | min, max, média, mediana, desvio |
| `get_features` | leitura de feições |
| `get_layer_extent` | bounding box, com reprojeção |

### Geoprocessamento

O QGIS tem mais de mil algoritmos; não há uma tool por operação. As três
primeiras expõem o registro inteiro:

| Tool | Para quê |
|---|---|
| `list_algorithms` | procura algoritmos por palavra-chave |
| `describe_algorithm` | esquema exato de parâmetros |
| `validate_algorithm_parameters` | checagem sem executar |
| `run_algorithm` | executa, em segundo plano |
| `add_layer_from_source` | carrega arquivo como camada |
| `remove_layer` | descarta camada da sessão |

### Projeto e exportação

| Tool | Para quê |
|---|---|
| `open_project` / `save_project` / `close_project` | ciclo de vida do `.qgz` |
| `set_layer_visibility` / `rename_layer` / `move_layer` | árvore de camadas |
| `export_layer` | grava vetorial em arquivo |
| `export_map_image` | renderiza o mapa em PNG |
| `list_layouts` / `export_layout` | layouts de impressão em PDF/PNG/SVG |

Operações destrutivas exigem autorização explícita (`discard_unsaved`,
`overwrite`), que começa desligada.

### Simbologia

| Tool | Para quê |
|---|---|
| `list_color_ramps` | rampas de cor disponíveis |
| `get_layer_style` | como a camada está pintada |
| `set_single_symbol` | símbolo único |
| `set_categorized_style` | uma cor por valor de atributo |
| `set_graduated_style` | mapa coroplético por faixas |
| `set_raster_style` | pseudocor em banda de raster |
| `set_layer_labels` | rótulos |
| `set_layer_opacity` | transparência |
| `load_style_file` / `save_style_file` | estilos `.qml` |

## Arquitetura

O QGIS é uma aplicação Qt de thread única. O servidor HTTP roda numa thread de
fundo para não congelar a interface, mas tocar em `QgsProject`, camadas ou
`iface` a partir dela causa segfault.

Todo acesso ao QGIS passa por `core.call_qgis(fn)`, que enfileira `fn` na thread
principal por sinal com conexão `QueuedConnection` e aguarda o resultado de
forma assíncrona. Nenhuma tool chama a API do QGIS diretamente.

Geoprocessamento vai além disso: `run_algorithm` usa `QgsProcessingAlgRunnerTask`
para rodar em segundo plano, mantendo a interface responsiva. Algoritmos com a
flag `NoThreading` — vários do GDAL — caem automaticamente no caminho síncrono.

## Desenvolvimento

Os testes rodam sem o QGIS instalado, contra um QGIS simulado em
`tests/qgis_stub/`:

```powershell
py -m venv .venv
.venv\Scripts\Activate.ps1
pip install -e ".[dev]"
pytest
```

Use um Python comum aqui, não o do QGIS: a suíte roda contra o stub e não deve
sujar o ambiente do QGIS. Para reinstalar o plugin depois de uma alteração,
repita o `robocopy` do passo 2 e recarregue com o *Plugin Reloader*.
(`scripts/deploy.sh` faz o mesmo a partir do WSL, se você desenvolve de lá.)

Isso valida a lógica pura — serialização, validação, truncamento, proteções e o
formato das mensagens de erro. **Não** valida as chamadas ao PyQGIS real:
assinaturas divergentes e diferenças entre builds Qt5 e Qt6 só aparecem no QGIS
de verdade.

## Problemas comuns

**O plugin não aparece na lista.** A pasta copiada precisa se chamar `qgis_mcp`
e conter `metadata.txt` e `__init__.py`. Confira em
**Complementos → Gerenciar → Inválidos**, que mostra o erro de carregamento.

**"Dependência ausente" ao iniciar.** O `fastmcp` foi instalado no Python errado.
Refaça o passo 1 usando o OSGeo4W Shell, não o PowerShell nem o Prompt de
Comando comum.

**"Não foi possível ouvir na porta".** Outra sessão do QGIS já está usando a
porta. Feche-a, ou rode `setx QGIS_MCP_PORT 9877` e reabra o QGIS.

**Alterei o código e nada mudou.** O Python do QGIS mantém os módulos em cache.
Rode o `robocopy` do passo 2 de novo e recarregue com o plugin *Plugin
Reloader*, ou reinicie o QGIS.

**A interface congela.** Alguma tool está bloqueando a thread principal por mais
tempo que o esperado. O timeout padrão é de 30s para leituras e 600s para
geoprocessamento; o log da aba *QGIS MCP* mostra o que estava rodando.

**O Antigravity não lista as tools do `qgis`.** Na ordem: o servidor foi
iniciado no menu do QGIS (a aba *QGIS MCP* do log mostra o endereço); o campo no
`mcp_config.json` é `serverUrl`, não `url`; a URL termina em `/mcp`; e você
clicou no botão de recarregar da tela **MCP Servers** depois de salvar. Um
servidor declarado enquanto o QGIS estava fechado continua marcado como falho
até esse recarregar.

**O Spark não conecta na URL do ngrok.** Confirme os três pontos, nessa ordem:
o servidor responde em `http://127.0.0.1:9876/mcp` na própria máquina; o painel
do ngrok em `http://127.0.0.1:4040` mostra a requisição chegando; e a URL
cadastrada termina em `/mcp`. Se a requisição chega e volta 4xx, é o cabeçalho
`Host` — verifique se o túnel subiu com `--host-header=rewrite`.

**Funcionava e parou depois de reiniciar o ngrok.** No plano gratuito o
subdomínio é sorteado a cada execução. Remova o app personalizado no Spark e
cadastre-o de novo com a URL nova.