the start of the merge
This commit is contained in:
@@ -20,5 +20,5 @@ more later
|
|||||||
|
|
||||||
# docs/ pair with `superpowers` and other skills
|
# docs/ pair with `superpowers` and other skills
|
||||||
|
|
||||||
superpowers, a Codex-cli plugin, is a set of skills. check paths given by Codex
|
superpowers, a Codex-cli plugin, is a set of skills. plugin provided skills live in nonstandard paths. check paths given by Codex
|
||||||
if not found, might be stale hash
|
if not found, might be stale hash, try find new hash
|
||||||
|
|||||||
@@ -259,19 +259,19 @@ config_reloaded
|
|||||||
- Modify: `src/wf_mcp/broker/server.py`
|
- Modify: `src/wf_mcp/broker/server.py`
|
||||||
- Test: `tests/wf_mcp/test_unified_server.py`
|
- Test: `tests/wf_mcp/test_unified_server.py`
|
||||||
|
|
||||||
- [ ] Build one FastMCP server from `BrokerConfig`.
|
- [x] Build one FastMCP server from `BrokerConfig`.
|
||||||
- [ ] Register local workflow tools with namespaced names:
|
- [ ] Register local workflow tools with namespaced names:
|
||||||
- `wf.workflow.list_artifacts`
|
- [x] `wf.workflow.list_artifacts`
|
||||||
- `wf.workflow.create_artifact_from_plan`
|
- [x] `wf.workflow.create_artifact_from_plan`
|
||||||
- `wf.workflow.save_artifact`
|
- [x] `wf.workflow.save_artifact`
|
||||||
- `wf.workflow.list_deployments`
|
- [x] `wf.workflow.list_deployments`
|
||||||
- `wf.workflow.save_deployment`
|
- [x] `wf.workflow.save_deployment`
|
||||||
- `wf.workflow.validate_deployment`
|
- [x] `wf.workflow.validate_deployment`
|
||||||
- `wf.workflow.run_deployment`
|
- [x] `wf.workflow.run_deployment`
|
||||||
- [ ] Register admin tools only when admin exposure is enabled.
|
- [ ] Register admin tools only when admin exposure is enabled.
|
||||||
- [ ] Project upstream tools using the transparent proxy path.
|
- [x] Project upstream tools using the transparent proxy path.
|
||||||
- [ ] Keep existing `broker` and `proxy` CLI modes during migration.
|
- [x] Keep existing `broker` and `proxy` CLI modes during migration.
|
||||||
- [ ] Add `unified` CLI mode.
|
- [x] Add `unified` CLI mode.
|
||||||
- [ ] Do not make unified default until manual Inspector/Codex tests pass.
|
- [ ] Do not make unified default until manual Inspector/Codex tests pass.
|
||||||
|
|
||||||
## Phase 4: Namespacing And Collision Policy
|
## Phase 4: Namespacing And Collision Policy
|
||||||
|
|||||||
+14
-3
@@ -12,6 +12,7 @@ from .broker import (
|
|||||||
run_broker_server,
|
run_broker_server,
|
||||||
run_transparent_proxy_server,
|
run_transparent_proxy_server,
|
||||||
)
|
)
|
||||||
|
from .server import run_unified_proxy_server
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
@@ -34,8 +35,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
serve.add_argument(
|
serve.add_argument(
|
||||||
"--mode",
|
"--mode",
|
||||||
default="proxy",
|
default="proxy",
|
||||||
choices=["broker", "proxy"],
|
choices=["broker", "proxy", "unified"],
|
||||||
help="Run admin/workflow broker mode or transparent proxy mode.",
|
help="Run broker mode, transparent proxy mode, or unified mode.",
|
||||||
)
|
)
|
||||||
serve.add_argument(
|
serve.add_argument(
|
||||||
"--resources-as-tools",
|
"--resources-as-tools",
|
||||||
@@ -122,8 +123,18 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
prompts_as_tools=args.prompts_as_tools,
|
prompts_as_tools=args.prompts_as_tools,
|
||||||
search_tools=args.search_tools,
|
search_tools=args.search_tools,
|
||||||
)
|
)
|
||||||
else:
|
elif args.mode == "broker":
|
||||||
run_broker_server(args.config, args.transport)
|
run_broker_server(args.config, args.transport)
|
||||||
|
else:
|
||||||
|
config = load_broker_config(args.config)
|
||||||
|
run_unified_proxy_server(
|
||||||
|
config,
|
||||||
|
args.transport,
|
||||||
|
config_path=args.config,
|
||||||
|
resources_as_tools=args.resources_as_tools,
|
||||||
|
prompts_as_tools=args.prompts_as_tools,
|
||||||
|
search_tools=args.search_tools,
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
service = _service_from_config(args.config)
|
service = _service_from_config(args.config)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .unified import (
|
||||||
|
create_unified_proxy_client,
|
||||||
|
create_unified_proxy_server,
|
||||||
|
run_unified_proxy_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_unified_proxy_client",
|
||||||
|
"create_unified_proxy_server",
|
||||||
|
"run_unified_proxy_server",
|
||||||
|
]
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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 ..broker.config import build_service_from_config
|
||||||
|
from ..broker.transport import normalize_transport
|
||||||
|
from ..models import BrokerConfig
|
||||||
|
from ..transparent_proxy.runtime import TransparentProxyRuntime
|
||||||
|
from ..workflow_surface import WorkflowSurfaceHandlers
|
||||||
|
|
||||||
|
|
||||||
|
def create_unified_proxy_server(
|
||||||
|
config: BrokerConfig,
|
||||||
|
*,
|
||||||
|
config_path: str | Path | None = None,
|
||||||
|
resources_as_tools: bool = False,
|
||||||
|
prompts_as_tools: bool = False,
|
||||||
|
search_tools: bool = False,
|
||||||
|
) -> FastMCP[Any]:
|
||||||
|
"""Create one MCP server with upstream proxy, admin, and workflow tools."""
|
||||||
|
runtime = TransparentProxyRuntime(
|
||||||
|
config,
|
||||||
|
config_path=config_path,
|
||||||
|
resources_as_tools=resources_as_tools,
|
||||||
|
prompts_as_tools=prompts_as_tools,
|
||||||
|
search_tools=search_tools,
|
||||||
|
)
|
||||||
|
service = build_service_from_config(config)
|
||||||
|
_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,
|
||||||
|
) -> 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,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
) -> 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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_workflow_tools(
|
||||||
|
server: FastMCP[Any],
|
||||||
|
handlers: WorkflowSurfaceHandlers,
|
||||||
|
) -> None:
|
||||||
|
"""Register stable workflow tools on the unified MCP surface."""
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.list_artifacts")
|
||||||
|
async def list_artifacts() -> dict[str, Any]:
|
||||||
|
return await handlers.list_artifacts()
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.save_artifact")
|
||||||
|
async def save_artifact(artifact: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return await handlers.save_artifact(artifact)
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.create_artifact_from_plan")
|
||||||
|
async def create_artifact_from_plan(
|
||||||
|
artifact_id: str,
|
||||||
|
version: int,
|
||||||
|
title: str,
|
||||||
|
plan: dict[str, Any],
|
||||||
|
outcomes: list[str],
|
||||||
|
description: str | None = None,
|
||||||
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
|
created_from_catalog_version: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.create_artifact_from_plan(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
plan=plan,
|
||||||
|
outcomes=outcomes,
|
||||||
|
required_capabilities=required_capabilities,
|
||||||
|
created_from_catalog_version=created_from_catalog_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.inspect_artifact")
|
||||||
|
async def inspect_artifact(
|
||||||
|
artifact_id: str,
|
||||||
|
version: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.inspect_artifact(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.list_deployments")
|
||||||
|
async def list_deployments() -> dict[str, Any]:
|
||||||
|
return await handlers.list_deployments()
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.save_deployment")
|
||||||
|
async def save_deployment(deployment: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return await handlers.save_deployment(deployment)
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.validate_deployment")
|
||||||
|
async def validate_deployment(deployment_id: str) -> dict[str, Any]:
|
||||||
|
return await handlers.validate_deployment(deployment_id=deployment_id)
|
||||||
|
|
||||||
|
@server.tool(name="wf.workflow.run_deployment")
|
||||||
|
async def run_deployment(
|
||||||
|
deployment_id: str,
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.run_deployment(
|
||||||
|
deployment_id=deployment_id,
|
||||||
|
workflow_input=workflow_input,
|
||||||
|
)
|
||||||
@@ -64,6 +64,22 @@ def test_build_parser_accepts_proxy_compatibility_flags() -> None:
|
|||||||
assert args.search_tools is True
|
assert args.search_tools is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_parser_accepts_unified_mode() -> None:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(
|
||||||
|
[
|
||||||
|
"--config",
|
||||||
|
"wf_mcp.config.json",
|
||||||
|
"serve",
|
||||||
|
"--mode",
|
||||||
|
"unified",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert args.command == "serve"
|
||||||
|
assert args.mode == "unified"
|
||||||
|
|
||||||
|
|
||||||
def test_cli_connections_prints_configured_connections(capsys) -> None:
|
def test_cli_connections_prints_configured_connections(capsys) -> None:
|
||||||
tmp_path = local_temp_root() / "cli_connections_test"
|
tmp_path = local_temp_root() / "cli_connections_test"
|
||||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||||
|
from wf_mcp.server import create_unified_proxy_client
|
||||||
|
|
||||||
|
from .test_support import fixture_server_path, local_temp_root
|
||||||
|
|
||||||
|
|
||||||
|
def _structured(result: Any) -> dict[str, Any]:
|
||||||
|
content = result.structured_content
|
||||||
|
assert isinstance(content, dict)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def test_unified_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
||||||
|
config = BrokerConfig(
|
||||||
|
store_root=local_temp_root() / "unified_server_store",
|
||||||
|
connections=[
|
||||||
|
ConnectionConfig(
|
||||||
|
id="fixture.personal",
|
||||||
|
server="fixture",
|
||||||
|
account="personal",
|
||||||
|
metadata={
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": sys.executable,
|
||||||
|
"args": [fixture_server_path()],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run_proxy() -> None:
|
||||||
|
client = create_unified_proxy_client(config)
|
||||||
|
async with client:
|
||||||
|
tools = await client.list_tools()
|
||||||
|
names = [tool.name for tool in tools]
|
||||||
|
assert "fixture.personal_echo_tool" in names
|
||||||
|
assert "wf.admin.list_connections" in names
|
||||||
|
assert "wf.workflow.list_artifacts" in names
|
||||||
|
assert "wf.workflow.run_deployment" in names
|
||||||
|
|
||||||
|
echo_result = await client.call_tool(
|
||||||
|
"fixture.personal_echo_tool",
|
||||||
|
{"text": "hello"},
|
||||||
|
)
|
||||||
|
artifacts_result = await client.call_tool("wf.workflow.list_artifacts")
|
||||||
|
|
||||||
|
assert _structured(echo_result)["echoed"] == "hello"
|
||||||
|
assert _structured(artifacts_result)["nodes"] == []
|
||||||
|
|
||||||
|
asyncio.run(run_proxy())
|
||||||
Reference in New Issue
Block a user