rationalbloks-mcp 0.1.14__tar.gz → 0.1.19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rationalbloks-mcp
3
- Version: 0.1.14
3
+ Version: 0.1.19
4
4
  Summary: Enterprise-grade MCP Server for RationalBloks - Connect AI agents to your backend projects
5
5
  Project-URL: Homepage, https://rationalbloks.com
6
6
  Project-URL: Documentation, https://rationalbloks.com/docs/mcp
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rationalbloks-mcp"
7
- version = "0.1.14"
7
+ version = "0.1.19"
8
8
  description = "Enterprise-grade MCP Server for RationalBloks - Connect AI agents to your backend projects"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -26,15 +26,17 @@ from .server import RationalBloksMCPServer
26
26
  from .client import RationalBloksClient
27
27
  from .tools import TOOLS
28
28
 
29
- # Version - single source of truth read from pyproject.toml metadata
30
- try:
31
- from importlib.metadata import version as _get_version
32
- __version__ = _get_version("rationalbloks-mcp")
33
- except Exception:
34
- __version__ = "0.1.10" # Fallback matches pyproject.toml
29
+ # ============================================================================
30
+ # VERSION - Single Source of Truth (Chain Mantra)
31
+ # ============================================================================
32
+ # Read from pyproject.toml via importlib.metadata
33
+ # If this fails, the package is not installed correctly - fail immediately
34
+ # NO FALLBACKS - there is only ONE correct path
35
+ from importlib.metadata import version as _get_version
36
+ __version__ = _get_version("rationalbloks-mcp")
35
37
 
36
38
  __author__ = "RationalBloks"
37
- __all__ = ["RationalBloksMCPServer", "RationalBloksClient", "TOOLS", "main"]
39
+ __all__ = ["RationalBloksMCPServer", "RationalBloksClient", "TOOLS", "main", "__version__"]
38
40
 
39
41
 
40
42
  def main() -> None:
@@ -21,12 +21,19 @@ import os
21
21
  import time
22
22
  from typing import Any
23
23
 
24
- # Version - read dynamically to avoid circular import
25
- try:
26
- from importlib.metadata import version as _get_version
27
- __version__ = _get_version("rationalbloks-mcp")
28
- except Exception:
29
- __version__ = "0.1.10"
24
+ # ============================================================================
25
+ # VERSION - Import from package root (Chain Mantra: Single Source of Truth)
26
+ # ============================================================================
27
+ # Deferred import to avoid circular dependency during module loading
28
+ # The version is set after the module is fully loaded via __init__.py
29
+ _version_cache: str | None = None
30
+
31
+ def _get_version() -> str:
32
+ global _version_cache
33
+ if _version_cache is None:
34
+ from importlib.metadata import version
35
+ _version_cache = version("rationalbloks-mcp")
36
+ return _version_cache
30
37
 
31
38
  # Default configuration (can be overridden via environment variables)
32
39
  GATEWAY_URL = os.environ.get("RATIONALBLOKS_BASE_URL", "https://logicblok.rationalbloks.com")
@@ -34,6 +41,9 @@ REQUEST_TIMEOUT = float(os.environ.get("RATIONALBLOKS_TIMEOUT", "30"))
34
41
  MAX_RETRIES = 3
35
42
  RETRY_DELAY = 1.0 # Base delay in seconds, exponentially increases
36
43
 
44
+ # Public API
45
+ __all__ = ["RationalBloksClient"]
46
+
37
47
 
38
48
  class RationalBloksClient:
39
49
  # HTTP client for MCP Gateway communication
@@ -59,7 +69,7 @@ class RationalBloksClient:
59
69
  headers={
60
70
  "Authorization": f"Bearer {api_key}",
61
71
  "Content-Type": "application/json",
62
- "User-Agent": f"rationalbloks-mcp/{__version__}"
72
+ "User-Agent": f"rationalbloks-mcp/{_get_version()}"
63
73
  },
64
74
  timeout=self.timeout,
65
75
  follow_redirects=True
@@ -38,19 +38,23 @@ from mcp.types import (
38
38
  PromptArgument,
39
39
  PromptMessage,
40
40
  GetPromptResult,
41
- Resource
41
+ Resource,
42
+ Icon
42
43
  )
43
44
  from starlette.requests import Request
44
45
 
45
46
  from .client import RationalBloksClient
46
47
  from .tools import TOOLS
47
48
 
48
- # Version - read dynamically to avoid circular import
49
- try:
50
- from importlib.metadata import version as _get_version
51
- __version__ = _get_version("rationalbloks-mcp")
52
- except Exception:
53
- __version__ = "0.1.10"
49
+ # ============================================================================
50
+ # VERSION - Import from package root (Chain Mantra: Single Source of Truth)
51
+ # ============================================================================
52
+ # Deferred import to avoid circular dependency during module loading
53
+ from importlib.metadata import version as _get_pkg_version
54
+ __version__ = _get_pkg_version("rationalbloks-mcp")
55
+
56
+ # Public API
57
+ __all__ = ["RationalBloksMCPServer"]
54
58
 
