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.
@@ -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,2 @@
1
+ [console_scripts]
2
+ hane-mcp-client = hane_mcp_client:main
@@ -0,0 +1,6 @@
1
+ fastmcp>=3.2.4
2
+ pypdf>=4.0
3
+
4
+ [dev]
5
+ build
6
+ twine
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+