hane-mcp-client 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hane_mcp_client-1.2.0/PKG-INFO +83 -0
- hane_mcp_client-1.2.0/README.md +58 -0
- hane_mcp_client-1.2.0/hane_mcp_client.egg-info/PKG-INFO +83 -0
- hane_mcp_client-1.2.0/hane_mcp_client.egg-info/SOURCES.txt +9 -0
- hane_mcp_client-1.2.0/hane_mcp_client.egg-info/dependency_links.txt +1 -0
- hane_mcp_client-1.2.0/hane_mcp_client.egg-info/entry_points.txt +2 -0
- hane_mcp_client-1.2.0/hane_mcp_client.egg-info/requires.txt +6 -0
- hane_mcp_client-1.2.0/hane_mcp_client.egg-info/top_level.txt +1 -0
- hane_mcp_client-1.2.0/hane_mcp_client.py +576 -0
- hane_mcp_client-1.2.0/pyproject.toml +45 -0
- hane_mcp_client-1.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hane-mcp-client
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Cliente MCP leve para o servidor HANE — extracao de entidades e analise semantica de documentos ERP/fiscal/juridico
|
|
5
|
+
Author-email: HaneIA Tecnologia <contato@haneia.com.br>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/JacionSilva/hane
|
|
8
|
+
Project-URL: Repository, https://github.com/JacionSilva/hane
|
|
9
|
+
Keywords: mcp,ner,nlp,hane,advpl,totvs,fiscal,juridico,claude
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: fastmcp>=3.2.4
|
|
21
|
+
Requires-Dist: pypdf>=4.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build; extra == "dev"
|
|
24
|
+
Requires-Dist: twine; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# hane-mcp-client
|
|
27
|
+
|
|
28
|
+
Cliente MCP leve para o servidor **HANE** — extração de entidades e análise semântica de documentos ERP, fiscal e jurídico.
|
|
29
|
+
|
|
30
|
+
Conecta o Claude Code (e qualquer LLM compatível com MCP) ao pipeline HANE sem expor o código-fonte do servidor.
|
|
31
|
+
|
|
32
|
+
## Instalação
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install hane-mcp-client
|
|
36
|
+
# ou, sem instalar permanentemente:
|
|
37
|
+
uvx hane-mcp-client
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuração no Claude Code
|
|
41
|
+
|
|
42
|
+
Adicione ao `~/.claude.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"hane": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["hane-mcp-client"],
|
|
50
|
+
"env": {
|
|
51
|
+
"HANE_MODE": "rest",
|
|
52
|
+
"HANE_API_URL": "http://localhost:8000",
|
|
53
|
+
"HANE_API_KEY": "sua_api_key"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Modos de operação
|
|
61
|
+
|
|
62
|
+
| Variável `HANE_MODE` | Descrição |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `rest` | Chama a REST API HANE (local ou remota) |
|
|
65
|
+
| `mcp` | Conecta diretamente ao servidor MCP HANE via HTTP |
|
|
66
|
+
|
|
67
|
+
## Ferramentas disponíveis
|
|
68
|
+
|
|
69
|
+
- `extract_entities` — extração de entidades por domínio (ERP, fiscal, jurídico, código)
|
|
70
|
+
- `annotate_file_local` — processa arquivo do disco sem expor conteúdo ao LLM
|
|
71
|
+
- `compare_documents` — diff semântico entre dois documentos
|
|
72
|
+
- `estimate_tokens` — estima economia de tokens antes de processar
|
|
73
|
+
- `get_status` — status do servidor HANE
|
|
74
|
+
|
|
75
|
+
## Requisitos
|
|
76
|
+
|
|
77
|
+
- Python 3.10+
|
|
78
|
+
- Servidor HANE acessível (on-premise via Docker ou SaaS em haneia.com.br)
|
|
79
|
+
- API Key HaneIA
|
|
80
|
+
|
|
81
|
+
## Documentação
|
|
82
|
+
|
|
83
|
+
[haneia.com.br](https://haneia.com.br) · [contato@haneia.com.br](mailto:contato@haneia.com.br)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# hane-mcp-client
|
|
2
|
+
|
|
3
|
+
Cliente MCP leve para o servidor **HANE** — extração de entidades e análise semântica de documentos ERP, fiscal e jurídico.
|
|
4
|
+
|
|
5
|
+
Conecta o Claude Code (e qualquer LLM compatível com MCP) ao pipeline HANE sem expor o código-fonte do servidor.
|
|
6
|
+
|
|
7
|
+
## Instalação
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install hane-mcp-client
|
|
11
|
+
# ou, sem instalar permanentemente:
|
|
12
|
+
uvx hane-mcp-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuração no Claude Code
|
|
16
|
+
|
|
17
|
+
Adicione ao `~/.claude.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"hane": {
|
|
23
|
+
"command": "uvx",
|
|
24
|
+
"args": ["hane-mcp-client"],
|
|
25
|
+
"env": {
|
|
26
|
+
"HANE_MODE": "rest",
|
|
27
|
+
"HANE_API_URL": "http://localhost:8000",
|
|
28
|
+
"HANE_API_KEY": "sua_api_key"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Modos de operação
|
|
36
|
+
|
|
37
|
+
| Variável `HANE_MODE` | Descrição |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `rest` | Chama a REST API HANE (local ou remota) |
|
|
40
|
+
| `mcp` | Conecta diretamente ao servidor MCP HANE via HTTP |
|
|
41
|
+
|
|
42
|
+
## Ferramentas disponíveis
|
|
43
|
+
|
|
44
|
+
- `extract_entities` — extração de entidades por domínio (ERP, fiscal, jurídico, código)
|
|
45
|
+
- `annotate_file_local` — processa arquivo do disco sem expor conteúdo ao LLM
|
|
46
|
+
- `compare_documents` — diff semântico entre dois documentos
|
|
47
|
+
- `estimate_tokens` — estima economia de tokens antes de processar
|
|
48
|
+
- `get_status` — status do servidor HANE
|
|
49
|
+
|
|
50
|
+
## Requisitos
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- Servidor HANE acessível (on-premise via Docker ou SaaS em haneia.com.br)
|
|
54
|
+
- API Key HaneIA
|
|
55
|
+
|
|
56
|
+
## Documentação
|
|
57
|
+
|
|
58
|
+
[haneia.com.br](https://haneia.com.br) · [contato@haneia.com.br](mailto:contato@haneia.com.br)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hane-mcp-client
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Cliente MCP leve para o servidor HANE — extracao de entidades e analise semantica de documentos ERP/fiscal/juridico
|
|
5
|
+
Author-email: HaneIA Tecnologia <contato@haneia.com.br>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/JacionSilva/hane
|
|
8
|
+
Project-URL: Repository, https://github.com/JacionSilva/hane
|
|
9
|
+
Keywords: mcp,ner,nlp,hane,advpl,totvs,fiscal,juridico,claude
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: fastmcp>=3.2.4
|
|
21
|
+
Requires-Dist: pypdf>=4.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build; extra == "dev"
|
|
24
|
+
Requires-Dist: twine; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# hane-mcp-client
|
|
27
|
+
|
|
28
|
+
Cliente MCP leve para o servidor **HANE** — extração de entidades e análise semântica de documentos ERP, fiscal e jurídico.
|
|
29
|
+
|
|
30
|
+
Conecta o Claude Code (e qualquer LLM compatível com MCP) ao pipeline HANE sem expor o código-fonte do servidor.
|
|
31
|
+
|
|
32
|
+
## Instalação
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install hane-mcp-client
|
|
36
|
+
# ou, sem instalar permanentemente:
|
|
37
|
+
uvx hane-mcp-client
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuração no Claude Code
|
|
41
|
+
|
|
42
|
+
Adicione ao `~/.claude.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"hane": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["hane-mcp-client"],
|
|
50
|
+
"env": {
|
|
51
|
+
"HANE_MODE": "rest",
|
|
52
|
+
"HANE_API_URL": "http://localhost:8000",
|
|
53
|
+
"HANE_API_KEY": "sua_api_key"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Modos de operação
|
|
61
|
+
|
|
62
|
+
| Variável `HANE_MODE` | Descrição |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `rest` | Chama a REST API HANE (local ou remota) |
|
|
65
|
+
| `mcp` | Conecta diretamente ao servidor MCP HANE via HTTP |
|
|
66
|
+
|
|
67
|
+
## Ferramentas disponíveis
|
|
68
|
+
|
|
69
|
+
- `extract_entities` — extração de entidades por domínio (ERP, fiscal, jurídico, código)
|
|
70
|
+
- `annotate_file_local` — processa arquivo do disco sem expor conteúdo ao LLM
|
|
71
|
+
- `compare_documents` — diff semântico entre dois documentos
|
|
72
|
+
- `estimate_tokens` — estima economia de tokens antes de processar
|
|
73
|
+
- `get_status` — status do servidor HANE
|
|
74
|
+
|
|
75
|
+
## Requisitos
|
|
76
|
+
|
|
77
|
+
- Python 3.10+
|
|
78
|
+
- Servidor HANE acessível (on-premise via Docker ou SaaS em haneia.com.br)
|
|
79
|
+
- API Key HaneIA
|
|
80
|
+
|
|
81
|
+
## Documentação
|
|
82
|
+
|
|
83
|
+
[haneia.com.br](https://haneia.com.br) · [contato@haneia.com.br](mailto:contato@haneia.com.br)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
hane_mcp_client.py
|
|
3
|
+
pyproject.toml
|
|
4
|
+
hane_mcp_client.egg-info/PKG-INFO
|
|
5
|
+
hane_mcp_client.egg-info/SOURCES.txt
|
|
6
|
+
hane_mcp_client.egg-info/dependency_links.txt
|
|
7
|
+
hane_mcp_client.egg-info/entry_points.txt
|
|
8
|
+
hane_mcp_client.egg-info/requires.txt
|
|
9
|
+
hane_mcp_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hane_mcp_client
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HANE MCP Client (v1.2 — LGPD anonimização)
|
|
3
|
+
|
|
4
|
+
MCP Server leve para instalação no cliente.
|
|
5
|
+
Conecta o Claude Code ao servidor HANE para extração de entidades e análise semântica
|
|
6
|
+
de documentos ERP, fiscal, jurídico e código ADVPL.
|
|
7
|
+
|
|
8
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
9
|
+
MODOS DE OPERAÇÃO (HANE_MODE)
|
|
10
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
11
|
+
|
|
12
|
+
HANE_MODE=rest (padrão)
|
|
13
|
+
Chama a REST API local (Docker) via HTTP.
|
|
14
|
+
O texto nunca sai da máquina do cliente — LGPD não se aplica ao trânsito.
|
|
15
|
+
Requer: Docker com hane-api:latest rodando (porta 8000).
|
|
16
|
+
|
|
17
|
+
HANE_MODE=mcp
|
|
18
|
+
Chama o servidor HANE remoto via protocolo MCP over HTTP (ex: ngrok).
|
|
19
|
+
O texto transita por infraestrutura externa — LGPD se aplica.
|
|
20
|
+
Requer: hane_mcp.py rodando e exposto (ex: ngrok porta 8081).
|
|
21
|
+
Recomendado: ativar HANE_ANONYMIZE=true para dados pessoais.
|
|
22
|
+
|
|
23
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
24
|
+
VARIÁVEIS DE AMBIENTE
|
|
25
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
|
|
27
|
+
HANE_MODE : "rest" ou "mcp" (padrão: rest)
|
|
28
|
+
HANE_API_URL : URL base da REST API (padrão: http://localhost:8000)
|
|
29
|
+
HANE_MCP_URL : URL do servidor MCP remoto (padrão: http://localhost:8081/mcp)
|
|
30
|
+
HANE_API_KEY : chave REST (opcional)
|
|
31
|
+
HANE_MCP_TOKEN : Bearer token MCP (opcional)
|
|
32
|
+
HANE_ANONYMIZE : "true" para anonimizar CPF e (padrão: false)
|
|
33
|
+
e-mails pessoais antes do envio
|
|
34
|
+
Só tem efeito em HANE_MODE=mcp.
|
|
35
|
+
|
|
36
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
37
|
+
LGPD — ANONIMIZAÇÃO (Lei 13.709/2018)
|
|
38
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
39
|
+
|
|
40
|
+
A LGPD protege dados de pessoa natural (Art. 5º, I).
|
|
41
|
+
Pessoa jurídica (CNPJ, razão social) está fora do escopo.
|
|
42
|
+
|
|
43
|
+
O que é anonimizado quando HANE_ANONYMIZE=true:
|
|
44
|
+
• CPF — regex \\d{3}\\.?\\d{3}\\.?\\d{3}-?\\d{2}
|
|
45
|
+
→ substituído por [CPF]
|
|
46
|
+
• E-mail pessoal — domínios: gmail, hotmail, outlook, yahoo, icloud, live
|
|
47
|
+
→ substituído por [EMAIL]
|
|
48
|
+
|
|
49
|
+
O que NÃO é anonimizado (não é dado pessoal):
|
|
50
|
+
• CNPJ — identifica pessoa jurídica, não natural
|
|
51
|
+
• Razão social / nome de empresa
|
|
52
|
+
• Valores fiscais (ICMS, PIS, COFINS, CFOP, CST, NCM)
|
|
53
|
+
• Endereço de empresa
|
|
54
|
+
• Código-fonte ADVPL (dado técnico)
|
|
55
|
+
|
|
56
|
+
Quando usar HANE_ANONYMIZE:
|
|
57
|
+
✅ HANE_MODE=mcp — texto transita por servidor remoto (ngrok, VPS)
|
|
58
|
+
❌ HANE_MODE=rest — texto fica na máquina local (Docker), sem necessidade
|
|
59
|
+
|
|
60
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
61
|
+
CONFIGURAÇÃO NO CLAUDE CODE (~/.claude.json)
|
|
62
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
63
|
+
|
|
64
|
+
Opção 1 — uvx (PyPI, sem copiar arquivo):
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"hane": {
|
|
68
|
+
"command": "uvx",
|
|
69
|
+
"args": ["hane-mcp-client"],
|
|
70
|
+
"env": {
|
|
71
|
+
"HANE_API_URL": "http://localhost:8000",
|
|
72
|
+
"HANE_API_KEY": "SUA_API_KEY"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Opção 2 — python local (arquivo copiado):
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"hane": {
|
|
82
|
+
"command": "python",
|
|
83
|
+
"args": ["caminho/para/hane_mcp_client.py"],
|
|
84
|
+
"env": {
|
|
85
|
+
"HANE_MODE": "mcp",
|
|
86
|
+
"HANE_MCP_URL": "https://<seu-ngrok>.ngrok-free.dev/mcp",
|
|
87
|
+
"HANE_MCP_TOKEN": "seu_token_aqui",
|
|
88
|
+
"HANE_ANONYMIZE": "true"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
from __future__ import annotations
|
|
96
|
+
|
|
97
|
+
import asyncio
|
|
98
|
+
import json
|
|
99
|
+
import os
|
|
100
|
+
import re
|
|
101
|
+
import urllib.error
|
|
102
|
+
import urllib.request
|
|
103
|
+
from typing import Any
|
|
104
|
+
|
|
105
|
+
from fastmcp import FastMCP, Client
|
|
106
|
+
from fastmcp.client.transports import StreamableHttpTransport
|
|
107
|
+
|
|
108
|
+
HANE_MODE = os.environ.get("HANE_MODE", "rest").lower()
|
|
109
|
+
HANE_URL = os.environ.get("HANE_API_URL", "https://haneia.com.br").rstrip("/")
|
|
110
|
+
HANE_KEY = os.environ.get("HANE_API_KEY", "")
|
|
111
|
+
HANE_MCP_URL = os.environ.get("HANE_MCP_URL", "http://localhost:8081/mcp")
|
|
112
|
+
HANE_MCP_TOKEN = os.environ.get("HANE_MCP_TOKEN", "")
|
|
113
|
+
HANE_ANONYMIZE = os.environ.get("HANE_ANONYMIZE", "false").lower() == "true"
|
|
114
|
+
|
|
115
|
+
# Anonimização só faz sentido em modo remoto (texto transita por servidor externo)
|
|
116
|
+
_ANONYMIZE_ACTIVE = HANE_ANONYMIZE and HANE_MODE == "mcp"
|
|
117
|
+
|
|
118
|
+
# ── Padrões LGPD — apenas dados de pessoa natural (Art. 5º, I — Lei 13.709/2018) ──
|
|
119
|
+
# CNPJ e razão social identificam pessoa jurídica — fora do escopo LGPD — não mascarados.
|
|
120
|
+
_RE_CPF = re.compile(r"\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b")
|
|
121
|
+
_RE_EMAIL_PESSOAL = re.compile(
|
|
122
|
+
r"\b[A-Za-z0-9._%+\-]+@(?:gmail|hotmail|outlook|yahoo|icloud|live)\.[a-z]{2,}\b",
|
|
123
|
+
re.IGNORECASE,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _anonymize(text: str) -> tuple[str, dict[str, int]]:
|
|
128
|
+
"""
|
|
129
|
+
Mascara CPF e e-mails pessoais antes do envio ao servidor remoto.
|
|
130
|
+
|
|
131
|
+
Escopo LGPD (Art. 5º, I — Lei 13.709/2018):
|
|
132
|
+
Protege apenas dados de pessoa natural identificada ou identificável.
|
|
133
|
+
Pessoa jurídica (CNPJ, razão social) está fora do escopo — não é mascarada.
|
|
134
|
+
|
|
135
|
+
Retorna o texto anonimizado e um dict com contagem de substituições por tipo.
|
|
136
|
+
"""
|
|
137
|
+
counters: dict[str, int] = {"cpf": 0, "email": 0}
|
|
138
|
+
|
|
139
|
+
def _replace_cpf(m: re.Match) -> str:
|
|
140
|
+
counters["cpf"] += 1
|
|
141
|
+
return "[CPF]"
|
|
142
|
+
|
|
143
|
+
def _replace_email(m: re.Match) -> str:
|
|
144
|
+
counters["email"] += 1
|
|
145
|
+
return "[EMAIL]"
|
|
146
|
+
|
|
147
|
+
text = _RE_CPF.sub(_replace_cpf, text)
|
|
148
|
+
text = _RE_EMAIL_PESSOAL.sub(_replace_email, text)
|
|
149
|
+
return text, counters
|
|
150
|
+
|
|
151
|
+
mcp = FastMCP(
|
|
152
|
+
name="hane",
|
|
153
|
+
instructions=(
|
|
154
|
+
"Servidor HANE — extração de entidades e análise semântica de documentos.\n"
|
|
155
|
+
"Use extract_entities para extrair entidades de contratos, documentos fiscais ou qualquer texto.\n"
|
|
156
|
+
"Use compare_documents para identificar diferenças semânticas entre duas versões de um documento.\n"
|
|
157
|
+
"Use estimate_tokens para estimar a economia de tokens antes de processar.\n"
|
|
158
|
+
"Use get_status para verificar se a API HANE está online."
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ----------------------------------------------
|
|
164
|
+
# Transporte REST (modo padrão — Docker local)
|
|
165
|
+
# ----------------------------------------------
|
|
166
|
+
|
|
167
|
+
def _rest_post(path: str, body: dict) -> dict:
|
|
168
|
+
url = f"{HANE_URL}{path}"
|
|
169
|
+
data = json.dumps(body).encode("utf-8")
|
|
170
|
+
headers = {"Content-Type": "application/json"}
|
|
171
|
+
if HANE_KEY:
|
|
172
|
+
headers["X-API-Key"] = HANE_KEY
|
|
173
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
174
|
+
try:
|
|
175
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
176
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
177
|
+
except urllib.error.HTTPError as e:
|
|
178
|
+
return {"error": f"HTTP {e.code}", "detail": e.read().decode("utf-8", errors="replace")}
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
return {"error": str(exc)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _rest_post_file(path: str, domain: str = "auto", threshold: float = 0.45) -> dict:
|
|
184
|
+
"""Envia arquivo ao endpoint /annotate/file via multipart/form-data.
|
|
185
|
+
Usado como fallback OCR: o servidor aplica pytesseract para PDFs escaneados."""
|
|
186
|
+
import pathlib
|
|
187
|
+
import mimetypes
|
|
188
|
+
|
|
189
|
+
boundary = "HANEBoundary20260418"
|
|
190
|
+
file_path = pathlib.Path(path)
|
|
191
|
+
content = file_path.read_bytes()
|
|
192
|
+
mime = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
|
|
193
|
+
|
|
194
|
+
parts: list[bytes] = []
|
|
195
|
+
parts.append(
|
|
196
|
+
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{file_path.name}"\r\n'
|
|
197
|
+
f"Content-Type: {mime}\r\n\r\n".encode()
|
|
198
|
+
)
|
|
199
|
+
parts.append(content)
|
|
200
|
+
parts.append(
|
|
201
|
+
f"\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"threshold\"\r\n\r\n{threshold}\r\n".encode()
|
|
202
|
+
)
|
|
203
|
+
if domain != "auto":
|
|
204
|
+
parts.append(
|
|
205
|
+
f'--{boundary}\r\nContent-Disposition: form-data; name="dominio"\r\n\r\n{domain}\r\n'.encode()
|
|
206
|
+
)
|
|
207
|
+
parts.append(f"--{boundary}--\r\n".encode())
|
|
208
|
+
|
|
209
|
+
data = b"".join(parts)
|
|
210
|
+
url = f"{HANE_URL}/annotate/file"
|
|
211
|
+
headers = {"Content-Type": f"multipart/form-data; boundary={boundary}"}
|
|
212
|
+
if HANE_KEY:
|
|
213
|
+
headers["X-API-Key"] = HANE_KEY
|
|
214
|
+
|
|
215
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
216
|
+
try:
|
|
217
|
+
with urllib.request.urlopen(req, timeout=180) as resp:
|
|
218
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
219
|
+
except urllib.error.HTTPError as e:
|
|
220
|
+
return {"error": f"HTTP {e.code}", "detail": e.read().decode("utf-8", errors="replace")}
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
return {"error": str(exc)}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _rest_get(path: str) -> dict:
|
|
226
|
+
url = f"{HANE_URL}{path}"
|
|
227
|
+
headers = {"X-API-Key": HANE_KEY} if HANE_KEY else {}
|
|
228
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
229
|
+
try:
|
|
230
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
231
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
232
|
+
except urllib.error.HTTPError as e:
|
|
233
|
+
return {"error": f"HTTP {e.code}", "detail": e.read().decode("utf-8", errors="replace")}
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
return {"error": str(exc)}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ----------------------------------------------
|
|
239
|
+
# Transporte MCP over HTTP (ngrok / remoto)
|
|
240
|
+
# ----------------------------------------------
|
|
241
|
+
|
|
242
|
+
def _mcp_call(tool: str, arguments: dict) -> dict:
|
|
243
|
+
"""Chama uma tool no servidor HANE MCP remoto via StreamableHttpTransport."""
|
|
244
|
+
headers = {"ngrok-skip-browser-warning": "true"}
|
|
245
|
+
if HANE_MCP_TOKEN:
|
|
246
|
+
headers["Authorization"] = f"Bearer {HANE_MCP_TOKEN}"
|
|
247
|
+
|
|
248
|
+
async def _run():
|
|
249
|
+
transport = StreamableHttpTransport(url=HANE_MCP_URL, headers=headers)
|
|
250
|
+
async with Client(transport) as client:
|
|
251
|
+
result = await client.call_tool(tool, arguments)
|
|
252
|
+
return result.data or {}
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
return asyncio.run(_run())
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
return {"error": str(exc)}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ----------------------------------------------
|
|
261
|
+
# Dispatcher — escolhe REST ou MCP
|
|
262
|
+
# ----------------------------------------------
|
|
263
|
+
|
|
264
|
+
def _annotate(text: str, threshold: float = 0.45, domain: str = "auto") -> dict:
|
|
265
|
+
anonymized_info: dict[str, Any] = {}
|
|
266
|
+
if _ANONYMIZE_ACTIVE:
|
|
267
|
+
text, counters = _anonymize(text)
|
|
268
|
+
if any(counters.values()):
|
|
269
|
+
anonymized_info = {"lgpd_anonimizacao": counters}
|
|
270
|
+
|
|
271
|
+
if HANE_MODE == "mcp":
|
|
272
|
+
args: dict[str, Any] = {"text": text, "domain": domain}
|
|
273
|
+
result = _mcp_call("extract_entities", args)
|
|
274
|
+
else:
|
|
275
|
+
payload: dict[str, Any] = {"text": text, "threshold": threshold}
|
|
276
|
+
if domain != "auto":
|
|
277
|
+
payload["dominio"] = domain
|
|
278
|
+
result = _rest_post("/annotate", payload)
|
|
279
|
+
|
|
280
|
+
if anonymized_info:
|
|
281
|
+
result.update(anonymized_info)
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _health() -> dict:
|
|
286
|
+
if HANE_MODE == "mcp":
|
|
287
|
+
return _mcp_call("get_status", {})
|
|
288
|
+
return _rest_get("/health")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ----------------------------------------------
|
|
292
|
+
# Ferramentas MCP
|
|
293
|
+
# ----------------------------------------------
|
|
294
|
+
|
|
295
|
+
@mcp.tool()
|
|
296
|
+
def extract_entities(
|
|
297
|
+
text: str,
|
|
298
|
+
threshold: float = 0.45,
|
|
299
|
+
domain: str = "auto",
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""
|
|
302
|
+
Extrai entidades nomeadas de um texto usando o modelo HANE.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
text: Texto a analisar (contrato, documento fiscal, texto livre, código...).
|
|
306
|
+
threshold: Confiança mínima para incluir entidade (0.0–1.0). Padrão: 0.45.
|
|
307
|
+
domain: Domínio de extração. Use "auto" para detecção automática.
|
|
308
|
+
Outros valores possíveis: "juridico", "fiscal", "rh", "advpl".
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Dicionário com:
|
|
312
|
+
- entities: lista de entidades com text, label, score, start, end
|
|
313
|
+
- entity_count: total de entidades encontradas
|
|
314
|
+
- entities_by_label: entidades agrupadas por categoria
|
|
315
|
+
- tokens_original: tokens antes da compressão HANE
|
|
316
|
+
- tokens_processed: tokens após compressão HANE
|
|
317
|
+
- token_savings_pct: percentual de economia de tokens
|
|
318
|
+
- latency_ms: tempo de processamento em milissegundos
|
|
319
|
+
- lgpd_anonimizacao: contagem de CPFs/e-mails mascarados (apenas se HANE_ANONYMIZE=true)
|
|
320
|
+
"""
|
|
321
|
+
result = _annotate(text, threshold, domain)
|
|
322
|
+
|
|
323
|
+
by_label: dict[str, list[str]] = {}
|
|
324
|
+
for ent in result.get("entities", []):
|
|
325
|
+
lbl = ent.get("label", "?")
|
|
326
|
+
txt = ent.get("text", "")
|
|
327
|
+
if lbl not in by_label:
|
|
328
|
+
by_label[lbl] = []
|
|
329
|
+
if txt not in by_label[lbl]:
|
|
330
|
+
by_label[lbl].append(txt)
|
|
331
|
+
|
|
332
|
+
result["entities_by_label"] = by_label
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@mcp.tool()
|
|
337
|
+
def compare_documents(
|
|
338
|
+
text_a: str,
|
|
339
|
+
text_b: str,
|
|
340
|
+
threshold: float = 0.45,
|
|
341
|
+
) -> dict[str, Any]:
|
|
342
|
+
"""
|
|
343
|
+
Compara dois documentos semanticamente e identifica o que mudou.
|
|
344
|
+
|
|
345
|
+
Útil para comparar versões de contratos, cláusulas, regulamentos ou documentos fiscais.
|
|
346
|
+
A comparação é feita por entidades extraídas — não por diferença de texto bruto.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
text_a: Documento original / versão de referência.
|
|
350
|
+
text_b: Documento novo / versão atualizada.
|
|
351
|
+
threshold: Confiança mínima para considerar uma entidade (padrão: 0.45).
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Dicionário com:
|
|
355
|
+
- resumo: {removidas, novas, mantidas, total_a, total_b}
|
|
356
|
+
- so_em_a: entidades presentes apenas no documento A (removidas ou substituídas)
|
|
357
|
+
- so_em_b: entidades presentes apenas no documento B (novas ou adicionadas)
|
|
358
|
+
- em_ambos: entidades em ambos, com score_a, score_b e delta de confiança
|
|
359
|
+
- metricas: economia de tokens e latência total
|
|
360
|
+
"""
|
|
361
|
+
res_a = _annotate(text_a, threshold)
|
|
362
|
+
if "error" in res_a:
|
|
363
|
+
return {"error": f"Falha ao processar documento A: {res_a['error']}"}
|
|
364
|
+
|
|
365
|
+
res_b = _annotate(text_b, threshold)
|
|
366
|
+
if "error" in res_b:
|
|
367
|
+
return {"error": f"Falha ao processar documento B: {res_b['error']}"}
|
|
368
|
+
|
|
369
|
+
def _score(e: dict) -> float:
|
|
370
|
+
return float(e.get("score") or e.get("confidence") or 0.0)
|
|
371
|
+
|
|
372
|
+
def _index(result: dict) -> dict:
|
|
373
|
+
idx: dict[str, dict] = {}
|
|
374
|
+
for e in result.get("entities", []):
|
|
375
|
+
key = e["text"].lower() + "||" + e["label"]
|
|
376
|
+
if key not in idx or _score(e) > _score(idx[key]):
|
|
377
|
+
idx[key] = e
|
|
378
|
+
return idx
|
|
379
|
+
|
|
380
|
+
idx_a = _index(res_a)
|
|
381
|
+
idx_b = _index(res_b)
|
|
382
|
+
keys_a = set(idx_a)
|
|
383
|
+
keys_b = set(idx_b)
|
|
384
|
+
|
|
385
|
+
so_em_a = [
|
|
386
|
+
{"text": idx_a[k]["text"], "label": idx_a[k]["label"], "score_a": _score(idx_a[k])}
|
|
387
|
+
for k in keys_a - keys_b
|
|
388
|
+
]
|
|
389
|
+
so_em_b = [
|
|
390
|
+
{"text": idx_b[k]["text"], "label": idx_b[k]["label"], "score_b": _score(idx_b[k])}
|
|
391
|
+
for k in keys_b - keys_a
|
|
392
|
+
]
|
|
393
|
+
em_ambos = [
|
|
394
|
+
{
|
|
395
|
+
"text": idx_a[k]["text"],
|
|
396
|
+
"label": idx_a[k]["label"],
|
|
397
|
+
"score_a": round(_score(idx_a[k]), 3),
|
|
398
|
+
"score_b": round(_score(idx_b[k]), 3),
|
|
399
|
+
"delta": round(_score(idx_b[k]) - _score(idx_a[k]), 3),
|
|
400
|
+
}
|
|
401
|
+
for k in keys_a & keys_b
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
"ok": True,
|
|
406
|
+
"resumo": {
|
|
407
|
+
"removidas": len(so_em_a),
|
|
408
|
+
"novas": len(so_em_b),
|
|
409
|
+
"mantidas": len(em_ambos),
|
|
410
|
+
"total_a": len(idx_a),
|
|
411
|
+
"total_b": len(idx_b),
|
|
412
|
+
},
|
|
413
|
+
"so_em_a": so_em_a,
|
|
414
|
+
"so_em_b": so_em_b,
|
|
415
|
+
"em_ambos": em_ambos,
|
|
416
|
+
"metricas": {
|
|
417
|
+
"economia_pct_a": res_a.get("token_savings_pct"),
|
|
418
|
+
"economia_pct_b": res_b.get("token_savings_pct"),
|
|
419
|
+
"latencia_ms_total": (res_a.get("latency_ms") or 0) + (res_b.get("latency_ms") or 0),
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@mcp.tool()
|
|
425
|
+
def estimate_tokens(text: str) -> dict[str, Any]:
|
|
426
|
+
"""
|
|
427
|
+
Estima a economia de tokens que o HANE proporcionaria ao processar este texto.
|
|
428
|
+
Operação leve — não carrega o modelo, não consome GPU.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
text: Texto para estimativa.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Dicionário com:
|
|
435
|
+
- tokens_estimados: estimativa de tokens do texto original
|
|
436
|
+
- economia_estimada: percentual estimado de redução
|
|
437
|
+
- tokens_apos_hane: estimativa de tokens após processamento
|
|
438
|
+
- recomendacao: orientação sobre quando vale processar com HANE
|
|
439
|
+
"""
|
|
440
|
+
tokens_est = max(1, len(text) // 4)
|
|
441
|
+
|
|
442
|
+
if tokens_est < 100:
|
|
443
|
+
economia = 50
|
|
444
|
+
recomendacao = "Texto curto — economia moderada. Use para textos maiores."
|
|
445
|
+
elif tokens_est < 500:
|
|
446
|
+
economia = 70
|
|
447
|
+
recomendacao = "Texto médio — boa economia esperada."
|
|
448
|
+
else:
|
|
449
|
+
economia = 78
|
|
450
|
+
recomendacao = "Texto longo — alta economia. HANE é muito eficiente aqui."
|
|
451
|
+
|
|
452
|
+
tokens_apos = int(tokens_est * (1 - economia / 100))
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"tokens_estimados": tokens_est,
|
|
456
|
+
"economia_estimada": economia,
|
|
457
|
+
"tokens_apos_hane": tokens_apos,
|
|
458
|
+
"recomendacao": recomendacao,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@mcp.tool()
|
|
463
|
+
def get_status() -> dict[str, Any]:
|
|
464
|
+
"""
|
|
465
|
+
Verifica o estado da API HANE (saúde, versão, modelo carregado) e
|
|
466
|
+
exibe a configuração ativa de privacidade (LGPD).
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Dicionário com status, versão, modo de operação e configuração LGPD.
|
|
470
|
+
"""
|
|
471
|
+
result = _health()
|
|
472
|
+
result["api_url"] = HANE_MCP_URL if HANE_MODE == "mcp" else HANE_URL
|
|
473
|
+
result["mode"] = HANE_MODE
|
|
474
|
+
result["lgpd"] = {
|
|
475
|
+
"anonimizacao_ativa": _ANONYMIZE_ACTIVE,
|
|
476
|
+
"escopo": "CPF e e-mails pessoais (gmail/hotmail/outlook/yahoo/icloud/live)" if _ANONYMIZE_ACTIVE else "desativada",
|
|
477
|
+
"nao_mascarado": "CNPJ, razão social, valores fiscais (pessoa jurídica — fora do escopo LGPD Art. 5º I)",
|
|
478
|
+
"motivo_inativo": (
|
|
479
|
+
None if _ANONYMIZE_ACTIVE
|
|
480
|
+
else "HANE_MODE=rest — texto não transita por servidor externo, anonimização desnecessária"
|
|
481
|
+
if HANE_MODE == "rest" and HANE_ANONYMIZE
|
|
482
|
+
else "HANE_ANONYMIZE não definido — defina HANE_ANONYMIZE=true para ativar em modo remoto"
|
|
483
|
+
),
|
|
484
|
+
}
|
|
485
|
+
return result
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@mcp.tool()
|
|
489
|
+
def annotate_file_local(
|
|
490
|
+
path: str,
|
|
491
|
+
threshold: float = 0.45,
|
|
492
|
+
domain: str = "auto",
|
|
493
|
+
) -> dict[str, Any]:
|
|
494
|
+
"""
|
|
495
|
+
Lê um arquivo do disco local e envia o texto direto à API HANE — sem passar pelo contexto do Claude.
|
|
496
|
+
|
|
497
|
+
Este é o Fluxo 3 de máxima eficiência: o Claude nunca vê o conteúdo bruto do arquivo,
|
|
498
|
+
apenas as entidades comprimidas retornadas pelo HANE (~79 tokens em vez de ~1.000+).
|
|
499
|
+
|
|
500
|
+
Diferença em relação a extract_entities:
|
|
501
|
+
- extract_entities: Claude lê o arquivo → tokens entram no contexto → envia ao HANE → custo dobra
|
|
502
|
+
- annotate_file_local: cliente lê o arquivo → envia direto ao HANE → Claude recebe só entidades
|
|
503
|
+
|
|
504
|
+
Suporta: .txt, .py, .prw, .prx, .tlpp, .js, .ts, .java, .sql, .md, .pdf
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
path: Caminho do arquivo no disco local.
|
|
508
|
+
threshold: Confiança mínima para incluir entidade (0.0–1.0). Padrão: 0.45.
|
|
509
|
+
domain: Domínio de extração. Use "auto" para detecção automática.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Dicionário com entidades, economia de tokens, latência e métricas de qualidade.
|
|
513
|
+
"""
|
|
514
|
+
import pathlib
|
|
515
|
+
|
|
516
|
+
file_path = pathlib.Path(path)
|
|
517
|
+
if not file_path.exists():
|
|
518
|
+
return {"error": f"Arquivo não encontrado: {path}"}
|
|
519
|
+
if not file_path.is_file():
|
|
520
|
+
return {"error": f"Caminho não é um arquivo: {path}"}
|
|
521
|
+
|
|
522
|
+
ext = file_path.suffix.lower()
|
|
523
|
+
try:
|
|
524
|
+
if ext == ".pdf":
|
|
525
|
+
from pypdf import PdfReader
|
|
526
|
+
reader = PdfReader(str(file_path))
|
|
527
|
+
paginas = len(reader.pages)
|
|
528
|
+
text = "\n".join(p.extract_text() or "" for p in reader.pages)
|
|
529
|
+
|
|
530
|
+
# PDF escaneado: pypdf não extraiu texto suficiente.
|
|
531
|
+
# Fallback: envia o arquivo ao servidor, que aplica OCR via pytesseract.
|
|
532
|
+
if len(text.strip()) < 100 and HANE_MODE == "rest":
|
|
533
|
+
result = _rest_post_file(str(file_path), domain=domain, threshold=threshold)
|
|
534
|
+
if "error" not in result:
|
|
535
|
+
result["arquivo"] = str(file_path.resolve())
|
|
536
|
+
result["tamanho_bytes"] = file_path.stat().st_size
|
|
537
|
+
result["paginas_pdf"] = paginas
|
|
538
|
+
result["ocr_fallback"] = True
|
|
539
|
+
return result
|
|
540
|
+
else:
|
|
541
|
+
paginas = None
|
|
542
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
543
|
+
except Exception as exc:
|
|
544
|
+
return {"error": f"Erro ao ler arquivo: {exc}"}
|
|
545
|
+
|
|
546
|
+
if not text.strip():
|
|
547
|
+
return {"error": "Arquivo vazio ou sem conteúdo legível. Se for um PDF escaneado, verifique se o servidor tem pytesseract instalado."}
|
|
548
|
+
|
|
549
|
+
result = _annotate(text, threshold, domain)
|
|
550
|
+
|
|
551
|
+
by_label: dict[str, list[str]] = {}
|
|
552
|
+
for ent in result.get("entities", []):
|
|
553
|
+
lbl = ent.get("label", "?")
|
|
554
|
+
txt = ent.get("text", "")
|
|
555
|
+
if lbl not in by_label:
|
|
556
|
+
by_label[lbl] = []
|
|
557
|
+
if txt not in by_label[lbl]:
|
|
558
|
+
by_label[lbl].append(txt)
|
|
559
|
+
|
|
560
|
+
result["entities_by_label"] = by_label
|
|
561
|
+
result["arquivo"] = str(file_path.resolve())
|
|
562
|
+
result["tamanho_bytes"] = file_path.stat().st_size
|
|
563
|
+
if paginas:
|
|
564
|
+
result["paginas_pdf"] = paginas
|
|
565
|
+
return result
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# ----------------------------------------------
|
|
569
|
+
# Entry point
|
|
570
|
+
# ----------------------------------------------
|
|
571
|
+
|
|
572
|
+
def main() -> None:
|
|
573
|
+
mcp.run()
|
|
574
|
+
|
|
575
|
+
if __name__ == "__main__":
|
|
576
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools]
|
|
6
|
+
py-modules = ["hane_mcp_client"]
|
|
7
|
+
|
|
8
|
+
[tool.setuptools.dynamic]
|
|
9
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "hane-mcp-client"
|
|
13
|
+
version = "1.2.0"
|
|
14
|
+
description = "Cliente MCP leve para o servidor HANE — extracao de entidades e analise semantica de documentos ERP/fiscal/juridico"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
requires-python = ">=3.10"
|
|
17
|
+
license = {text = "Proprietary"}
|
|
18
|
+
authors = [
|
|
19
|
+
{name = "HaneIA Tecnologia", email = "contato@haneia.com.br"},
|
|
20
|
+
]
|
|
21
|
+
keywords = ["mcp", "ner", "nlp", "hane", "advpl", "totvs", "fiscal", "juridico", "claude"]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Text Processing :: Linguistic",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"fastmcp>=3.2.4",
|
|
34
|
+
"pypdf>=4.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["build", "twine"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
hane-mcp-client = "hane_mcp_client:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/JacionSilva/hane"
|
|
45
|
+
Repository = "https://github.com/JacionSilva/hane"
|