55
59
 
56
60
  # ============================================================================
@@ -159,6 +163,10 @@ class RationalBloksMCPServer:
159
163
  version=__version__,
160
164
  instructions="RationalBloks MCP Server - Enterprise Backend-as-a-Service for AI agents. Build production APIs from JSON schemas.",
161
165
  website_url="https://rationalbloks.com",
166
+ icons=[
167
+ Icon(src="https://rationalbloks.com/logo.svg", mimeType="image/svg+xml"),
168
+ Icon(src="https://rationalbloks.com/logo.png", mimeType="image/png", sizes=["128x128"]),
169
+ ],
162
170
  )
163
171
  self._setup_handlers()
164
172
 
@@ -346,19 +354,22 @@ class RationalBloksMCPServer:
346
354
  description=f"Deployment status and metadata for {project_name}",
347
355
  mimeType="application/json"
348
356
  ))
349
- except Exception:
350
- # Fail silently for dynamic resources
351
- # WHY: Static resources still available
352
- pass
357
+ except Exception as e:
358
+ # Log warning but continue - static resources are still available
359
+ # Chain Mantra: Dynamic resources are optional, but failures must be visible
360
+ print(f"[rationalbloks-mcp] Warning: Failed to list dynamic resources: {e}", file=sys.stderr)
353
361
 
354
362
  return resources
355
363
 
356
364
  @self.server.read_resource()
357
- async def read_resource(uri: str) -> str:
365
+ async def read_resource(uri) -> str:
358
366
  # Read a specific resource by URI
359
367
  # Handles both static docs and dynamic project resources
360
368
  # WHY: Provides AI agents access to docs and project schemas
361
369
 
370
+ # Convert AnyUrl to string for comparison
371
+ uri_str = str(uri)
372
+
362
373
  # Static documentation resources - no auth required
363
374
  static_docs = {
364
375
  "rationalbloks://docs/getting-started": DOCS_GETTING_STARTED,
@@ -366,8 +377,8 @@ class RationalBloksMCPServer:
366
377
  "rationalbloks://docs/api-reference": DOCS_API_REFERENCE,
367
378
  }
368
379
 
369
- if uri in static_docs:
370
- return static_docs[uri]
380
+ if uri_str in static_docs:
381
+ return static_docs[uri_str]
371
382
 
372
383
  # Dynamic project resources - require authentication
373
384
  client = self._get_client_for_request()
@@ -375,12 +386,12 @@ class RationalBloksMCPServer:
375
386
  raise ValueError("Authentication required to read project resources")
376
387
 
377
388
  # Parse URI: rationalbloks://project/{id}/{type}
378
- if not uri.startswith("rationalbloks://project/"):
379
- raise ValueError(f"Invalid URI format: {uri}")
389
+ if not uri_str.startswith("rationalbloks://project/"):
390
+ raise ValueError(f"Invalid URI format: {uri_str}")
380
391
 
381
- parts = uri.replace("rationalbloks://project/", "").split("/")
392
+ parts = uri_str.replace("rationalbloks://project/", "").split("/")
382
393
  if len(parts) != 2:
383
- raise ValueError(f"Invalid URI format: {uri}")
394
+ raise ValueError(f"Invalid URI format: {uri_str}")
384
395
 
385
396
  project_id, resource_type = parts
386
397
 
@@ -399,9 +410,14 @@ class RationalBloksMCPServer:
399
410
  @self.server.call_tool()
400
411
  async def call_tool(name: str, arguments: dict) -> list[TextContent]:
401
412
  # Execute a tool with provided arguments
402
- # Single code path: get client → execute → format response
413
+ # Single code path: validate → get client → execute → format response
403
414
  # WHY: Core MCP operation - all tool invocations flow through here
404
415
 
416
+ # Validate tool name exists
417
+ valid_tools = [t["name"] for t in TOOLS]
418
+ if name not in valid_tools:
419
+ raise ValueError(f"Unknown tool: {name}")
420
+
405
421
  try:
406
422
  client = self._get_client_for_request()
407
423
 
@@ -429,24 +445,33 @@ class RationalBloksMCPServer:
429
445
  # Get the appropriate client for the current request
430
446
  # STDIO mode: Returns pre-configured client with environment API key
431
447
  # HTTP mode: Extracts API key from Authorization Bearer header per-request
432
- # WHY: Dual transport requires different authentication strategies
448
+ # Chain Mantra: Single path through code, explicit None returns
433
449
 
450
+ # STDIO mode - return pre-configured client
434
451
  if not self.http_mode:
435
452
  return self.client
