remove just about everything bad coming from broker
This commit is contained in:
@@ -17,9 +17,9 @@
|
|||||||
`PersistentSessionFactory` exist, and config-built services use the runtime
|
`PersistentSessionFactory` exist, and config-built services use the runtime
|
||||||
pool for generated workflow node execution while discovery/catalog refreshes
|
pool for generated workflow node execution while discovery/catalog refreshes
|
||||||
still use short-lived SDK adapter sessions.
|
still use short-lived SDK adapter sessions.
|
||||||
- Remaining work starts at hiding/removing unsafe raw public `call_tool`
|
- Unsafe raw public `call_tool` surfaces have been deleted. Remaining work starts
|
||||||
surfaces and then renaming the legacy `transparent_proxy` package to the
|
at renaming the legacy `transparent_proxy` package to the clearer
|
||||||
clearer proxy/provider-layer package.
|
proxy/provider-layer package.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,12 @@ The broker keeps two related catalog views:
|
|||||||
- `get_catalog()` is the backend MCP catalog. It only includes enabled upstream
|
- `get_catalog()` is the backend MCP catalog. It only includes enabled upstream
|
||||||
connection snapshots loaded from storage.
|
connection snapshots loaded from storage.
|
||||||
- `get_planner_catalog()` is the workflow-planning catalog. It includes backend
|
- `get_planner_catalog()` is the workflow-planning catalog. It includes backend
|
||||||
connection snapshots plus broker-local system sources such as `wf.std` and
|
connection snapshots plus broker-local system sources such as `wf.std`.
|
||||||
`wf.mcp`.
|
|
||||||
|
|
||||||
Broker-local sources are not fake MCP backend connections. They are registered
|
Broker-local sources are not fake MCP backend connections. They are registered
|
||||||
as service spec sources so raw workflow plans can address nodes like
|
as service spec sources so raw workflow plans can address nodes like
|
||||||
`wf.std.runtime_error` and `wf.mcp.call_tool` without polluting connection status,
|
`wf.std.runtime_error` without polluting connection status, auth, adapter lookup,
|
||||||
auth, adapter lookup, or persisted backend catalog snapshots.
|
or persisted backend catalog snapshots.
|
||||||
|
|
||||||
The longer-term source model is described in
|
The longer-term source model is described in
|
||||||
[`wf_mcp_capability_sources.md`](wf_mcp_capability_sources.md). In that model,
|
[`wf_mcp_capability_sources.md`](wf_mcp_capability_sources.md). In that model,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ connections into that model rather than owning the model itself.
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
CapabilitySource
|
CapabilitySource
|
||||||
id: "wf.std" | "wf.docs" | "wf.mcp" | "wf.admin" | "<server>.<account>"
|
id: "wf.std" | "wf.docs" | "wf.admin" | "<server>.<account>"
|
||||||
kind: "system" | "connection"
|
kind: "system" | "connection"
|
||||||
enabled: bool
|
enabled: bool
|
||||||
visibility:
|
visibility:
|
||||||
@@ -94,25 +94,14 @@ Expected capabilities:
|
|||||||
|
|
||||||
### `wf.mcp`
|
### `wf.mcp`
|
||||||
|
|
||||||
Workflow runtime helpers for interacting with MCP backends.
|
Reserved for future workflow-safe MCP helpers.
|
||||||
|
|
||||||
- Planner-visible: yes, for workflow node specs.
|
`wf.mcp` currently owns no public NodeSpecs. The previous raw
|
||||||
- MCP-client-visible: maybe, for docs/prompts/resources, not admin mutation.
|
`wf.mcp.call_tool` helper was deleted because it duplicated the proxy tool
|
||||||
- Admin-dashboard-visible: yes, for inspection and source toggling.
|
surface and used the wrong abstraction for stateful servers. Workflow authors
|
||||||
- MCP tools: normally none.
|
should use generated connection NodeSpecs, saved wrappers, or
|
||||||
- Workflow safety: mixed. Individual capabilities must be marked.
|
`wf.workflow.call_capability` when they need to test a workflow-facing
|
||||||
|
capability.
|
||||||
Expected capabilities:
|
|
||||||
|
|
||||||
- `node_specs`: currently `wf.mcp.call_tool`.
|
|
||||||
- Near-term node specs may include `wf.mcp.read_resource` and
|
|
||||||
`wf.mcp.get_prompt`.
|
|
||||||
- Advanced/escape-hatch node specs may include:
|
|
||||||
`wf.mcp.invoke_method`, `wf.mcp.send_notification`.
|
|
||||||
- `prompts/resources`: docs for building MCP-backed workflows.
|
|
||||||
|
|
||||||
`wf.mcp` is not the admin namespace. It should mean "workflow can interact with
|
|
||||||
MCP capabilities."
|
|
||||||
|
|
||||||
### `wf.admin`
|
### `wf.admin`
|
||||||
|
|
||||||
@@ -128,7 +117,7 @@ Expected capabilities:
|
|||||||
|
|
||||||
- `tools`: list sources, enable source, disable source, refresh catalog, list
|
- `tools`: list sources, enable source, disable source, refresh catalog, list
|
||||||
connections, add/update/remove connections, view config, reload config, inspect
|
connections, add/update/remove connections, view config, reload config, inspect
|
||||||
events, inspect proxy tools, call/debug upstream capabilities.
|
events, and inspect proxy tools.
|
||||||
- `prompts/resources`: admin documentation may be useful later.
|
- `prompts/resources`: admin documentation may be useful later.
|
||||||
|
|
||||||
This source is privileged. A normal client LLM should not automatically see
|
This source is privileged. A normal client LLM should not automatically see
|
||||||
@@ -174,7 +163,6 @@ and capability is safe or explicitly allowed for workflow
|
|||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- include `wf.std.runtime_error`
|
- include `wf.std.runtime_error`
|
||||||
- include `wf.mcp.call_tool`
|
|
||||||
- include `everything.default.echo`
|
- include `everything.default.echo`
|
||||||
- exclude `wf.admin.disable_source`
|
- exclude `wf.admin.disable_source`
|
||||||
|
|
||||||
@@ -200,7 +188,7 @@ Examples:
|
|||||||
|
|
||||||
- `wf.std.workflow_manual`
|
- `wf.std.workflow_manual`
|
||||||
- `wf.std.error_handling_guide`
|
- `wf.std.error_handling_guide`
|
||||||
- `wf.mcp.mcp_workflow_guide`
|
- MCP workflow guides under `wf.docs`
|
||||||
|
|
||||||
These are MCP-visible without implying the source exposes MCP tools.
|
These are MCP-visible without implying the source exposes MCP tools.
|
||||||
|
|
||||||
@@ -241,8 +229,8 @@ The code now has the first capability-source layer in place.
|
|||||||
directly; the old `SpecSource` compatibility layer has been removed.
|
directly; the old `SpecSource` compatibility layer has been removed.
|
||||||
- `wf.std` owns current `wf_authoring.ops` workflow node specs under
|
- `wf.std` owns current `wf_authoring.ops` workflow node specs under
|
||||||
`wf.std.*`.
|
`wf.std.*`.
|
||||||
- `wf.mcp` owns workflow MCP runtime node specs, currently
|
- `wf.mcp` is reserved for future workflow-safe MCP helpers and currently owns
|
||||||
`wf.mcp.call_tool`.
|
no public NodeSpecs.
|
||||||
- `wf.admin` owns privileged admin capability metadata and is not planner-visible
|
- `wf.admin` owns privileged admin capability metadata and is not planner-visible
|
||||||
by default.
|
by default.
|
||||||
- Transparent proxy admin tools now use dotted `wf.admin.*` names through
|
- Transparent proxy admin tools now use dotted `wf.admin.*` names through
|
||||||
@@ -266,7 +254,7 @@ Current code has several useful pieces but the boundaries are blurred.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `wf_authoring.ops` | reusable workflow node specs | `wf.std.node_specs` |
|
| `wf_authoring.ops` | reusable workflow node specs | `wf.std.node_specs` |
|
||||||
| `wf_core` built-in reducers | reusable workflow state reducers | `wf.std.reducers` |
|
| `wf_core` built-in reducers | reusable workflow state reducers | `wf.std.reducers` |
|
||||||
| `wf_mcp.broker.service.builtins` | local workflow specs | `wf.std`, `wf.mcp` |
|
| `wf_mcp.broker.service.builtins` | local workflow specs | `wf.std` |
|
||||||
| `wf_mcp.broker.tools` | compatibility wrapper over shared service-admin registration | `wf.admin.tools` |
|
| `wf_mcp.broker.tools` | compatibility wrapper over shared service-admin registration | `wf.admin.tools` |
|
||||||
| `wf_mcp.admin_surface.tools` | shared service-backed admin tool registration | `wf.admin.tools` |
|
| `wf_mcp.admin_surface.tools` | shared service-backed admin tool registration | `wf.admin.tools` |
|
||||||
| `wf_mcp.transparent_proxy.admin` | proxy-backed public admin tools | `wf.admin.tools` |
|
| `wf_mcp.transparent_proxy.admin` | proxy-backed public admin tools | `wf.admin.tools` |
|
||||||
|
|||||||
+5
-4
@@ -191,10 +191,9 @@ Not all of these need workflow semantics immediately, but they should still have
|
|||||||
|
|
||||||
The broker also needs a more protocol-native mode than the current generic broker tools.
|
The broker also needs a more protocol-native mode than the current generic broker tools.
|
||||||
|
|
||||||
The current broker tools are useful:
|
The current broker tools are useful for catalog and control-plane inspection:
|
||||||
|
|
||||||
- `get_catalog`
|
- `get_catalog`
|
||||||
- `call_broker_tool`
|
|
||||||
- `read_broker_resource`
|
- `read_broker_resource`
|
||||||
- `render_broker_prompt`
|
- `render_broker_prompt`
|
||||||
- `invoke_broker_method`
|
- `invoke_broker_method`
|
||||||
@@ -235,7 +234,9 @@ Example desired tool entry:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This is different from `call_broker_tool`. `call_broker_tool` is a convenience wrapper. Mirrored tools are actual MCP tools exposed by the broker.
|
Mirrored tools are actual MCP tools exposed by the broker/proxy surface. Do not
|
||||||
|
reintroduce a generic `call_tool` wrapper for upstream tools; it duplicates the
|
||||||
|
proxy plane and is the wrong abstraction for stateful MCP servers.
|
||||||
|
|
||||||
### Official protocol boundary
|
### Official protocol boundary
|
||||||
|
|
||||||
@@ -333,7 +334,7 @@ For a client like Inspector to show an elicitation form from an upstream server,
|
|||||||
Near-term stance:
|
Near-term stance:
|
||||||
|
|
||||||
- mirror normal tools/resources/prompts first
|
- mirror normal tools/resources/prompts first
|
||||||
- keep `call_broker_tool` and raw method invocation as debugging fallbacks
|
- keep raw method invocation only for explicit protocol/admin diagnostics
|
||||||
- test the Everything Reference Server elicitation tool as the first serious proxy pressure test
|
- test the Everything Reference Server elicitation tool as the first serious proxy pressure test
|
||||||
- only claim elicitation/sampling support once a live Inspector flow works end to end
|
- only claim elicitation/sampling support once a live Inspector flow works end to end
|
||||||
|
|
||||||
|
|||||||
@@ -401,15 +401,12 @@ Local system sources use the same binding mechanism. For example:
|
|||||||
```text
|
```text
|
||||||
artifact reference: wf.std.replace
|
artifact reference: wf.std.replace
|
||||||
runtime binding: wf.std -> wf.std
|
runtime binding: wf.std -> wf.std
|
||||||
|
|
||||||
artifact reference: wf.mcp.call_tool
|
|
||||||
runtime binding: wf.mcp -> wf.mcp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
These self-bindings are not external account choices. They keep dependency
|
These self-bindings are not external account choices. They keep dependency
|
||||||
resolution uniform across local system sources and upstream connection sources.
|
resolution uniform across local system sources and upstream connection sources.
|
||||||
They may become implicit later, but today deployments should include them when
|
They may become implicit later, but today deployments should include them when
|
||||||
validation reports `binding_missing` for `wf.std` or `wf.mcp`.
|
validation reports `binding_missing` for `wf.std`.
|
||||||
|
|
||||||
or:
|
or:
|
||||||
|
|
||||||
|
|||||||
@@ -88,30 +88,5 @@ class BrokerAdminHandlers:
|
|||||||
**error_payload(exc),
|
**error_payload(exc),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def call_broker_tool(
|
|
||||||
self,
|
|
||||||
connection_id: str,
|
|
||||||
tool_name: str,
|
|
||||||
arguments: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
try:
|
|
||||||
return {
|
|
||||||
"connection_id": connection_id,
|
|
||||||
"tool_name": tool_name,
|
|
||||||
"ok": True,
|
|
||||||
**await self.service.call_tool(
|
|
||||||
connection_id,
|
|
||||||
tool_name,
|
|
||||||
arguments=arguments,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {
|
|
||||||
"connection_id": connection_id,
|
|
||||||
"tool_name": tool_name,
|
|
||||||
"ok": False,
|
|
||||||
**error_payload(exc),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_broker_events(self) -> list[dict[str, Any]]:
|
def get_broker_events(self) -> list[dict[str, Any]]:
|
||||||
return [asdict(event) for event in self.service.list_events()]
|
return [asdict(event) for event in self.service.list_events()]
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ def register_service_admin_tools(
|
|||||||
"read_resource": "read_broker_resource",
|
"read_resource": "read_broker_resource",
|
||||||
"render_prompt": "render_broker_prompt",
|
"render_prompt": "render_broker_prompt",
|
||||||
"invoke_method": "invoke_broker_method",
|
"invoke_method": "invoke_broker_method",
|
||||||
"call_tool": "call_broker_tool",
|
|
||||||
"get_events": "get_broker_events",
|
"get_events": "get_broker_events",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,22 +141,6 @@ def register_service_admin_tools(
|
|||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
|
|
||||||
@server.tool(
|
|
||||||
name=name("call_tool"),
|
|
||||||
title="Call Tool",
|
|
||||||
description="Call one upstream MCP tool through the broker service layer.",
|
|
||||||
)
|
|
||||||
async def call_tool(
|
|
||||||
connection_id: str,
|
|
||||||
tool_name: str,
|
|
||||||
arguments: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await handlers.call_broker_tool(
|
|
||||||
connection_id,
|
|
||||||
tool_name,
|
|
||||||
arguments=arguments,
|
|
||||||
)
|
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name=name("get_events"),
|
name=name("get_events"),
|
||||||
title="Get Events",
|
title="Get Events",
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ Build workflows from current capabilities instead of assuming a stale catalog.
|
|||||||
Use `get_planner_catalog` when you need the current workflow-capability view.
|
Use `get_planner_catalog` when you need the current workflow-capability view.
|
||||||
Use `list_sources` when you need to understand source ownership, visibility,
|
Use `list_sources` when you need to understand source ownership, visibility,
|
||||||
and capability kinds.
|
and capability kinds.
|
||||||
Use `call_broker_tool` to test an upstream MCP tool manually before wrapping it
|
Use source-projected workflow capabilities or directly exposed proxy tools to
|
||||||
into a workflow.
|
test the smallest reusable piece before wrapping it into a workflow.
|
||||||
|
|
||||||
Prefer namespaced capabilities, inspect before you rely on them, and test the
|
Prefer namespaced capabilities, inspect before you rely on them, and test the
|
||||||
smallest reusable piece before saving a larger workflow artifact.
|
smallest reusable piece before saving a larger workflow artifact.
|
||||||
|
|||||||
@@ -1,30 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Protocol
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from wf_authoring import NodeSpec, coalesce, constant, default_if_none, first_item
|
||||||
|
from wf_authoring import first_item_maybe, first_item_or_none, is_empty, last_item
|
||||||
from wf_authoring import (
|
from wf_authoring import last_item_or_none, length, node, pick_key, pick_path
|
||||||
NodeReturn,
|
from wf_authoring import project_fields, rename_fields, runtime_error, truthy
|
||||||
NodeSpec,
|
|
||||||
coalesce,
|
|
||||||
constant,
|
|
||||||
default_if_none,
|
|
||||||
first_item,
|
|
||||||
first_item_maybe,
|
|
||||||
first_item_or_none,
|
|
||||||
is_empty,
|
|
||||||
last_item,
|
|
||||||
last_item_or_none,
|
|
||||||
length,
|
|
||||||
node,
|
|
||||||
pick_key,
|
|
||||||
pick_path,
|
|
||||||
project_fields,
|
|
||||||
rename_fields,
|
|
||||||
runtime_error,
|
|
||||||
truthy,
|
|
||||||
)
|
|
||||||
from wf_core.runtime.ops.merges import DEFAULT_REDUCER_DEFINITIONS
|
from wf_core.runtime.ops.merges import DEFAULT_REDUCER_DEFINITIONS
|
||||||
|
|
||||||
from wf_platform import (
|
from wf_platform import (
|
||||||
@@ -42,7 +23,7 @@ BUILTIN_CONNECTION_ID = "wf.std"
|
|||||||
"""Internal source id for workflow standard-library node specs."""
|
"""Internal source id for workflow standard-library node specs."""
|
||||||
|
|
||||||
MCP_SOURCE_ID = "wf.mcp"
|
MCP_SOURCE_ID = "wf.mcp"
|
||||||
"""Internal source id for broker MCP utility node specs."""
|
"""Reserved source id for future workflow-safe MCP utility node specs."""
|
||||||
|
|
||||||
|
|
||||||
AUTHORING_STD_SPECS: tuple[NodeSpec[Any, Any], ...] = (
|
AUTHORING_STD_SPECS: tuple[NodeSpec[Any, Any], ...] = (
|
||||||
@@ -66,43 +47,6 @@ AUTHORING_STD_SPECS: tuple[NodeSpec[Any, Any], ...] = (
|
|||||||
"""Existing authoring ops that are also exposed through the workflow stdlib."""
|
"""Existing authoring ops that are also exposed through the workflow stdlib."""
|
||||||
|
|
||||||
|
|
||||||
class ToolCaller(Protocol):
|
|
||||||
"""Small service boundary needed by the broker-local MCP utility nodes."""
|
|
||||||
|
|
||||||
async def call_tool(
|
|
||||||
self,
|
|
||||||
connection_id: str,
|
|
||||||
tool_name: str,
|
|
||||||
*,
|
|
||||||
arguments: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
|
|
||||||
class McpCallToolInput(BaseModel):
|
|
||||||
"""Input for calling a proxied MCP tool from inside a workflow."""
|
|
||||||
|
|
||||||
connection_id: str = Field(description="Connection id that owns the MCP tool.")
|
|
||||||
tool_name: str = Field(description="Local tool name on the upstream MCP server.")
|
|
||||||
arguments: dict[str, Any] = Field(
|
|
||||||
default_factory=dict,
|
|
||||||
description="JSON-compatible arguments passed to the upstream tool.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class McpCallToolOutput(BaseModel):
|
|
||||||
"""Normalized output returned by a proxied MCP tool call."""
|
|
||||||
|
|
||||||
outcome: str = Field(description="Workflow outcome reported by the upstream tool.")
|
|
||||||
output: dict[str, Any] = Field(
|
|
||||||
default_factory=dict,
|
|
||||||
description="JSON-compatible tool result payload.",
|
|
||||||
)
|
|
||||||
meta: dict[str, Any] = Field(
|
|
||||||
default_factory=dict,
|
|
||||||
description="Adapter metadata returned with the tool result.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def builtin_specs() -> dict[str, NodeSpec[Any, Any]]:
|
def builtin_specs() -> dict[str, NodeSpec[Any, Any]]:
|
||||||
"""Return built-in NodeSpecs available to raw broker workflow plans."""
|
"""Return built-in NodeSpecs available to raw broker workflow plans."""
|
||||||
specs = [
|
specs = [
|
||||||
@@ -126,30 +70,7 @@ def builtin_reducer_definitions():
|
|||||||
return dict(DEFAULT_REDUCER_DEFINITIONS)
|
return dict(DEFAULT_REDUCER_DEFINITIONS)
|
||||||
|
|
||||||
|
|
||||||
def mcp_specs(service: ToolCaller) -> dict[str, NodeSpec[Any, Any]]:
|
def builtin_sources() -> dict[str, CapabilitySource]:
|
||||||
"""Return service-bound MCP utility specs available to raw plans."""
|
|
||||||
|
|
||||||
@node(
|
|
||||||
name="call_tool",
|
|
||||||
outcomes=("ok", "error"),
|
|
||||||
input_model=McpCallToolInput,
|
|
||||||
output_model=McpCallToolOutput,
|
|
||||||
description="Call a tool on a registered MCP connection.",
|
|
||||||
)
|
|
||||||
async def call_tool(payload: McpCallToolInput) -> NodeReturn[McpCallToolOutput]:
|
|
||||||
result = await service.call_tool(
|
|
||||||
payload.connection_id,
|
|
||||||
payload.tool_name,
|
|
||||||
arguments=payload.arguments,
|
|
||||||
)
|
|
||||||
output = McpCallToolOutput.model_validate(result)
|
|
||||||
return NodeReturn(outcome=output.outcome, output=output)
|
|
||||||
|
|
||||||
qualified_specs = [qualify_spec(MCP_SOURCE_ID, call_tool)]
|
|
||||||
return {spec.name: spec for spec in qualified_specs}
|
|
||||||
|
|
||||||
|
|
||||||
def builtin_sources(service: ToolCaller) -> dict[str, CapabilitySource]:
|
|
||||||
"""Return all broker-local capability sources."""
|
"""Return all broker-local capability sources."""
|
||||||
return {
|
return {
|
||||||
BUILTIN_CONNECTION_ID: CapabilitySource(
|
BUILTIN_CONNECTION_ID: CapabilitySource(
|
||||||
@@ -168,12 +89,4 @@ def builtin_sources(service: ToolCaller) -> dict[str, CapabilitySource]:
|
|||||||
permissions=SourcePermissions(safe_for_workflow=True),
|
permissions=SourcePermissions(safe_for_workflow=True),
|
||||||
description="Workflow standard-library nodes.",
|
description="Workflow standard-library nodes.",
|
||||||
),
|
),
|
||||||
MCP_SOURCE_ID: CapabilitySource(
|
|
||||||
id=MCP_SOURCE_ID,
|
|
||||||
kind="system",
|
|
||||||
capabilities=CapabilityBuckets(node_specs=mcp_specs(service)),
|
|
||||||
visibility=SourceVisibility(planner=True, admin_dashboard=True),
|
|
||||||
permissions=SourcePermissions(calls_upstream=True),
|
|
||||||
description="Broker MCP utility nodes.",
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class WfMcpService:
|
|||||||
_store_root(self.store)
|
_store_root(self.store)
|
||||||
)
|
)
|
||||||
if self.include_builtin_specs:
|
if self.include_builtin_specs:
|
||||||
for source in builtin_sources(self).values():
|
for source in builtin_sources().values():
|
||||||
self.register_capability_source(source)
|
self.register_capability_source(source)
|
||||||
self.register_capability_source(admin_source())
|
self.register_capability_source(admin_source())
|
||||||
|
|
||||||
@@ -449,41 +449,6 @@ class WfMcpService:
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def call_tool(
|
|
||||||
self,
|
|
||||||
connection_id: str,
|
|
||||||
tool_name: str,
|
|
||||||
*,
|
|
||||||
arguments: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
connection = self.connections.get(connection_id)
|
|
||||||
adapter = require_adapter(connection, self.adapters)
|
|
||||||
auth = self.load_auth(connection_id)
|
|
||||||
capability_id = qualify_node_name(connection_id, tool_name)
|
|
||||||
payload = arguments or {}
|
|
||||||
self._record_event(
|
|
||||||
make_event(
|
|
||||||
"tool_call_started",
|
|
||||||
connection_id=connection_id,
|
|
||||||
capability_id=capability_id,
|
|
||||||
payload={"argument_keys": sorted(payload.keys())},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = await adapter.call_tool(connection, auth, tool_name, payload)
|
|
||||||
self._record_event(
|
|
||||||
make_event(
|
|
||||||
"tool_call_completed",
|
|
||||||
connection_id=connection_id,
|
|
||||||
capability_id=capability_id,
|
|
||||||
payload={"outcome": result.outcome},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"outcome": result.outcome,
|
|
||||||
"output": result.output,
|
|
||||||
"meta": result.meta,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def send_notification(
|
async def send_notification(
|
||||||
self,
|
self,
|
||||||
connection_id: str,
|
connection_id: str,
|
||||||
@@ -794,20 +759,29 @@ class WfMcpService:
|
|||||||
self,
|
self,
|
||||||
entry: CatalogNodeEntry,
|
entry: CatalogNodeEntry,
|
||||||
) -> NodeSpec[Any, Any]:
|
) -> NodeSpec[Any, Any]:
|
||||||
"""Rebuild an executable tool wrapper from a stored catalog node entry."""
|
"""Rebuild an executable tool wrapper from a stored catalog node entry.
|
||||||
|
|
||||||
|
Snapshot entries store schema/name metadata, not Python functions. This
|
||||||
|
helper reconstructs the same generated NodeSpec shape and routes calls
|
||||||
|
through `_tool_executor_for()`, so hydrated specs use the persistent MCP
|
||||||
|
runtime when the service has one configured.
|
||||||
|
"""
|
||||||
model_prefix = entry.qualified_name.replace(".", "_").replace("-", "_")
|
model_prefix = entry.qualified_name.replace(".", "_").replace("-", "_")
|
||||||
input_model = _model_from_schema(f"{model_prefix}_Input", entry.input_schema)
|
input_model = _model_from_schema(f"{model_prefix}_Input", entry.input_schema)
|
||||||
output_model = _model_from_schema(f"{model_prefix}_Output", entry.output_schema)
|
output_model = _model_from_schema(f"{model_prefix}_Output", entry.output_schema)
|
||||||
|
|
||||||
async def invoke_tool(payload: BaseModel) -> NodeReturn[BaseModel]:
|
async def invoke_tool(payload: BaseModel) -> NodeReturn[BaseModel]:
|
||||||
result = await self.call_tool(
|
connection = self.connections.get(entry.connection_id)
|
||||||
entry.connection_id,
|
auth = self.load_auth(entry.connection_id)
|
||||||
|
result = await self._tool_executor_for(connection).call_tool(
|
||||||
|
connection,
|
||||||
|
auth,
|
||||||
entry.local_name,
|
entry.local_name,
|
||||||
arguments=payload.model_dump(),
|
payload.model_dump(exclude_unset=True),
|
||||||
)
|
)
|
||||||
return NodeReturn(
|
return NodeReturn(
|
||||||
outcome=result["outcome"],
|
outcome=result.outcome,
|
||||||
output=output_model.model_validate(result["output"]),
|
output=output_model.model_validate(result.output),
|
||||||
)
|
)
|
||||||
|
|
||||||
return NodeSpec(
|
return NodeSpec(
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ def _auth_headers(auth: AuthRecord | None) -> dict[str, str]:
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class PersistentSessionFactory:
|
class PersistentSessionFactory:
|
||||||
"""Create initialized persistent MCP sessions for configured connections."""
|
"""Create initialized persistent MCP sessions for configured connections.
|
||||||
|
|
||||||
|
Input connection metadata must describe either stdio transport
|
||||||
|
(`command`, optional `args`/`env`/`cwd`) or streamable HTTP transport
|
||||||
|
(`url`). The returned session owns its transport stack and closes it through
|
||||||
|
the `PersistentMcpSession.close_callback`.
|
||||||
|
"""
|
||||||
|
|
||||||
async def create(
|
async def create(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ def connection_runtime_fingerprint(
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class McpRuntimePool:
|
class McpRuntimePool:
|
||||||
"""Cache one persistent MCP runtime per unchanged connection fingerprint."""
|
"""Cache one persistent MCP runtime per unchanged connection fingerprint.
|
||||||
|
|
||||||
|
Callers provide full `ConnectionConfig` and optional `AuthRecord` on every
|
||||||
|
call. The pool decides whether that identity still maps to the existing
|
||||||
|
upstream MCP session. If command, URL, account, or auth changes, the old
|
||||||
|
session is closed and replaced.
|
||||||
|
"""
|
||||||
|
|
||||||
session_factory: SessionFactory
|
session_factory: SessionFactory
|
||||||
_sessions: dict[str, tuple[str, PersistentMcpSession]] = field(default_factory=dict)
|
_sessions: dict[str, tuple[str, PersistentMcpSession]] = field(default_factory=dict)
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ from ..sdk.converters import tool_result_to_call_result
|
|||||||
|
|
||||||
@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.
|
||||||
|
|
||||||
|
`client` is an initialized MCP SDK `ClientSession`. `call_tool()` returns
|
||||||
|
this project's normalized `ToolCallResult`, not the SDK result object, so
|
||||||
|
generated workflow nodes do not need to know MCP wire result shapes.
|
||||||
|
"""
|
||||||
|
|
||||||
connection: ConnectionConfig
|
connection: ConnectionConfig
|
||||||
auth: AuthRecord | None
|
auth: AuthRecord | None
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ _JSON_TYPE_MAP: dict[str, object] = {
|
|||||||
|
|
||||||
|
|
||||||
def _python_type_from_schema(schema: object) -> object:
|
def _python_type_from_schema(schema: object) -> object:
|
||||||
"""Map a small JSON Schema subset into a Pydantic field annotation."""
|
"""Map the supported JSON Schema subset into a Pydantic annotation.
|
||||||
|
|
||||||
|
Input is expected to be an MCP tool property schema. This helper is not a
|
||||||
|
full JSON Schema compiler; unsupported shapes intentionally become `Any`
|
||||||
|
so discovery remains tolerant while the original schema contract is still
|
||||||
|
preserved on the generated NodeSpec.
|
||||||
|
"""
|
||||||
if not isinstance(schema, dict):
|
if not isinstance(schema, dict):
|
||||||
return Any
|
return Any
|
||||||
|
|
||||||
@@ -65,14 +71,25 @@ def _field_default(
|
|||||||
property_schema: object,
|
property_schema: object,
|
||||||
required: set[str],
|
required: set[str],
|
||||||
) -> object:
|
) -> object:
|
||||||
"""Return the Pydantic field default for a JSON Schema property."""
|
"""Return the Pydantic default for one MCP tool input property.
|
||||||
|
|
||||||
|
Required fields use `...`; optional fields default to `None`; explicit JSON
|
||||||
|
Schema defaults win. Runtime calls later use `exclude_unset=True`, so absent
|
||||||
|
optional fields are omitted instead of being sent upstream as explicit null.
|
||||||
|
"""
|
||||||
if isinstance(property_schema, dict) and "default" in property_schema:
|
if isinstance(property_schema, dict) and "default" in property_schema:
|
||||||
return property_schema["default"]
|
return property_schema["default"]
|
||||||
return ... if field_name in required else None
|
return ... if field_name in required else None
|
||||||
|
|
||||||
|
|
||||||
def _model_from_schema(name: str, schema: dict[str, Any]) -> type[BaseModel]:
|
def _model_from_schema(name: str, schema: dict[str, Any]) -> type[BaseModel]:
|
||||||
"""Create a loose Pydantic adapter model for an MCP JSON Schema object."""
|
"""Create a loose Pydantic adapter model for an MCP JSON Schema object.
|
||||||
|
|
||||||
|
Input should be an object-like JSON Schema with `properties` and optional
|
||||||
|
`required`. The returned model is only the Python-call boundary for a
|
||||||
|
generated NodeSpec; the original JSON Schema remains the public contract via
|
||||||
|
`input_schema_contract` / `output_schema_contract`.
|
||||||
|
"""
|
||||||
properties = cast(dict[str, Any], schema.get("properties", {}))
|
properties = cast(dict[str, Any], schema.get("properties", {}))
|
||||||
required = set(cast(list[str], schema.get("required", [])))
|
required = set(cast(list[str], schema.get("required", [])))
|
||||||
field_defs: dict[str, tuple[object, object]] = {}
|
field_defs: dict[str, tuple[object, object]] = {}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ def test_create_broker_server_exposes_tools_resources_and_prompts() -> None:
|
|||||||
assert "get_planner_catalog" in tool_names
|
assert "get_planner_catalog" in tool_names
|
||||||
assert "list_sources" in tool_names
|
assert "list_sources" in tool_names
|
||||||
assert "invoke_broker_method" in tool_names
|
assert "invoke_broker_method" in tool_names
|
||||||
assert "call_broker_tool" in tool_names
|
|
||||||
assert "catalog.all" in resource_names
|
assert "catalog.all" in resource_names
|
||||||
assert "events.all" in resource_names
|
assert "events.all" in resource_names
|
||||||
assert "status.all" in resource_names
|
assert "status.all" in resource_names
|
||||||
@@ -89,7 +88,6 @@ def test_create_broker_server_exposes_tools_resources_and_prompts() -> None:
|
|||||||
planner_catalog = cast(dict[str, Any], cast(object, planner_catalog_raw))
|
planner_catalog = cast(dict[str, Any], cast(object, planner_catalog_raw))
|
||||||
planner_names = [node["qualified_name"] for node in planner_catalog["nodes"]]
|
planner_names = [node["qualified_name"] for node in planner_catalog["nodes"]]
|
||||||
assert "demo.personal.echo_tool" in planner_names
|
assert "demo.personal.echo_tool" in planner_names
|
||||||
assert "wf.mcp.call_tool" in planner_names
|
|
||||||
assert "wf.std.runtime_error" in planner_names
|
assert "wf.std.runtime_error" in planner_names
|
||||||
|
|
||||||
_content, all_sources_payload_raw = asyncio.run(
|
_content, all_sources_payload_raw = asyncio.run(
|
||||||
@@ -153,35 +151,6 @@ def test_broker_refresh_tool_returns_structured_error() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_broker_call_tool_returns_structured_result() -> None:
|
|
||||||
service = WfMcpService(store=FileStore(local_temp_root() / "broker_tool_store"))
|
|
||||||
service.register_connection(
|
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
||||||
)
|
|
||||||
service.register_adapter("demo", FakeAdapter())
|
|
||||||
|
|
||||||
server = create_broker_server(service)
|
|
||||||
|
|
||||||
_content, structured = asyncio.run(
|
|
||||||
server.call_tool(
|
|
||||||
"call_broker_tool",
|
|
||||||
{
|
|
||||||
"connection_id": "demo.personal",
|
|
||||||
"tool_name": "echo_tool",
|
|
||||||
"arguments": {"text": "hello"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert structured == {
|
|
||||||
"connection_id": "demo.personal",
|
|
||||||
"tool_name": "echo_tool",
|
|
||||||
"ok": True,
|
|
||||||
"outcome": "ok",
|
|
||||||
"output": {"echoed": "hello"},
|
|
||||||
"meta": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_broker_lists_workflow_artifacts_from_artifact_store() -> None:
|
def test_broker_lists_workflow_artifacts_from_artifact_store() -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "broker_artifacts")
|
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "broker_artifacts")
|
||||||
artifact_store.save_artifact(_artifact())
|
artifact_store.save_artifact(_artifact())
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ def test_namespaced_tool_parser_rejects_unknown_and_admin_names() -> None:
|
|||||||
def test_admin_namespace_is_distinct_from_wf_mcp_runtime_source() -> None:
|
def test_admin_namespace_is_distinct_from_wf_mcp_runtime_source() -> None:
|
||||||
assert ADMIN_NAMESPACE == "wf.admin"
|
assert ADMIN_NAMESPACE == "wf.admin"
|
||||||
assert is_admin_tool_name("wf.admin.list_connections") is True
|
assert is_admin_tool_name("wf.admin.list_connections") is True
|
||||||
assert is_admin_tool_name("wf.mcp.call_tool") is False
|
assert is_admin_tool_name("wf.std.runtime_error") is False
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert "wf.admin.read_resource" in names
|
assert "wf.admin.read_resource" in names
|
||||||
assert "wf.admin.render_prompt" in names
|
assert "wf.admin.render_prompt" in names
|
||||||
assert "wf.admin.invoke_method" in names
|
assert "wf.admin.invoke_method" in names
|
||||||
assert "wf.admin.call_tool" in names
|
|
||||||
assert "wf.admin.get_events" in names
|
assert "wf.admin.get_events" in names
|
||||||
assert "wf.workflow.list_artifacts" in names
|
assert "wf.workflow.list_artifacts" in names
|
||||||
assert "wf.workflow.list_capabilities" in names
|
assert "wf.workflow.list_capabilities" in names
|
||||||
@@ -173,7 +172,6 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
}
|
}
|
||||||
assert "wf.admin" in source_ids
|
assert "wf.admin" in source_ids
|
||||||
assert "wf.docs" in source_ids
|
assert "wf.docs" in source_ids
|
||||||
assert "wf.mcp" in source_ids
|
|
||||||
assert "wf.std" in source_ids
|
assert "wf.std" in source_ids
|
||||||
|
|
||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|||||||
@@ -101,10 +101,7 @@ def test_service_installs_builtin_stdlib_specs_by_default() -> None:
|
|||||||
"wf.std.runtime_error"
|
"wf.std.runtime_error"
|
||||||
in service.capability_sources["wf.std"].capabilities.node_specs
|
in service.capability_sources["wf.std"].capabilities.node_specs
|
||||||
)
|
)
|
||||||
assert (
|
assert "wf.mcp" not in service.capability_sources
|
||||||
"wf.mcp.call_tool"
|
|
||||||
in service.capability_sources["wf.mcp"].capabilities.node_specs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_service_installs_default_draft_workspace_store() -> None:
|
def test_service_installs_default_draft_workspace_store() -> None:
|
||||||
@@ -147,9 +144,6 @@ def test_service_lists_all_capability_sources_with_owned_capability_names() -> N
|
|||||||
assert std_source["capabilities"]["tools"] == []
|
assert std_source["capabilities"]["tools"] == []
|
||||||
assert std_source["reducer_count"] == 6
|
assert std_source["reducer_count"] == 6
|
||||||
|
|
||||||
mcp_source = sources_by_id["wf.mcp"]
|
|
||||||
assert mcp_source["capabilities"]["node_specs"] == ["wf.mcp.call_tool"]
|
|
||||||
|
|
||||||
admin_source = sources_by_id["wf.admin"]
|
admin_source = sources_by_id["wf.admin"]
|
||||||
assert admin_source["visibility"]["planner"] is False
|
assert admin_source["visibility"]["planner"] is False
|
||||||
assert "wf.admin.list_sources" in admin_source["capabilities"]["tools"]
|
assert "wf.admin.list_sources" in admin_source["capabilities"]["tools"]
|
||||||
@@ -158,11 +152,11 @@ def test_service_lists_all_capability_sources_with_owned_capability_names() -> N
|
|||||||
def test_service_lists_compact_source_summaries() -> None:
|
def test_service_lists_compact_source_summaries() -> None:
|
||||||
service = WfMcpService(store=FileStore(local_temp_root() / "source_summaries"))
|
service = WfMcpService(store=FileStore(local_temp_root() / "source_summaries"))
|
||||||
|
|
||||||
payload = service.list_source_summaries(limit=2)
|
payload = service.list_source_summaries(limit=1)
|
||||||
|
|
||||||
assert len(payload["sources"]) == 2
|
assert len(payload["sources"]) == 1
|
||||||
assert payload["total"] >= 2
|
assert payload["total"] >= 2
|
||||||
assert payload["next_cursor"] == "2"
|
assert payload["next_cursor"] == "1"
|
||||||
assert "capabilities" not in payload["sources"][0]
|
assert "capabilities" not in payload["sources"][0]
|
||||||
|
|
||||||
full_page = service.list_source_summaries(limit=100)
|
full_page = service.list_source_summaries(limit=100)
|
||||||
@@ -215,7 +209,6 @@ def test_service_sources_have_visibility_and_capability_buckets() -> None:
|
|||||||
service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store"))
|
service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store"))
|
||||||
|
|
||||||
std_source = service.capability_sources["wf.std"]
|
std_source = service.capability_sources["wf.std"]
|
||||||
mcp_source = service.capability_sources["wf.mcp"]
|
|
||||||
|
|
||||||
assert std_source.id == "wf.std"
|
assert std_source.id == "wf.std"
|
||||||
assert std_source.kind == "system"
|
assert std_source.kind == "system"
|
||||||
@@ -225,11 +218,6 @@ def test_service_sources_have_visibility_and_capability_buckets() -> None:
|
|||||||
assert "wf.std.runtime_error" in std_source.capabilities.node_specs
|
assert "wf.std.runtime_error" in std_source.capabilities.node_specs
|
||||||
assert not std_source.capabilities.tools
|
assert not std_source.capabilities.tools
|
||||||
|
|
||||||
assert mcp_source.id == "wf.mcp"
|
|
||||||
assert mcp_source.visibility.planner is True
|
|
||||||
assert mcp_source.permissions.calls_upstream is True
|
|
||||||
assert "wf.mcp.call_tool" in mcp_source.capabilities.node_specs
|
|
||||||
|
|
||||||
|
|
||||||
def test_wf_admin_source_exists_but_is_not_planner_visible() -> None:
|
def test_wf_admin_source_exists_but_is_not_planner_visible() -> None:
|
||||||
service = WfMcpService(store=FileStore(local_temp_root() / "admin_source_store"))
|
service = WfMcpService(store=FileStore(local_temp_root() / "admin_source_store"))
|
||||||
@@ -256,7 +244,6 @@ def test_service_can_disable_builtin_stdlib_specs() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert "wf.std" not in service.capability_sources
|
assert "wf.std" not in service.capability_sources
|
||||||
assert "wf.mcp" not in service.capability_sources
|
|
||||||
|
|
||||||
|
|
||||||
def test_service_planner_catalog_excludes_hidden_sources() -> None:
|
def test_service_planner_catalog_excludes_hidden_sources() -> None:
|
||||||
@@ -298,10 +285,8 @@ def test_service_catalog_split_keeps_system_specs_out_of_backend_catalog() -> No
|
|||||||
|
|
||||||
assert backend_payload["nodes"] == []
|
assert backend_payload["nodes"] == []
|
||||||
planner_node_names = {node["qualified_name"] for node in planner_payload["nodes"]}
|
planner_node_names = {node["qualified_name"] for node in planner_payload["nodes"]}
|
||||||
assert "wf.mcp.call_tool" in planner_node_names
|
|
||||||
assert "wf.std.runtime_error" in planner_node_names
|
assert "wf.std.runtime_error" in planner_node_names
|
||||||
available_names = {entry.qualified_name for entry in service.list_available_specs()}
|
available_names = {entry.qualified_name for entry in service.list_available_specs()}
|
||||||
assert "wf.mcp.call_tool" in available_names
|
|
||||||
assert "wf.std.runtime_error" in available_names
|
assert "wf.std.runtime_error" in available_names
|
||||||
|
|
||||||
|
|
||||||
@@ -813,69 +798,6 @@ def test_service_records_tool_call_events() -> None:
|
|||||||
assert tool_events[1].payload["outcome"] == "ok"
|
assert tool_events[1].payload["outcome"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
def test_service_can_call_upstream_tool_through_wf_mcp_system_node() -> None:
|
|
||||||
service = WfMcpService(store=FileStore(local_temp_root() / "system_tool_store"))
|
|
||||||
service.register_connection(
|
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
||||||
)
|
|
||||||
service.register_adapter("demo", FakeAdapter())
|
|
||||||
|
|
||||||
plan = _raw_plan(
|
|
||||||
name="system_tool_plan",
|
|
||||||
input_schema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"connection_id": {"type": "string"},
|
|
||||||
"tool_name": {"type": "string"},
|
|
||||||
"arguments": {"type": "object"},
|
|
||||||
},
|
|
||||||
"required": ["connection_id", "tool_name", "arguments"],
|
|
||||||
},
|
|
||||||
state_schema={
|
|
||||||
"fields": {
|
|
||||||
"tool_result": {"type": "object"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
output_schema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"tool_result": {"type": "object"}},
|
|
||||||
"required": ["tool_result"],
|
|
||||||
},
|
|
||||||
start="call_tool",
|
|
||||||
nodes=[
|
|
||||||
{
|
|
||||||
"id": "call_tool",
|
|
||||||
"type": "node",
|
|
||||||
"node": "wf.mcp.call_tool",
|
|
||||||
"in_map": {
|
|
||||||
"input.connection_id": "connection_id",
|
|
||||||
"input.tool_name": "tool_name",
|
|
||||||
"input.arguments": "arguments",
|
|
||||||
},
|
|
||||||
"out_map": {"output": "state.tool_result"},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
edges=[
|
|
||||||
{"from": "call_tool", "outcome": "ok", "to": END},
|
|
||||||
{"from": "call_tool", "outcome": "error", "to": END},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
run = asyncio.run(
|
|
||||||
service.run_workflow_from_plan(
|
|
||||||
plan,
|
|
||||||
{
|
|
||||||
"connection_id": "demo.personal",
|
|
||||||
"tool_name": "echo_tool",
|
|
||||||
"arguments": {"text": "hello"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert run.status == RunStatus.COMPLETED
|
|
||||||
assert run.output["tool_result"]["echoed"] == "hello"
|
|
||||||
|
|
||||||
|
|
||||||
def test_service_can_inspect_resources_and_prompts() -> None:
|
def test_service_can_inspect_resources_and_prompts() -> None:
|
||||||
service = WfMcpService(store=FileStore(local_temp_root() / "inspect_store"))
|
service = WfMcpService(store=FileStore(local_temp_root() / "inspect_store"))
|
||||||
service.register_connection(
|
service.register_connection(
|
||||||
@@ -1000,31 +922,6 @@ def test_service_can_invoke_raw_method_and_notification() -> None:
|
|||||||
assert "raw_notification_completed" in event_kinds
|
assert "raw_notification_completed" in event_kinds
|
||||||
|
|
||||||
|
|
||||||
def test_service_can_call_upstream_tool_directly() -> None:
|
|
||||||
service = WfMcpService(store=FileStore(local_temp_root() / "direct_tool_store"))
|
|
||||||
service.register_connection(
|
|
||||||
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
||||||
)
|
|
||||||
service.register_adapter("demo", FakeAdapter())
|
|
||||||
|
|
||||||
result = asyncio.run(
|
|
||||||
service.call_tool(
|
|
||||||
"demo.personal",
|
|
||||||
"echo_tool",
|
|
||||||
arguments={"text": "hello"},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result == {
|
|
||||||
"outcome": "ok",
|
|
||||||
"output": {"echoed": "hello"},
|
|
||||||
"meta": {},
|
|
||||||
}
|
|
||||||
event_kinds = [event.kind for event in service.list_events()]
|
|
||||||
assert "tool_call_started" in event_kinds
|
|
||||||
assert "tool_call_completed" in event_kinds
|
|
||||||
|
|
||||||
|
|
||||||
def test_generated_specs_use_injected_tool_executor() -> None:
|
def test_generated_specs_use_injected_tool_executor() -> None:
|
||||||
class RecordingExecutor:
|
class RecordingExecutor:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|||||||
@@ -99,17 +99,17 @@ def test_workflow_surface_lists_planner_visible_capabilities() -> None:
|
|||||||
assert "wf.admin.list_sources" not in names
|
assert "wf.admin.list_sources" not in names
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_filters_capabilities_by_source() -> None:
|
def test_workflow_surface_filters_stdlib_capabilities_by_source() -> None:
|
||||||
handlers = _handlers(
|
handlers = _handlers(
|
||||||
FileWorkflowArtifactStore(local_temp_root() / "surface_filtered_caps")
|
FileWorkflowArtifactStore(local_temp_root() / "surface_filtered_caps")
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = asyncio.run(handlers.list_capabilities(source_id="wf.mcp"))
|
payload = asyncio.run(handlers.list_capabilities(source_id="wf.std", query="truthy"))
|
||||||
|
|
||||||
assert [capability["name"] for capability in payload["capabilities"]] == [
|
assert [capability["name"] for capability in payload["capabilities"]] == [
|
||||||
"wf.mcp.call_tool"
|
"wf.std.truthy"
|
||||||
]
|
]
|
||||||
assert payload["capabilities"][0]["source_id"] == "wf.mcp"
|
assert payload["capabilities"][0]["source_id"] == "wf.std"
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_lists_saved_wrapper_capabilities() -> None:
|
def test_workflow_surface_lists_saved_wrapper_capabilities() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user