Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ line-length = 120

[tool.isort]
profile = "black"

[tool.pytest.ini_options]
markers = [
"asyncio: mark test as an async test (used by pytest-asyncio)",
]
asyncio_default_fixture_loop_scope = "function"
181 changes: 146 additions & 35 deletions recce/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,68 @@ def create_state_loader(review_mode, cloud_mode, state_file, cloud_options):
exit(1)


def create_state_loader_by_args(state_file=None, **kwargs):
"""
Create a state loader based on CLI arguments.

This function handles the cloud options logic that is shared between
server and mcp-server commands.

Args:
state_file: Optional path to state file
**kwargs: CLI arguments including api_token, cloud, review, session_id, share_url, etc.

Returns:
state_loader: The created state loader instance
"""
from rich.console import Console

console = Console()

api_token = kwargs.get("api_token")
is_review = kwargs.get("review", False)
is_cloud = kwargs.get("cloud", False)
cloud_options = None

# Handle share_url and session_id
share_url = kwargs.get("share_url")
session_id = kwargs.get("session_id")

if share_url:
share_id = share_url.split("/")[-1]
if not share_id:
console.print("[[red]Error[/red]] Invalid share URL format.")
exit(1)

if is_cloud:
# Cloud mode
if share_url:
is_review = kwargs["review"] = True
cloud_options = {
"host": kwargs.get("state_file_host"),
"api_token": api_token,
"share_id": share_id,
}
elif session_id:
is_review = kwargs["review"] = True
cloud_options = {
"host": kwargs.get("state_file_host"),
"api_token": api_token,
"session_id": session_id,
}
else:
cloud_options = {
"host": kwargs.get("state_file_host"),
"github_token": kwargs.get("cloud_token"),
"password": kwargs.get("password"),
}

# Create state loader
state_loader = create_state_loader(is_review, is_cloud, state_file, cloud_options)

return state_loader


def handle_debug_flag(**kwargs):
if kwargs.get("debug"):
import logging
Expand Down Expand Up @@ -454,52 +516,23 @@ def server(host, port, lifetime, state_file=None, **kwargs):
"read_only": False,
}
console = Console()
cloud_options = None

# Prepare API token
try:
api_token = prepare_api_token(**kwargs)
kwargs["api_token"] = api_token
except RecceConfigException:
show_invalid_api_token_message()
exit(1)
auth_options = {
"api_token": api_token,
}

share_url = kwargs.get("share_url")
session_id = kwargs.get("session_id")
if share_url:
share_id = share_url.split("/")[-1]
if not share_id:
console.print("[[red]Error[/red]] Invalid share URL format.")
exit(1)

if is_cloud:
# Cloud mode
if share_url:
is_review = kwargs["review"] = True
cloud_options = {
"host": kwargs.get("state_file_host"),
"api_token": api_token,
"share_id": share_id,
}
elif session_id:
is_review = kwargs["review"] = True
cloud_options = {
"host": kwargs.get("state_file_host"),
"api_token": api_token,
"session_id": session_id,
}
else:
cloud_options = {
"host": kwargs.get("state_file_host"),
"github_token": kwargs.get("cloud_token"),
"password": kwargs.get("password"),
}
else:
# Check Single Environment Onboarding Mode if the review mode is False
# Check Single Environment Onboarding Mode if not in cloud mode and not in review mode
if not is_cloud and not is_review:
project_dir_path = Path(kwargs.get("project_dir") or "./")
target_base_path = project_dir_path.joinpath(Path(kwargs.get("target_base_path", "target-base")))
if not target_base_path.is_dir() and not is_review:
if not target_base_path.is_dir():
# Mark as single env onboarding mode if user provides the target-path only
flag["single_env_onboarding"] = True
flag["show_relaunch_hint"] = True
Expand All @@ -520,7 +553,8 @@ def server(host, port, lifetime, state_file=None, **kwargs):
# Onboarding State logic update here
update_onboarding_state(api_token, flag.get("single_env_onboarding"))

state_loader = create_state_loader(is_review, is_cloud, state_file, cloud_options)
# Create state loader using shared function
state_loader = create_state_loader_by_args(state_file, **kwargs)

if not state_loader.verify():
error, hint = state_loader.error_and_hint
Expand Down Expand Up @@ -1598,5 +1632,82 @@ def read_only(ctx, state_file=None, **kwargs):
ctx.invoke(server, state_file=state_file, **kwargs)


@cli.command(cls=TrackCommand)
@add_options(dbt_related_options)
@add_options(sqlmesh_related_options)
@add_options(recce_options)
@add_options(recce_dbt_artifact_dir_options)
@add_options(recce_cloud_options)
@add_options(recce_cloud_auth_options)
@add_options(recce_hidden_options)
def mcp_server(**kwargs):
"""
[Experiment] Start the Recce MCP (Model Context Protocol) server

The MCP server provides a stdio-based interface for AI assistants and tools
to interact with Recce's data validation capabilities.

Available tools:
- get_lineage_diff: Get lineage differences between environments
- row_count_diff: Compare row counts between environments
- query: Execute SQL queries with dbt templating
- query_diff: Compare query results between environments
- profile_diff: Generate statistical profiles and compare

Examples:\n

\b
# Start the MCP server
recce mcp-server

\b
# Start with custom dbt configuration
recce mcp-server --target prod --project-dir ./my_project
"""
from rich.console import Console

console = Console()
try:
# Import here to avoid import errors if mcp is not installed
from recce.mcp_server import run_mcp_server
except ImportError as e:
console.print(f"[[red]Error[/red]] Failed to import MCP server: {e}")
console.print(r"Please install the MCP package: pip install 'recce\[mcp]'")
exit(1)

# Initialize Recce Config
RecceConfig(config_file=kwargs.get("config"))

handle_debug_flag(**kwargs)

# Prepare API token
try:
api_token = prepare_api_token(**kwargs)
kwargs["api_token"] = api_token
except RecceConfigException:
show_invalid_api_token_message()
exit(1)

# Create state loader using shared function (if cloud mode is enabled)
is_cloud = kwargs.get("cloud", False)
if is_cloud:
state_loader = create_state_loader_by_args(None, **kwargs)
kwargs["state_loader"] = state_loader

try:
console.print("Starting Recce MCP Server...")
console.print("Available tools: get_lineage_diff, row_count_diff, query, query_diff, profile_diff")

# Run the async server
asyncio.run(run_mcp_server(**kwargs))
except Exception as e:
console.print(f"[[red]Error[/red]] Failed to start MCP server: {e}")
if kwargs.get("debug"):
import traceback

traceback.print_exc()
exit(1)


if __name__ == "__main__":
cli()
Loading