436
453
 
437
454
  # HTTP mode - extract API key from Authorization: Bearer header
438
- try:
439
- ctx = self.server.request_context
440
- if ctx.request and isinstance(ctx.request, Request):
441
- auth_header = ctx.request.headers.get("authorization", "")
442
- if auth_header.startswith("Bearer "):
443
- api_key = auth_header[7:] # Remove "Bearer " prefix
444
- if api_key.startswith("rb_sk_"):
445
- return RationalBloksClient(api_key)
446
- except (LookupError, AttributeError):
447
- pass
455
+ BEARER_PREFIX = "Bearer "
456
+
457
+ # Get request context (may not exist if called outside request)
458
+ ctx = getattr(self.server, 'request_context', None)
459
+ if ctx is None:
460
+ return None
461
+
462
+ request = getattr(ctx, 'request', None)
463
+ if request is None or not isinstance(request, Request):
464
+ return None
465
+
466
+ auth_header = request.headers.get("authorization", "")
467
+ if not auth_header.startswith(BEARER_PREFIX):
468
+ return None
469
+
470
+ api_key = auth_header[len(BEARER_PREFIX):]
471
+ if not api_key.startswith("rb_sk_"):
472
+ return None
448
473
 
449
- return None
474
+ return RationalBloksClient(api_key)
450
475
 
451
476
  def _get_init_options(self) -> InitializationOptions:
452
477
  # Get MCP initialization options for STDIO transport
@@ -544,11 +569,13 @@ class RationalBloksMCPServer:
544
569
  "configSchema": {
545
570
  "type": "object",
546
571
  "title": "RationalBloks Configuration",
572
+ "required": [], # All properties are optional - Smithery quality score
547
573
  "properties": {
548
574
  "apiKey": {
549
575
  "type": "string",
550
576
  "title": "API Key",
551
- "description": "Your RationalBloks API key (get it from https://rationalbloks.com/settings)",
577
+ "description": "Your RationalBloks API key (get it from https://rationalbloks.com/settings). Optional for browsing documentation.",
578
+ "default": "",
552
579
  "x-from": {"header": "authorization"}
553
580
  },
554
581
  "baseUrl": {
@@ -570,8 +597,7 @@ class RationalBloksMCPServer:
570
597
  "default": "INFO",
571
598
  "enum": ["DEBUG", "INFO", "WARNING", "ERROR"]
572
599
  }
573
- },
574
- "required": ["apiKey"]
600
+ }
575
601
  }
576
602
  })
577
603
 
@@ -599,12 +625,18 @@ class RationalBloksMCPServer:
599
625
  yield
600
626
 
601
627
  # Build Starlette ASGI application
628
+ # Multiple paths for MCP endpoint compatibility:
629
+ # - /sse: SSE endpoint for Smithery and cloud clients (documented URL)
630
+ # - /mcp: Alternative path for clarity
631
+ # - /: Root fallback for direct connections
602
632
  app = Starlette(
603
633
  debug=False,
604
634
  routes=[
605
635
  Route("/.well-known/mcp/server-card.json", endpoint=server_card, methods=["GET"]),
606
636
  Route("/health", endpoint=health, methods=["GET"]),
607
- Mount("/", app=handle_streamable), # MCP JSON-RPC endpoint at root
637
+ Mount("/sse", app=handle_streamable), # Primary SSE endpoint (matches frontend docs)
638
+ Mount("/mcp", app=handle_streamable), # Alternative MCP path
639
+ Mount("/", app=handle_streamable), # Root fallback for direct connections
608
640
  ],
609
641
  lifespan=lifespan,
610
642
  )
@@ -624,6 +656,9 @@ class RationalBloksMCPServer:
624
656
  host = os.environ.get("HOST", "0.0.0.0")
625
657
 
626
658
  print(f"[rationalbloks-mcp] Streamable HTTP server starting on {host}:{port}", file=sys.stderr)
627
- print(f"[rationalbloks-mcp] MCP endpoint: http://{host}:{port}/mcp", file=sys.stderr)
659
+ print(f"[rationalbloks-mcp] MCP endpoints:", file=sys.stderr)
660
+ print(f"[rationalbloks-mcp] - http://{host}:{port}/sse (primary)", file=sys.stderr)
661
+ print(f"[rationalbloks-mcp] - http://{host}:{port}/mcp (alternative)", file=sys.stderr)
662
+ print(f"[rationalbloks-mcp] - http://{host}:{port}/ (root fallback)", file=sys.stderr)
628
663
 
629
664
  uvicorn.run(app, host=host, port=port, log_level="info")
@@ -17,6 +17,9 @@
17
17
  # - Write Operations (7): Create, update, deploy, delete
18
18
  # ============================================================================
19
19
 
