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.
- hexastack_mcp-0.0.0/PKG-INFO +168 -0
- hexastack_mcp-0.0.0/README.md +153 -0
- hexastack_mcp-0.0.0/pyproject.toml +58 -0
- hexastack_mcp-0.0.0/pyproject.toml.orig +53 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/__init__.py +7 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/adapters/__init__.py +0 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/adapters/fastapi.py +58 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/adapters/stdio.py +30 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/domain/__init__.py +0 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/domain/exceptions.py +29 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/domain/metadata.py +45 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/__init__.py +0 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/autodiscovery.py +75 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/bootstrap.py +108 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/config.py +70 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/decorators.py +132 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/registries/__init__.py +0 -0
- hexastack_mcp-0.0.0/src/hexastack_mcp/infra/registries/server.py +256 -0
|
@@ -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
|
+
[](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
|
+
[](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
|
+
]
|
|
File without changes
|
|
@@ -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))
|
|
File without changes
|
|
@@ -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
|
+
]
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
@@ -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
|
+
]
|