goodbye modes 2 working on it
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# Retire Legacy MCP Modes Plan
|
# Retire Legacy MCP Modes Plan
|
||||||
|
|
||||||
> **Status:** current execution plan for the next cleanup pass.
|
> **Status:** completed retirement pass; kept as the decision record.
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Make **unified mode** the only public MCP server mode exposed by `wf-mcp`.
|
Make one public MCP server surface exposed by `wf-mcp`.
|
||||||
|
|
||||||
`broker` mode and `proxy` mode were useful while the unified surface was being
|
`broker` mode and `proxy` mode were useful while the unified surface was being
|
||||||
built, but keeping all three as public launch modes now creates the wrong mental
|
built, but keeping all three as public launch modes now creates the wrong mental
|
||||||
@@ -12,7 +12,7 @@ model:
|
|||||||
|
|
||||||
- broker mode suggests local workflow/admin tools are a separate product surface
|
- broker mode suggests local workflow/admin tools are a separate product surface
|
||||||
- proxy mode suggests upstream projection is a separate product surface
|
- proxy mode suggests upstream projection is a separate product surface
|
||||||
- unified mode is already the real target: one server that exposes local
|
- the combined server is already the real target: one server that exposes local
|
||||||
capabilities and proxied upstream capabilities together
|
capabilities and proxied upstream capabilities together
|
||||||
|
|
||||||
The public product should have one server mode. The implementation may still
|
The public product should have one server mode. The implementation may still
|
||||||
@@ -62,7 +62,7 @@ Where historical explanation is useful, call them legacy migration surfaces.
|
|||||||
|
|
||||||
Delete or rewrite tests whose only purpose is to prove the old public mode split.
|
Delete or rewrite tests whose only purpose is to prove the old public mode split.
|
||||||
Keep behavior tests for the underlying capabilities when those behaviors still
|
Keep behavior tests for the underlying capabilities when those behaviors still
|
||||||
exist through unified mode.
|
exist through the public server.
|
||||||
|
|
||||||
## What Stays
|
## What Stays
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ source of confusion.
|
|||||||
|
|
||||||
### Shared services
|
### Shared services
|
||||||
|
|
||||||
Keep the service/config/store/runtime objects that unified mode already uses.
|
Keep the service/config/store/runtime objects that the public server already uses.
|
||||||
This pass is about removing duplicate **entrypoints**, not rewriting the
|
This pass is about removing duplicate **entrypoints**, not rewriting the
|
||||||
underlying architecture.
|
underlying architecture.
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ The cleanup should reduce surfaces, not churn the capability vocabulary.
|
|||||||
2. Remove or privatize old broker/proxy server launch functions that only exist
|
2. Remove or privatize old broker/proxy server launch functions that only exist
|
||||||
for the retired public modes.
|
for the retired public modes.
|
||||||
3. Collapse docs/help text that still describe three user-facing modes.
|
3. Collapse docs/help text that still describe three user-facing modes.
|
||||||
4. Keep implementation reuse through the existing unified server path.
|
4. Keep implementation reuse through the existing server path.
|
||||||
5. Update tests so they assert unified behavior directly instead of branching on
|
5. Update tests so they assert server behavior directly instead of branching on
|
||||||
legacy mode names.
|
legacy mode names.
|
||||||
|
|
||||||
## Non-Goals For This Pass
|
## Non-Goals For This Pass
|
||||||
@@ -123,13 +123,13 @@ Those topics already have separate docs and should stay separate.
|
|||||||
|
|
||||||
- `wf-mcp serve` has one public MCP server behavior.
|
- `wf-mcp serve` has one public MCP server behavior.
|
||||||
- No public docs imply that broker/proxy are still supported product modes.
|
- No public docs imply that broker/proxy are still supported product modes.
|
||||||
- Unified mode continues to expose:
|
- The server continues to expose:
|
||||||
- proxied upstream capabilities
|
- proxied upstream capabilities
|
||||||
- stable local workflow tools
|
- stable local workflow tools
|
||||||
- optional admin tools
|
- optional admin tools
|
||||||
- The test suite passes with the old public mode split removed.
|
- The test suite passes with the old public mode split removed.
|
||||||
- Remaining roadmap docs still point to the real unresolved work instead of
|
- Remaining roadmap docs still point to the real unresolved work instead of
|
||||||
making the reader rediscover why unified mode exists.
|
making the reader rediscover why the combined server exists.
|
||||||
|
|
||||||
## Follow-On Work After This Pass
|
## Follow-On Work After This Pass
|
||||||
|
|
||||||
|
|||||||
@@ -194,17 +194,19 @@ The code now has the first capability-source layer in place.
|
|||||||
- `wf.mcp` owns workflow MCP runtime node specs, currently
|
- `wf.mcp` owns workflow MCP runtime node specs, currently
|
||||||
`wf.mcp.call_tool`.
|
`wf.mcp.call_tool`.
|
||||||
- `wf.admin` owns privileged admin capability metadata and is not planner-visible
|
- `wf.admin` owns privileged admin capability metadata and is not planner-visible
|
||||||
or MCP-client-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
|
||||||
`LdaNamespace`.
|
`LdaNamespace`.
|
||||||
- `wf.admin` and `wf.mcp` are reserved connection ids.
|
- `wf.admin` and `wf.mcp` are reserved connection ids.
|
||||||
- Planner catalog, `list_available_specs()`, and workflow spec resolution respect
|
- Planner catalog, `list_available_specs()`, and workflow spec resolution respect
|
||||||
source `enabled` and `visibility.planner`.
|
source `enabled` and `visibility.planner`.
|
||||||
|
|
||||||
Broker MCP tools still expose compatibility names such as `list_connections` and
|
Legacy broker MCP tools still expose compatibility names such as
|
||||||
`get_planner_catalog`. Their metadata belongs to `wf.admin`, but dotted
|
`list_connections` and `get_planner_catalog` on the compatibility-only broker
|
||||||
`wf.admin.*` broker tool projection is intentionally deferred until admin MCP
|
server constructor. They now reuse the same service-admin registrar as the
|
||||||
exposure is explicit.
|
public server, with only the visible names changed. The public server projects
|
||||||
|
service-backed admin tools such as `wf.admin.list_sources` alongside
|
||||||
|
proxy-backed admin tools under the same `wf.admin.*` namespace.
|
||||||
|
|
||||||
## Current Code Mapping
|
## Current Code Mapping
|
||||||
|
|
||||||
@@ -214,8 +216,9 @@ 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_mcp.broker.service.builtins` | local workflow specs | `wf.std`, `wf.mcp` |
|
| `wf_mcp.broker.service.builtins` | local workflow specs | `wf.std`, `wf.mcp` |
|
||||||
| `wf_mcp.broker.tools` | compatibility broker MCP admin/control tools | `wf.admin.tools` |
|
| `wf_mcp.broker.tools` | compatibility wrapper over shared service-admin registration | `wf.admin.tools` |
|
||||||
| `wf_mcp.transparent_proxy.admin` | dotted proxy MCP admin/control tools | `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` |
|
||||||
| discovered MCP tools | upstream tools and workflow wrappers | connection source |
|
| discovered MCP tools | upstream tools and workflow wrappers | connection source |
|
||||||
| broker resources/prompts | catalog/status/planning context | likely `wf.admin` or docs sources |
|
| broker resources/prompts | catalog/status/planning context | likely `wf.admin` or docs sources |
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
from .handlers import BrokerAdminHandlers, ProxyAdminRuntime, TransparentAdminHandlers
|
from .handlers import BrokerAdminHandlers, ProxyAdminRuntime, TransparentAdminHandlers
|
||||||
|
from .tools import register_service_admin_tools
|
||||||
|
|
||||||
__all__ = ["BrokerAdminHandlers", "ProxyAdminRuntime", "TransparentAdminHandlers"]
|
__all__ = [
|
||||||
|
"BrokerAdminHandlers",
|
||||||
|
"ProxyAdminRuntime",
|
||||||
|
"TransparentAdminHandlers",
|
||||||
|
"register_service_admin_tools",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from wf_mcp.broker.service import WfMcpService
|
||||||
|
|
||||||
|
from .handlers.broker import BrokerAdminHandlers
|
||||||
|
|
||||||
|
|
||||||
|
def register_service_admin_tools(
|
||||||
|
server: Any,
|
||||||
|
service: WfMcpService,
|
||||||
|
*,
|
||||||
|
namespace: str | None = "wf.admin",
|
||||||
|
legacy_names: bool = False,
|
||||||
|
include_connection_tools: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""Register service-backed admin/control tools on an MCP server.
|
||||||
|
|
||||||
|
The public server uses dotted `wf.admin.*` names. The retired broker server
|
||||||
|
constructor still asks for bare compatibility names, so the namespace stays
|
||||||
|
configurable while the implementation remains single-sourced.
|
||||||
|
"""
|
||||||
|
handlers = BrokerAdminHandlers(service)
|
||||||
|
|
||||||
|
legacy_name_map = {
|
||||||
|
"read_resource": "read_broker_resource",
|
||||||
|
"render_prompt": "render_broker_prompt",
|
||||||
|
"invoke_method": "invoke_broker_method",
|
||||||
|
"call_tool": "call_broker_tool",
|
||||||
|
"get_events": "get_broker_events",
|
||||||
|
}
|
||||||
|
|
||||||
|
def name(local_name: str) -> str:
|
||||||
|
visible_name = legacy_name_map.get(local_name, local_name) if legacy_names else local_name
|
||||||
|
return visible_name if namespace is None else f"{namespace}.{visible_name}"
|
||||||
|
|
||||||
|
if include_connection_tools:
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("list_connections"),
|
||||||
|
title="List Connections",
|
||||||
|
description="List configured MCP connections known to this server.",
|
||||||
|
)
|
||||||
|
async def list_connections() -> list[dict[str, Any]]:
|
||||||
|
return handlers.list_connections()
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("get_connection_statuses"),
|
||||||
|
title="Get Connection Statuses",
|
||||||
|
description="Show configured MCP connection status and catalog counts.",
|
||||||
|
)
|
||||||
|
async def get_connection_statuses() -> list[dict[str, Any]]:
|
||||||
|
return handlers.get_connection_statuses()
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("refresh_connection_catalog"),
|
||||||
|
title="Refresh Connection Catalog",
|
||||||
|
description="Refresh one connection catalog snapshot from its upstream MCP server.",
|
||||||
|
)
|
||||||
|
async def refresh_connection_catalog(connection_id: str) -> dict[str, Any]:
|
||||||
|
return await handlers.refresh_connection_catalog(connection_id)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("get_catalog"),
|
||||||
|
title="Get Catalog",
|
||||||
|
description="Return the current upstream MCP capability catalog.",
|
||||||
|
)
|
||||||
|
async def get_catalog() -> dict[str, Any]:
|
||||||
|
return handlers.get_catalog()
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("get_planner_catalog"),
|
||||||
|
title="Get Planner Catalog",
|
||||||
|
description="Return the planner catalog including local workflow sources.",
|
||||||
|
)
|
||||||
|
async def get_planner_catalog() -> dict[str, Any]:
|
||||||
|
return handlers.get_planner_catalog()
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("list_spec_sources"),
|
||||||
|
title="List Spec Sources",
|
||||||
|
description="List planner-visible sources that currently provide node specs.",
|
||||||
|
)
|
||||||
|
async def list_spec_sources() -> list[dict[str, Any]]:
|
||||||
|
return handlers.list_spec_sources()
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("list_sources"),
|
||||||
|
title="List Sources",
|
||||||
|
description="List configured capability sources and what each source owns.",
|
||||||
|
)
|
||||||
|
async def list_sources() -> list[dict[str, Any]]:
|
||||||
|
return handlers.list_sources()
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("read_resource"),
|
||||||
|
title="Read Resource",
|
||||||
|
description="Read a broker-catalog resource by qualified name.",
|
||||||
|
)
|
||||||
|
async def read_resource(qualified_name: str) -> dict[str, Any]:
|
||||||
|
return await handlers.read_broker_resource(qualified_name)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("render_prompt"),
|
||||||
|
title="Render Prompt",
|
||||||
|
description="Render a broker-catalog prompt by qualified name.",
|
||||||
|
)
|
||||||
|
async def render_prompt(
|
||||||
|
qualified_name: str,
|
||||||
|
arguments: dict[str, str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.render_broker_prompt(
|
||||||
|
qualified_name,
|
||||||
|
arguments=arguments,
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name=name("invoke_method"),
|
||||||
|
title="Invoke Method",
|
||||||
|
description="Invoke a raw MCP method on one configured connection.",
|
||||||
|
)
|
||||||
|
async def invoke_method(
|
||||||
|
connection_id: str,
|
||||||
|
method: str,
|
||||||
|
params: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.invoke_broker_method(
|
||||||
|
connection_id,
|
||||||
|
method,
|
||||||
|
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(
|
||||||
|
name=name("get_events"),
|
||||||
|
title="Get Events",
|
||||||
|
description="Return locally recorded broker/platform events.",
|
||||||
|
)
|
||||||
|
async def get_events() -> list[dict[str, Any]]:
|
||||||
|
return handlers.get_broker_events()
|
||||||
@@ -1,79 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from ..admin_surface import BrokerAdminHandlers
|
from ..admin_surface import register_service_admin_tools
|
||||||
from .service import WfMcpService
|
from .service import WfMcpService
|
||||||
|
|
||||||
|
|
||||||
def register_broker_tools(server: FastMCP, service: WfMcpService) -> None:
|
def register_broker_tools(server: FastMCP, service: WfMcpService) -> None:
|
||||||
"""Register broker tool handlers on a FastMCP server."""
|
"""Register legacy bare-name broker tools from the shared admin registrar."""
|
||||||
handlers = BrokerAdminHandlers(service)
|
register_service_admin_tools(
|
||||||
|
server,
|
||||||
# These MCP tool names are compatibility exports. Their capability metadata
|
service,
|
||||||
# belongs to the wf.admin source; future admin-enabled servers can project
|
namespace=None,
|
||||||
# dotted wf.admin.* names from that source.
|
legacy_names=True,
|
||||||
@server.tool()
|
)
|
||||||
async def list_connections() -> list[dict[str, Any]]:
|
|
||||||
return handlers.list_connections()
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def get_connection_statuses() -> list[dict[str, Any]]:
|
|
||||||
return handlers.get_connection_statuses()
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def refresh_connection_catalog(connection_id: str) -> dict[str, Any]:
|
|
||||||
return await handlers.refresh_connection_catalog(connection_id)
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def get_catalog() -> dict[str, Any]:
|
|
||||||
return handlers.get_catalog()
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def get_planner_catalog() -> dict[str, Any]:
|
|
||||||
return handlers.get_planner_catalog()
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def list_spec_sources() -> list[dict[str, Any]]:
|
|
||||||
return handlers.list_spec_sources()
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def list_sources() -> list[dict[str, Any]]:
|
|
||||||
return handlers.list_sources()
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def read_broker_resource(qualified_name: str) -> dict[str, Any]:
|
|
||||||
return await handlers.read_broker_resource(qualified_name)
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def render_broker_prompt(
|
|
||||||
qualified_name: str,
|
|
||||||
arguments: dict[str, str] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await handlers.render_broker_prompt(qualified_name, arguments=arguments)
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def invoke_broker_method(
|
|
||||||
connection_id: str,
|
|
||||||
method: str,
|
|
||||||
params: dict[str, Any] | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await handlers.invoke_broker_method(connection_id, method, params=params)
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
async def call_broker_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()
|
|
||||||
async def get_broker_events() -> list[dict[str, Any]]:
|
|
||||||
return handlers.get_broker_events()
|
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ from .broker import (
|
|||||||
build_service_from_config,
|
build_service_from_config,
|
||||||
load_broker_config,
|
load_broker_config,
|
||||||
)
|
)
|
||||||
from .server import run_unified_proxy_server
|
from .server import run_server
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
@@ -115,7 +115,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
|
|
||||||
if args.command == "serve":
|
if args.command == "serve":
|
||||||
config = load_broker_config(args.config)
|
config = load_broker_config(args.config)
|
||||||
run_unified_proxy_server(
|
run_server(
|
||||||
config,
|
config,
|
||||||
args.transport,
|
args.transport,
|
||||||
config_path=args.config,
|
config_path=args.config,
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
from .unified import (
|
from .core import create_server, create_server_client, run_server
|
||||||
create_unified_proxy_client,
|
|
||||||
create_unified_proxy_server,
|
|
||||||
run_unified_proxy_server,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"create_unified_proxy_client",
|
"create_server",
|
||||||
"create_unified_proxy_server",
|
"create_server_client",
|
||||||
"run_unified_proxy_server",
|
"run_server",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
from fastmcp.client import Client
|
||||||
|
from fastmcp.client.transports.memory import FastMCPTransport
|
||||||
|
|
||||||
|
from ..admin_surface import register_service_admin_tools
|
||||||
|
from ..broker.config import build_service_from_config
|
||||||
|
from ..broker.transport import normalize_transport
|
||||||
|
from ..models import BrokerConfig
|
||||||
|
from ..transparent_proxy.runtime import ProxyRuntime
|
||||||
|
from ..workflow_surface import register_workflow_tools
|
||||||
|
|
||||||
|
|
||||||
|
def create_server(
|
||||||
|
config: BrokerConfig,
|
||||||
|
*,
|
||||||
|
config_path: str | Path | None = None,
|
||||||
|
resources_as_tools: bool = False,
|
||||||
|
prompts_as_tools: bool = False,
|
||||||
|
search_tools: bool = False,
|
||||||
|
admin_tools: bool = True,
|
||||||
|
) -> FastMCP[Any]:
|
||||||
|
"""Create the public MCP server with proxy, admin, and workflow tools."""
|
||||||
|
service = build_service_from_config(config)
|
||||||
|
runtime = ProxyRuntime(
|
||||||
|
config,
|
||||||
|
config_path=config_path,
|
||||||
|
resources_as_tools=resources_as_tools,
|
||||||
|
prompts_as_tools=prompts_as_tools,
|
||||||
|
search_tools=search_tools,
|
||||||
|
admin_tools=admin_tools,
|
||||||
|
event_bus=service.event_bus,
|
||||||
|
)
|
||||||
|
if admin_tools:
|
||||||
|
register_service_admin_tools(
|
||||||
|
runtime.server,
|
||||||
|
service,
|
||||||
|
include_connection_tools=False,
|
||||||
|
)
|
||||||
|
register_workflow_tools(runtime.server, service)
|
||||||
|
return runtime.server
|
||||||
|
|
||||||
|
|
||||||
|
def run_server(
|
||||||
|
config: BrokerConfig,
|
||||||
|
transport: str = "stdio",
|
||||||
|
*,
|
||||||
|
config_path: str | Path | None = None,
|
||||||
|
resources_as_tools: bool = False,
|
||||||
|
prompts_as_tools: bool = False,
|
||||||
|
search_tools: bool = False,
|
||||||
|
admin_tools: bool = True,
|
||||||
|
) -> None:
|
||||||
|
server = create_server(
|
||||||
|
config,
|
||||||
|
config_path=config_path,
|
||||||
|
resources_as_tools=resources_as_tools,
|
||||||
|
prompts_as_tools=prompts_as_tools,
|
||||||
|
search_tools=search_tools,
|
||||||
|
admin_tools=admin_tools,
|
||||||
|
)
|
||||||
|
server.run(transport=normalize_transport(transport), show_banner=False)
|
||||||
|
|
||||||
|
|
||||||
|
def create_server_client(
|
||||||
|
config: BrokerConfig,
|
||||||
|
*,
|
||||||
|
config_path: str | Path | None = None,
|
||||||
|
resources_as_tools: bool = False,
|
||||||
|
prompts_as_tools: bool = False,
|
||||||
|
search_tools: bool = False,
|
||||||
|
admin_tools: bool = True,
|
||||||
|
) -> Client[FastMCPTransport]:
|
||||||
|
return Client(
|
||||||
|
FastMCPTransport(
|
||||||
|
create_server(
|
||||||
|
config,
|
||||||
|
config_path=config_path,
|
||||||
|
resources_as_tools=resources_as_tools,
|
||||||
|
prompts_as_tools=prompts_as_tools,
|
||||||
|
search_tools=search_tools,
|
||||||
|
admin_tools=admin_tools,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
from .handlers import WorkflowSurfaceHandlers
|
from .handlers import WorkflowSurfaceHandlers
|
||||||
|
from .tools import register_workflow_tools
|
||||||
|
|
||||||
__all__ = ["WorkflowSurfaceHandlers"]
|
__all__ = ["WorkflowSurfaceHandlers", "register_workflow_tools"]
|
||||||
|
|||||||
@@ -1,92 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from fastmcp.client import Client
|
|
||||||
from fastmcp.client.transports.memory import FastMCPTransport
|
|
||||||
|
|
||||||
from ..broker.config import build_service_from_config
|
from wf_mcp.broker.service import WfMcpService
|
||||||
from ..broker.transport import normalize_transport
|
|
||||||
from ..models import BrokerConfig
|
from .handlers import WorkflowSurfaceHandlers
|
||||||
from ..transparent_proxy.runtime import ProxyRuntime
|
|
||||||
from ..workflow_surface import WorkflowSurfaceHandlers
|
|
||||||
|
|
||||||
|
|
||||||
def create_unified_proxy_server(
|
def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None:
|
||||||
config: BrokerConfig,
|
"""Register stable workflow tools on the public MCP server surface."""
|
||||||
*,
|
handlers = WorkflowSurfaceHandlers(service)
|
||||||
config_path: str | Path | None = None,
|
|
||||||
resources_as_tools: bool = False,
|
|
||||||
prompts_as_tools: bool = False,
|
|
||||||
search_tools: bool = False,
|
|
||||||
admin_tools: bool = True,
|
|
||||||
) -> FastMCP[Any]:
|
|
||||||
"""Create one MCP server with upstream proxy, admin, and workflow tools."""
|
|
||||||
service = build_service_from_config(config)
|
|
||||||
runtime = ProxyRuntime(
|
|
||||||
config,
|
|
||||||
config_path=config_path,
|
|
||||||
resources_as_tools=resources_as_tools,
|
|
||||||
prompts_as_tools=prompts_as_tools,
|
|
||||||
search_tools=search_tools,
|
|
||||||
admin_tools=admin_tools,
|
|
||||||
event_bus=service.event_bus,
|
|
||||||
)
|
|
||||||
_register_workflow_tools(runtime.server, WorkflowSurfaceHandlers(service))
|
|
||||||
return runtime.server
|
|
||||||
|
|
||||||
|
|
||||||
def run_unified_proxy_server(
|
|
||||||
config: BrokerConfig,
|
|
||||||
transport: str = "stdio",
|
|
||||||
*,
|
|
||||||
config_path: str | Path | None = None,
|
|
||||||
resources_as_tools: bool = False,
|
|
||||||
prompts_as_tools: bool = False,
|
|
||||||
search_tools: bool = False,
|
|
||||||
admin_tools: bool = True,
|
|
||||||
) -> None:
|
|
||||||
server = create_unified_proxy_server(
|
|
||||||
config,
|
|
||||||
config_path=config_path,
|
|
||||||
resources_as_tools=resources_as_tools,
|
|
||||||
prompts_as_tools=prompts_as_tools,
|
|
||||||
search_tools=search_tools,
|
|
||||||
admin_tools=admin_tools,
|
|
||||||
)
|
|
||||||
server.run(transport=normalize_transport(transport), show_banner=False)
|
|
||||||
|
|
||||||
|
|
||||||
def create_unified_proxy_client(
|
|
||||||
config: BrokerConfig,
|
|
||||||
*,
|
|
||||||
config_path: str | Path | None = None,
|
|
||||||
resources_as_tools: bool = False,
|
|
||||||
prompts_as_tools: bool = False,
|
|
||||||
search_tools: bool = False,
|
|
||||||
admin_tools: bool = True,
|
|
||||||
) -> Client[FastMCPTransport]:
|
|
||||||
return Client(
|
|
||||||
FastMCPTransport(
|
|
||||||
create_unified_proxy_server(
|
|
||||||
config,
|
|
||||||
config_path=config_path,
|
|
||||||
resources_as_tools=resources_as_tools,
|
|
||||||
prompts_as_tools=prompts_as_tools,
|
|
||||||
search_tools=search_tools,
|
|
||||||
admin_tools=admin_tools,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _register_workflow_tools(
|
|
||||||
server: FastMCP[Any],
|
|
||||||
handlers: WorkflowSurfaceHandlers,
|
|
||||||
) -> None:
|
|
||||||
"""Register stable workflow tools on the unified MCP surface."""
|
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.list_artifacts",
|
name="wf.workflow.list_artifacts",
|
||||||
@@ -107,9 +32,7 @@ def _register_workflow_tools(
|
|||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.create_artifact_from_plan",
|
name="wf.workflow.create_artifact_from_plan",
|
||||||
title="Create Workflow Artifact From Plan",
|
title="Create Workflow Artifact From Plan",
|
||||||
description=(
|
description="Validate a raw workflow plan and save it as a versioned artifact.",
|
||||||
"Validate a raw workflow plan and save it as a versioned artifact."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
async def create_artifact_from_plan(
|
async def create_artifact_from_plan(
|
||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
@@ -137,10 +60,7 @@ def _register_workflow_tools(
|
|||||||
title="Inspect Workflow Artifact",
|
title="Inspect Workflow Artifact",
|
||||||
description="Return the full saved artifact for artifact_id and version.",
|
description="Return the full saved artifact for artifact_id and version.",
|
||||||
)
|
)
|
||||||
async def inspect_artifact(
|
async def inspect_artifact(artifact_id: str, version: int) -> dict[str, Any]:
|
||||||
artifact_id: str,
|
|
||||||
version: int,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
return await handlers.inspect_artifact(
|
return await handlers.inspect_artifact(
|
||||||
artifact_id=artifact_id,
|
artifact_id=artifact_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -165,9 +85,7 @@ def _register_workflow_tools(
|
|||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.validate_deployment",
|
name="wf.workflow.validate_deployment",
|
||||||
title="Validate Workflow Deployment",
|
title="Validate Workflow Deployment",
|
||||||
description=(
|
description="Check whether a deployment_id can run with currently enabled sources.",
|
||||||
"Check whether a deployment_id can run with currently enabled sources."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
async def validate_deployment(deployment_id: str) -> dict[str, Any]:
|
async def validate_deployment(deployment_id: str) -> dict[str, Any]:
|
||||||
return await handlers.validate_deployment(deployment_id=deployment_id)
|
return await handlers.validate_deployment(deployment_id=deployment_id)
|
||||||
@@ -5,7 +5,7 @@ import sys
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||||
from wf_mcp.server import create_unified_proxy_client
|
from wf_mcp.server import create_server_client
|
||||||
|
|
||||||
from .test_support import fixture_server_path, local_temp_root
|
from .test_support import fixture_server_path, local_temp_root
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ def _structured(result: Any) -> dict[str, Any]:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def test_unified_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "unified_server_store",
|
store_root=local_temp_root() / "unified_server_store",
|
||||||
connections=[
|
connections=[
|
||||||
@@ -34,12 +34,23 @@ def test_unified_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def run_proxy() -> None:
|
async def run_proxy() -> None:
|
||||||
client = create_unified_proxy_client(config)
|
client = create_server_client(config)
|
||||||
async with client:
|
async with client:
|
||||||
tools = await client.list_tools()
|
tools = await client.list_tools()
|
||||||
names = [tool.name for tool in tools]
|
names = [tool.name for tool in tools]
|
||||||
assert "fixture.personal.echo_tool" in names
|
assert "fixture.personal.echo_tool" in names
|
||||||
assert "wf.admin.list_connections" in names
|
assert "wf.admin.list_connections" in names
|
||||||
|
assert "wf.admin.get_connection_statuses" in names
|
||||||
|
assert "wf.admin.refresh_connection_catalog" in names
|
||||||
|
assert "wf.admin.get_catalog" in names
|
||||||
|
assert "wf.admin.get_planner_catalog" in names
|
||||||
|
assert "wf.admin.list_spec_sources" in names
|
||||||
|
assert "wf.admin.list_sources" in names
|
||||||
|
assert "wf.admin.read_resource" in names
|
||||||
|
assert "wf.admin.render_prompt" 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.workflow.list_artifacts" in names
|
assert "wf.workflow.list_artifacts" in names
|
||||||
assert "wf.workflow.run_deployment" in names
|
assert "wf.workflow.run_deployment" in names
|
||||||
|
|
||||||
@@ -48,14 +59,21 @@ def test_unified_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
{"text": "hello"},
|
{"text": "hello"},
|
||||||
)
|
)
|
||||||
artifacts_result = await client.call_tool("wf.workflow.list_artifacts")
|
artifacts_result = await client.call_tool("wf.workflow.list_artifacts")
|
||||||
|
sources_result = await client.call_tool("wf.admin.list_sources")
|
||||||
|
|
||||||
assert _structured(echo_result)["echoed"] == "hello"
|
assert _structured(echo_result)["echoed"] == "hello"
|
||||||
assert _structured(artifacts_result)["nodes"] == []
|
assert _structured(artifacts_result)["nodes"] == []
|
||||||
|
source_ids = {
|
||||||
|
source["id"] for source in _structured(sources_result)["result"]
|
||||||
|
}
|
||||||
|
assert "wf.admin" in source_ids
|
||||||
|
assert "wf.mcp" in source_ids
|
||||||
|
assert "wf.std" in source_ids
|
||||||
|
|
||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
|
|
||||||
def test_unified_server_can_hide_admin_tools() -> None:
|
def test_server_can_hide_admin_tools() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "unified_no_admin_store",
|
store_root=local_temp_root() / "unified_no_admin_store",
|
||||||
connections=[
|
connections=[
|
||||||
@@ -73,7 +91,7 @@ def test_unified_server_can_hide_admin_tools() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def run_proxy() -> None:
|
async def run_proxy() -> None:
|
||||||
client = create_unified_proxy_client(config, admin_tools=False)
|
client = create_server_client(config, admin_tools=False)
|
||||||
async with client:
|
async with client:
|
||||||
tools = await client.list_tools()
|
tools = await client.list_tools()
|
||||||
names = [tool.name for tool in tools]
|
names = [tool.name for tool in tools]
|
||||||
@@ -84,14 +102,14 @@ def test_unified_server_can_hide_admin_tools() -> None:
|
|||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
|
|
||||||
def test_unified_workflow_tools_have_human_metadata() -> None:
|
def test_workflow_tools_have_human_metadata() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "unified_metadata_store",
|
store_root=local_temp_root() / "unified_metadata_store",
|
||||||
connections=[],
|
connections=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run_proxy() -> None:
|
async def run_proxy() -> None:
|
||||||
client = create_unified_proxy_client(config, admin_tools=False)
|
client = create_server_client(config, admin_tools=False)
|
||||||
async with client:
|
async with client:
|
||||||
tools = await client.list_tools()
|
tools = await client.list_tools()
|
||||||
by_name = {tool.name: tool for tool in tools}
|
by_name = {tool.name: tool for tool in tools}
|
||||||
@@ -106,14 +124,14 @@ def test_unified_workflow_tools_have_human_metadata() -> None:
|
|||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
|
|
||||||
def test_unified_admin_tools_have_human_metadata() -> None:
|
def test_admin_tools_have_human_metadata() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "unified_admin_metadata_store",
|
store_root=local_temp_root() / "unified_admin_metadata_store",
|
||||||
connections=[],
|
connections=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run_proxy() -> None:
|
async def run_proxy() -> None:
|
||||||
client = create_unified_proxy_client(config)
|
client = create_server_client(config)
|
||||||
async with client:
|
async with client:
|
||||||
tools = await client.list_tools()
|
tools = await client.list_tools()
|
||||||
by_name = {tool.name: tool for tool in tools}
|
by_name = {tool.name: tool for tool in tools}
|
||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
"id": "playwright.default",
|
"id": "playwright.default",
|
||||||
"server": "playwright",
|
"server": "playwright",
|
||||||
"account": "default",
|
"account": "default",
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"transport": "stdio",
|
"transport": "stdio",
|
||||||
"command": "pnpx",
|
"command": "pnpx",
|
||||||
|
|||||||
Reference in New Issue
Block a user