sankhya-mcp 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/sankhya-mcp.js +25 -0
- package/package.json +21 -0
- package/pyproject.toml +35 -0
- package/src/__init__.py +0 -0
- package/src/data/index.db +0 -0
- package/src/server.py +183 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcio Nogueira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# sankhya-mcp
|
|
2
|
+
|
|
3
|
+
> **Projeto em fase inicial — novas versões e funcionalidades em breve.**
|
|
4
|
+
|
|
5
|
+
Servidor MCP (Model Context Protocol) para documentação do **Sankhya ERP**, com busca semântica local via RAG. Permite que o Claude (VS Code, Claude Desktop, etc.) responda perguntas precisas sobre o sistema Sankhya sem necessidade de chaves de API em runtime.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Como funciona
|
|
10
|
+
|
|
11
|
+
O conhecimento fica embutido no pacote como um índice vetorial local (`sqlite-vec`). O servidor roda localmente via STDIO e responde às queries do Claude com os trechos de documentação mais relevantes.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Usuário pergunta ao Claude
|
|
15
|
+
↓
|
|
16
|
+
Claude chama search_docs (MCP tool)
|
|
17
|
+
↓
|
|
18
|
+
Servidor busca no índice local (sem API externa)
|
|
19
|
+
↓
|
|
20
|
+
Claude responde com base na documentação do Sankhya
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Instalação — VS Code
|
|
26
|
+
|
|
27
|
+
Adicione ao `.mcp.json` do seu projeto:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"sankhya-docs": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["sankhya-mcp"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Instalação — Claude Desktop
|
|
41
|
+
|
|
42
|
+
Adicione ao `claude_desktop_config.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"sankhya-docs": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["sankhya-mcp"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Coleções disponíveis
|
|
58
|
+
|
|
59
|
+
| Coleção | Descrição |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `boletos-emissao` | Emissão de boletos e exportação de XMLs de NF-e |
|
|
62
|
+
| `dashboards-html5` | Criação e otimização de dashboards HTML5 |
|
|
63
|
+
| `reabertura-ops` | Correção e reabertura de ordens de produção |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Exemplos de perguntas
|
|
68
|
+
|
|
69
|
+
- *Como exportar XMLs de NF-e no Sankhya?*
|
|
70
|
+
- *Como criar um dashboard HTML5?*
|
|
71
|
+
- *Como otimizar queries para dashboards?*
|
|
72
|
+
- *Como reabrir uma ordem de produção?*
|
|
73
|
+
- *Como corrigir apontamentos de OP com quantidade errada?*
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Stack
|
|
78
|
+
|
|
79
|
+
- Python 3.12+
|
|
80
|
+
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) + FastMCP
|
|
81
|
+
- [sqlite-vec](https://github.com/asg017/sqlite-vec) — índice vetorial embutido
|
|
82
|
+
- [fastembed](https://github.com/qdrant/fastembed) — embeddings locais (sem API externa)
|
|
83
|
+
- Modelo: `paraphrase-multilingual-MiniLM-L12-v2` (384 dims, multilingual)
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Status
|
|
88
|
+
|
|
89
|
+
Este projeto está em **fase inicial de desenvolvimento**. Em breve:
|
|
90
|
+
|
|
91
|
+
- Novas coleções de documentação
|
|
92
|
+
- Melhorias na qualidade da busca
|
|
93
|
+
- Publicação no npm para uso via `npx`
|
|
94
|
+
|
|
95
|
+
Sugestões e contribuições são bem-vindas via [Issues](https://github.com/mfnogueira/sankhya-mcp/issues).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require("child_process");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const packageDir = path.join(__dirname, "..");
|
|
6
|
+
const serverScript = path.join(packageDir, "src", "server.py");
|
|
7
|
+
|
|
8
|
+
const proc = spawn("uv", ["run", "python", serverScript], {
|
|
9
|
+
cwd: packageDir,
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
proc.on("error", (err) => {
|
|
14
|
+
if (err.code === "ENOENT") {
|
|
15
|
+
process.stderr.write(
|
|
16
|
+
"Error: 'uv' is required but not installed.\n" +
|
|
17
|
+
"Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh\n"
|
|
18
|
+
);
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
21
|
+
}
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
proc.on("exit", (code) => process.exit(code ?? 0));
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sankhya-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Sankhya ERP documentation — semantic search via sqlite-vec",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": ["mcp", "sankhya", "erp", "documentation", "rag"],
|
|
7
|
+
"homepage": "https://github.com/mfnogueira/sankhya-mcp",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/mfnogueira/sankhya-mcp.git"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"sankhya-mcp": "./bin/sankhya-mcp.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
"pyproject.toml",
|
|
19
|
+
"README.md"
|
|
20
|
+
]
|
|
21
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sankhya-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for Sankhya ERP documentation — semantic search via sqlite-vec"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mcp[cli]>=1.2.0",
|
|
9
|
+
"sqlite-vec>=0.1.0",
|
|
10
|
+
"fastembed>=0.4.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
ingest = [
|
|
15
|
+
"openai>=1.0.0",
|
|
16
|
+
"python-dotenv>=1.0.0",
|
|
17
|
+
"tiktoken>=0.7.0",
|
|
18
|
+
"numpy>=1.26.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
sankhya-mcp = "src.server:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist.force-include]
|
|
32
|
+
"src/data/index.db" = "src/data/index.db"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
35
|
+
"src/data/index.db" = "src/data/index.db"
|
package/src/__init__.py
ADDED
|
File without changes
|
|
Binary file
|
package/src/server.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Servidor MCP para documentação do Sankhya — transporte STDIO.
|
|
3
|
+
|
|
4
|
+
IMPORTANTE: nunca usar print() — corrompe o protocolo JSON-RPC.
|
|
5
|
+
Todo output de log vai para sys.stderr.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import sqlite3
|
|
10
|
+
import struct
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import sqlite_vec
|
|
15
|
+
from fastembed import TextEmbedding
|
|
16
|
+
from mcp.server.fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
# ── Logging ──────────────────────────────────────────────────────────────────
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
stream=sys.stderr,
|
|
21
|
+
level=logging.INFO,
|
|
22
|
+
format="%(asctime)s %(name)s %(message)s",
|
|
23
|
+
datefmt="%H:%M:%S",
|
|
24
|
+
)
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# ── Constantes ───────────────────────────────────────────────────────────────
|
|
28
|
+
DB_PATH = Path(__file__).parent / "data" / "index.db"
|
|
29
|
+
MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
|
30
|
+
EMBEDDING_DIM = 384
|
|
31
|
+
|
|
32
|
+
# ── Modelo de embedding (carregado sob demanda) ───────────────────────────────
|
|
33
|
+
_model: TextEmbedding | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_model() -> TextEmbedding:
|
|
37
|
+
global _model
|
|
38
|
+
if _model is None:
|
|
39
|
+
logger.info(f"Carregando modelo de embedding: {MODEL_NAME}")
|
|
40
|
+
_model = TextEmbedding(model_name=MODEL_NAME)
|
|
41
|
+
logger.info("Modelo carregado.")
|
|
42
|
+
return _model
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _embed_query(text: str) -> bytes:
|
|
46
|
+
"""Gera o embedding da query e serializa para bytes (formato sqlite-vec)."""
|
|
47
|
+
model = _get_model()
|
|
48
|
+
vectors = list(model.embed([text]))
|
|
49
|
+
vec = vectors[0].tolist()
|
|
50
|
+
return struct.pack(f"{len(vec)}f", *vec)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_db() -> sqlite3.Connection:
|
|
54
|
+
con = sqlite3.connect(DB_PATH)
|
|
55
|
+
con.enable_load_extension(True)
|
|
56
|
+
sqlite_vec.load(con)
|
|
57
|
+
con.enable_load_extension(False)
|
|
58
|
+
return con
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── FastMCP ───────────────────────────────────────────────────────────────────
|
|
62
|
+
mcp = FastMCP("sankhya-docs")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@mcp.tool()
|
|
66
|
+
def search_docs(query: str, collection: str | None = None, top_k: int = 5) -> str:
|
|
67
|
+
"""Busca documentação do Sankhya ERP com base em uma query semântica.
|
|
68
|
+
|
|
69
|
+
Use esta ferramenta quando o usuário perguntar sobre funcionalidades,
|
|
70
|
+
configurações, erros, fluxos de trabalho ou qualquer aspecto do sistema
|
|
71
|
+
Sankhya. Retorna os trechos de documentação mais relevantes.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
query: Pergunta ou termo a buscar (em português ou inglês).
|
|
75
|
+
collection: Nome da coleção para filtrar resultados (opcional).
|
|
76
|
+
Use list_collections() para ver as coleções disponíveis.
|
|
77
|
+
top_k: Número de resultados a retornar (padrão: 5).
|
|
78
|
+
"""
|
|
79
|
+
if not DB_PATH.exists():
|
|
80
|
+
return "Índice de documentação não encontrado. Execute o pipeline de ingestão primeiro."
|
|
81
|
+
|
|
82
|
+
logger.info(f"search_docs: query='{query}' collection={collection!r} top_k={top_k}")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
query_vec = _embed_query(query)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
logger.error(f"Erro ao gerar embedding: {exc}")
|
|
88
|
+
return f"Erro ao processar a query: {exc}"
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
con = _get_db()
|
|
92
|
+
|
|
93
|
+
# Busca os k mais próximos (ampliado para filtrar por coleção depois)
|
|
94
|
+
candidates = top_k * 10 if collection else top_k
|
|
95
|
+
|
|
96
|
+
rows = con.execute(
|
|
97
|
+
"""
|
|
98
|
+
SELECT c.text, c.source_file, c.collection, c.type, e.distance
|
|
99
|
+
FROM (
|
|
100
|
+
SELECT rowid, distance
|
|
101
|
+
FROM embeddings
|
|
102
|
+
WHERE embedding MATCH ?
|
|
103
|
+
AND k = ?
|
|
104
|
+
ORDER BY distance
|
|
105
|
+
) e
|
|
106
|
+
JOIN chunks c ON c.id = e.rowid
|
|
107
|
+
""",
|
|
108
|
+
(query_vec, candidates),
|
|
109
|
+
).fetchall()
|
|
110
|
+
|
|
111
|
+
con.close()
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.error(f"Erro na consulta ao índice: {exc}")
|
|
114
|
+
return f"Erro ao consultar o índice: {exc}"
|
|
115
|
+
|
|
116
|
+
# Filtra por coleção se solicitado
|
|
117
|
+
if collection:
|
|
118
|
+
rows = [r for r in rows if r[2] == collection]
|
|
119
|
+
|
|
120
|
+
rows = rows[:top_k]
|
|
121
|
+
|
|
122
|
+
if not rows:
|
|
123
|
+
msg = f"Nenhum resultado encontrado para '{query}'"
|
|
124
|
+
if collection:
|
|
125
|
+
msg += f" na coleção '{collection}'"
|
|
126
|
+
return msg
|
|
127
|
+
|
|
128
|
+
# Formata a resposta
|
|
129
|
+
parts: list[str] = [f"## Resultados para: {query}\n"]
|
|
130
|
+
|
|
131
|
+
for i, (text, source_file, coll, doc_type, distance) in enumerate(rows, 1):
|
|
132
|
+
similarity = round((1 - distance) * 100, 1)
|
|
133
|
+
type_label = "📷 Imagem" if doc_type == "image_description" else "📄 Documento"
|
|
134
|
+
parts.append(
|
|
135
|
+
f"### [{i}] {source_file} — {coll} ({type_label}, {similarity}% relevância)\n\n{text}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return "\n\n---\n\n".join(parts)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@mcp.tool()
|
|
142
|
+
def list_collections() -> str:
|
|
143
|
+
"""Lista todas as coleções de documentação do Sankhya disponíveis no índice.
|
|
144
|
+
|
|
145
|
+
Use esta ferramenta antes de search_docs quando quiser restringir a busca
|
|
146
|
+
a uma área específica do sistema, ou para dar ao usuário uma visão geral
|
|
147
|
+
do que está documentado.
|
|
148
|
+
"""
|
|
149
|
+
if not DB_PATH.exists():
|
|
150
|
+
return "Índice de documentação não encontrado. Execute o pipeline de ingestão primeiro."
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
con = sqlite3.connect(DB_PATH)
|
|
154
|
+
rows = con.execute(
|
|
155
|
+
"SELECT collection, COUNT(*) as total FROM chunks GROUP BY collection ORDER BY collection"
|
|
156
|
+
).fetchall()
|
|
157
|
+
con.close()
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
logger.error(f"Erro ao consultar coleções: {exc}")
|
|
160
|
+
return f"Erro ao consultar o índice: {exc}"
|
|
161
|
+
|
|
162
|
+
if not rows:
|
|
163
|
+
return "Índice vazio — nenhuma coleção encontrada."
|
|
164
|
+
|
|
165
|
+
total_chunks = sum(r[1] for r in rows)
|
|
166
|
+
lines = ["## Coleções de documentação disponíveis\n"]
|
|
167
|
+
|
|
168
|
+
for collection, count in rows:
|
|
169
|
+
lines.append(f"- **{collection}** — {count} trechos indexados")
|
|
170
|
+
|
|
171
|
+
lines.append(f"\n_Total: {total_chunks} trechos em {len(rows)} coleções._")
|
|
172
|
+
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
177
|
+
def main() -> None:
|
|
178
|
+
logger.info(f"Iniciando servidor sankhya-docs (DB: {DB_PATH})")
|
|
179
|
+
mcp.run(transport="stdio")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
main()
|