reflex-mcphost 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reflex_mcphost/__init__.py +296 -0
- reflex_mcphost/adapters/__init__.py +1 -0
- reflex_mcphost/adapters/langgraph.py +77 -0
- reflex_mcphost/adapters/pydantic_ai.py +90 -0
- reflex_mcphost/app/__init__.py +1 -0
- reflex_mcphost/app/cli.py +619 -0
- reflex_mcphost/app/web/__init__.py +7 -0
- reflex_mcphost/app/web/mcphost_app/__init__.py +1 -0
- reflex_mcphost/app/web/mcphost_app/mcphost_app.py +170 -0
- reflex_mcphost/app/web/rxconfig.py +35 -0
- reflex_mcphost/app/workspace.py +215 -0
- reflex_mcphost/core/__init__.py +4 -0
- reflex_mcphost/core/apps/__init__.py +9 -0
- reflex_mcphost/core/apps/bridge.py +612 -0
- reflex_mcphost/core/apps/constants.py +86 -0
- reflex_mcphost/core/apps/csp.py +177 -0
- reflex_mcphost/core/apps/messages.py +163 -0
- reflex_mcphost/core/apps/resource.py +102 -0
- reflex_mcphost/core/audit.py +473 -0
- reflex_mcphost/core/auth/__init__.py +1 -0
- reflex_mcphost/core/auth/callback_server.py +277 -0
- reflex_mcphost/core/auth/flow.py +84 -0
- reflex_mcphost/core/auth/oauth.py +578 -0
- reflex_mcphost/core/auth/token_store.py +323 -0
- reflex_mcphost/core/catalog.py +852 -0
- reflex_mcphost/core/config/__init__.py +1 -0
- reflex_mcphost/core/config/claude.py +18 -0
- reflex_mcphost/core/config/common.py +310 -0
- reflex_mcphost/core/config/cursor.py +18 -0
- reflex_mcphost/core/config/expand.py +69 -0
- reflex_mcphost/core/config/mcp_json.py +33 -0
- reflex_mcphost/core/config/models.py +292 -0
- reflex_mcphost/core/config/own.py +497 -0
- reflex_mcphost/core/config/paths.py +124 -0
- reflex_mcphost/core/config/vscode.py +19 -0
- reflex_mcphost/core/connection/__init__.py +1 -0
- reflex_mcphost/core/connection/era_cache.py +183 -0
- reflex_mcphost/core/connection/inflight.py +89 -0
- reflex_mcphost/core/connection/lease.py +71 -0
- reflex_mcphost/core/connection/manager.py +458 -0
- reflex_mcphost/core/connection/state.py +87 -0
- reflex_mcphost/core/connection/stderr.py +164 -0
- reflex_mcphost/core/connection/supervisor.py +926 -0
- reflex_mcphost/core/connection/transports.py +364 -0
- reflex_mcphost/core/consent/__init__.py +5 -0
- reflex_mcphost/core/consent/index.py +152 -0
- reflex_mcphost/core/consent/models.py +243 -0
- reflex_mcphost/core/consent/outcome.py +39 -0
- reflex_mcphost/core/consent/policy.py +370 -0
- reflex_mcphost/core/consent/preview.py +224 -0
- reflex_mcphost/core/consent/store.py +328 -0
- reflex_mcphost/core/db.py +121 -0
- reflex_mcphost/core/db_models.py +84 -0
- reflex_mcphost/core/errors.py +156 -0
- reflex_mcphost/core/events.py +211 -0
- reflex_mcphost/core/handlers/__init__.py +1 -0
- reflex_mcphost/core/handlers/base.py +37 -0
- reflex_mcphost/core/handlers/capabilities.py +89 -0
- reflex_mcphost/core/handlers/elicitation.py +218 -0
- reflex_mcphost/core/handlers/logging.py +48 -0
- reflex_mcphost/core/handlers/roots.py +105 -0
- reflex_mcphost/core/handlers/sampling.py +336 -0
- reflex_mcphost/core/hmac_key.py +71 -0
- reflex_mcphost/core/host.py +559 -0
- reflex_mcphost/core/invoker.py +817 -0
- reflex_mcphost/core/logging.py +162 -0
- reflex_mcphost/core/migrations/0001_initial.py +144 -0
- reflex_mcphost/core/migrations/__init__.py +5 -0
- reflex_mcphost/core/migrations/runner.py +128 -0
- reflex_mcphost/core/mrtr.py +504 -0
- reflex_mcphost/core/outcomes.py +42 -0
- reflex_mcphost/core/prompter.py +213 -0
- reflex_mcphost/core/registry.py +387 -0
- reflex_mcphost/core/schema.py +743 -0
- reflex_mcphost/core/secret_store.py +150 -0
- reflex_mcphost/core/secrets.py +64 -0
- reflex_mcphost/core/toolset/__init__.py +11 -0
- reflex_mcphost/core/toolset/aliases.py +74 -0
- reflex_mcphost/core/toolset/filters.py +38 -0
- reflex_mcphost/core/toolset/toolset.py +193 -0
- reflex_mcphost/core/trust.py +269 -0
- reflex_mcphost/core/types.py +101 -0
- reflex_mcphost/py.typed +0 -0
- reflex_mcphost/toolset/__init__.py +12 -0
- reflex_mcphost/ui/__init__.py +60 -0
- reflex_mcphost/ui/actions.py +340 -0
- reflex_mcphost/ui/apps_bridge.py +474 -0
- reflex_mcphost/ui/assets/__init__.py +38 -0
- reflex_mcphost/ui/assets/mcp_app_relay.js +93 -0
- reflex_mcphost/ui/components/__init__.py +23 -0
- reflex_mcphost/ui/components/audit_table.py +136 -0
- reflex_mcphost/ui/components/catalog_browser.py +361 -0
- reflex_mcphost/ui/components/consent_center.py +468 -0
- reflex_mcphost/ui/components/filters.py +9 -0
- reflex_mcphost/ui/components/mcp_app_frame.py +160 -0
- reflex_mcphost/ui/components/permissions_table.py +131 -0
- reflex_mcphost/ui/components/schema_form.py +148 -0
- reflex_mcphost/ui/components/servers_panel.py +250 -0
- reflex_mcphost/ui/install.py +440 -0
- reflex_mcphost/ui/models.py +1169 -0
- reflex_mcphost/ui/prompter.py +346 -0
- reflex_mcphost/ui/proxy/__init__.py +23 -0
- reflex_mcphost/ui/proxy/proxy.html +117 -0
- reflex_mcphost/ui/proxy/server.py +457 -0
- reflex_mcphost/ui/state.py +1015 -0
- reflex_mcphost-0.1.0.dist-info/METADATA +303 -0
- reflex_mcphost-0.1.0.dist-info/RECORD +110 -0
- reflex_mcphost-0.1.0.dist-info/WHEEL +4 -0
- reflex_mcphost-0.1.0.dist-info/entry_points.txt +2 -0
- reflex_mcphost-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""reflex-mcphost: an open MCP host built with Reflex (public API re-exports, API Spec §3.6).
|
|
2
|
+
|
|
3
|
+
The public names are resolved lazily (PEP 562) so ``import reflex_mcphost`` stays cheap and
|
|
4
|
+
pulls in neither the MCP SDK nor SQLite until a name is first used.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from importlib import import_module
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
# Libraries never configure output (REQ-NFR-040): applications attach their own handlers.
|
|
15
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__: str = version("reflex-mcphost")
|
|
19
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree without metadata
|
|
20
|
+
__version__ = "0.0.0"
|
|
21
|
+
|
|
22
|
+
_EXPORTS: dict[str, str] = {
|
|
23
|
+
# facade and configuration (API §4.1-4.3)
|
|
24
|
+
"Host": "reflex_mcphost.core.host",
|
|
25
|
+
"HostConfig": "reflex_mcphost.core.config.models",
|
|
26
|
+
"ServerDefinition": "reflex_mcphost.core.config.models",
|
|
27
|
+
"StdioTransport": "reflex_mcphost.core.config.models",
|
|
28
|
+
"HttpTransport": "reflex_mcphost.core.config.models",
|
|
29
|
+
"OAuthSettings": "reflex_mcphost.core.config.models",
|
|
30
|
+
"ConfigOrigin": "reflex_mcphost.core.config.models",
|
|
31
|
+
"SourceKind": "reflex_mcphost.core.config.models",
|
|
32
|
+
"ShadowedDefinition": "reflex_mcphost.core.config.models",
|
|
33
|
+
"ConfigProblem": "reflex_mcphost.core.config.models",
|
|
34
|
+
"ConfigProblemCode": "reflex_mcphost.core.config.models",
|
|
35
|
+
"RegistryDiff": "reflex_mcphost.core.config.models",
|
|
36
|
+
"ServerRegistry": "reflex_mcphost.core.registry",
|
|
37
|
+
# connections (API §4.5)
|
|
38
|
+
"ConnectionManager": "reflex_mcphost.core.connection.manager",
|
|
39
|
+
"ServerState": "reflex_mcphost.core.connection.state",
|
|
40
|
+
"ServerStatus": "reflex_mcphost.core.connection.state",
|
|
41
|
+
"LogEntry": "reflex_mcphost.core.connection.stderr",
|
|
42
|
+
# catalog (API §4.6)
|
|
43
|
+
"Catalog": "reflex_mcphost.core.catalog",
|
|
44
|
+
"ToolEntry": "reflex_mcphost.core.catalog",
|
|
45
|
+
"ResourceEntry": "reflex_mcphost.core.catalog",
|
|
46
|
+
"ResourceTemplateEntry": "reflex_mcphost.core.catalog",
|
|
47
|
+
"PromptEntry": "reflex_mcphost.core.catalog",
|
|
48
|
+
"ResourceContents": "reflex_mcphost.core.catalog",
|
|
49
|
+
"PromptResult": "reflex_mcphost.core.catalog",
|
|
50
|
+
# identifiers and consumers (API §3.3, §4.4)
|
|
51
|
+
"Consumer": "reflex_mcphost.core.types",
|
|
52
|
+
"ConsumerKind": "reflex_mcphost.core.types",
|
|
53
|
+
"Fqn": "reflex_mcphost.core.types",
|
|
54
|
+
# human-in-the-loop seam (API §4.10; Phase 2 subset)
|
|
55
|
+
"Prompter": "reflex_mcphost.core.prompter",
|
|
56
|
+
"TrustRequest": "reflex_mcphost.core.prompter",
|
|
57
|
+
# events (API §4.11)
|
|
58
|
+
"EventBus": "reflex_mcphost.core.events",
|
|
59
|
+
"HostEvent": "reflex_mcphost.core.events",
|
|
60
|
+
"ServerStateChanged": "reflex_mcphost.core.events",
|
|
61
|
+
"RegistryChanged": "reflex_mcphost.core.events",
|
|
62
|
+
"CatalogChanged": "reflex_mcphost.core.events",
|
|
63
|
+
"ResourceUpdated": "reflex_mcphost.core.events",
|
|
64
|
+
"ServerLog": "reflex_mcphost.core.events",
|
|
65
|
+
"EventsDropped": "reflex_mcphost.core.events",
|
|
66
|
+
# consent, invoker, audit (API §4.7-§4.9) and their events (§4.11)
|
|
67
|
+
"AuditLog": "reflex_mcphost.core.audit",
|
|
68
|
+
"AuditPage": "reflex_mcphost.core.audit",
|
|
69
|
+
"AuditQuery": "reflex_mcphost.core.audit",
|
|
70
|
+
"AuditRecord": "reflex_mcphost.core.audit",
|
|
71
|
+
"ConsentAnswer": "reflex_mcphost.core.consent.models",
|
|
72
|
+
"ConsentDecision": "reflex_mcphost.core.consent.models",
|
|
73
|
+
"ConsentOutcome": "reflex_mcphost.core.consent.outcome",
|
|
74
|
+
"ConsentPolicy": "reflex_mcphost.core.consent.policy",
|
|
75
|
+
"ConsentRequest": "reflex_mcphost.core.consent.models",
|
|
76
|
+
"ConsentRequested": "reflex_mcphost.core.events",
|
|
77
|
+
"ConsentResolved": "reflex_mcphost.core.events",
|
|
78
|
+
"ConsentRulesChanged": "reflex_mcphost.core.events",
|
|
79
|
+
"ConsentRule": "reflex_mcphost.core.consent.models",
|
|
80
|
+
"ConsentTarget": "reflex_mcphost.core.consent.models",
|
|
81
|
+
"InvocationError": "reflex_mcphost.core.invoker",
|
|
82
|
+
"InvocationFinished": "reflex_mcphost.core.events",
|
|
83
|
+
"InvocationHandle": "reflex_mcphost.core.invoker",
|
|
84
|
+
"InvocationOutcome": "reflex_mcphost.core.outcomes",
|
|
85
|
+
"InvocationProgress": "reflex_mcphost.core.events",
|
|
86
|
+
"InvocationResult": "reflex_mcphost.core.invoker",
|
|
87
|
+
"InvocationStarted": "reflex_mcphost.core.events",
|
|
88
|
+
"Invoker": "reflex_mcphost.core.invoker",
|
|
89
|
+
"OperationKind": "reflex_mcphost.core.consent.outcome",
|
|
90
|
+
"Progress": "reflex_mcphost.core.outcomes",
|
|
91
|
+
# OAuth flow and sampling seam (API §4.5, §4.14)
|
|
92
|
+
"AuthorizationFlow": "reflex_mcphost.core.auth.flow",
|
|
93
|
+
"SamplingProvider": "reflex_mcphost.core.handlers.sampling",
|
|
94
|
+
# errors (API §3.4)
|
|
95
|
+
"AuditUnavailableError": "reflex_mcphost.core.audit",
|
|
96
|
+
"AuditRecordNotFoundError": "reflex_mcphost.core.audit",
|
|
97
|
+
"ConsentStoreError": "reflex_mcphost.core.consent.store",
|
|
98
|
+
"UnknownRuleError": "reflex_mcphost.core.consent.store",
|
|
99
|
+
"MCPHostError": "reflex_mcphost.core.errors",
|
|
100
|
+
"ConfigError": "reflex_mcphost.core.errors",
|
|
101
|
+
"UnknownServerError": "reflex_mcphost.core.errors",
|
|
102
|
+
"UnknownEntryError": "reflex_mcphost.core.errors",
|
|
103
|
+
"ServerUnavailableError": "reflex_mcphost.core.errors",
|
|
104
|
+
"CapabilityNotSupportedError": "reflex_mcphost.core.errors",
|
|
105
|
+
"AuthorizationError": "reflex_mcphost.core.errors",
|
|
106
|
+
"ServerRequestError": "reflex_mcphost.core.errors",
|
|
107
|
+
"SecretStoreError": "reflex_mcphost.core.errors",
|
|
108
|
+
"HostClosedError": "reflex_mcphost.core.errors",
|
|
109
|
+
"InputRequestError": "reflex_mcphost.core.errors",
|
|
110
|
+
"UnknownToolError": "reflex_mcphost.core.errors",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
__all__ = [
|
|
114
|
+
"AuditLog",
|
|
115
|
+
"AuditPage",
|
|
116
|
+
"AuditQuery",
|
|
117
|
+
"AuditRecord",
|
|
118
|
+
"AuditRecordNotFoundError",
|
|
119
|
+
"AuditUnavailableError",
|
|
120
|
+
"AuthorizationError",
|
|
121
|
+
"AuthorizationFlow",
|
|
122
|
+
"CapabilityNotSupportedError",
|
|
123
|
+
"Catalog",
|
|
124
|
+
"CatalogChanged",
|
|
125
|
+
"ConfigError",
|
|
126
|
+
"ConfigOrigin",
|
|
127
|
+
"ConfigProblem",
|
|
128
|
+
"ConfigProblemCode",
|
|
129
|
+
"ConnectionManager",
|
|
130
|
+
"ConsentAnswer",
|
|
131
|
+
"ConsentDecision",
|
|
132
|
+
"ConsentOutcome",
|
|
133
|
+
"ConsentPolicy",
|
|
134
|
+
"ConsentRequest",
|
|
135
|
+
"ConsentRequested",
|
|
136
|
+
"ConsentResolved",
|
|
137
|
+
"ConsentRule",
|
|
138
|
+
"ConsentRulesChanged",
|
|
139
|
+
"ConsentStoreError",
|
|
140
|
+
"ConsentTarget",
|
|
141
|
+
"Consumer",
|
|
142
|
+
"ConsumerKind",
|
|
143
|
+
"EventBus",
|
|
144
|
+
"EventsDropped",
|
|
145
|
+
"Fqn",
|
|
146
|
+
"Host",
|
|
147
|
+
"HostClosedError",
|
|
148
|
+
"HostConfig",
|
|
149
|
+
"HostEvent",
|
|
150
|
+
"HttpTransport",
|
|
151
|
+
"InputRequestError",
|
|
152
|
+
"InvocationError",
|
|
153
|
+
"InvocationFinished",
|
|
154
|
+
"InvocationHandle",
|
|
155
|
+
"InvocationOutcome",
|
|
156
|
+
"InvocationProgress",
|
|
157
|
+
"InvocationResult",
|
|
158
|
+
"InvocationStarted",
|
|
159
|
+
"Invoker",
|
|
160
|
+
"LogEntry",
|
|
161
|
+
"MCPHostError",
|
|
162
|
+
"OAuthSettings",
|
|
163
|
+
"OperationKind",
|
|
164
|
+
"Progress",
|
|
165
|
+
"PromptEntry",
|
|
166
|
+
"PromptResult",
|
|
167
|
+
"Prompter",
|
|
168
|
+
"RegistryChanged",
|
|
169
|
+
"RegistryDiff",
|
|
170
|
+
"ResourceContents",
|
|
171
|
+
"ResourceEntry",
|
|
172
|
+
"ResourceTemplateEntry",
|
|
173
|
+
"ResourceUpdated",
|
|
174
|
+
"SamplingProvider",
|
|
175
|
+
"SecretStoreError",
|
|
176
|
+
"ServerDefinition",
|
|
177
|
+
"ServerLog",
|
|
178
|
+
"ServerRegistry",
|
|
179
|
+
"ServerRequestError",
|
|
180
|
+
"ServerState",
|
|
181
|
+
"ServerStateChanged",
|
|
182
|
+
"ServerStatus",
|
|
183
|
+
"ServerUnavailableError",
|
|
184
|
+
"ShadowedDefinition",
|
|
185
|
+
"SourceKind",
|
|
186
|
+
"StdioTransport",
|
|
187
|
+
"ToolEntry",
|
|
188
|
+
"TrustRequest",
|
|
189
|
+
"UnknownEntryError",
|
|
190
|
+
"UnknownRuleError",
|
|
191
|
+
"UnknownServerError",
|
|
192
|
+
"UnknownToolError",
|
|
193
|
+
"__version__",
|
|
194
|
+
]
|
|
195
|
+
"""Kept literal for static tools; equal to ``_EXPORTS`` plus ``__version__`` (tested)."""
|
|
196
|
+
|
|
197
|
+
if TYPE_CHECKING: # pragma: no cover - static re-exports for type checkers and IDEs
|
|
198
|
+
from reflex_mcphost.core.audit import (
|
|
199
|
+
AuditLog,
|
|
200
|
+
AuditPage,
|
|
201
|
+
AuditQuery,
|
|
202
|
+
AuditRecord,
|
|
203
|
+
AuditRecordNotFoundError,
|
|
204
|
+
AuditUnavailableError,
|
|
205
|
+
)
|
|
206
|
+
from reflex_mcphost.core.auth.flow import AuthorizationFlow
|
|
207
|
+
from reflex_mcphost.core.catalog import (
|
|
208
|
+
Catalog,
|
|
209
|
+
PromptEntry,
|
|
210
|
+
PromptResult,
|
|
211
|
+
ResourceContents,
|
|
212
|
+
ResourceEntry,
|
|
213
|
+
ResourceTemplateEntry,
|
|
214
|
+
ToolEntry,
|
|
215
|
+
)
|
|
216
|
+
from reflex_mcphost.core.config.models import (
|
|
217
|
+
ConfigOrigin,
|
|
218
|
+
ConfigProblem,
|
|
219
|
+
ConfigProblemCode,
|
|
220
|
+
HostConfig,
|
|
221
|
+
HttpTransport,
|
|
222
|
+
OAuthSettings,
|
|
223
|
+
RegistryDiff,
|
|
224
|
+
ServerDefinition,
|
|
225
|
+
ShadowedDefinition,
|
|
226
|
+
SourceKind,
|
|
227
|
+
StdioTransport,
|
|
228
|
+
)
|
|
229
|
+
from reflex_mcphost.core.connection.manager import ConnectionManager
|
|
230
|
+
from reflex_mcphost.core.connection.state import ServerState, ServerStatus
|
|
231
|
+
from reflex_mcphost.core.connection.stderr import LogEntry
|
|
232
|
+
from reflex_mcphost.core.consent.models import (
|
|
233
|
+
ConsentAnswer,
|
|
234
|
+
ConsentDecision,
|
|
235
|
+
ConsentRequest,
|
|
236
|
+
ConsentRule,
|
|
237
|
+
ConsentTarget,
|
|
238
|
+
)
|
|
239
|
+
from reflex_mcphost.core.consent.outcome import ConsentOutcome, OperationKind
|
|
240
|
+
from reflex_mcphost.core.consent.policy import ConsentPolicy
|
|
241
|
+
from reflex_mcphost.core.consent.store import ConsentStoreError, UnknownRuleError
|
|
242
|
+
from reflex_mcphost.core.errors import (
|
|
243
|
+
AuthorizationError,
|
|
244
|
+
CapabilityNotSupportedError,
|
|
245
|
+
ConfigError,
|
|
246
|
+
HostClosedError,
|
|
247
|
+
InputRequestError,
|
|
248
|
+
MCPHostError,
|
|
249
|
+
SecretStoreError,
|
|
250
|
+
ServerRequestError,
|
|
251
|
+
ServerUnavailableError,
|
|
252
|
+
UnknownEntryError,
|
|
253
|
+
UnknownServerError,
|
|
254
|
+
UnknownToolError,
|
|
255
|
+
)
|
|
256
|
+
from reflex_mcphost.core.events import (
|
|
257
|
+
CatalogChanged,
|
|
258
|
+
ConsentRequested,
|
|
259
|
+
ConsentResolved,
|
|
260
|
+
ConsentRulesChanged,
|
|
261
|
+
EventBus,
|
|
262
|
+
EventsDropped,
|
|
263
|
+
HostEvent,
|
|
264
|
+
InvocationFinished,
|
|
265
|
+
InvocationProgress,
|
|
266
|
+
InvocationStarted,
|
|
267
|
+
RegistryChanged,
|
|
268
|
+
ResourceUpdated,
|
|
269
|
+
ServerLog,
|
|
270
|
+
ServerStateChanged,
|
|
271
|
+
)
|
|
272
|
+
from reflex_mcphost.core.handlers.sampling import SamplingProvider
|
|
273
|
+
from reflex_mcphost.core.host import Host
|
|
274
|
+
from reflex_mcphost.core.invoker import (
|
|
275
|
+
InvocationError,
|
|
276
|
+
InvocationHandle,
|
|
277
|
+
InvocationResult,
|
|
278
|
+
Invoker,
|
|
279
|
+
)
|
|
280
|
+
from reflex_mcphost.core.outcomes import InvocationOutcome, Progress
|
|
281
|
+
from reflex_mcphost.core.prompter import Prompter, TrustRequest
|
|
282
|
+
from reflex_mcphost.core.registry import ServerRegistry
|
|
283
|
+
from reflex_mcphost.core.types import Consumer, ConsumerKind, Fqn
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def __getattr__(name: str) -> Any:
|
|
287
|
+
module = _EXPORTS.get(name)
|
|
288
|
+
if module is None:
|
|
289
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
290
|
+
value = getattr(import_module(module), name)
|
|
291
|
+
globals()[name] = value # cache: later lookups skip __getattr__
|
|
292
|
+
return value
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def __dir__() -> list[str]:
|
|
296
|
+
return sorted(__all__)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional agent-framework adapters, installed as extras (API §4.13)."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""LangChain/LangGraph adapter (extra ``langgraph``; REQ-TLS-007, API §4.13, spike S-6).
|
|
2
|
+
|
|
3
|
+
``as_langchain_tools(toolset)`` returns one ``HostTool`` per ``Toolset.list()`` entry. The
|
|
4
|
+
tools are a snapshot: call it again after ``catalog.changed`` (API §4.13). ``HostTool`` is a
|
|
5
|
+
direct ``langchain_core.tools.BaseTool`` subclass (Art. 2) — not ``StructuredTool``, which
|
|
6
|
+
drops or overrides arguments named ``config``/``run_manager`` and fails on ``self`` (S-6):
|
|
7
|
+
the parsed argument dict is passed to ``_arun`` as one positional argument and ``_arun``
|
|
8
|
+
declares neither ``config`` nor ``run_manager``, so LangChain injects nothing. The dict
|
|
9
|
+
``args_schema`` is the Server's input schema unchanged; LangChain does not validate it,
|
|
10
|
+
the Host's Invoker does (REQ-INV-002). Every error outcome, and an alias gone since the
|
|
11
|
+
snapshot (``UnknownToolError`` — only that error; any other ``KeyError`` propagates), is
|
|
12
|
+
raised as ``ToolException(text)`` and, with ``handle_tool_error=True``, becomes
|
|
13
|
+
``ToolMessage(status="error")`` — LangGraph's ``ToolNode`` re-raises any other exception and
|
|
14
|
+
fails the graph (REQ-TLS-005). Only ``langchain_core`` is imported; effects go only through
|
|
15
|
+
``Toolset.call`` (Art. 3).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from langchain_core.tools import BaseTool, ToolException
|
|
24
|
+
except ImportError as exc: # pragma: no cover - exercised in a subprocess (F5-05)
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"reflex_mcphost.adapters.langgraph needs the 'langgraph' extra: "
|
|
27
|
+
"pip install 'reflex-mcphost[langgraph]'"
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
from pydantic import Field
|
|
31
|
+
|
|
32
|
+
from reflex_mcphost.toolset import Toolset, UnknownToolError
|
|
33
|
+
|
|
34
|
+
__all__ = ["as_langchain_tools"]
|
|
35
|
+
|
|
36
|
+
ASYNC_ONLY = "reflex-mcphost tools are async-only; use ainvoke"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HostTool(BaseTool):
|
|
40
|
+
"""One Host tool as a LangChain tool (async-only)."""
|
|
41
|
+
|
|
42
|
+
toolset: Toolset = Field(exclude=True)
|
|
43
|
+
handle_tool_error: Any = True
|
|
44
|
+
|
|
45
|
+
def _to_args_and_kwargs(
|
|
46
|
+
self, tool_input: str | dict[str, Any], tool_call_id: str | None
|
|
47
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
48
|
+
parsed = self._parse_input(tool_input, tool_call_id)
|
|
49
|
+
if not isinstance(parsed, dict): # a str input is rejected by _parse_input already
|
|
50
|
+
raise ToolException(f"{self.name} takes a JSON object of arguments")
|
|
51
|
+
return (dict(parsed),), {}
|
|
52
|
+
|
|
53
|
+
def _run(self, *args: Any, **kwargs: Any) -> str:
|
|
54
|
+
raise NotImplementedError(ASYNC_ONLY)
|
|
55
|
+
|
|
56
|
+
async def _arun(self, arguments: dict[str, Any]) -> str:
|
|
57
|
+
try:
|
|
58
|
+
outcome = await self.toolset.call(self.name, arguments)
|
|
59
|
+
except UnknownToolError: # only a vanished alias; any other KeyError propagates
|
|
60
|
+
raise ToolException(f"Tool is no longer available: {self.name}") from None
|
|
61
|
+
if outcome.is_error:
|
|
62
|
+
raise ToolException(outcome.text)
|
|
63
|
+
return outcome.text
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def as_langchain_tools(toolset: Toolset) -> list[BaseTool]:
|
|
67
|
+
"""Snapshot ``toolset`` as LangChain tools (e.g. for ``langgraph.prebuilt.ToolNode``)."""
|
|
68
|
+
return [
|
|
69
|
+
HostTool(
|
|
70
|
+
name=spec.name,
|
|
71
|
+
description=spec.description,
|
|
72
|
+
args_schema=spec.input_schema,
|
|
73
|
+
metadata={"fqn": str(spec.fqn)},
|
|
74
|
+
toolset=toolset,
|
|
75
|
+
)
|
|
76
|
+
for spec in toolset.list()
|
|
77
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""PydanticAI adapter (extra ``pydantic-ai``; REQ-TLS-006, API §4.13, spike S-6).
|
|
2
|
+
|
|
3
|
+
``as_pydantic_ai_toolset(toolset)`` returns a ``HostToolset``, a subclass of PydanticAI's
|
|
4
|
+
own ``AbstractToolset`` (Art. 2). PydanticAI calls ``get_tools`` before every model request
|
|
5
|
+
and the adapter re-reads ``Toolset.list()`` each time, so catalog changes show up within a
|
|
6
|
+
run (REQ-TLS-004). Arguments are accepted as any JSON object: the Host's Invoker validates
|
|
7
|
+
them against the tool's input schema (REQ-INV-002), as PydanticAI's own ``MCPToolset`` leaves
|
|
8
|
+
validation to the server. Every error outcome — and an alias that vanished since the
|
|
9
|
+
listing (``UnknownToolError``) — is raised as ``ToolFailed(text)``, which PydanticAI returns
|
|
10
|
+
to the model as a failed tool result without spending the retry budget (REQ-TLS-005). Any
|
|
11
|
+
other exception, including a plain ``KeyError``, is a bug and propagates. Effects go only
|
|
12
|
+
through ``Toolset.call`` (Art. 3).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import pydantic_core
|
|
21
|
+
from pydantic_ai import RunContext, ToolFailed
|
|
22
|
+
from pydantic_ai.tools import ToolDefinition
|
|
23
|
+
from pydantic_ai.toolsets import AbstractToolset, ToolsetTool
|
|
24
|
+
except ImportError as exc: # pragma: no cover - exercised in a subprocess (F5-05)
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"reflex_mcphost.adapters.pydantic_ai needs the 'pydantic-ai' extra: "
|
|
27
|
+
"pip install 'reflex-mcphost[pydantic-ai]'"
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
from reflex_mcphost.toolset import Toolset, UnknownToolError
|
|
31
|
+
|
|
32
|
+
__all__ = ["as_pydantic_ai_toolset"]
|
|
33
|
+
|
|
34
|
+
_ANY_OBJECT = pydantic_core.SchemaValidator(
|
|
35
|
+
pydantic_core.core_schema.dict_schema(
|
|
36
|
+
pydantic_core.core_schema.str_schema(), pydantic_core.core_schema.any_schema()
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
"""Accept any JSON object; the Invoker validates against the tool's input schema."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HostToolset(AbstractToolset[Any]):
|
|
43
|
+
"""The Host's tools as a PydanticAI toolset (one per ``Toolset``)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, toolset: Toolset) -> None:
|
|
46
|
+
self._toolset = toolset
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def id(self) -> str | None:
|
|
50
|
+
"""No id: durable execution (which needs one) is out of scope (API §4.13)."""
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def toolset(self) -> Toolset:
|
|
55
|
+
"""The wrapped framework-neutral Toolset."""
|
|
56
|
+
return self._toolset
|
|
57
|
+
|
|
58
|
+
async def get_tools(self, ctx: RunContext[Any]) -> dict[str, ToolsetTool[Any]]:
|
|
59
|
+
"""The current listing, re-read on every run step (REQ-TLS-004)."""
|
|
60
|
+
return {
|
|
61
|
+
spec.name: ToolsetTool(
|
|
62
|
+
toolset=self,
|
|
63
|
+
tool_def=ToolDefinition(
|
|
64
|
+
name=spec.name,
|
|
65
|
+
description=spec.description,
|
|
66
|
+
parameters_json_schema=spec.input_schema,
|
|
67
|
+
metadata={"fqn": str(spec.fqn)},
|
|
68
|
+
),
|
|
69
|
+
max_retries=1,
|
|
70
|
+
args_validator=_ANY_OBJECT,
|
|
71
|
+
)
|
|
72
|
+
for spec in self._toolset.list()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async def call_tool(
|
|
76
|
+
self, name: str, tool_args: dict[str, Any], ctx: RunContext[Any], tool: ToolsetTool[Any]
|
|
77
|
+
) -> Any:
|
|
78
|
+
"""Invoke through ``Toolset.call``; error outcomes raise ``ToolFailed(text)``."""
|
|
79
|
+
try:
|
|
80
|
+
outcome = await self._toolset.call(name, tool_args)
|
|
81
|
+
except UnknownToolError: # only a vanished alias; any other KeyError propagates
|
|
82
|
+
raise ToolFailed(f"Tool is no longer available: {name}") from None
|
|
83
|
+
if outcome.is_error:
|
|
84
|
+
raise ToolFailed(outcome.text)
|
|
85
|
+
return outcome.text
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def as_pydantic_ai_toolset(toolset: Toolset) -> AbstractToolset[Any]:
|
|
89
|
+
"""Expose ``toolset`` as a PydanticAI toolset: ``Agent(model, toolsets=[it])``."""
|
|
90
|
+
return HostToolset(toolset)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reference app and CLI (`reflex-mcphost`)."""
|