feat: add stateful runtime listings
This commit is contained in:
@@ -256,6 +256,10 @@ implementation state.
|
|||||||
- Completed: broker content access now prefers a configured stateful MCP
|
- Completed: broker content access now prefers a configured stateful MCP
|
||||||
runtime for `read_resource` and `get_prompt`, with one-shot adapter fallback.
|
runtime for `read_resource` and `get_prompt`, with one-shot adapter fallback.
|
||||||
Catalog refresh/discovery remains one-shot by policy.
|
Catalog refresh/discovery remains one-shot by policy.
|
||||||
|
- Completed: stateful MCP runtime now has protocol slices for tools,
|
||||||
|
resources, and prompts, and can route session-scoped `list_resources` and
|
||||||
|
`list_prompts` through the owner task. Catalog refresh still uses one-shot
|
||||||
|
adapter policy.
|
||||||
- Auth/source secrets boundary: keep registry desired state separate from
|
- Auth/source secrets boundary: keep registry desired state separate from
|
||||||
upstream credentials, and surface missing auth as validation diagnostics.
|
upstream credentials, and surface missing auth as validation diagnostics.
|
||||||
The contract is now specified in
|
The contract is now specified in
|
||||||
|
|||||||
@@ -123,7 +123,10 @@ First slices should move leaf modules only and leave `wf_mcp` re-export shims:
|
|||||||
13. Complete: broker content access now prefers configured `StatefulMcpRuntime`
|
13. Complete: broker content access now prefers configured `StatefulMcpRuntime`
|
||||||
for resource and prompt reads, falling back to the one-shot adapter when no
|
for resource and prompt reads, falling back to the one-shot adapter when no
|
||||||
stateful runtime is configured. Catalog refresh/discovery remains one-shot.
|
stateful runtime is configured. Catalog refresh/discovery remains one-shot.
|
||||||
14. Upstream transport/discovery/session services.
|
14. Complete: stateful MCP runtime protocols split into tool/resource/prompt
|
||||||
|
slices. Runtime can route `list_resources` and `list_prompts` through the
|
||||||
|
owner task for session-scoped listings; catalog refresh remains one-shot.
|
||||||
|
15. Upstream transport/discovery/session services.
|
||||||
|
|
||||||
Each slice should add import-direction tests so the new source-provider package
|
Each slice should add import-direction tests so the new source-provider package
|
||||||
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
|
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
from wf_sources_mcp.sdk import (
|
from wf_sources_mcp.sdk import (
|
||||||
BackendAdapter,
|
BackendAdapter,
|
||||||
McpSdkAdapter,
|
McpSdkAdapter,
|
||||||
|
PromptRuntime,
|
||||||
|
ResourceRuntime,
|
||||||
StatefulMcpRuntime,
|
StatefulMcpRuntime,
|
||||||
ToolCallResult,
|
ToolCallResult,
|
||||||
|
ToolRuntime,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ["BackendAdapter", "McpSdkAdapter", "StatefulMcpRuntime", "ToolCallResult"]
|
__all__ = [
|
||||||
|
"BackendAdapter",
|
||||||
|
"McpSdkAdapter",
|
||||||
|
"PromptRuntime",
|
||||||
|
"ResourceRuntime",
|
||||||
|
"StatefulMcpRuntime",
|
||||||
|
"ToolCallResult",
|
||||||
|
"ToolRuntime",
|
||||||
|
]
|
||||||
|
|||||||
+11
-1
@@ -5,10 +5,20 @@ Canonical implementation lives in `wf_sources_mcp.sdk`.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from wf_sources_mcp.sdk import BackendAdapter, StatefulMcpRuntime, ToolCallResult
|
from wf_sources_mcp.sdk import (
|
||||||
|
BackendAdapter,
|
||||||
|
PromptRuntime,
|
||||||
|
ResourceRuntime,
|
||||||
|
StatefulMcpRuntime,
|
||||||
|
ToolCallResult,
|
||||||
|
ToolRuntime,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BackendAdapter",
|
"BackendAdapter",
|
||||||
|
"PromptRuntime",
|
||||||
|
"ResourceRuntime",
|
||||||
"StatefulMcpRuntime",
|
"StatefulMcpRuntime",
|
||||||
"ToolCallResult",
|
"ToolCallResult",
|
||||||
|
"ToolRuntime",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from typing import Any, Generic, TypeVar
|
|||||||
from mcp.client.session import ClientSession
|
from mcp.client.session import ClientSession
|
||||||
|
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource
|
||||||
from wf_sources_mcp.client import McpSourceClient, open_mcp_session
|
from wf_sources_mcp.client import McpSourceClient, open_mcp_session
|
||||||
from wf_sources_mcp.connections import McpSourceConnection
|
from wf_sources_mcp.connections import McpSourceConnection
|
||||||
from wf_sources_mcp.sdk import ToolCallResult
|
from wf_sources_mcp.sdk import ToolCallResult
|
||||||
@@ -44,6 +45,8 @@ class PersistentSessionFactory:
|
|||||||
call_callback=owner.call_tool,
|
call_callback=owner.call_tool,
|
||||||
read_resource_callback=owner.read_resource,
|
read_resource_callback=owner.read_resource,
|
||||||
get_prompt_callback=owner.get_prompt,
|
get_prompt_callback=owner.get_prompt,
|
||||||
|
list_resources_callback=owner.list_resources,
|
||||||
|
list_prompts_callback=owner.list_prompts,
|
||||||
close_callback=owner.close,
|
close_callback=owner.close,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,6 +166,20 @@ class _SessionOwner:
|
|||||||
run=lambda client: client.get_prompt(prompt_name, arguments),
|
run=lambda client: client.get_prompt(prompt_name, arguments),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def list_resources(self) -> list[DiscoveredResource]:
|
||||||
|
"""Submit resource listing through the generic owner-task operation queue."""
|
||||||
|
return await self.submit(
|
||||||
|
operation="list_resources",
|
||||||
|
run=lambda client: client.list_resources(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_prompts(self) -> list[DiscoveredPrompt]:
|
||||||
|
"""Submit prompt listing through the generic owner-task operation queue."""
|
||||||
|
return await self.submit(
|
||||||
|
operation="list_prompts",
|
||||||
|
run=lambda client: client.list_prompts(),
|
||||||
|
)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""Ask the owner task to close the MCP transport in its own scope."""
|
"""Ask the owner task to close the MCP transport in its own scope."""
|
||||||
task = self._task
|
task = self._task
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from inspect import isawaitable
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource
|
||||||
from wf_sources_mcp.connections import McpSourceConnection
|
from wf_sources_mcp.connections import McpSourceConnection
|
||||||
from wf_sources_mcp.sdk import ToolCallResult
|
from wf_sources_mcp.sdk import ToolCallResult
|
||||||
|
|
||||||
@@ -102,6 +103,22 @@ class McpRuntimePool:
|
|||||||
session = await self.get_session(connection, auth)
|
session = await self.get_session(connection, auth)
|
||||||
return await session.get_prompt(prompt_name, arguments)
|
return await session.get_prompt(prompt_name, arguments)
|
||||||
|
|
||||||
|
async def list_resources(
|
||||||
|
self,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
) -> list[DiscoveredResource]:
|
||||||
|
session = await self.get_session(connection, auth)
|
||||||
|
return await session.list_resources()
|
||||||
|
|
||||||
|
async def list_prompts(
|
||||||
|
self,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
) -> list[DiscoveredPrompt]:
|
||||||
|
session = await self.get_session(connection, auth)
|
||||||
|
return await session.list_prompts()
|
||||||
|
|
||||||
async def close_connection(self, connection_id: str) -> None:
|
async def close_connection(self, connection_id: str) -> None:
|
||||||
current = self._sessions.pop(connection_id, None)
|
current = self._sessions.pop(connection_id, None)
|
||||||
if current is not None:
|
if current is not None:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from mcp.client.session import ClientSession
|
|||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource
|
||||||
from wf_sources_mcp.connections import McpSourceConnection
|
from wf_sources_mcp.connections import McpSourceConnection
|
||||||
from wf_sources_mcp.sdk import ToolCallResult
|
from wf_sources_mcp.sdk import ToolCallResult
|
||||||
from wf_sources_mcp.sdk.converters import tool_result_to_call_result
|
from wf_sources_mcp.sdk.converters import tool_result_to_call_result
|
||||||
@@ -18,20 +19,18 @@ RawPromptGetter = Callable[
|
|||||||
[str, dict[str, str] | None],
|
[str, dict[str, str] | None],
|
||||||
Awaitable[dict[str, Any]],
|
Awaitable[dict[str, Any]],
|
||||||
]
|
]
|
||||||
|
RawResourceLister = Callable[[], Awaitable[list[DiscoveredResource]]]
|
||||||
|
RawPromptLister = Callable[[], Awaitable[list[DiscoveredPrompt]]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class PersistentMcpSession:
|
class PersistentMcpSession:
|
||||||
"""Long-lived MCP execution handle for one configured connection.
|
"""Long-lived MCP execution handle for one configured connection.
|
||||||
|
|
||||||
Production sessions use `call_callback` because MCP transports are entered
|
Production sessions route all MCP operations through the owner-task queue
|
||||||
inside an AnyIO cancel scope and must be called and closed by that same
|
for session safety. `client` remains available for simple injected/fake
|
||||||
owner task. `client` remains available for simple injected/fake sessions in
|
sessions in tests. `call_tool()` always normalizes SDK results for workflow
|
||||||
tests. `call_tool()` always normalizes SDK results for workflow nodes.
|
nodes.
|
||||||
|
|
||||||
This runtime intentionally exposes only tool calls for now. Shared
|
|
||||||
non-tool operations live on `McpSourceClient`; routing them through the
|
|
||||||
owner task is a separate runtime-expansion slice.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
connection: McpSourceConnection
|
connection: McpSourceConnection
|
||||||
@@ -40,6 +39,8 @@ class PersistentMcpSession:
|
|||||||
call_callback: RawToolCaller | None = None
|
call_callback: RawToolCaller | None = None
|
||||||
read_resource_callback: RawResourceReader | None = None
|
read_resource_callback: RawResourceReader | None = None
|
||||||
get_prompt_callback: RawPromptGetter | None = None
|
get_prompt_callback: RawPromptGetter | None = None
|
||||||
|
list_resources_callback: RawResourceLister | None = None
|
||||||
|
list_prompts_callback: RawPromptLister | None = None
|
||||||
close_callback: Callable[[], Awaitable[None]] | None = None
|
close_callback: Callable[[], Awaitable[None]] | None = None
|
||||||
|
|
||||||
async def call_tool(
|
async def call_tool(
|
||||||
@@ -74,6 +75,28 @@ class PersistentMcpSession:
|
|||||||
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
||||||
raise RuntimeError("persistent MCP session has no prompt transport")
|
raise RuntimeError("persistent MCP session has no prompt transport")
|
||||||
|
|
||||||
|
async def list_resources(self) -> list[DiscoveredResource]:
|
||||||
|
"""List MCP resources through the owner task or injected session."""
|
||||||
|
if self.list_resources_callback is not None:
|
||||||
|
return await self.list_resources_callback()
|
||||||
|
if self.client is not None:
|
||||||
|
from wf_sources_mcp.sdk.converters import resource_to_discovered
|
||||||
|
|
||||||
|
result = await self.client.list_resources()
|
||||||
|
return [resource_to_discovered(resource) for resource in result.resources]
|
||||||
|
raise RuntimeError("persistent MCP session has no resource list transport")
|
||||||
|
|
||||||
|
async def list_prompts(self) -> list[DiscoveredPrompt]:
|
||||||
|
"""List MCP prompts through the owner task or injected session."""
|
||||||
|
if self.list_prompts_callback is not None:
|
||||||
|
return await self.list_prompts_callback()
|
||||||
|
if self.client is not None:
|
||||||
|
from wf_sources_mcp.sdk.converters import prompt_to_discovered
|
||||||
|
|
||||||
|
result = await self.client.list_prompts()
|
||||||
|
return [prompt_to_discovered(prompt) for prompt in result.prompts]
|
||||||
|
raise RuntimeError("persistent MCP session has no prompt list transport")
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""Close the transport/session stack owned by the runtime factory."""
|
"""Close the transport/session stack owned by the runtime factory."""
|
||||||
if self.close_callback is not None:
|
if self.close_callback is not None:
|
||||||
|
|||||||
@@ -8,14 +8,25 @@ from .converters import (
|
|||||||
tool_to_discovered,
|
tool_to_discovered,
|
||||||
workflow_output_schema_from_mcp_tool_schema,
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
)
|
)
|
||||||
from .protocols import BackendAdapter, StatefulMcpRuntime, ToolCallResult, ToolExecutor
|
from .protocols import (
|
||||||
|
BackendAdapter,
|
||||||
|
PromptRuntime,
|
||||||
|
ResourceRuntime,
|
||||||
|
StatefulMcpRuntime,
|
||||||
|
ToolCallResult,
|
||||||
|
ToolExecutor,
|
||||||
|
ToolRuntime,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BackendAdapter",
|
"BackendAdapter",
|
||||||
"McpSdkAdapter",
|
"McpSdkAdapter",
|
||||||
|
"PromptRuntime",
|
||||||
|
"ResourceRuntime",
|
||||||
"StatefulMcpRuntime",
|
"StatefulMcpRuntime",
|
||||||
"ToolCallResult",
|
"ToolCallResult",
|
||||||
"ToolExecutor",
|
"ToolExecutor",
|
||||||
|
"ToolRuntime",
|
||||||
"prompt_to_discovered",
|
"prompt_to_discovered",
|
||||||
"resource_to_discovered",
|
"resource_to_discovered",
|
||||||
"tool_result_to_call_result",
|
"tool_result_to_call_result",
|
||||||
|
|||||||
@@ -82,13 +82,8 @@ class BackendAdapter(Protocol):
|
|||||||
) -> ToolCallResult: ...
|
) -> ToolCallResult: ...
|
||||||
|
|
||||||
|
|
||||||
class ToolExecutor(Protocol):
|
class ToolRuntime(Protocol):
|
||||||
"""Runtime boundary for executing MCP tools from workflow nodes.
|
"""Runtime boundary for executing MCP tools from workflow nodes."""
|
||||||
|
|
||||||
Discovery can stay one-shot, but workflow execution needs this smaller
|
|
||||||
protocol so persistent runtime pools can replace one-shot adapters without
|
|
||||||
changing generated NodeSpecs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def call_tool(
|
async def call_tool(
|
||||||
self,
|
self,
|
||||||
@@ -99,12 +94,18 @@ class ToolExecutor(Protocol):
|
|||||||
) -> ToolCallResult: ...
|
) -> ToolCallResult: ...
|
||||||
|
|
||||||
|
|
||||||
class StatefulMcpRuntime(ToolExecutor, Protocol):
|
class ToolExecutor(ToolRuntime, Protocol):
|
||||||
"""Stateful execution/read boundary for configured MCP sources.
|
"""Compatibility name for workflow-node tool execution."""
|
||||||
|
|
||||||
Implementations keep source session state across calls. Discovery/catalog
|
|
||||||
refresh may still use one-shot adapters by policy.
|
class ResourceRuntime(Protocol):
|
||||||
"""
|
"""Stateful resource operations for configured MCP sources."""
|
||||||
|
|
||||||
|
async def list_resources(
|
||||||
|
self,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
) -> list[DiscoveredResource]: ...
|
||||||
|
|
||||||
async def read_resource(
|
async def read_resource(
|
||||||
self,
|
self,
|
||||||
@@ -113,6 +114,16 @@ class StatefulMcpRuntime(ToolExecutor, Protocol):
|
|||||||
uri: str,
|
uri: str,
|
||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class PromptRuntime(Protocol):
|
||||||
|
"""Stateful prompt operations for configured MCP sources."""
|
||||||
|
|
||||||
|
async def list_prompts(
|
||||||
|
self,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
) -> list[DiscoveredPrompt]: ...
|
||||||
|
|
||||||
async def get_prompt(
|
async def get_prompt(
|
||||||
self,
|
self,
|
||||||
connection: McpSourceConnection,
|
connection: McpSourceConnection,
|
||||||
@@ -122,9 +133,20 @@ class StatefulMcpRuntime(ToolExecutor, Protocol):
|
|||||||
) -> dict[str, Any]: ...
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class StatefulMcpRuntime(ToolRuntime, ResourceRuntime, PromptRuntime, Protocol):
|
||||||
|
"""Stateful execution/read/list boundary for configured MCP sources.
|
||||||
|
|
||||||
|
Implementations keep source session state across calls. Catalog refresh may
|
||||||
|
still use one-shot adapters by policy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BackendAdapter",
|
"BackendAdapter",
|
||||||
|
"PromptRuntime",
|
||||||
|
"ResourceRuntime",
|
||||||
"StatefulMcpRuntime",
|
"StatefulMcpRuntime",
|
||||||
"ToolCallResult",
|
"ToolCallResult",
|
||||||
"ToolExecutor",
|
"ToolExecutor",
|
||||||
|
"ToolRuntime",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -120,6 +120,21 @@ def test_wf_mcp_sdk_protocol_shims_reexport_wf_sources_mcp_sdk() -> None:
|
|||||||
assert CompatBaseToolCallResult is ToolCallResult
|
assert CompatBaseToolCallResult is ToolCallResult
|
||||||
assert CompatBaseStatefulMcpRuntime is StatefulMcpRuntime
|
assert CompatBaseStatefulMcpRuntime is StatefulMcpRuntime
|
||||||
|
|
||||||
|
from wf_mcp.sdk import PromptRuntime as CompatPromptRuntime
|
||||||
|
from wf_mcp.sdk import ResourceRuntime as CompatResourceRuntime
|
||||||
|
from wf_mcp.sdk import ToolRuntime as CompatToolRuntime
|
||||||
|
from wf_mcp.sdk.base import PromptRuntime as CompatBasePromptRuntime
|
||||||
|
from wf_mcp.sdk.base import ResourceRuntime as CompatBaseResourceRuntime
|
||||||
|
from wf_mcp.sdk.base import ToolRuntime as CompatBaseToolRuntime
|
||||||
|
from wf_sources_mcp.sdk import PromptRuntime, ResourceRuntime, ToolRuntime
|
||||||
|
|
||||||
|
assert CompatPromptRuntime is PromptRuntime
|
||||||
|
assert CompatResourceRuntime is ResourceRuntime
|
||||||
|
assert CompatToolRuntime is ToolRuntime
|
||||||
|
assert CompatBasePromptRuntime is PromptRuntime
|
||||||
|
assert CompatBaseResourceRuntime is ResourceRuntime
|
||||||
|
assert CompatBaseToolRuntime is ToolRuntime
|
||||||
|
|
||||||
|
|
||||||
def test_wf_mcp_runtime_protocol_shim_reexports_wf_sources_mcp_tool_executor() -> None:
|
def test_wf_mcp_runtime_protocol_shim_reexports_wf_sources_mcp_tool_executor() -> None:
|
||||||
from wf_mcp.runtime import ToolExecutor as CompatRuntimeToolExecutor
|
from wf_mcp.runtime import ToolExecutor as CompatRuntimeToolExecutor
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ from typing import Any
|
|||||||
import pytest
|
import pytest
|
||||||
from mcp.client.session import ClientSession
|
from mcp.client.session import ClientSession
|
||||||
from mcp.types import CallToolResult as RawCallToolResult
|
from mcp.types import CallToolResult as RawCallToolResult
|
||||||
from mcp.types import TextContent
|
from mcp.types import (
|
||||||
|
ListPromptsResult,
|
||||||
|
ListResourcesResult,
|
||||||
|
Prompt,
|
||||||
|
Resource,
|
||||||
|
TextContent,
|
||||||
|
)
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
|
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
@@ -136,6 +142,31 @@ class _FakeFactory(PersistentSessionFactory):
|
|||||||
},
|
},
|
||||||
)()
|
)()
|
||||||
|
|
||||||
|
async def list_resources(self) -> ListResourcesResult:
|
||||||
|
return ListResourcesResult(
|
||||||
|
resources=[
|
||||||
|
Resource(
|
||||||
|
uri=AnyUrl("fixture://docs/runtime"),
|
||||||
|
name="resource.runtime",
|
||||||
|
title="Runtime Resource",
|
||||||
|
description="Runtime-scoped resource.",
|
||||||
|
mimeType="text/plain",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_prompts(self) -> ListPromptsResult:
|
||||||
|
return ListPromptsResult(
|
||||||
|
prompts=[
|
||||||
|
Prompt(
|
||||||
|
name="prompt.runtime",
|
||||||
|
title="Runtime Prompt",
|
||||||
|
description="Runtime-scoped prompt.",
|
||||||
|
arguments=[],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return _FakeClient() # type: ignore[return-value]
|
return _FakeClient() # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
@@ -209,6 +240,8 @@ def test_persistent_session_public_runtime_exposes_safe_read_operations() -> Non
|
|||||||
assert "call_tool" in public_operations
|
assert "call_tool" in public_operations
|
||||||
assert "read_resource" in public_operations
|
assert "read_resource" in public_operations
|
||||||
assert "get_prompt" in public_operations
|
assert "get_prompt" in public_operations
|
||||||
|
assert "list_resources" in public_operations
|
||||||
|
assert "list_prompts" in public_operations
|
||||||
assert "invoke_method" not in public_operations
|
assert "invoke_method" not in public_operations
|
||||||
assert "send_notification" not in public_operations
|
assert "send_notification" not in public_operations
|
||||||
|
|
||||||
@@ -298,3 +331,54 @@ async def test_runtime_pool_reuses_session_for_tool_resource_and_prompt() -> Non
|
|||||||
"prompt.summarize:{'text': 'hello'}"
|
"prompt.summarize:{'text': 'hello'}"
|
||||||
)
|
)
|
||||||
assert factory.created_connections == [connection]
|
assert factory.created_connections == [connection]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persistent_session_factory_routes_resource_and_prompt_lists() -> None:
|
||||||
|
factory = _FakeFactory()
|
||||||
|
session = await factory.create(_connection(), None)
|
||||||
|
|
||||||
|
resources = await session.list_resources()
|
||||||
|
prompts = await session.list_prompts()
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
assert resources[0].name == "resource.runtime"
|
||||||
|
assert resources[0].uri == "fixture://docs/runtime"
|
||||||
|
assert prompts[0].name == "prompt.runtime"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_runtime_pool_reuses_session_for_resource_and_prompt_lists() -> None:
|
||||||
|
factory = _FakeFactory()
|
||||||
|
pool = McpRuntimePool(factory.create)
|
||||||
|
connection = _connection()
|
||||||
|
|
||||||
|
resources = await pool.list_resources(connection, None)
|
||||||
|
prompts = await pool.list_prompts(connection, None)
|
||||||
|
await pool.close_all()
|
||||||
|
|
||||||
|
assert resources[0].name == "resource.runtime"
|
||||||
|
assert prompts[0].name == "prompt.runtime"
|
||||||
|
assert factory.created_connections == [connection]
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_pool_satisfies_stateful_protocol_static_shape() -> None:
|
||||||
|
from wf_sources_mcp.sdk import (
|
||||||
|
PromptRuntime,
|
||||||
|
ResourceRuntime,
|
||||||
|
StatefulMcpRuntime,
|
||||||
|
ToolRuntime,
|
||||||
|
)
|
||||||
|
|
||||||
|
factory = _FakeFactory()
|
||||||
|
pool = McpRuntimePool(factory.create)
|
||||||
|
|
||||||
|
tool_runtime: ToolRuntime = pool
|
||||||
|
resource_runtime: ResourceRuntime = pool
|
||||||
|
prompt_runtime: PromptRuntime = pool
|
||||||
|
stateful_runtime: StatefulMcpRuntime = pool
|
||||||
|
|
||||||
|
assert tool_runtime is pool
|
||||||
|
assert resource_runtime is pool
|
||||||
|
assert prompt_runtime is pool
|
||||||
|
assert stateful_runtime is pool
|
||||||
|
|||||||
@@ -82,3 +82,19 @@ def test_stateful_mcp_runtime_protocol_shape() -> None:
|
|||||||
|
|
||||||
assert StatefulMcpRuntime.__name__ == "StatefulMcpRuntime"
|
assert StatefulMcpRuntime.__name__ == "StatefulMcpRuntime"
|
||||||
assert ToolExecutor.__name__ == "ToolExecutor"
|
assert ToolExecutor.__name__ == "ToolExecutor"
|
||||||
|
|
||||||
|
|
||||||
|
def test_stateful_runtime_protocol_slices_export() -> None:
|
||||||
|
from wf_sources_mcp.sdk import (
|
||||||
|
PromptRuntime,
|
||||||
|
ResourceRuntime,
|
||||||
|
StatefulMcpRuntime,
|
||||||
|
ToolExecutor,
|
||||||
|
ToolRuntime,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ToolRuntime.__name__ == "ToolRuntime"
|
||||||
|
assert ResourceRuntime.__name__ == "ResourceRuntime"
|
||||||
|
assert PromptRuntime.__name__ == "PromptRuntime"
|
||||||
|
assert ToolExecutor.__name__ == "ToolExecutor"
|
||||||
|
assert StatefulMcpRuntime.__name__ == "StatefulMcpRuntime"
|
||||||
|
|||||||
Reference in New Issue
Block a user