factumstack-mcp 0.1.0__py3-none-any.whl

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,2 @@
1
+ # factumstack_mcp/__init__.py
2
+ # Vacío para que sea un paquete válido
@@ -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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: factumstack-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP Bridge for FactumStack Scientific Auditing
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: mcp>=0.1.0
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,7 @@
1
+ factumstack_mcp/__init__.py,sha256=R4VlPzeGB3NYA_sR99Pmxv1NwFfgbS0VNenXMIARBEQ,73
2
+ factumstack_mcp/bridge.py,sha256=ldbsmZwQ2r7t4wW-h0usUA9B7t_31Wmcpxo-u5Dpcfw,8755
3
+ factumstack_mcp-0.1.0.dist-info/METADATA,sha256=2tgoL6fjImPritey7-0ExhT4LFDKanzd9MfI9GSjWi4,238
4
+ factumstack_mcp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ factumstack_mcp-0.1.0.dist-info/entry_points.txt,sha256=s0xJFfLQpTVPpDe5URqXD6bUGDH9nE0u7ENJ3CMLmCc,63
6
+ factumstack_mcp-0.1.0.dist-info/top_level.txt,sha256=tGwkMURdubDiVftJTg6m-cIEVcbmJocD9TY39kD-D5U,16
7
+ factumstack_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ factumstack-mcp = factumstack_mcp.bridge:run
@@ -0,0 +1 @@
1
+ factumstack_mcp