pi-python-core 0.8.2__py3-none-any.whl → 0.9.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.
- pi_python/_version.py +1 -1
- pi_python/mcp.py +93 -10
- pi_python/plugins/__init__.py +993 -0
- pi_python/plugins/_compose.py +270 -0
- pi_python/plugins/_frontmatter.py +444 -0
- pi_python/plugins/_resources.py +385 -0
- pi_python/plugins/_subagents.py +269 -0
- pi_python/tools.py +23 -14
- {pi_python_core-0.8.2.dist-info → pi_python_core-0.9.0.dist-info}/METADATA +8 -6
- {pi_python_core-0.8.2.dist-info → pi_python_core-0.9.0.dist-info}/RECORD +13 -8
- {pi_python_core-0.8.2.dist-info → pi_python_core-0.9.0.dist-info}/WHEEL +0 -0
- {pi_python_core-0.8.2.dist-info → pi_python_core-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {pi_python_core-0.8.2.dist-info → pi_python_core-0.9.0.dist-info}/licenses/NOTICE +0 -0
pi_python/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.9.0"
|
pi_python/mcp.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Use the tools of an MCP server as Agent tools.
|
|
2
2
|
|
|
3
3
|
`mcp_tools(session)` wraps every tool of a connected session from the official `mcp` SDK
|
|
4
|
-
(1.x or 2.x); `connect_stdio(...)` also starts a stdio server and closes it afterwards
|
|
4
|
+
(1.x or 2.x); `connect_stdio(...)` also starts a stdio server and closes it afterwards, and
|
|
5
|
+
`connect_http(...)` connects to a streamable HTTP server.
|
|
5
6
|
The conversion follows Pi's MCP adapter: text and images pass through, embedded text and
|
|
6
7
|
image resources are unwrapped, other blocks become short text placeholders, and MCP's
|
|
7
8
|
`isError` marks a failed call. Install with ``pip install 'pi-python-core[mcp]'``.
|
|
@@ -16,7 +17,7 @@ import json
|
|
|
16
17
|
import re
|
|
17
18
|
import warnings
|
|
18
19
|
from collections.abc import AsyncIterator, Iterable, Mapping, Sequence
|
|
19
|
-
from contextlib import asynccontextmanager
|
|
20
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from .errors import ConfigurationError
|
|
@@ -177,11 +178,93 @@ async def connect_stdio(
|
|
|
177
178
|
tools = await mcp_tools(session, prefix=prefix, names=names)
|
|
178
179
|
yield tools
|
|
179
180
|
except BaseExceptionGroup as group:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
_raise_single(group)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@asynccontextmanager
|
|
185
|
+
async def connect_http(
|
|
186
|
+
url: str,
|
|
187
|
+
*,
|
|
188
|
+
headers: Mapping[str, str] | None = None,
|
|
189
|
+
prefix: str | None = None,
|
|
190
|
+
names: Iterable[str] | None = None,
|
|
191
|
+
init_timeout: float = 30.0,
|
|
192
|
+
) -> AsyncIterator[list[Tool]]:
|
|
193
|
+
"""Connect to a streamable HTTP MCP server, yield its tools, and disconnect afterwards.
|
|
194
|
+
|
|
195
|
+
``async with connect_http("https://example.org/mcp", headers={...}) as tools: ...``
|
|
196
|
+
|
|
197
|
+
The legacy SSE transport is not supported, as in Pi.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
from mcp import ClientSession # type: ignore[import-not-found,unused-ignore]
|
|
201
|
+
from mcp.client import streamable_http as transport # type: ignore[import-not-found,unused-ignore]
|
|
202
|
+
except ImportError as exc:
|
|
203
|
+
raise ImportError(
|
|
204
|
+
"connect_http needs the MCP SDK: pip install 'pi-python-core[mcp]'"
|
|
205
|
+
) from exc
|
|
206
|
+
from mcp.shared._httpx_utils import create_mcp_http_client # type: ignore[import-not-found,unused-ignore]
|
|
207
|
+
|
|
208
|
+
origin = _origin(url)
|
|
209
|
+
|
|
210
|
+
async def same_origin(request: Any) -> None:
|
|
211
|
+
# Some SDK 1.x versions follow redirects to any origin, which would send the
|
|
212
|
+
# headers (often an API key) there. Redirects within the server's origin, such
|
|
213
|
+
# as /mcp to /mcp/, still work. SDK 2.x applies the same rule itself.
|
|
214
|
+
target = _origin(str(request.url))
|
|
215
|
+
if target != origin and target != ("https", *origin[1:]):
|
|
216
|
+
raise ConnectionError(
|
|
217
|
+
f"MCP server at {origin[1]} redirected to another origin ({target[1]});"
|
|
218
|
+
" not following"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def client_without_redirects(*args: Any, **kwargs: Any) -> Any:
|
|
222
|
+
client = create_mcp_http_client(*args, **kwargs)
|
|
223
|
+
hooks = dict(client.event_hooks)
|
|
224
|
+
hooks["request"] = [*hooks.get("request", []), same_origin]
|
|
225
|
+
client.event_hooks = hooks
|
|
226
|
+
return client
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
async with AsyncExitStack() as stack:
|
|
230
|
+
if hasattr(transport, "streamable_http_client"): # SDK 2.x and late 1.x
|
|
231
|
+
client = await stack.enter_async_context(
|
|
232
|
+
client_without_redirects(headers=dict(headers or {}))
|
|
233
|
+
)
|
|
234
|
+
streams = await stack.enter_async_context(
|
|
235
|
+
transport.streamable_http_client(url, http_client=client)
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
streams = await stack.enter_async_context(
|
|
239
|
+
transport.streamablehttp_client( # type: ignore[attr-defined,unused-ignore]
|
|
240
|
+
url,
|
|
241
|
+
headers=dict(headers or {}),
|
|
242
|
+
httpx_client_factory=client_without_redirects,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
session = await stack.enter_async_context(ClientSession(streams[0], streams[1]))
|
|
246
|
+
async with asyncio.timeout(init_timeout):
|
|
247
|
+
await session.initialize()
|
|
248
|
+
tools = await mcp_tools(session, prefix=prefix, names=names)
|
|
249
|
+
yield tools
|
|
250
|
+
except BaseExceptionGroup as group:
|
|
251
|
+
_raise_single(group)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _origin(url: str) -> tuple[str, str, int | None]:
|
|
255
|
+
from urllib.parse import urlsplit
|
|
256
|
+
|
|
257
|
+
parts = urlsplit(url)
|
|
258
|
+
default = {"http": 80, "https": 443}.get(parts.scheme)
|
|
259
|
+
return parts.scheme, parts.hostname or "", parts.port or default
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _raise_single(group: BaseExceptionGroup) -> None:
|
|
263
|
+
# The SDK's task groups wrap a single failure ("Connection closed", a timeout, or an
|
|
264
|
+
# error from the caller's block), sometimes twice; raise that failure itself.
|
|
265
|
+
error: BaseException = group
|
|
266
|
+
while isinstance(error, BaseExceptionGroup) and len(error.exceptions) == 1:
|
|
267
|
+
error = error.exceptions[0]
|
|
268
|
+
if error is group:
|
|
269
|
+
raise group
|
|
270
|
+
raise error from None
|