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.
Files changed (110) hide show
  1. reflex_mcphost/__init__.py +296 -0
  2. reflex_mcphost/adapters/__init__.py +1 -0
  3. reflex_mcphost/adapters/langgraph.py +77 -0
  4. reflex_mcphost/adapters/pydantic_ai.py +90 -0
  5. reflex_mcphost/app/__init__.py +1 -0
  6. reflex_mcphost/app/cli.py +619 -0
  7. reflex_mcphost/app/web/__init__.py +7 -0
  8. reflex_mcphost/app/web/mcphost_app/__init__.py +1 -0
  9. reflex_mcphost/app/web/mcphost_app/mcphost_app.py +170 -0
  10. reflex_mcphost/app/web/rxconfig.py +35 -0
  11. reflex_mcphost/app/workspace.py +215 -0
  12. reflex_mcphost/core/__init__.py +4 -0
  13. reflex_mcphost/core/apps/__init__.py +9 -0
  14. reflex_mcphost/core/apps/bridge.py +612 -0
  15. reflex_mcphost/core/apps/constants.py +86 -0
  16. reflex_mcphost/core/apps/csp.py +177 -0
  17. reflex_mcphost/core/apps/messages.py +163 -0
  18. reflex_mcphost/core/apps/resource.py +102 -0
  19. reflex_mcphost/core/audit.py +473 -0
  20. reflex_mcphost/core/auth/__init__.py +1 -0
  21. reflex_mcphost/core/auth/callback_server.py +277 -0
  22. reflex_mcphost/core/auth/flow.py +84 -0
  23. reflex_mcphost/core/auth/oauth.py +578 -0
  24. reflex_mcphost/core/auth/token_store.py +323 -0
  25. reflex_mcphost/core/catalog.py +852 -0
  26. reflex_mcphost/core/config/__init__.py +1 -0
  27. reflex_mcphost/core/config/claude.py +18 -0
  28. reflex_mcphost/core/config/common.py +310 -0
  29. reflex_mcphost/core/config/cursor.py +18 -0
  30. reflex_mcphost/core/config/expand.py +69 -0
  31. reflex_mcphost/core/config/mcp_json.py +33 -0
  32. reflex_mcphost/core/config/models.py +292 -0
  33. reflex_mcphost/core/config/own.py +497 -0
  34. reflex_mcphost/core/config/paths.py +124 -0
  35. reflex_mcphost/core/config/vscode.py +19 -0
  36. reflex_mcphost/core/connection/__init__.py +1 -0
  37. reflex_mcphost/core/connection/era_cache.py +183 -0
  38. reflex_mcphost/core/connection/inflight.py +89 -0
  39. reflex_mcphost/core/connection/lease.py +71 -0
  40. reflex_mcphost/core/connection/manager.py +458 -0
  41. reflex_mcphost/core/connection/state.py +87 -0
  42. reflex_mcphost/core/connection/stderr.py +164 -0
  43. reflex_mcphost/core/connection/supervisor.py +926 -0
  44. reflex_mcphost/core/connection/transports.py +364 -0
  45. reflex_mcphost/core/consent/__init__.py +5 -0
  46. reflex_mcphost/core/consent/index.py +152 -0
  47. reflex_mcphost/core/consent/models.py +243 -0
  48. reflex_mcphost/core/consent/outcome.py +39 -0
  49. reflex_mcphost/core/consent/policy.py +370 -0
  50. reflex_mcphost/core/consent/preview.py +224 -0
  51. reflex_mcphost/core/consent/store.py +328 -0
  52. reflex_mcphost/core/db.py +121 -0
  53. reflex_mcphost/core/db_models.py +84 -0
  54. reflex_mcphost/core/errors.py +156 -0
  55. reflex_mcphost/core/events.py +211 -0
  56. reflex_mcphost/core/handlers/__init__.py +1 -0
  57. reflex_mcphost/core/handlers/base.py +37 -0
  58. reflex_mcphost/core/handlers/capabilities.py +89 -0
  59. reflex_mcphost/core/handlers/elicitation.py +218 -0
  60. reflex_mcphost/core/handlers/logging.py +48 -0
  61. reflex_mcphost/core/handlers/roots.py +105 -0
  62. reflex_mcphost/core/handlers/sampling.py +336 -0
  63. reflex_mcphost/core/hmac_key.py +71 -0
  64. reflex_mcphost/core/host.py +559 -0
  65. reflex_mcphost/core/invoker.py +817 -0
  66. reflex_mcphost/core/logging.py +162 -0
  67. reflex_mcphost/core/migrations/0001_initial.py +144 -0
  68. reflex_mcphost/core/migrations/__init__.py +5 -0
  69. reflex_mcphost/core/migrations/runner.py +128 -0
  70. reflex_mcphost/core/mrtr.py +504 -0
  71. reflex_mcphost/core/outcomes.py +42 -0
  72. reflex_mcphost/core/prompter.py +213 -0
  73. reflex_mcphost/core/registry.py +387 -0
  74. reflex_mcphost/core/schema.py +743 -0
  75. reflex_mcphost/core/secret_store.py +150 -0
  76. reflex_mcphost/core/secrets.py +64 -0
  77. reflex_mcphost/core/toolset/__init__.py +11 -0
  78. reflex_mcphost/core/toolset/aliases.py +74 -0
  79. reflex_mcphost/core/toolset/filters.py +38 -0
  80. reflex_mcphost/core/toolset/toolset.py +193 -0
  81. reflex_mcphost/core/trust.py +269 -0
  82. reflex_mcphost/core/types.py +101 -0
  83. reflex_mcphost/py.typed +0 -0
  84. reflex_mcphost/toolset/__init__.py +12 -0
  85. reflex_mcphost/ui/__init__.py +60 -0
  86. reflex_mcphost/ui/actions.py +340 -0
  87. reflex_mcphost/ui/apps_bridge.py +474 -0
  88. reflex_mcphost/ui/assets/__init__.py +38 -0
  89. reflex_mcphost/ui/assets/mcp_app_relay.js +93 -0
  90. reflex_mcphost/ui/components/__init__.py +23 -0
  91. reflex_mcphost/ui/components/audit_table.py +136 -0
  92. reflex_mcphost/ui/components/catalog_browser.py +361 -0
  93. reflex_mcphost/ui/components/consent_center.py +468 -0
  94. reflex_mcphost/ui/components/filters.py +9 -0
  95. reflex_mcphost/ui/components/mcp_app_frame.py +160 -0
  96. reflex_mcphost/ui/components/permissions_table.py +131 -0
  97. reflex_mcphost/ui/components/schema_form.py +148 -0
  98. reflex_mcphost/ui/components/servers_panel.py +250 -0
  99. reflex_mcphost/ui/install.py +440 -0
  100. reflex_mcphost/ui/models.py +1169 -0
  101. reflex_mcphost/ui/prompter.py +346 -0
  102. reflex_mcphost/ui/proxy/__init__.py +23 -0
  103. reflex_mcphost/ui/proxy/proxy.html +117 -0
  104. reflex_mcphost/ui/proxy/server.py +457 -0
  105. reflex_mcphost/ui/state.py +1015 -0
  106. reflex_mcphost-0.1.0.dist-info/METADATA +303 -0
  107. reflex_mcphost-0.1.0.dist-info/RECORD +110 -0
  108. reflex_mcphost-0.1.0.dist-info/WHEEL +4 -0
  109. reflex_mcphost-0.1.0.dist-info/entry_points.txt +2 -0
  110. 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`)."""