the start of the merge

This commit is contained in:
lda
2026-05-13 16:44:15 +07:00 Verified
parent d90a8b3968
commit c36614d46a
7 changed files with 253 additions and 16 deletions
+2 -2
View File
@@ -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
View File
@@ -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)
+11
View File
@@ -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",
]
+144
View File
@@ -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,
)
+16
View File
@@ -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)
+55
View File
@@ -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())