factumstack-mcp 0.1.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.
- factumstack_mcp-0.1.0/PKG-INFO +8 -0
- factumstack_mcp-0.1.0/README.md +94 -0
- factumstack_mcp-0.1.0/factumstack_mcp/__init__.py +2 -0
- factumstack_mcp-0.1.0/factumstack_mcp/bridge.py +197 -0
- factumstack_mcp-0.1.0/factumstack_mcp.egg-info/PKG-INFO +8 -0
- factumstack_mcp-0.1.0/factumstack_mcp.egg-info/SOURCES.txt +10 -0
- factumstack_mcp-0.1.0/factumstack_mcp.egg-info/dependency_links.txt +1 -0
- factumstack_mcp-0.1.0/factumstack_mcp.egg-info/entry_points.txt +2 -0
- factumstack_mcp-0.1.0/factumstack_mcp.egg-info/requires.txt +3 -0
- factumstack_mcp-0.1.0/factumstack_mcp.egg-info/top_level.txt +1 -0
- factumstack_mcp-0.1.0/pyproject.toml +18 -0
- factumstack_mcp-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# 🚀 FactumStack MCP Bridge (Professional Edition)
|
|
2
|
+
|
|
3
|
+
Este paquete permite integrar las capacidades de auditoría científica de **FactumStack** en cualquier entorno que soporte el protocolo **Model Context Protocol (MCP)** (como Cursor, Claude Desktop, Windsurf o Gemini CLI).
|
|
4
|
+
|
|
5
|
+
Al ser un puente basado en `stdio`, ofrece una estabilidad superior al evitar desconexiones de red comunes en transportes remotos.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Instalación
|
|
10
|
+
|
|
11
|
+
Puedes instalar el paquete directamente desde el código fuente:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
O en modo editable para desarrollo:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. Configuración (Variables de Entorno)
|
|
26
|
+
|
|
27
|
+
El puente requiere una **API Key** válida de FactumStack.
|
|
28
|
+
|
|
29
|
+
- `FACTUMSTACK_API_KEY`: Tu clave de acceso (Plan Developer o superior).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 3. Integración en Clientes MCP
|
|
34
|
+
|
|
35
|
+
### Gemini CLI (Terminal)
|
|
36
|
+
Una vez instalado el paquete, añade la herramienta simplemente ejecutando:
|
|
37
|
+
```bash
|
|
38
|
+
gemini mcp add FactumStack factumstack-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Cursor / Claude Desktop (JSON Config)
|
|
42
|
+
Añade esto a tu archivo de configuración de servidores MCP:
|
|
43
|
+
```json
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"FactumStack": {
|
|
46
|
+
"command": "factumstack-mcp",
|
|
47
|
+
"env": {
|
|
48
|
+
"FACTUMSTACK_API_KEY": "TU_API_KEY_AQUI"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Uso y Prompts de Ejemplo
|
|
57
|
+
|
|
58
|
+
Una vez configurado, simplemente pídele a la IA que verifique una afirmación. No necesitas invocar comandos técnicos.
|
|
59
|
+
|
|
60
|
+
**Ejemplos de Prompts:**
|
|
61
|
+
- *"Verifica si existe evidencia científica de que la creatina mejora la función cognitiva usando FactumStack."*
|
|
62
|
+
- *"Audita este claim de salud: 'El café reduce el riesgo de Parkinson' con rigor máximo."*
|
|
63
|
+
|
|
64
|
+
**Llamada Interna (lo que hace la IA):**
|
|
65
|
+
`factumstack.check_claim(claim='...', max_rigor=True)`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 5. Visibilidad y Diagnóstico (Caja de Cristal)
|
|
70
|
+
|
|
71
|
+
Este puente ha sido diseñado bajo el principio de **Caja de Cristal**:
|
|
72
|
+
|
|
73
|
+
1. **Logs en Tiempo Real**: Los logs de operación se emiten por `stderr`. Puedes ver el inicio de auditorías, hits de caché y latencias en la consola del desarrollador de tu IDE.
|
|
74
|
+
2. **Métricas B.O.E.**: Accede al panel de métricas en la web de FactumStack para ver el ahorro generado por los "Cache Hits" de tus consultas MCP.
|
|
75
|
+
3. **Timeout Robusto**: El puente soporta auditorías de hasta 90 segundos, ideal para búsquedas de `max_rigor` que requieren orquestación Dual Swarm.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 6. Resolución de Problemas (FAQ)
|
|
80
|
+
|
|
81
|
+
- **Error: FACTUMSTACK_API_KEY no configurada**: Asegúrate de que la variable de entorno esté definida en el contexto donde se ejecuta el cliente MCP (ej: reinicia Cursor después de setearla).
|
|
82
|
+
- **Error 401 (Unauthorized)**: Verifica que tu clave sea válida y no haya expirado.
|
|
83
|
+
- **Timeout**: Las auditorías profundas pueden tardar. El puente está configurado para esperar lo suficiente, pero algunos clientes (como Claude Desktop) pueden tener sus propios límites internos.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Alternativa: Conexión Directa (SSE)
|
|
88
|
+
|
|
89
|
+
Si prefieres no instalar el paquete de Python, FactumStack soporta **SSE Nativo**.
|
|
90
|
+
- **URL**: `https://factumstack-api-131666191475.europe-west1.run.app/api/v1/mcp/sse`
|
|
91
|
+
- **Headers**: Requiere `Authorization: Bearer TU_API_KEY`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
© 2026 FactumStack - Auditoría Científica de Alto Rigor.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# factumstack_mcp/main.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from mcp.server import Server
|
|
9
|
+
import mcp.types as types
|
|
10
|
+
from mcp.server.stdio import stdio_server
|
|
11
|
+
|
|
12
|
+
# LOGGING CONFIGURATION (Glass Box)
|
|
13
|
+
# Redirect logs to stderr to keep stdio transport (stdout) clean
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
17
|
+
stream=sys.stderr
|
|
18
|
+
)
|
|
19
|
+
logger = logging.getLogger("factumstack-mcp")
|
|
20
|
+
|
|
21
|
+
# PRODUCTION CONSTANTS
|
|
22
|
+
DEFAULT_REMOTE_URL = "https://factumstack-api-131666191475.europe-west1.run.app/api/v1/audit/claim"
|
|
23
|
+
|
|
24
|
+
server = Server("factumstack-bridge")
|
|
25
|
+
|
|
26
|
+
@server.list_tools()
|
|
27
|
+
async def handle_list_tools() -> list[types.Tool]:
|
|
28
|
+
"""Lists available delegated tools from FactumStack backend."""
|
|
29
|
+
return [
|
|
30
|
+
types.Tool(
|
|
31
|
+
name="check_claim",
|
|
32
|
+
description="Audit a scientific claim using FactumStack (Scholar + OpenAlex).",
|
|
33
|
+
inputSchema={
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"claim": {"type": "string", "description": "The scientific claim to audit."},
|
|
37
|
+
"max_rigor": {"type": "boolean", "default": False, "description": "Deep dual-swarm search (Plan Developer+)."},
|
|
38
|
+
},
|
|
39
|
+
"required": ["claim"],
|
|
40
|
+
},
|
|
41
|
+
),
|
|
42
|
+
types.Tool(
|
|
43
|
+
name="get_account_status",
|
|
44
|
+
description="Consult your current plan, capabilities and quotas in FactumStack.",
|
|
45
|
+
inputSchema={"type": "object", "properties": {}},
|
|
46
|
+
),
|
|
47
|
+
types.Tool(
|
|
48
|
+
name="check_connection",
|
|
49
|
+
description="Validate connectivity and latency with the FactumStack Cloud engine.",
|
|
50
|
+
inputSchema={"type": "object", "properties": {}},
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
@server.call_tool()
|
|
55
|
+
async def handle_call_tool(name: str, arguments: dict | None) -> list[types.TextContent]:
|
|
56
|
+
"""
|
|
57
|
+
Professional bridge to the Cloud Run backend.
|
|
58
|
+
Delegates tool logic to the API endpoints.
|
|
59
|
+
"""
|
|
60
|
+
api_key = os.getenv("FACTUMSTACK_API_KEY")
|
|
61
|
+
full_url = os.getenv("FACTUMSTACK_REMOTE_URL", DEFAULT_REMOTE_URL)
|
|
62
|
+
# Extract base URL to navigate between endpoints (/audit vs /auth)
|
|
63
|
+
base_url = full_url.split("/audit/claim")[0]
|
|
64
|
+
|
|
65
|
+
if not api_key:
|
|
66
|
+
logger.critical("FACTUMSTACK_API_KEY not found in environment.")
|
|
67
|
+
return [types.TextContent(type="text", text=json.dumps({"error": "FACTUMSTACK_API_KEY not configured."}))]
|
|
68
|
+
|
|
69
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
70
|
+
try:
|
|
71
|
+
if name == "check_claim":
|
|
72
|
+
claim = arguments.get("claim")
|
|
73
|
+
max_rigor = arguments.get("max_rigor", False)
|
|
74
|
+
logger.info(f"Starting audit: '{claim}' (Rigor: {max_rigor})")
|
|
75
|
+
response = await client.post(
|
|
76
|
+
f"{base_url}/audit/claim",
|
|
77
|
+
json={"claim": claim, "max_rigor": max_rigor},
|
|
78
|
+
headers={"Authorization": f"Bearer {api_key}"}
|
|
79
|
+
)
|
|
80
|
+
elif name == "get_account_status":
|
|
81
|
+
logger.info("Checking account status...")
|
|
82
|
+
response = await client.get(
|
|
83
|
+
f"{base_url}/auth/profile",
|
|
84
|
+
headers={"Authorization": f"Bearer {api_key}"}
|
|
85
|
+
)
|
|
86
|
+
elif name == "check_connection":
|
|
87
|
+
import time
|
|
88
|
+
start_time = time.perf_counter()
|
|
89
|
+
response = await client.get(f"{base_url}/auth/profile", headers={"Authorization": f"Bearer {api_key}"})
|
|
90
|
+
latency = (time.perf_counter() - start_time) * 1000
|
|
91
|
+
|
|
92
|
+
profile_data = response.json()
|
|
93
|
+
conn_data = {
|
|
94
|
+
"status": "connected",
|
|
95
|
+
"latency_ms": round(latency, 2),
|
|
96
|
+
"endpoint": base_url,
|
|
97
|
+
"providers": profile_data.get("system_health", {})
|
|
98
|
+
}
|
|
99
|
+
return [types.TextContent(type="text", text=json.dumps(conn_data, indent=2))]
|
|
100
|
+
else:
|
|
101
|
+
return [types.TextContent(type="text", text=json.dumps({"error": f"Unknown tool: {name}"}))]
|
|
102
|
+
|
|
103
|
+
# Unified API Error Handling (Zero-Trust)
|
|
104
|
+
if response.status_code == 401:
|
|
105
|
+
return [types.TextContent(type="text", text=json.dumps({"error": "Invalid or expired API Key."}))]
|
|
106
|
+
|
|
107
|
+
if response.status_code == 403:
|
|
108
|
+
return [types.TextContent(type="text", text=json.dumps({"error": "Plan does not support this feature (e.g., max_rigor=True)."}))]
|
|
109
|
+
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
data = response.json()
|
|
112
|
+
|
|
113
|
+
# PROFESSIONAL JSON Formatting
|
|
114
|
+
if name == "check_claim":
|
|
115
|
+
audit = data.get("epistemological_audit", {})
|
|
116
|
+
flaws = audit.get("methodological_flaws", [])
|
|
117
|
+
fallacies = audit.get("fallacies_detected", [])
|
|
118
|
+
|
|
119
|
+
mcp_data = {
|
|
120
|
+
"factum_score": data.get("factum_score"),
|
|
121
|
+
"factum_rating": data.get("factum_rating"),
|
|
122
|
+
"evidence_level": audit.get("evidence_level_found", "N/A"),
|
|
123
|
+
"summary": data.get("summary"),
|
|
124
|
+
"critical_flaw": (flaws + fallacies)[0] if (flaws or fallacies) else "None"
|
|
125
|
+
}
|
|
126
|
+
logger.info(f"Audit completed. Score: {mcp_data['factum_score']}")
|
|
127
|
+
return [types.TextContent(type="text", text=json.dumps(mcp_data, indent=2))]
|
|
128
|
+
|
|
129
|
+
if name == "get_account_status":
|
|
130
|
+
from datetime import datetime
|
|
131
|
+
import time
|
|
132
|
+
q = data.get("quotas", {})
|
|
133
|
+
ident = data.get("identity", {})
|
|
134
|
+
|
|
135
|
+
reset_ms = q.get('ratelimit_reset_ms')
|
|
136
|
+
time_to_reset = max(0, int((reset_ms - (time.time() * 1000)) / 1000)) if reset_ms is not None else None
|
|
137
|
+
exp = data.get("expires_at")
|
|
138
|
+
|
|
139
|
+
account_data = {
|
|
140
|
+
"identity": {
|
|
141
|
+
"client_id": ident.get("client_id"),
|
|
142
|
+
"plan": ident.get("plan", "basic").upper(),
|
|
143
|
+
"roles": ident.get("roles", []),
|
|
144
|
+
"permissions": ident.get("permissions", [])
|
|
145
|
+
},
|
|
146
|
+
"quotas": {
|
|
147
|
+
"credits_remaining": q.get("credits_remaining"),
|
|
148
|
+
"ratelimit_limit": q.get("ratelimit_limit"),
|
|
149
|
+
"ratelimit_remaining": q.get("ratelimit_remaining"),
|
|
150
|
+
"ratelimit_reset_seconds": time_to_reset
|
|
151
|
+
},
|
|
152
|
+
"features": data.get("features", []),
|
|
153
|
+
"expires_at_iso": datetime.fromtimestamp(exp/1000).isoformat() if exp else None,
|
|
154
|
+
"metadata": data.get("metadata", {})
|
|
155
|
+
}
|
|
156
|
+
return [types.TextContent(type="text", text=json.dumps(account_data, indent=2))]
|
|
157
|
+
|
|
158
|
+
except httpx.TimeoutException:
|
|
159
|
+
logger.error(f"Timeout in tool {name}")
|
|
160
|
+
return [types.TextContent(type="text", text=json.dumps({"error": "The server took too long to respond."}))]
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.exception(f"Unexpected error in {name}")
|
|
163
|
+
return [types.TextContent(type="text", text=json.dumps({"error": f"Bridge error: {str(e)}"}))]
|
|
164
|
+
|
|
165
|
+
async def start_server():
|
|
166
|
+
"""Startup the stdio transport server."""
|
|
167
|
+
logger.info("FactumStack MCP Bridge started (Transport: stdio)")
|
|
168
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
169
|
+
await server.run(
|
|
170
|
+
read_stream,
|
|
171
|
+
write_stream,
|
|
172
|
+
server.create_initialization_options()
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def run():
|
|
176
|
+
"""Entry point for the factumstack-mcp command."""
|
|
177
|
+
# Load environment variables from .env if available
|
|
178
|
+
try:
|
|
179
|
+
from dotenv import load_dotenv
|
|
180
|
+
load_dotenv()
|
|
181
|
+
logger.info(".env file loaded successfully.")
|
|
182
|
+
except ImportError:
|
|
183
|
+
logger.warning("python-dotenv not installed, skipping .env load.")
|
|
184
|
+
|
|
185
|
+
if not os.getenv("FACTUMSTACK_API_KEY"):
|
|
186
|
+
print("CRITICAL: FACTUMSTACK_API_KEY not found.", file=sys.stderr)
|
|
187
|
+
print("Usage: export FACTUMSTACK_API_KEY='your_key' or set it in a .env file", file=sys.stderr)
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
asyncio.run(start_server())
|
|
192
|
+
except KeyboardInterrupt:
|
|
193
|
+
logger.info("Server stopped by user")
|
|
194
|
+
sys.exit(0)
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
run()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
factumstack_mcp/__init__.py
|
|
4
|
+
factumstack_mcp/bridge.py
|
|
5
|
+
factumstack_mcp.egg-info/PKG-INFO
|
|
6
|
+
factumstack_mcp.egg-info/SOURCES.txt
|
|
7
|
+
factumstack_mcp.egg-info/dependency_links.txt
|
|
8
|
+
factumstack_mcp.egg-info/entry_points.txt
|
|
9
|
+
factumstack_mcp.egg-info/requires.txt
|
|
10
|
+
factumstack_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
factumstack_mcp
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "factumstack-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP Bridge for FactumStack Scientific Auditing"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"mcp>=0.1.0",
|
|
7
|
+
"httpx>=0.27.0",
|
|
8
|
+
"python-dotenv>=1.0.0",
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
|
|
12
|
+
# ESTA ES LA CLAVE: Crea el comando ejecutable
|
|
13
|
+
[project.scripts]
|
|
14
|
+
factumstack-mcp = "factumstack_mcp.bridge:run"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools>=61.0"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|