remove just about everything bad coming from broker

This commit is contained in:
lda
2026-05-19 23:35:08 +07:00 Verified
parent 53a9e7f85d
commit 5bda3e3770
19 changed files with 99 additions and 371 deletions
+3 -3
View File
@@ -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.
--- ---
+3 -4
View File
@@ -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,
+13 -25
View File
@@ -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
View File
@@ -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
+1 -4
View File
@@ -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()]
-17
View File
@@ -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",
+2 -2
View File
@@ -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.
+7 -94
View File
@@ -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.",
),
} }
+16 -42
View File
@@ -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(
+7 -1
View File
@@ -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,
+7 -1
View File
@@ -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)
+6 -1
View File
@@ -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
+20 -3
View File
@@ -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]] = {}
-31
View File
@@ -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())
+1 -1
View File
@@ -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
-2
View File
@@ -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())
+4 -107
View File
@@ -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:
+4 -4
View File
@@ -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: