Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
26a4cce
WIP - add mcp example
yuval-qf Oct 13, 2025
bbaac20
Merge branch 'main' into feature/FIRE-800-mcp-support
yuval-qf Oct 19, 2025
da4e592
Fix mcp agent
yuval-qf Oct 19, 2025
40048a1
Fix mcp agent
yuval-qf Oct 19, 2025
a830b10
typing
yuval-qf Oct 19, 2025
442bb83
Add docs
yuval-qf Oct 19, 2025
2d7d54f
Add base logic to support different transports
yuval-qf Oct 19, 2025
c8c086b
Use local sdk for github action tests
yuval-qf Oct 19, 2025
d926a5b
Add TransportType to sdk __init__/__all__
yuval-qf Oct 19, 2025
ae56669
fix gh tests
yuval-qf Oct 19, 2025
d34d404
Fix cyclical import
yuval-qf Oct 19, 2025
3d06933
Add missing files
yuval-qf Oct 19, 2025
8b2cf93
Fix missing kwarg
yuval-qf Oct 20, 2025
ee76845
cicd fix
yuval-qf Oct 20, 2025
54fe40d
Fix import
yuval-qf Oct 20, 2025
81cec81
Merge branch 'main' into feature/FIRE-831-mcp-transport-support
yuval-qf Oct 20, 2025
43e0f56
Merge branch 'main' into feature/FIRE-800-mcp-support
yuval-qf Oct 20, 2025
67b11eb
Merge branch 'feature/FIRE-800-mcp-support' into feature/FIRE-831-mcp…
yuval-qf Oct 20, 2025
69e064a
Add mcp transport
yuval-qf Oct 20, 2025
8b80718
formatting
yuval-qf Oct 20, 2025
0430690
transport -> protocol
yuval-qf Oct 20, 2025
2692157
Merge branch 'main' into feature/FIRE-800-mcp-support
yuval-qf Oct 20, 2025
09e8181
formatting
yuval-qf Oct 20, 2025
f25ff97
Merge branch 'feature/FIRE-800-mcp-support' into feature/FIRE-831-mcp…
yuval-qf Oct 20, 2025
1524c80
Fix session id
yuval-qf Oct 20, 2025
eddf17a
Improve import time
yuval-qf Oct 20, 2025
1082509
Add mcp support to cli
yuval-qf Oct 20, 2025
0660908
Fix lefthook
yuval-qf Oct 20, 2025
84acb33
Rename example folder
yuval-qf Oct 20, 2025
0f313cc
Merge branch 'feature/FIRE-800-mcp-support' into feature/FIRE-831-mcp…
yuval-qf Oct 20, 2025
79dbd0a
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 20, 2025
a7fb351
Merge branch 'main' into feature/FIRE-831-mcp-transport-support
yuval-qf Oct 20, 2025
ff31cb7
revert unreleated changes
yuval-qf Oct 20, 2025
712b93c
Rabbit cr
yuval-qf Oct 20, 2025
21fff0d
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 20, 2025
4f07617
Add transport support
yuval-qf Oct 20, 2025
a83836e
mypy fixes
yuval-qf Oct 21, 2025
2495179
Add missing argument
yuval-qf Oct 21, 2025
3431701
Add missing argument
yuval-qf Oct 21, 2025
1468e0a
Add missing argument
yuval-qf Oct 21, 2025
629128d
Fix tests
yuval-qf Oct 21, 2025
67ed649
Add transport support
yuval-qf Oct 21, 2025
23e8b6c
Improve import time
yuval-qf Oct 21, 2025
427400c
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 21, 2025
ebdf875
Rabbit cr
yuval-qf Oct 21, 2025
dfbb5ff
Update launch.json path
yuval-qf Oct 21, 2025
3ac16ad
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 21, 2025
dd87e37
Add transport to a2a to create a clean convention
yuval-qf Oct 21, 2025
1acff05
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 22, 2025
65801a0
Add transport to a2a in rogue-cli
yuval-qf Oct 22, 2025
e67b99e
CR
yuval-qf Oct 22, 2025
c36e371
Fix partial import
yuval-qf Oct 22, 2025
6d18a8e
Run mcp example from maing
yuval-qf Oct 22, 2025
f669cd1
Merge branch 'main' into feature/FIRE-831-mcp-transport-support
yuval-qf Oct 22, 2025
473e0bb
Fix typo
yuval-qf Oct 22, 2025
ca9e33a
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 22, 2025
09c13d9
CR
yuval-qf Oct 22, 2025
f28b81b
Rabbit cr - Add click options to mcp examples
yuval-qf Oct 22, 2025
1875a30
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 22, 2025
24f1e74
Merge branch 'main' into feature/FIRE-831-mcp-transport-support
yuval-qf Oct 22, 2025
abf857a
Merge branch 'feature/FIRE-831-mcp-transport-support' into feature/FI…
yuval-qf Oct 22, 2025
ee15fca
Merge branch 'main' into feature/FIRE-832-mcp-support-rogue-cli
yuval-qf Oct 22, 2025
75bd3ed
Merge branch 'main' into feature/FIRE-832-mcp-support-rogue-cli
yuval-qf Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@
],
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Rogue CLI MCP",
"type": "debugpy",
"request": "launch",
"python": "./.venv/bin/python",
"module": "rogue",
"args": [
"cli",
"--evaluated-agent-url",
"http://localhost:10001/mcp",
"--protocol",
"mcp",
"--judge-llm",
"openai/o4-mini",
"--workdir",
"./examples/tshirt_store_agent/.rogue"
],
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Rogue AIO",
"type": "debugpy",
Expand Down
9 changes: 8 additions & 1 deletion rogue/models/cli_input.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path

from pydantic import BaseModel, Field, HttpUrl, SecretStr, model_validator
from rogue_sdk.types import AuthType, Scenarios
from rogue_sdk.types import AuthType, Protocol, Scenarios, Transport


class CLIInput(BaseModel):
Expand All @@ -10,6 +10,8 @@ class CLIInput(BaseModel):
"""

workdir: Path = Path(".") / ".rogue"
protocol: Protocol
transport: Transport
evaluated_agent_url: HttpUrl
evaluated_agent_auth_type: AuthType = AuthType.NO_AUTH
evaluated_agent_credentials: SecretStr | None = None
Expand Down Expand Up @@ -52,6 +54,8 @@ class PartialCLIInput(BaseModel):
"""

workdir: Path = Path(".") / ".rogue"
protocol: Protocol = Field(default=Protocol.A2A)
transport: Transport = None # type: ignore # fixed in model_post_init
evaluated_agent_url: HttpUrl | None = Field(default=None)
evaluated_agent_auth_type: AuthType = Field(default=AuthType.NO_AUTH)
evaluated_agent_credentials: SecretStr | None = Field(default=None)
Expand All @@ -73,3 +77,6 @@ def model_post_init(self, __context):
self.output_report_file = self.workdir / "report.md"
if self.business_context_file is None:
self.business_context_file = self.workdir / "business_context.md"

if self.transport is None:
self.transport = self.protocol.get_default_transport()
132 changes: 118 additions & 14 deletions rogue/run_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@

import requests
from a2a.types import AgentCard
from fastmcp import Client
from fastmcp.client import SSETransport, StreamableHttpTransport
from loguru import logger
from pydantic import SecretStr, ValidationError
from rich.console import Console
from rich.markdown import Markdown
from rogue_sdk import RogueClientConfig, RogueSDK
from rogue_sdk.types import AgentConfig, AuthType, EvaluationResults, Scenarios
from rogue_sdk.types import (
PROTOCOL_TO_TRANSPORTS,
AgentConfig,
AuthType,
EvaluationResults,
Protocol,
Scenarios,
Transport,
)

from .models.cli_input import CLIInput, PartialCLIInput