20
+ # Public API
21
+ __all__ = ["TOOLS"]
22
+
20
23
  TOOLS = [
21
24
  # =========================================================================
22
25
  # READ TOOLS (11 total) - All readOnlyHint=True
@@ -45,6 +48,7 @@ TOOLS = [
45
48
  "description": "Get detailed information about a specific project",
46
49
  "inputSchema": {
47
50
  "type": "object",
51
+ "description": "Specify the project to retrieve",
48
52
  "properties": {
49
53
  "project_id": {
50
54
  "type": "string",
@@ -67,6 +71,7 @@ TOOLS = [
67
71
  "description": "Get the JSON schema definition of a project",
68
72
  "inputSchema": {
69
73
  "type": "object",
74
+ "description": "Specify the project whose schema to retrieve",
70
75
  "properties": {
71
76
  "project_id": {
72
77
  "type": "string",
@@ -107,6 +112,7 @@ TOOLS = [
107
112
  "description": "Check the status of a deployment job",
108
113
  "inputSchema": {
109
114
  "type": "object",
115
+ "description": "Specify the job to check",
110
116
  "properties": {
111
117
  "job_id": {
112
118
  "type": "string",
@@ -129,6 +135,7 @@ TOOLS = [
129
135
  "description": "Get detailed project info including deployment status and resource usage",
130
136
  "inputSchema": {
131
137
  "type": "object",
138
+ "description": "Specify the project to get info for",
132
139
  "properties": {
133
140
  "project_id": {
134
141
  "type": "string",
@@ -151,6 +158,7 @@ TOOLS = [
151
158
  "description": "Get the deployment and version history (git commits) for a project",
152
159
  "inputSchema": {
153
160
  "type": "object",
161
+ "description": "Specify the project to get history for",
154
162
  "properties": {
155
163
  "project_id": {
156
164
  "type": "string",
@@ -209,6 +217,7 @@ TOOLS = [
209
217
  "description": "Get resource usage metrics (CPU, memory) for a project",
210
218
  "inputSchema": {
211
219
  "type": "object",
220
+ "description": "Specify the project to get usage metrics for",
212
221
  "properties": {
213
222
  "project_id": {
214
223
  "type": "string",
@@ -231,6 +240,7 @@ TOOLS = [
231
240
  "description": "Get the schema as it was at a specific version/commit",
232
241
  "inputSchema": {
233
242
  "type": "object",
243
+ "description": "Specify the project and version to retrieve schema for",
234
244
  "properties": {
235
245
  "project_id": {
236
246
  "type": "string",
@@ -261,6 +271,7 @@ TOOLS = [
261
271
  "description": "Create a new RationalBloks project from a JSON schema",
262
272
  "inputSchema": {
263
273
  "type": "object",
274
+ "description": "Project name, schema definition, and optional description",
264
275
  "properties": {
265
276
  "name": {
266
277
  "type": "string",
@@ -291,6 +302,7 @@ TOOLS = [
291
302
  "description": "Update a project's schema (saves to database, does NOT deploy)",
292
303
  "inputSchema": {
293
304
  "type": "object",
305
+ "description": "Project to update and new schema definition",
294
306
  "properties": {
295
307
  "project_id": {
296
308
  "type": "string",
@@ -317,6 +329,7 @@ TOOLS = [
317
329
  "description": "Deploy a project to the staging environment",
318
330
  "inputSchema": {
319
331
  "type": "object",
332
+ "description": "Specify the project to deploy to staging",
320
333
  "properties": {
321
334
  "project_id": {
322
335
  "type": "string",
@@ -339,6 +352,7 @@ TOOLS = [
339
352
  "description": "Promote staging to production (requires paid plan)",
340
353
  "inputSchema": {
341
354
  "type": "object",
355
+ "description": "Specify the project to deploy to production",
342
356
  "properties": {
343
357
  "project_id": {
344
358
  "type": "string",
@@ -361,6 +375,7 @@ TOOLS = [
361
375
  "description": "Delete a project (removes GitHub repo, K8s deployments, and database)",
362
376
  "inputSchema": {
363
377
  "type": "object",
378
+ "description": "Specify the project to permanently delete",
364
379
  "properties": {
365
380
  "project_id": {
366
381
  "type": "string",
@@ -383,6 +398,7 @@ TOOLS = [
383
398
  "description": "Rollback a project to a previous version",
384
399
  "inputSchema": {
385
400
  "type": "object",
401
+ "description": "Project, version, and environment for rollback",
386
402
  "properties": {
387
403
  "project_id": {
388
404
  "type": "string",
@@ -413,6 +429,7 @@ TOOLS = [
413
429
  "description": "Rename a project (changes display name, not project_code)",
414
430
  "inputSchema": {
415
431
  "type": "object",
432
+ "description": "Project to rename and new display name",
416
433
  "properties": {
417
434
  "project_id": {
418
435
  "type": "string",