hexastack-mcp 0.0.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,168 @@
1
+ Metadata-Version: 2.3
2
+ Name: hexastack-mcp
3
+ Version: 0.0.0
4
+ Summary: Model Context Protocol (MCP) adapter and AI agent tool integration for Hexastack
5
+ Author: Richard West
6
+ Author-email: Richard West <dopplereffect.us@gmail.com>
7
+ Requires-Dist: hexastack-core
8
+ Requires-Dist: hexastack-cqrs
9
+ Requires-Dist: mcp>=1.3.0
10
+ Requires-Dist: fastapi>=0.141.1 ; extra == 'fastapi'
11
+ Requires-Dist: hexastack-fastapi ; extra == 'fastapi'
12
+ Requires-Python: >=3.13
13
+ Provides-Extra: fastapi
14
+ Description-Content-Type: text/markdown
15
+
16
+ # hexastack-mcp
17
+
18
+ > Model Context Protocol (MCP) adapter and AI agent tool integration for Hexastack.
19
+
20
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
21
+
22
+ ---
23
+
24
+ ## 1. Overview & Capabilities
25
+
26
+ `hexastack-mcp` exposes Hexastack application services directly to AI agents (Claude Desktop, Cursor, Antigravity, custom LLM agents) via the official Anthropic Model Context Protocol:
27
+
28
+ - **Automatic CQRS Tool Generation (`@mcp_tool`)**: Decorates CQRS Command and Query models, converting them into structured LLM tools with JSON Schema validation.
29
+ - **Resource URI Providers (`@mcp_resource`)**: Exposes read endpoints and system diagnostics as readable MCP resources (`hexastack://schema`, `hexastack://info`).
30
+ - **Prompt Templates (`@mcp_prompt`)**: Configures reusable workflow prompts for LLM clients.
31
+ - **Multiple Transports**:
32
+ - **Standard I/O (`stdio`)**: Local process communication for desktop assistants and subprocess agents.
33
+ - **Server-Sent Events (`sse`)**: HTTP SSE transport via FastAPI for remote agent networks.
34
+ - **Single-Pass Reflection**: Discovers decorated tools and resources in Phase 3 module scanning via `create_mcp_visitor`.
35
+
36
+ ---
37
+
38
+ ## 2. Package Anatomy & Key Components
39
+
40
+ ```
41
+ hexastack_mcp/
42
+ ├── domain/ # McpToolMetadata, McpResourceMetadata, McpPromptMetadata, McpError
43
+ ├── adapters/ # stdio runner, sse/FastAPI router, FastMCP server wrapper
44
+ └── infra/
45
+ ├── bootstrap.py # McpBootstrapper (order=40)
46
+ ├── config.py # HexastackMcpConfig
47
+ ├── decorators.py# @mcp_tool, @mcp_resource, @mcp_prompt
48
+ ├── autodiscovery.py # create_mcp_visitor
49
+ └── registries/ # server.py (McpServerRegistry)
50
+ ```
51
+
52
+ ### Key Exports
53
+
54
+ | Category | Exports |
55
+ |---|---|
56
+ | **Adapters** | `run_stdio_server`, `create_sse_router`, `mount_mcp_sse` |
57
+ | **Bootstrap** | `McpBootstrapper` (order=40), `HexastackMcpConfig` |
58
+ | **Decorators** | `@mcp_tool`, `@mcp_resource`, `@mcp_prompt` |
59
+ | **Registries** | `McpServerRegistry`, `get_mcp_registry` |
60
+
61
+ ---
62
+
63
+ ## 3. Monorepo & Sibling Relationships
64
+
65
+ ```mermaid
66
+ graph TD
67
+ subgraph Agents ["AI Agent Clients (Claude, Cursor, Antigravity)"]
68
+ STDIO_CLIENT["Local Desktop / Subprocess (stdio)"]
69
+ SSE_CLIENT["Remote Agent Network (SSE / HTTP)"]
70
+ end
71
+
72
+ subgraph McpAdapter ["hexastack-mcp"]
73
+ SERVER["FastMCP / MCP Server"]
74
+ REG["McpServerRegistry (Tools, Resources, Prompts)"]
75
+ DISPATCH["CQRS Tool Dispatcher"]
76
+ end
77
+
78
+ subgraph CQRSExecution ["hexastack-cqrs"]
79
+ CBUS["CommandBusPort"]
80
+ QBUS["QueryBusPort"]
81
+ end
82
+
83
+ subgraph WebIntegration ["hexastack-fastapi (Optional)"]
84
+ FASTAPI_APP["FastAPI Application (SSE Endpoint)"]
85
+ end
86
+
87
+ STDIO_CLIENT --> SERVER
88
+ SSE_CLIENT --> FASTAPI_APP
89
+ FASTAPI_APP --> SERVER
90
+ SERVER --> REG
91
+ REG --> DISPATCH
92
+ DISPATCH -->|dispatches commands| CBUS
93
+ DISPATCH -->|dispatches queries| QBUS
94
+ ```
95
+
96
+ ### Explicit Dependencies (Direct)
97
+ - `hexastack-core`: DI container, configuration registry, base exceptions.
98
+ - `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
99
+ - `mcp>=1.3.0`: Official Anthropic Model Context Protocol SDK.
100
+
101
+ ### Implied / Behavioral Relationships (DI-Mediated)
102
+ - **FastAPI SSE Integration**: `McpBootstrapper` (order=40) attaches SSE endpoints to the active `FastAPI` instance when `hexastack-fastapi` is present and `auto_mount_fastapi=true`.
103
+ - **CQRS Dispatching**: When an LLM executes an MCP tool, the adapter resolves `CommandBusPort` or `QueryBusPort` to run the command through the full middleware pipeline.
104
+
105
+ ### Optional Integrations (Extras)
106
+ - `[fastapi]`: Installs `hexastack-fastapi` and `fastapi>=0.141.1` for remote SSE transport over HTTP.
107
+
108
+ ---
109
+
110
+ ## 4. Installation
111
+
112
+ ```bash
113
+ # Standalone stdio transport
114
+ pip install hexastack-mcp
115
+
116
+ # With FastAPI remote SSE transport
117
+ pip install "hexastack-mcp[fastapi]"
118
+
119
+ # Via umbrella package
120
+ pip install "hexastack[mcp]"
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 5. Configuration Reference
126
+
127
+ ```toml
128
+ [hexastack.mcp]
129
+ server_name = "Hexastack MCP Server"
130
+ server_version = "0.1.0"
131
+ sse_path = "/sse" # Route prefix for SSE transport
132
+ auto_mount_fastapi = true # Auto-mount SSE router into FastAPI on bootstrap
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 6. Quickstart Example
138
+
139
+ ```python
140
+ from dataclasses import dataclass
141
+ from hexastack_core.infra.bootstrap import bootstrap
142
+ from hexastack_cqrs.domain.query import Query
143
+ from hexastack_cqrs.infra.decorators import query_handler
144
+ from hexastack_mcp.infra.decorators import mcp_tool
145
+
146
+
147
+ # 1. Define CQRS Query & Handler
148
+ @dataclass(frozen=True)
149
+ class CheckSystemStatusQuery(Query):
150
+ service: str = "database"
151
+
152
+
153
+ @query_handler(CheckSystemStatusQuery)
154
+ class CheckSystemStatusHandler:
155
+ def __call__(self, qry: CheckSystemStatusQuery) -> dict[str, str]:
156
+ return {"service": qry.service, "status": "HEALTHY"}
157
+
158
+
159
+ # 2. Expose as MCP Tool for AI Agents
160
+ mcp_tool(
161
+ name="check_status",
162
+ description="Check the real-time operational status of internal subsystems.",
163
+ )(CheckSystemStatusQuery)
164
+
165
+ # 3. Bootstrap Runtime and Run MCP Server
166
+ runtime = bootstrap(packages_to_scan=[__name__])
167
+ mcp_server = runtime.get("mcp_server")
168
+ ```
@@ -0,0 +1,153 @@
1
+ # hexastack-mcp
2
+
3
+ > Model Context Protocol (MCP) adapter and AI agent tool integration for Hexastack.
4
+
5
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
6
+
7
+ ---
8
+
9
+ ## 1. Overview & Capabilities
10
+
11
+ `hexastack-mcp` exposes Hexastack application services directly to AI agents (Claude Desktop, Cursor, Antigravity, custom LLM agents) via the official Anthropic Model Context Protocol:
12
+
13
+ - **Automatic CQRS Tool Generation (`@mcp_tool`)**: Decorates CQRS Command and Query models, converting them into structured LLM tools with JSON Schema validation.
14
+ - **Resource URI Providers (`@mcp_resource`)**: Exposes read endpoints and system diagnostics as readable MCP resources (`hexastack://schema`, `hexastack://info`).
15
+ - **Prompt Templates (`@mcp_prompt`)**: Configures reusable workflow prompts for LLM clients.
16
+ - **Multiple Transports**:
17
+ - **Standard I/O (`stdio`)**: Local process communication for desktop assistants and subprocess agents.
18
+ - **Server-Sent Events (`sse`)**: HTTP SSE transport via FastAPI for remote agent networks.
19
+ - **Single-Pass Reflection**: Discovers decorated tools and resources in Phase 3 module scanning via `create_mcp_visitor`.
20
+
21
+ ---
22
+
23
+ ## 2. Package Anatomy & Key Components
24
+
25
+ ```
26
+ hexastack_mcp/
27
+ ├── domain/ # McpToolMetadata, McpResourceMetadata, McpPromptMetadata, McpError
28
+ ├── adapters/ # stdio runner, sse/FastAPI router, FastMCP server wrapper
29
+ └── infra/
30
+ ├── bootstrap.py # McpBootstrapper (order=40)
31
+ ├── config.py # HexastackMcpConfig
32
+ ├── decorators.py# @mcp_tool, @mcp_resource, @mcp_prompt
33
+ ├── autodiscovery.py # create_mcp_visitor
34
+ └── registries/ # server.py (McpServerRegistry)
35
+ ```
36
+
37
+ ### Key Exports
38
+
39
+ | Category | Exports |
40
+ |---|---|
41
+ | **Adapters** | `run_stdio_server`, `create_sse_router`, `mount_mcp_sse` |
42
+ | **Bootstrap** | `McpBootstrapper` (order=40), `HexastackMcpConfig` |
43
+ | **Decorators** | `@mcp_tool`, `@mcp_resource`, `@mcp_prompt` |
44
+ | **Registries** | `McpServerRegistry`, `get_mcp_registry` |
45
+
46
+ ---
47
+
48
+ ## 3. Monorepo & Sibling Relationships
49
+
50
+ ```mermaid
51
+ graph TD
52
+ subgraph Agents ["AI Agent Clients (Claude, Cursor, Antigravity)"]
53
+ STDIO_CLIENT["Local Desktop / Subprocess (stdio)"]
54
+ SSE_CLIENT["Remote Agent Network (SSE / HTTP)"]
55
+ end
56
+
57
+ subgraph McpAdapter ["hexastack-mcp"]
58
+ SERVER["FastMCP / MCP Server"]
59
+ REG["McpServerRegistry (Tools, Resources, Prompts)"]
60
+ DISPATCH["CQRS Tool Dispatcher"]
61
+ end
62
+
63
+ subgraph CQRSExecution ["hexastack-cqrs"]
64
+ CBUS["CommandBusPort"]
65
+ QBUS["QueryBusPort"]
66
+ end
67
+
68
+ subgraph WebIntegration ["hexastack-fastapi (Optional)"]
69
+ FASTAPI_APP["FastAPI Application (SSE Endpoint)"]
70
+ end
71
+
72
+ STDIO_CLIENT --> SERVER
73
+ SSE_CLIENT --> FASTAPI_APP
74
+ FASTAPI_APP --> SERVER
75
+ SERVER --> REG
76
+ REG --> DISPATCH
77
+ DISPATCH -->|dispatches commands| CBUS
78
+ DISPATCH -->|dispatches queries| QBUS
79
+ ```
80
+
81
+ ### Explicit Dependencies (Direct)
82
+ - `hexastack-core`: DI container, configuration registry, base exceptions.
83
+ - `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
84
+ - `mcp>=1.3.0`: Official Anthropic Model Context Protocol SDK.
85
+
86
+ ### Implied / Behavioral Relationships (DI-Mediated)
87
+ - **FastAPI SSE Integration**: `McpBootstrapper` (order=40) attaches SSE endpoints to the active `FastAPI` instance when `hexastack-fastapi` is present and `auto_mount_fastapi=true`.
88
+ - **CQRS Dispatching**: When an LLM executes an MCP tool, the adapter resolves `CommandBusPort` or `QueryBusPort` to run the command through the full middleware pipeline.
89
+
90
+ ### Optional Integrations (Extras)
91
+ - `[fastapi]`: Installs `hexastack-fastapi` and `fastapi>=0.141.1` for remote SSE transport over HTTP.
92
+
93
+ ---
94
+
95
+ ## 4. Installation
96
+
97
+ ```bash
98
+ # Standalone stdio transport
99
+ pip install hexastack-mcp
100
+
101
+ # With FastAPI remote SSE transport
102
+ pip install "hexastack-mcp[fastapi]"
103
+
104
+ # Via umbrella package
105
+ pip install "hexastack[mcp]"
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 5. Configuration Reference
111
+
112
+ ```toml
113
+ [hexastack.mcp]
114
+ server_name = "Hexastack MCP Server"
115
+ server_version = "0.1.0"
116
+ sse_path = "/sse" # Route prefix for SSE transport
117
+ auto_mount_fastapi = true # Auto-mount SSE router into FastAPI on bootstrap
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 6. Quickstart Example
123
+
124
+ ```python
125
+ from dataclasses import dataclass
126
+ from hexastack_core.infra.bootstrap import bootstrap
127
+ from hexastack_cqrs.domain.query import Query
128
+ from hexastack_cqrs.infra.decorators import query_handler
129
+ from hexastack_mcp.infra.decorators import mcp_tool
130
+
131
+
132
+ # 1. Define CQRS Query & Handler
133
+ @dataclass(frozen=True)
134
+ class CheckSystemStatusQuery(Query):
135
+ service: str = "database"
136
+
137
+
138
+ @query_handler(CheckSystemStatusQuery)
139
+ class CheckSystemStatusHandler:
140
+ def __call__(self, qry: CheckSystemStatusQuery) -> dict[str, str]:
141
+ return {"service": qry.service, "status": "HEALTHY"}
142
+
143
+
144
+ # 2. Expose as MCP Tool for AI Agents
145
+ mcp_tool(
146
+ name="check_status",
147
+ description="Check the real-time operational status of internal subsystems.",
148
+ )(CheckSystemStatusQuery)
149
+
150
+ # 3. Bootstrap Runtime and Run MCP Server
151
+ runtime = bootstrap(packages_to_scan=[__name__])
152
+ mcp_server = runtime.get("mcp_server")
153
+ ```
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "hexastack-mcp"
3
+ version = "0.0.0"
4
+ description = "Model Context Protocol (MCP) adapter and AI agent tool integration for Hexastack"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "hexastack-core",
9
+ "hexastack-cqrs",
10
+ "mcp>=1.3.0",
11
+ ]
12
+
13
+ [[project.authors]]
14
+ name = "Richard West"
15
+ email = "dopplereffect.us@gmail.com"
16
+
17
+ [project.optional-dependencies]
18
+ fastapi = [
19
+ "fastapi>=0.141.1",
20
+ "hexastack-fastapi",
21
+ ]
22
+
23
+ [project.entry-points."hexastack.bootstrappers"]
24
+ mcp = "hexastack_mcp.infra.bootstrap:McpBootstrapper"
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.12.3,<0.13.0"]
28
+ build-backend = "uv_build"
29
+
30
+ [tool.uv.sources.hexastack-core]
31
+ workspace = true
32
+
33
+ [tool.uv.sources.hexastack-cqrs]
34
+ workspace = true
35
+
36
+ [tool.uv.sources.hexastack-fastapi]
37
+ workspace = true
38
+
39
+ [tool.importlinter]
40
+ root_packages = ["hexastack_mcp"]
41
+
42
+ [[tool.importlinter.contracts]]
43
+ name = "Hexagonal architecture layer hierarchy"
44
+ type = "layers"
45
+ containers = ["hexastack_mcp"]
46
+ layers = [
47
+ "adapters",
48
+ "domain",
49
+ ]
50
+
51
+ [[tool.importlinter.contracts]]
52
+ name = "Forbidden imports for domain"
53
+ type = "forbidden"
54
+ source_modules = ["hexastack_mcp.domain"]
55
+ forbidden_modules = [
56
+ "hexastack_mcp.adapters",
57
+ "hexastack_mcp.infra",
58
+ ]
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "hexastack-mcp"
3
+ version = "0.0.0"
4
+ description = "Model Context Protocol (MCP) adapter and AI agent tool integration for Hexastack"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Richard West", email = "dopplereffect.us@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "hexastack-core",
12
+ "hexastack-cqrs",
13
+ "mcp>=1.3.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ fastapi = [
18
+ "fastapi>=0.141.1",
19
+ "hexastack-fastapi",
20
+ ]
21
+
22
+ [project.entry-points."hexastack.bootstrappers"]
23
+ mcp = "hexastack_mcp.infra.bootstrap:McpBootstrapper"
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.12.3,<0.13.0"]
27
+ build-backend = "uv_build"
28
+
29
+ [tool.uv.sources]
30
+ hexastack-core = { workspace = true }
31
+ hexastack-cqrs = { workspace = true }
32
+ hexastack-fastapi = { workspace = true }
33
+
34
+ [tool.importlinter]
35
+ root_packages = ["hexastack_mcp"]
36
+
37
+ [[tool.importlinter.contracts]]
38
+ name = "Hexagonal architecture layer hierarchy"
39
+ type = "layers"
40
+ containers = ["hexastack_mcp"]
41
+ layers = [
42
+ "adapters",
43
+ "domain",
44
+ ]
45
+
46
+ [[tool.importlinter.contracts]]
47
+ name = "Forbidden imports for domain"
48
+ type = "forbidden"
49
+ source_modules = ["hexastack_mcp.domain"]
50
+ forbidden_modules = [
51
+ "hexastack_mcp.adapters",
52
+ "hexastack_mcp.infra",
53
+ ]
@@ -0,0 +1,7 @@
1
+ from hexastack_mcp import adapters, domain, infra
2
+
3
+ __all__ = [
4
+ "adapters",
5
+ "domain",
6
+ "infra",
7
+ ]
@@ -0,0 +1,58 @@
1
+ from typing import Any
2
+
3
+ from mcp.server.fastmcp import FastMCP as McpServer
4
+ from mcp.server.transport_security import TransportSecuritySettings
5
+ from starlette.applications import Starlette
6
+
7
+ __all__ = [
8
+ "create_mcp_sse_app",
9
+ "mount_mcp_sse",
10
+ ]
11
+
12
+
13
+ def create_mcp_sse_app(
14
+ server: McpServer,
15
+ sse_path: str = "/sse",
16
+ message_path: str = "/messages/",
17
+ transport_security: TransportSecuritySettings | None = None,
18
+ ) -> Starlette:
19
+ """Create a Starlette ASGI application handling MCP SSE transport.
20
+
21
+ Args:
22
+ server: Configured McpServer instance.
23
+ sse_path: Endpoint path for SSE streams.
24
+ message_path: Endpoint path for posting messages.
25
+ transport_security: Optional TransportSecuritySettings instance.
26
+
27
+ Returns:
28
+ Starlette ASGI application.
29
+ """
30
+ return server.sse_app()
31
+
32
+
33
+ def mount_mcp_sse(
34
+ app: Any,
35
+ server: McpServer,
36
+ path_prefix: str = "/mcp",
37
+ sse_path: str = "/sse",
38
+ transport_security: TransportSecuritySettings | None = None,
39
+ ) -> None:
40
+ """Mount MCP SSE endpoints onto a FastAPI/Starlette application.
41
+
42
+ Notes/Architectural Intent:
43
+ Enables remote LLM orchestration frameworks to connect to Hexastack
44
+ services over HTTP SSE streams.
45
+
46
+ Args:
47
+ app: Target FastAPI or Starlette application.
48
+ server: Configured McpServer instance.
49
+ path_prefix: Route prefix where the SSE sub-app is mounted.
50
+ sse_path: SSE stream path within the sub-app.
51
+ transport_security: Optional TransportSecuritySettings instance.
52
+ """
53
+ sse_app = create_mcp_sse_app(
54
+ server,
55
+ sse_path=sse_path,
56
+ transport_security=transport_security,
57
+ )
58
+ app.mount(path_prefix, sse_app)
@@ -0,0 +1,30 @@
1
+ import asyncio
2
+
3
+ from mcp.server.fastmcp import FastMCP as McpServer
4
+
5
+ __all__ = [
6
+ "run_stdio_async",
7
+ "run_stdio_server",
8
+ ]
9
+
10
+
11
+ async def run_stdio_async(server: McpServer) -> None:
12
+ """Asynchronously execute the MCP stdio communication loop.
13
+
14
+ Args:
15
+ server: Configured McpServer instance.
16
+ """
17
+ await server.run_stdio_async()
18
+
19
+
20
+ def run_stdio_server(server: McpServer) -> None:
21
+ """Synchronously execute the MCP stdio communication loop via asyncio.run().
22
+
23
+ Notes/Architectural Intent:
24
+ Standard entrypoint for CLI stdio execution when orchestrated by
25
+ Claude Desktop, Cursor, or external LLM tool executors.
26
+
27
+ Args:
28
+ server: Configured McpServer instance.
29
+ """
30
+ asyncio.run(run_stdio_async(server))
@@ -0,0 +1,29 @@
1
+ from hexastack_core.domain.exceptions import HexastackError
2
+
3
+
4
+ class McpError(HexastackError):
5
+ """Base exception for all Model Context Protocol (MCP) adapter errors.
6
+
7
+ Notes/Architectural Intent:
8
+ Maintains consistency across the unified Hexastack exception tree.
9
+ """
10
+
11
+
12
+ class ToolExecutionError(McpError):
13
+ """Exception raised when an MCP tool execution fails.
14
+
15
+ Notes/Architectural Intent:
16
+ Carries context when command or query dispatching from an MCP client
17
+ encounters an error.
18
+ """
19
+
20
+
21
+ class ResourceNotFoundError(McpError):
22
+ """Exception raised when a requested MCP resource URI is not found."""
23
+
24
+
25
+ __all__ = [
26
+ "McpError",
27
+ "ResourceNotFoundError",
28
+ "ToolExecutionError",
29
+ ]
@@ -0,0 +1,45 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class McpToolMetadata:
8
+ """Metadata for exposing a Command, Query, or function as an MCP Tool.
9
+
10
+ Notes/Architectural Intent:
11
+ Parsed by McpServerRegistry and single-pass autodiscovery visitors
12
+ to register MCP tools on the FastMCP/MCP server.
13
+ """
14
+
15
+ name: str
16
+ description: str | None = None
17
+ kind: str = "command" # "command", "query", or "function"
18
+ target: Any | None = None
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class McpResourceMetadata:
23
+ """Metadata for exposing data or endpoints as an MCP Resource."""
24
+
25
+ uri: str
26
+ name: str
27
+ description: str | None = None
28
+ mime_type: str = "application/json"
29
+ handler: Callable[..., Any] | None = None
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class McpPromptMetadata:
34
+ """Metadata for exposing prompt templates to AI agents."""
35
+
36
+ name: str
37
+ description: str | None = None
38
+ handler: Callable[..., Any] | None = None
39
+
40
+
41
+ __all__ = [
42
+ "McpPromptMetadata",
43
+ "McpResourceMetadata",
44
+ "McpToolMetadata",
45
+ ]
@@ -0,0 +1,75 @@
1
+ from collections.abc import Sequence
2
+ from types import ModuleType
3
+ from typing import Any
4
+
5
+ from hexastack_core.infra.autodiscovery import (
6
+ DiscoveryVisitor,
7
+ scan_modules,
8
+ )
9
+ from hexastack_mcp.domain.metadata import (
10
+ McpPromptMetadata,
11
+ McpResourceMetadata,
12
+ McpToolMetadata,
13
+ )
14
+ from hexastack_mcp.infra.decorators import (
15
+ _MCP_PROMPT_ATTR,
16
+ _MCP_RESOURCE_ATTR,
17
+ _MCP_TOOL_ATTR,
18
+ )
19
+ from hexastack_mcp.infra.registries.server import McpServerRegistry
20
+
21
+ __all__ = [
22
+ "autodiscover_mcp_elements",
23
+ "create_mcp_visitor",
24
+ ]
25
+
26
+
27
+ def autodiscover_mcp_elements(
28
+ packages_to_scan: Sequence[str | ModuleType],
29
+ registry: McpServerRegistry,
30
+ ) -> McpServerRegistry:
31
+ """Discover decorated MCP tools, resources, and prompts from packages.
32
+
33
+ Args:
34
+ packages_to_scan: Sequence of package names or module objects to inspect.
35
+ registry: Target McpServerRegistry instance.
36
+
37
+ Returns:
38
+ The populated McpServerRegistry instance.
39
+ """
40
+ visitor = create_mcp_visitor(registry)
41
+ scan_modules(packages_to_scan, [visitor])
42
+ return registry
43
+
44
+
45
+ def create_mcp_visitor(
46
+ registry: McpServerRegistry,
47
+ ) -> DiscoveryVisitor:
48
+ """Create a DiscoveryVisitor callback for single-pass MCP element discovery.
49
+
50
+ Notes/Architectural Intent:
51
+ Inspects discovered classes and functions for MCP tool, resource, and prompt
52
+ decorator metadata, registering them into the supplied server registry
53
+ during single-pass reflection.
54
+
55
+ Args:
56
+ registry: Target McpServerRegistry instance.
57
+
58
+ Returns:
59
+ DiscoveryVisitor callable accepting (member, module).
60
+ """
61
+
62
+ def visitor(obj: Any, module: ModuleType) -> None:
63
+ tool_meta: McpToolMetadata | None = getattr(obj, _MCP_TOOL_ATTR, None)
64
+ if tool_meta is not None:
65
+ registry.register_tool(tool_meta)
66
+
67
+ res_meta: McpResourceMetadata | None = getattr(obj, _MCP_RESOURCE_ATTR, None)
68
+ if res_meta is not None:
69
+ registry.register_resource(res_meta)
70
+
71
+ prompt_meta: McpPromptMetadata | None = getattr(obj, _MCP_PROMPT_ATTR, None)
72
+ if prompt_meta is not None:
73
+ registry.register_prompt(prompt_meta)
74
+
75
+ return visitor
@@ -0,0 +1,108 @@
1
+ import importlib.util
2
+ from dataclasses import dataclass
3
+
4
+ from mcp.server.fastmcp import FastMCP as McpServer
5
+ from mcp.server.transport_security import TransportSecuritySettings
6
+
7
+ from hexastack_core.infra.bootstrap import (
8
+ BootstrapContext,
9
+ )
10
+ from hexastack_core.infra.registries.config import ConfigRegistry
11
+ from hexastack_core.ports.bootstrap import BootstrapperPort
12
+ from hexastack_mcp.infra.autodiscovery import create_mcp_visitor
13
+ from hexastack_mcp.infra.config import (
14
+ HexastackMcpConfig,
15
+ register_mcp_config,
16
+ )
17
+ from hexastack_mcp.infra.decorators import get_mcp_registry
18
+ from hexastack_mcp.infra.registries.server import McpServerRegistry
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class McpBootstrapResult:
23
+ """Dataclass holding initialized MCP server and configuration."""
24
+
25
+ config: HexastackMcpConfig
26
+ server: McpServer
27
+ registry: McpServerRegistry
28
+
29
+
30
+ class McpBootstrapper(BootstrapperPort):
31
+ """Bootstrap extension configuring Anthropic Model Context Protocol (MCP) server.
32
+
33
+ Notes/Architectural Intent:
34
+ Implements BootstrapperPort with order=40 (executing after CQRS order=20
35
+ and FastAPI order=30), registering the autodiscovery visitor, assembling
36
+ the McpServer instance with CQRS tool wrappers, and mounting SSE endpoints
37
+ onto FastAPI when present.
38
+ """
39
+
40
+ name: str = "mcp"
41
+ order: int = 40
42
+
43
+ def configure(self, context: BootstrapContext) -> None:
44
+ """Phase 2: Register visitor, assemble McpServer, and mount SSE endpoints.
45
+
46
+ Args:
47
+ context: BootstrapContext containing DI container, config, and properties.
48
+
49
+ Returns:
50
+ None.
51
+ """
52
+ cfg = context.get_config("mcp", HexastackMcpConfig)
53
+
54
+ registry = get_mcp_registry()
55
+
56
+ # Register visitor for single-pass reflective scanning (Phase 3)
57
+ visitor = create_mcp_visitor(registry)
58
+ context.register_visitor(visitor)
59
+
60
+ # 1. Build McpServer instance from registry and container
61
+ server = registry.build_server(config=cfg, container=context.container)
62
+
63
+ # 2. Register Server and Registry into DI container
64
+ context.container.add_instance(server, declared_class=McpServer)
65
+ context.container.add_instance(registry)
66
+
67
+ # 3. Mount onto FastAPI app if available and configured
68
+ if cfg.auto_mount_fastapi and importlib.util.find_spec("fastapi") is not None:
69
+ fastapi_app = context.properties.get("app")
70
+ if fastapi_app is not None:
71
+ from hexastack_mcp.adapters.fastapi import mount_mcp_sse
72
+
73
+ sec = TransportSecuritySettings(
74
+ enable_dns_rebinding_protection=cfg.enable_dns_rebinding_protection,
75
+ allowed_hosts=cfg.allowed_hosts,
76
+ )
77
+ mount_mcp_sse(
78
+ app=fastapi_app,
79
+ server=server,
80
+ path_prefix=cfg.sse_path,
81
+ transport_security=sec,
82
+ )
83
+
84
+ # 4. Store in context properties
85
+ result = McpBootstrapResult(
86
+ config=cfg,
87
+ server=server,
88
+ registry=registry,
89
+ )
90
+ context.properties["mcp_result"] = result
91
+ context.properties["mcp_server"] = server
92
+
93
+ def register_config(self, registry: ConfigRegistry) -> None:
94
+ """Phase 1: Register MCP configuration schema under 'mcp'.
95
+
96
+ Args:
97
+ registry: Target ConfigRegistry instance.
98
+
99
+ Returns:
100
+ None.
101
+ """
102
+ register_mcp_config(registry)
103
+
104
+
105
+ __all__ = [
106
+ "McpBootstrapper",
107
+ "McpBootstrapResult",
108
+ ]
@@ -0,0 +1,70 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from hexastack_core.infra.decorators import config_section
4
+ from hexastack_core.infra.registries.config import ConfigRegistry
5
+
6
+
7
+ @config_section("mcp")
8
+ class HexastackMcpConfig(BaseModel):
9
+ """Configuration schema for Hexastack Model Context Protocol (MCP) server.
10
+
11
+ Notes/Architectural Intent:
12
+ Controls server naming, SSE routing paths, transport security,
13
+ and automatic FastAPI endpoint mounting.
14
+ """
15
+
16
+ server_name: str = Field(
17
+ default="Hexastack MCP Server",
18
+ description="Name of the MCP server announced to AI clients.",
19
+ )
20
+ server_version: str = Field(
21
+ default="0.1.0",
22
+ description="Version string of the MCP server.",
23
+ )
24
+ sse_path: str = Field(
25
+ default="/sse",
26
+ description="Route path for SSE transport when mounted on FastAPI.",
27
+ )
28
+ auto_mount_fastapi: bool = Field(
29
+ default=True,
30
+ description="Automatically mount SSE routes onto FastAPI application during bootstrap.",
31
+ )
32
+ instructions: str | None = Field(
33
+ default="Hexastack domain service tools and resources for AI assistants.",
34
+ description="System prompt / instructions sent to MCP client on initialization.",
35
+ )
36
+ enable_dns_rebinding_protection: bool = Field(
37
+ default=True,
38
+ description="Enable DNS rebinding protection for incoming SSE requests.",
39
+ )
40
+ allowed_hosts: list[str] = Field(
41
+ default_factory=lambda: [
42
+ "127.0.0.1:*",
43
+ "localhost:*",
44
+ "testserver:*",
45
+ "testserver",
46
+ "127.0.0.1",
47
+ "localhost",
48
+ "[::1]:*",
49
+ "::1",
50
+ ],
51
+ description="Allowed Host header values for SSE transport security.",
52
+ )
53
+
54
+
55
+ __all__ = [
56
+ "HexastackMcpConfig",
57
+ "register_mcp_config",
58
+ ]
59
+
60
+
61
+ def register_mcp_config(registry: ConfigRegistry) -> None:
62
+ """Register MCP configuration schema under 'mcp'.
63
+
64
+ Args:
65
+ registry: Target ConfigRegistry instance.
66
+
67
+ Returns:
68
+ None.
69
+ """
70
+ registry.register_config_section("mcp", HexastackMcpConfig)
@@ -0,0 +1,132 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ from hexastack_mcp.domain.metadata import (
5
+ McpPromptMetadata,
6
+ McpResourceMetadata,
7
+ McpToolMetadata,
8
+ )
9
+ from hexastack_mcp.infra.registries.server import McpServerRegistry
10
+
11
+ _MCP_TOOL_ATTR = "__hexastack_mcp_tool__"
12
+ _MCP_RESOURCE_ATTR = "__hexastack_mcp_resource__"
13
+ _MCP_PROMPT_ATTR = "__hexastack_mcp_prompt__"
14
+
15
+ _default_registry = McpServerRegistry()
16
+
17
+
18
+ __all__ = [
19
+ "get_mcp_registry",
20
+ "mcp_prompt",
21
+ "mcp_resource",
22
+ "mcp_tool",
23
+ ]
24
+
25
+
26
+ def get_mcp_registry() -> McpServerRegistry:
27
+ """Return the global default McpServerRegistry instance.
28
+
29
+ Returns:
30
+ McpServerRegistry instance.
31
+ """
32
+ return _default_registry
33
+
34
+
35
+ def mcp_prompt(
36
+ name: str | None = None,
37
+ *,
38
+ description: str | None = None,
39
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
40
+ """Decorator exposing a function as an MCP Prompt template.
41
+
42
+ Args:
43
+ name: Name of the prompt.
44
+ description: Description of the prompt template.
45
+
46
+ Returns:
47
+ Decorator function.
48
+ """
49
+
50
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
51
+ prompt_name = name or getattr(fn, "__name__", "prompt")
52
+ meta = McpPromptMetadata(
53
+ name=prompt_name,
54
+ description=description or getattr(fn, "__doc__", None),
55
+ handler=fn,
56
+ )
57
+ setattr(fn, _MCP_PROMPT_ATTR, meta)
58
+ _default_registry.register_prompt(meta)
59
+ return fn
60
+
61
+ return decorator
62
+
63
+
64
+ def mcp_resource(
65
+ uri: str,
66
+ name: str | None = None,
67
+ *,
68
+ description: str | None = None,
69
+ mime_type: str = "application/json",
70
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
71
+ """Decorator exposing a function as a readable MCP Resource.
72
+
73
+ Args:
74
+ uri: MCP resource URI template (e.g. 'hexastack://info').
75
+ name: Optional resource display name.
76
+ description: Optional resource description.
77
+ mime_type: MIME type of the returned payload.
78
+
79
+ Returns:
80
+ Decorator function.
81
+ """
82
+
83
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
84
+ res_name = name or getattr(fn, "__name__", "resource")
85
+ meta = McpResourceMetadata(
86
+ uri=uri,
87
+ name=res_name,
88
+ description=description or getattr(fn, "__doc__", None),
89
+ mime_type=mime_type,
90
+ handler=fn,
91
+ )
92
+ setattr(fn, _MCP_RESOURCE_ATTR, meta)
93
+ _default_registry.register_resource(meta)
94
+ return fn
95
+
96
+ return decorator
97
+
98
+
99
+ def mcp_tool(
100
+ name: str | None = None,
101
+ *,
102
+ description: str | None = None,
103
+ kind: str = "command",
104
+ ) -> Callable[[Any], Any]:
105
+ """Decorator exposing a Command class, Query class, or function as an MCP Tool.
106
+
107
+ Notes/Architectural Intent:
108
+ Attaches discovery metadata for single-pass module scanning and registers
109
+ the tool in the default McpServerRegistry.
110
+
111
+ Args:
112
+ name: Name of the tool presented to AI agents. Defaults to kebab/snake class name.
113
+ description: Description of tool utility.
114
+ kind: 'command', 'query', or 'function'.
115
+
116
+ Returns:
117
+ Decorator function.
118
+ """
119
+
120
+ def decorator(target: Any) -> Any:
121
+ tool_name = name or getattr(target, "__name__", "tool")
122
+ meta = McpToolMetadata(
123
+ name=tool_name,
124
+ description=description or getattr(target, "__doc__", None),
125
+ kind=kind,
126
+ target=target,
127
+ )
128
+ setattr(target, _MCP_TOOL_ATTR, meta)
129
+ _default_registry.register_tool(meta)
130
+ return target
131
+
132
+ return decorator
@@ -0,0 +1,256 @@
1
+ import inspect
2
+ import json
3
+ import platform
4
+ import sys
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from mcp.server.fastmcp import FastMCP as McpServer
9
+ from rodi import Container
10
+
11
+ from hexastack_core.domain.command import Command
12
+ from hexastack_core.utils.inspection import inspect_model_parameters
13
+ from hexastack_cqrs.ports.buses import (
14
+ CommandBusPort,
15
+ QueryBusPort,
16
+ )
17
+ from hexastack_mcp.domain.exceptions import ToolExecutionError
18
+ from hexastack_mcp.domain.metadata import (
19
+ McpPromptMetadata,
20
+ McpResourceMetadata,
21
+ McpToolMetadata,
22
+ )
23
+ from hexastack_mcp.infra.config import HexastackMcpConfig
24
+
25
+
26
+ class McpServerRegistry:
27
+ """Registry maintaining registered MCP tools, resources, and prompt templates.
28
+
29
+ Notes/Architectural Intent:
30
+ Compiles declarative tool and resource definitions into an McpServer
31
+ (FastMCP) instance, binding CQRS dispatchers from the rodi DI Container.
32
+ """
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize empty MCP registry."""
36
+ self._tools: list[McpToolMetadata] = []
37
+ self._resources: list[McpResourceMetadata] = []
38
+ self._prompts: list[McpPromptMetadata] = []
39
+
40
+ def _create_cqrs_tool_wrapper(
41
+ self,
42
+ target_cls: type[Any],
43
+ kind: str,
44
+ container: Container,
45
+ ) -> Callable[..., Any]:
46
+ """Synthesize a typed callable from a Command or Query class for MCP schema generation."""
47
+ parameters = inspect_model_parameters(target_cls)
48
+
49
+ async def dynamic_mcp_tool(**kwargs: Any) -> Any:
50
+ try:
51
+ instance = target_cls(**kwargs)
52
+ return await self._dispatch_cqrs_instance(
53
+ instance, target_cls, kind, container
54
+ )
55
+ except Exception as exc:
56
+ raise ToolExecutionError(
57
+ f"Execution of MCP tool '{target_cls.__name__}' failed: {exc}"
58
+ ) from exc
59
+
60
+ # Set dynamic signature & annotations
61
+ setattr( # noqa: B010
62
+ dynamic_mcp_tool,
63
+ "__signature__",
64
+ inspect.Signature(parameters=parameters),
65
+ )
66
+ dynamic_mcp_tool.__annotations__ = {p.name: p.annotation for p in parameters}
67
+ dynamic_mcp_tool.__name__ = target_cls.__name__
68
+ dynamic_mcp_tool.__doc__ = target_cls.__doc__
69
+ return dynamic_mcp_tool
70
+
71
+ async def _dispatch_cqrs_instance(
72
+ self,
73
+ instance: Any,
74
+ target_cls: type[Any],
75
+ kind: str,
76
+ container: Container,
77
+ ) -> Any:
78
+ """Resolve bus from container and dispatch command/query instance."""
79
+ if kind == "command" or issubclass(target_cls, Command):
80
+ cbus = container.resolve(CommandBusPort)
81
+ result = cbus.dispatch(instance)
82
+ else:
83
+ qbus = container.resolve(QueryBusPort)
84
+ result = qbus.dispatch(instance)
85
+
86
+ if inspect.isawaitable(result):
87
+ result = await result
88
+ return result
89
+
90
+ def _mount_prompts(self, server: McpServer) -> None:
91
+ """Register all prompt templates onto McpServer instance."""
92
+ for prompt_meta in self._prompts:
93
+ if prompt_meta.handler is not None:
94
+ server.prompt(
95
+ name=prompt_meta.name,
96
+ description=prompt_meta.description,
97
+ )(prompt_meta.handler)
98
+
99
+ def _mount_resources(self, server: McpServer) -> None:
100
+ """Register all resource endpoints onto McpServer instance."""
101
+ for res_meta in self._resources:
102
+ if res_meta.handler is not None:
103
+ server.resource(
104
+ uri=res_meta.uri,
105
+ name=res_meta.name,
106
+ description=res_meta.description,
107
+ mime_type=res_meta.mime_type,
108
+ )(res_meta.handler)
109
+
110
+ def _mount_tools(self, server: McpServer, container: Container) -> None:
111
+ """Register all tool wrappers onto McpServer instance."""
112
+ for tool_meta in self._tools:
113
+ if inspect.isclass(tool_meta.target):
114
+ tool_fn = self._create_cqrs_tool_wrapper(
115
+ target_cls=tool_meta.target,
116
+ kind=tool_meta.kind,
117
+ container=container,
118
+ )
119
+ server.add_tool(
120
+ tool_fn,
121
+ name=tool_meta.name,
122
+ description=tool_meta.description or tool_fn.__doc__,
123
+ )
124
+ elif callable(tool_meta.target):
125
+ server.add_tool(
126
+ tool_meta.target,
127
+ name=tool_meta.name,
128
+ description=tool_meta.description or tool_meta.target.__doc__,
129
+ )
130
+
131
+ def _register_diagnostic_resources(
132
+ self, server: McpServer, config: HexastackMcpConfig
133
+ ) -> None:
134
+ """Register built-in system and registry diagnostic resources."""
135
+
136
+ @server.resource(
137
+ uri="hexastack://info",
138
+ name="system_info",
139
+ description="System platform and Hexastack framework runtime diagnostic information.",
140
+ mime_type="application/json",
141
+ )
142
+ def get_system_info_resource() -> str:
143
+ return json.dumps(
144
+ {
145
+ "platform": platform.platform(),
146
+ "python_version": sys.version,
147
+ "server_name": config.server_name,
148
+ "tools_count": len(self._tools),
149
+ "resources_count": len(self._resources),
150
+ "prompts_count": len(self._prompts),
151
+ },
152
+ indent=2,
153
+ )
154
+
155
+ @server.resource(
156
+ uri="hexastack://registry",
157
+ name="registry_manifest",
158
+ description="Manifest of all registered tools, resources, and prompt templates in Hexastack.",
159
+ mime_type="application/json",
160
+ )
161
+ def get_registry_manifest_resource() -> str:
162
+ return json.dumps(
163
+ {
164
+ "tools": [
165
+ {
166
+ "name": t.name,
167
+ "description": t.description,
168
+ "kind": t.kind,
169
+ }
170
+ for t in self._tools
171
+ ],
172
+ "resources": [
173
+ {
174
+ "uri": r.uri,
175
+ "name": r.name,
176
+ "description": r.description,
177
+ }
178
+ for r in self._resources
179
+ ],
180
+ "prompts": [
181
+ {"name": p.name, "description": p.description}
182
+ for p in self._prompts
183
+ ],
184
+ },
185
+ indent=2,
186
+ )
187
+
188
+ def build_server(
189
+ self,
190
+ config: HexastackMcpConfig,
191
+ container: Container,
192
+ ) -> McpServer:
193
+ """Construct and populate an McpServer instance from registered elements.
194
+
195
+ Args:
196
+ config: HexastackMcpConfig options.
197
+ container: Active rodi DI container for dependency resolution.
198
+
199
+ Returns:
200
+ Configured McpServer instance.
201
+ """
202
+ server = McpServer(
203
+ name=config.server_name,
204
+ instructions=config.instructions,
205
+ )
206
+
207
+ self._register_diagnostic_resources(server, config)
208
+ self._mount_tools(server, container)
209
+ self._mount_resources(server)
210
+ self._mount_prompts(server)
211
+
212
+ return server
213
+
214
+ @property
215
+ def prompts(self) -> list[McpPromptMetadata]:
216
+ return list(self._prompts)
217
+
218
+ def register_prompt(self, meta: McpPromptMetadata) -> None:
219
+ """Register prompt template metadata.
220
+
221
+ Args:
222
+ meta: McpPromptMetadata instance.
223
+ """
224
+ if meta not in self._prompts:
225
+ self._prompts.append(meta)
226
+
227
+ def register_resource(self, meta: McpResourceMetadata) -> None:
228
+ """Register resource metadata.
229
+
230
+ Args:
231
+ meta: McpResourceMetadata instance.
232
+ """
233
+ if meta not in self._resources:
234
+ self._resources.append(meta)
235
+
236
+ def register_tool(self, meta: McpToolMetadata) -> None:
237
+ """Register tool metadata.
238
+
239
+ Args:
240
+ meta: McpToolMetadata instance.
241
+ """
242
+ if meta not in self._tools:
243
+ self._tools.append(meta)
244
+
245
+ @property
246
+ def resources(self) -> list[McpResourceMetadata]:
247
+ return list(self._resources)
248
+
249
+ @property
250
+ def tools(self) -> list[McpToolMetadata]:
251
+ return list(self._tools)
252
+
253
+
254
+ __all__ = [
255
+ "McpServerRegistry",
256
+ ]