Expand All @@ -25,6 +35,26 @@ def set_cli_args(parser: ArgumentParser) -> None:
default="http://localhost:8000",
help="Rogue server URL",
)
parser.add_argument(
"--protocol",
choices=[e.value for e in Protocol],
default=Protocol.A2A.value,
type=Protocol,
help="Protocol used to communicate with the agent."
f"Valid options are: {[e.value for e in Protocol]}",
)
transport_options = ", ".join(
f"{protocol.value}: {[t.value for t in transports]}"
for protocol, transports in PROTOCOL_TO_TRANSPORTS.items()
)
parser.add_argument(
"--transport",
choices=[e.value for e in Transport],
type=Transport,
required=False,
help="Transport used to communicate with the agent. "
f"Valid options are based on the protocol: {transport_options}",
)
parser.add_argument(
"--evaluated-agent-url",
required=False,
Expand Down Expand Up @@ -96,6 +126,8 @@ def set_cli_args(parser: ArgumentParser) -> None:

async def run_scenarios(
rogue_server_url: str,
protocol: Protocol,
transport: Transport,
evaluated_agent_url: str,
evaluated_agent_auth_type: AuthType,
evaluated_agent_auth_credentials_secret: SecretStr | None,
Expand All @@ -120,6 +152,8 @@ async def run_scenarios(
# Use SDK for evaluation
return await _run_scenarios_with_sdk(
rogue_server_url=rogue_server_url,
protocol=protocol,
transport=transport,
evaluated_agent_url=evaluated_agent_url,
evaluated_agent_auth_type=evaluated_agent_auth_type,
evaluated_agent_auth_credentials=evaluated_agent_auth_credentials,
Expand All @@ -134,6 +168,8 @@ async def run_scenarios(

async def _run_scenarios_with_sdk(
rogue_server_url: str,
protocol: Protocol,
transport: Transport,
evaluated_agent_url: str,
evaluated_agent_auth_type: AuthType,
evaluated_agent_auth_credentials: str | None,
Expand All @@ -159,12 +195,15 @@ async def _run_scenarios_with_sdk(

# Run evaluation
job = await sdk.run_evaluation(
protocol=protocol,
transport=transport,
agent_url=evaluated_agent_url,
scenarios=scenarios,
business_context=business_context,
auth_type=evaluated_agent_auth_type,
auth_credentials=evaluated_agent_auth_credentials,
judge_model=judge_llm,
judge_llm_api_key=judge_llm_api_key,
deep_test=deep_test_mode,
)

Expand Down Expand Up @@ -323,28 +362,91 @@ def get_cli_input(cli_args: Namespace) -> CLIInput:
return cli_input


def get_agent_card(agent_url: str) -> AgentCard:
try:
response = requests.get(
f"{agent_url}/.well-known/agent.json",
timeout=5,
async def get_a2a_agent_card(
transport: Transport,
agent_url: str,
headers: dict[str, str] | None = None,
) -> AgentCard:
if transport == Transport.HTTP:
try:
response = requests.get(
f"{agent_url}/.well-known/agent.json",
timeout=5,
headers=headers,
)
return AgentCard.model_validate(response.json())
except Exception:
logger.debug(
"Failed to connect to agent",
extra={"agent_url": agent_url},
exc_info=True,
)
raise
else:
raise ValueError(f"Unsupported transport: {transport} for A2A protocol")


async def ping_mcp_server(
transport: Transport,
agent_url: str,
headers: dict[str, str] | None = None,
) -> None:
client: Client[StreamableHttpTransport | SSETransport]
if transport == Transport.STREAMABLE_HTTP:
client = Client[StreamableHttpTransport](
transport=StreamableHttpTransport(
url=agent_url,
headers=headers,
),
)
return AgentCard.model_validate(response.json())
except Exception:
logger.debug(
"Failed to connect to agent",
extra={"agent_url": agent_url},
exc_info=True,
elif transport == Transport.SSE:
client = Client[SSETransport](
transport=SSETransport(
url=agent_url,
headers=headers,
),
)
raise
else:
raise ValueError(f"Unsupported transport: {transport} for MCP protocol")

async with client:
await client.ping()


async def ping_agent(
protocol: Protocol,
transport: Transport,
agent_url: str,
agent_auth_type: AuthType,
agent_auth_credentials: SecretStr | None,
) -> None:
# TODO: move this to the server side
protocol_to_ping_function = {
Protocol.MCP: ping_mcp_server,
Protocol.A2A: get_a2a_agent_card,
}
if protocol not in protocol_to_ping_function:
raise ValueError(f"Unsupported protocol: {protocol}")

await protocol_to_ping_function[protocol](
transport=transport,
agent_url=agent_url,
headers=agent_auth_type.get_auth_header(agent_auth_credentials),
)


async def run_cli(args: Namespace) -> int:
cli_input = get_cli_input(args)
logger.debug("Running CLI", extra=cli_input.model_dump())

# fast fail if the agent is not reachable
get_agent_card(cli_input.evaluated_agent_url.encoded_string())
await ping_agent(
protocol=cli_input.protocol,
transport=cli_input.transport,
agent_url=cli_input.evaluated_agent_url.encoded_string(),
agent_auth_type=cli_input.evaluated_agent_auth_type,
agent_auth_credentials=cli_input.evaluated_agent_credentials,
)

scenarios = cli_input.get_scenarios_from_file()

Expand All @@ -356,6 +458,8 @@ async def run_cli(args: Namespace) -> int:
)
results, job_id = await run_scenarios(
rogue_server_url=args.rogue_server_url,
protocol=cli_input.protocol,
transport=cli_input.transport,
evaluated_agent_url=cli_input.evaluated_agent_url.encoded_string(),
evaluated_agent_auth_type=cli_input.evaluated_agent_auth_type,
evaluated_agent_auth_credentials_secret=cli_input.evaluated_agent_credentials,
Expand Down
5 changes: 5 additions & 0 deletions rogue/tests/models/test_cli_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest
from pydantic import HttpUrl, SecretStr, ValidationError
from pytest_mock import MockerFixture
from rogue_sdk.types import Protocol, Transport

from rogue.models.cli_input import AuthType, CLIInput

Expand All @@ -23,6 +24,8 @@ class TestCLIInput:
)
def test_check_auth_credentials(self, auth_type, credentials, should_raise):
input_data = {
"protocol": Protocol.A2A,
"transport": Transport.HTTP,
"evaluated_agent_url": "https://example.com",
"evaluated_agent_auth_type": auth_type,
"evaluated_agent_credentials": credentials,
Expand Down Expand Up @@ -80,6 +83,8 @@ class CLIInputWithMockScenarios(CLIInput):
)

cli_input = CLIInputWithMockScenarios(
protocol=Protocol.A2A,
transport=Transport.HTTP,
evaluated_agent_url=HttpUrl("https://example.com"),
judge_llm="example-model",
business_context="example-context",
Expand Down
8 changes: 7 additions & 1 deletion rogue/tests/test_run_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
from pydantic import HttpUrl, SecretStr
from pytest_mock import MockerFixture
from rogue_sdk.types import AuthType
from rogue_sdk.types import AuthType, Protocol, Transport

from rogue.models.cli_input import CLIInput
from rogue.run_cli import get_cli_input
Expand All @@ -23,6 +23,8 @@
),
CLIInput(
workdir=Path(".") / ".rogue",
protocol=Protocol.A2A,
transport=Transport.HTTP,
evaluated_agent_url=HttpUrl("https://localhost:10001"),
evaluated_agent_auth_type=AuthType.NO_AUTH,
evaluated_agent_credentials=None,
Expand All @@ -45,6 +47,8 @@
Namespace(business_context="my business"),
CLIInput(
workdir=Path(".") / ".rogue",
protocol=Protocol.A2A,
transport=Transport.HTTP,
evaluated_agent_url=HttpUrl("https://localhost:10001"),
evaluated_agent_auth_type=AuthType.API_KEY,
evaluated_agent_credentials=SecretStr("abc123"),
Expand All @@ -66,6 +70,8 @@
),
CLIInput(
workdir=Path(".") / ".rogue",
protocol=Protocol.A2A,
transport=Transport.HTTP,
evaluated_agent_url=HttpUrl("https://overriden_agent_url:10001"),
evaluated_agent_auth_type=AuthType.NO_AUTH,
evaluated_agent_credentials=None,
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/rogue_sdk/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ async def run_evaluation(

agent_config = AgentConfig(
protocol=protocol,
transport=transport,
transport=transport or protocol.get_default_transport(),
evaluated_agent_url=HttpUrl(agent_url),
evaluated_agent_auth_type=auth_type,
evaluated_agent_credentials=auth_credentials,
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/rogue_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class AgentConfig(BaseModel):
"""Configuration for the agent being evaluated."""

protocol: Protocol = Protocol.A2A
transport: Transport | None = None
transport: Transport = None # type: ignore # fixed in model_post_init
evaluated_agent_url: HttpUrl
evaluated_agent_auth_type: AuthType = Field(
default=AuthType.NO_AUTH,
Expand Down
Loading