recce-nightly 1.23.0.20251029__py3-none-any.whl → 1.23.0.20251031a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of recce-nightly might be problematic. Click here for more details.

recce/mcp_server.py ADDED
@@ -0,0 +1,276 @@
1
+ """
2
+ Recce MCP (Model Context Protocol) Server
3
+
4
+ This module implements a stdio-based MCP server that provides tools for
5
+ interacting with Recce's data validation capabilities.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ from typing import Any, Dict, List
12
+
13
+ from mcp.server import Server
14
+ from mcp.server.stdio import stdio_server
15
+ from mcp.types import TextContent, Tool
16
+
17
+ from recce.core import RecceContext, load_context
18
+ from recce.tasks.profile import ProfileDiffTask
19
+ from recce.tasks.query import QueryDiffTask, QueryTask
20
+ from recce.tasks.rowcount import RowCountDiffTask
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class RecceMCPServer:
26
+ """MCP Server for Recce data validation tools"""
27
+
28
+ def __init__(self, context: RecceContext):
29
+ self.context = context
30
+ self.server = Server("recce")
31
+ self._setup_handlers()
32
+
33
+ def _setup_handlers(self):
34
+ """Register all tool handlers"""
35
+
36
+ @self.server.list_tools()
37
+ async def list_tools() -> List[Tool]:
38
+ """List all available tools"""
39
+ return [
40
+ Tool(
41
+ name="get_lineage_diff",
42
+ description="Get the lineage diff between base and current environments. "
43
+ "Shows added, removed, and modified models.",
44
+ inputSchema={
45
+ "type": "object",
46
+ "properties": {
47
+ "select": {
48
+ "type": "string",
49
+ "description": "dbt selector syntax to filter models (optional)",
50
+ },
51
+ "exclude": {
52
+ "type": "string",
53
+ "description": "dbt selector syntax to exclude models (optional)",
54
+ },
55
+ "packages": {
56
+ "type": "array",
57
+ "items": {"type": "string"},
58
+ "description": "List of packages to filter (optional)",
59
+ },
60
+ },
61
+ },
62
+ ),
63
+ Tool(
64
+ name="row_count_diff",
65
+ description="Compare row counts between base and current environments for specified models.",
66
+ inputSchema={
67
+ "type": "object",
68
+ "properties": {
69
+ "node_names": {
70
+ "type": "array",
71
+ "items": {"type": "string"},
72
+ "description": "List of model names to check row counts (optional)",
73
+ },
74
+ "node_ids": {
75
+ "type": "array",
76
+ "items": {"type": "string"},
77
+ "description": "List of node IDs to check row counts (optional)",
78
+ },
79
+ "select": {
80
+ "type": "string",
81
+ "description": "dbt selector syntax to filter models (optional)",
82
+ },
83
+ "exclude": {
84
+ "type": "string",
85
+ "description": "dbt selector syntax to exclude models (optional)",
86
+ },
87
+ },
88
+ },
89
+ ),
90
+ Tool(
91
+ name="query",
92
+ description="Execute a SQL query on the current environment. "
93
+ "Supports Jinja templates with dbt macros like {{ ref('model_name') }}.",
94
+ inputSchema={
95
+ "type": "object",
96
+ "properties": {
97
+ "sql_template": {
98
+ "type": "string",
99
+ "description": "SQL query template with optional Jinja syntax",
100
+ },
101
+ "base": {
102
+ "type": "boolean",
103
+ "description": "Whether to run on base environment (default: false)",
104
+ "default": False,
105
+ },
106
+ },
107
+ "required": ["sql_template"],
108
+ },
109
+ ),
110
+ Tool(
111
+ name="query_diff",
112
+ description="Execute SQL queries on both base and current environments and compare results. "
113
+ "Supports primary keys for row-level comparison.",
114
+ inputSchema={
115
+ "type": "object",
116
+ "properties": {
117
+ "sql_template": {
118
+ "type": "string",
119
+ "description": "SQL query template for current environment",
120
+ },
121
+ "base_sql_template": {
122
+ "type": "string",
123
+ "description": "SQL query template for base environment (optional, defaults to sql_template)",
124
+ },
125
+ "primary_keys": {
126
+ "type": "array",
127
+ "items": {"type": "string"},
128
+ "description": "List of primary key columns for row comparison (optional)",
129
+ },
130
+ },
131
+ "required": ["sql_template"],
132
+ },
133
+ ),
134
+ Tool(
135
+ name="profile_diff",
136
+ description="Generate and compare statistical profiles (min, max, avg, distinct count, etc.) "
137
+ "for columns in a model between base and current environments.",
138
+ inputSchema={
139
+ "type": "object",
140
+ "properties": {
141
+ "model": {
142
+ "type": "string",
143
+ "description": "Model name to profile",
144
+ },
145
+ "columns": {
146
+ "type": "array",
147
+ "items": {"type": "string"},
148
+ "description": "List of column names to profile (optional, profiles all columns if not specified)",
149
+ },
150
+ },
151
+ "required": ["model"],
152
+ },
153
+ ),
154
+ ]
155
+
156
+ @self.server.call_tool()
157
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
158
+ """Handle tool calls"""
159
+ try:
160
+ if name == "get_lineage_diff":
161
+ result = await self._get_lineage_diff(arguments)
162
+ elif name == "row_count_diff":
163
+ result = await self._row_count_diff(arguments)
164
+ elif name == "query":
165
+ result = await self._query(arguments)
166
+ elif name == "query_diff":
167
+ result = await self._query_diff(arguments)
168
+ elif name == "profile_diff":
169
+ result = await self._profile_diff(arguments)
170
+ else:
171
+ raise ValueError(f"Unknown tool: {name}")
172
+
173
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
174
+ except Exception as e:
175
+ logger.exception(f"Error executing tool {name}")
176
+ return [TextContent(type="text", text=json.dumps({"error": str(e)}, indent=2))]
177
+
178
+ async def _get_lineage_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
179
+ """Get lineage diff between base and current"""
180
+ try:
181
+ # Get lineage diff from adapter (returns a Pydantic LineageDiff model)
182
+ lineage_diff = self.context.get_lineage_diff()
183
+ # Convert Pydantic model to dict for JSON serialization
184
+ return lineage_diff.model_dump(mode="json")
185
+ except Exception:
186
+ logger.exception("Error getting lineage diff")
187
+ raise
188
+
189
+ async def _row_count_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
190
+ """Execute row count diff task"""
191
+ try:
192
+ task = RowCountDiffTask(params=arguments)
193
+
194
+ # Execute task synchronously (it's already sync)
195
+ result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
196
+
197
+ return result
198
+ except Exception:
199
+ logger.exception("Error executing row count diff")
200
+ raise
201
+
202
+ async def _query(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
203
+ """Execute a query"""
204
+ try:
205
+ sql_template = arguments.get("sql_template")
206
+ is_base = arguments.get("base", False)
207
+
208
+ params = {"sql_template": sql_template}
209
+ task = QueryTask(params=params)
210
+ task.is_base = is_base
211
+
212
+ # Execute task
213
+ result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
214
+
215
+ # Convert to dict if it's a model
216
+ if hasattr(result, "model_dump"):
217
+ return result.model_dump(mode="json")
218
+ return result
219
+ except Exception:
220
+ logger.exception("Error executing query")
221
+ raise
222
+
223
+ async def _query_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
224
+ """Execute query diff task"""
225
+ try:
226
+ task = QueryDiffTask(params=arguments)
227
+
228
+ # Execute task
229
+ result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
230
+
231
+ # Convert to dict if it's a model
232
+ if hasattr(result, "model_dump"):
233
+ return result.model_dump(mode="json")
234
+ return result
235
+ except Exception:
236
+ logger.exception("Error executing query diff")
237
+ raise
238
+
239
+ async def _profile_diff(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
240
+ """Execute profile diff task"""
241
+ try:
242
+ task = ProfileDiffTask(params=arguments)
243
+
244
+ # Execute task
245
+ result = await asyncio.get_event_loop().run_in_executor(None, task.execute)
246
+
247
+ # Convert to dict if it's a model
248
+ if hasattr(result, "model_dump"):
249
+ return result.model_dump(mode="json")
250
+ return result
251
+ except Exception:
252
+ logger.exception("Error executing profile diff")
253
+ raise
254
+
255
+ async def run(self):
256
+ """Run the MCP server"""
257
+ async with stdio_server() as (read_stream, write_stream):
258
+ await self.server.run(read_stream, write_stream, self.server.create_initialization_options())
259
+
260
+
261
+ async def run_mcp_server(**kwargs):
262
+ """
263
+ Entry point for running the MCP server
264
+
265
+ Args:
266
+ **kwargs: Arguments for loading RecceContext (dbt options, etc.)
267
+ """
268
+ # Setup logging
269
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
270
+
271
+ # Load Recce context
272
+ context = load_context(**kwargs)
273
+
274
+ # Create and run server
275
+ server = RecceMCPServer(context)
276
+ await server.run()
recce/state/const.py CHANGED
@@ -21,6 +21,6 @@ RECCE_CLOUD_PASSWORD_MISSING = ErrorMessage(
21
21
  )
22
22
 
23
23
  RECCE_API_TOKEN_MISSING = ErrorMessage(
24
- error_message="No Recc API token is provided",
24
+ error_message="No Recce API token is provided",
25
25
  hint_message="Please login to Recce Cloud and copy the API token from the settings page",
26
26
  )
@@ -8,9 +8,7 @@ from recce.exceptions import RecceException
8
8
  from recce.pull_request import fetch_pr_metadata
9
9
 
10
10
  from ..util.io import SupportedFileTypes, file_io_factory
11
- from .const import (
12
- RECCE_CLOUD_TOKEN_MISSING,
13
- )
11
+ from .const import RECCE_API_TOKEN_MISSING
14
12
  from .state import RecceState
15
13
 
16
14
  logger = logging.getLogger("uvicorn")
@@ -55,7 +53,7 @@ class RecceStateLoader(ABC):
55
53
  self.catalog = "preview"
56
54
  self.share_id = self.cloud_options.get("share_id")
57
55
  else:
58
- raise RecceException(RECCE_CLOUD_TOKEN_MISSING.error_message)
56
+ raise RecceException(RECCE_API_TOKEN_MISSING.error_message)
59
57
 
60
58
  @property
61
59
  def token(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: recce-nightly
3
- Version: 1.23.0.20251029
3
+ Version: 1.23.0.20251031a0
4
4
  Summary: Environment diff tool for dbt
5
5
  Home-page: https://github.com/InfuseAI/recce
6
6
  Author: InfuseAI Dev Team
@@ -39,8 +39,11 @@ Requires-Dist: python-multipart
39
39
  Requires-Dist: GitPython
40
40
  Requires-Dist: PyGithub
41
41
  Requires-Dist: sqlglot
42
+ Provides-Extra: mcp
43
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
42
44
  Provides-Extra: dev
43
45
  Requires-Dist: pytest>=4.6; extra == "dev"
46
+ Requires-Dist: pytest-asyncio; extra == "dev"
44
47
  Requires-Dist: pytest-flake8; extra == "dev"
45
48
  Requires-Dist: black>=25.1.0; extra == "dev"
46
49
  Requires-Dist: isort>=6.0.1; extra == "dev"
@@ -1,7 +1,7 @@
1
- recce/VERSION,sha256=c46n2tbAF9VpZ7z1Mky2wXxGSSi6GfZ3DJFMd7rA_qY,16
1
+ recce/VERSION,sha256=KfJWiXeB8a7A5SLsRmYGbk8qKvTWEGgq2T3t6UwIJS4,18
2
2
  recce/__init__.py,sha256=gT5G7sJ78C1GHAWr4pna6UvVNxPKs29_EgFS_ia3DT4,2554
3
3
  recce/artifact.py,sha256=BdNW5eQQqEKu5cRzA_XusKMIN73V7-MTQP0VxnLdd9E,9265
4
- recce/cli.py,sha256=ws50nwIzWvYcLNPDfcI6N69k5DaaIONKxloqa-5UdE8,52989
4
+ recce/cli.py,sha256=ZhORH6w30u-xBFlYXe1nTvEkt3zEqOPcnZlG9whtX4Q,56601
5
5
  recce/config.py,sha256=noWimlOdGHAtBhNoApK5gRTQMwNPmUGAmZu39MV7szo,4697
6
6
  recce/connect_to_cloud.py,sha256=CT7ELcVCTNa9ukSp_gMrJ0Av2UCUpF9H03tIk9y7U-4,4768
7
7
  recce/core.py,sha256=Fph2kodFqzXy3cob80M9-nPVcv-Xxzme-E-bKFJ4cBQ,10754
@@ -9,6 +9,7 @@ recce/diff.py,sha256=L2_bzQ3__PO-0aeir8PHF8FvSOUmQ8WcDXgML1-mHdY,748
9
9
  recce/exceptions.py,sha256=SclQ678GrHGjw7p_ZFJ3vZaL_yMU5xABeIAm2u_W2bk,592
10
10
  recce/git.py,sha256=8Eg-6NzL-KjA3rT-ibbAyaCwGlzV0JqH3yGikrJNMDA,2344
11
11
  recce/github.py,sha256=vIwHTpB8YWEUcHQlW0nToUUKmGGO0NcmB_Q5RvxQLLE,7438
12
+ recce/mcp_server.py,sha256=NjR1q4pMzph0Ps1pQlFX2Go83ml1JQD4ilP_4mLH7QI,11461
12
13
  recce/pull_request.py,sha256=dLHsdiMtL4NC6Vkwp8Mq4baJmnqbm19lfiDti9MREN0,3850
13
14
  recce/run.py,sha256=ctp-K2FmrsGrS4xEZKDMbMtitNw2QKJ6Tq_0OvCWaaM,14080
14
15
  recce/server.py,sha256=FURnuOtooA4ezglEouV_-J-y2ECrNjd6vvSiROnm7Nc,22851
@@ -23,11 +24,13 @@ recce/apis/check_api.py,sha256=KMCXSMl1qqzx2jQgRqCrD4j_cY3EHBbM3H2-t-6saAU,6227
23
24
  recce/apis/check_func.py,sha256=gktbCcyk3WGvWRJJ-wDnwv7NrIny2nTHWLl1-kdiVRo,4183
24
25
  recce/apis/run_api.py,sha256=eOaxOxXDkH59uqGCd4blld7edavUx7JU_DCd2WAYrL8,3416
25
26
  recce/apis/run_func.py,sha256=6wC8TDU-h7TLr2VZH7HNsWaUVlQ9HBN5N_dwqfi4lMY,7440
26
- recce/data/404.html,sha256=Uf2QEM3obZQFhZi7F7OYjmPlXbQTBVsn8D9L_srTeJw,59085
27
+ recce/data/404.html,sha256=54wgc42EETu-yl0H_ed7Z9n6DGzOuZwFDma8epFw7QQ,59085
27
28
  recce/data/auth_callback.html,sha256=H-XfdlAFiw5VU2RpKBVQbwh1AIqJrPHrFA0S09nNJZA,94779
28
29
  recce/data/favicon.ico,sha256=B2mBumUOnzvUrXrqNkrc5QfdDXjzEXRcWkWur0fJ6sM,2565
29
- recce/data/index.html,sha256=wAMuaWDbPpy7XFe8iI5mp6_evy9Qx6ugqhuo5aQi_AM,77170
30
- recce/data/index.txt,sha256=8RLgnQrk6dzlyqYqg-Cu4X3giA3z6c_llJbnrma-soU,6406
30
+ recce/data/index.html,sha256=xiP2UJABklQHBxDO-oRjrWyb4K4nPpYfFjyTnanbzBM,77170
31
+ recce/data/index.txt,sha256=ZSdcpMDQqb6u6iRM5xF1z1phKZaH2ZMNmTMw4Bs2UK8,6406
32
+ recce/data/_next/static/8naFZUkIPYaSgMEmYrK_j/_buildManifest.js,sha256=oqgeL8q3W6T625zlDuGLFDWY3H4_3tlJCNWvj42kYcs,544
33
+ recce/data/_next/static/8naFZUkIPYaSgMEmYrK_j/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
31
34
  recce/data/_next/static/chunks/12f8fac4-a2fd98f42126999f.js,sha256=ILGeY9xCblLTFgnG9zdwGqe5jxxulBBz761KM3HRxMc,152792
32
35
  recce/data/_next/static/chunks/272-058426d7d18ce061.js,sha256=-MHfq8GlLLjSveELfok_YIHlMtuBkYY-hCsJc-EyQOA,10714
33
36
  recce/data/_next/static/chunks/3a92ee20-9c4519f55ecaa314.js,sha256=3pmoD3iJD1ZSIoY-v_9ZsGqXu-ILHdy5FiKF3vvaN2s,177908
@@ -68,8 +71,6 @@ recce/data/_next/static/css/5da8812af0008c76.css,sha256=1JLGRCtK9HwSjFvlp-iimWYY
68
71
  recce/data/_next/static/css/8edca58d4abcf908.css,sha256=Ab-wk7RhsXgiX5fkJsrCJ-EVvrS1hZtOFpI0uZQSgL0,8093
69
72
  recce/data/_next/static/css/abdb9814a3dd18bb.css,sha256=Do5x9FJaOIe-pSBlbk_PP7PQAYFbDXTTl_0PWY4cnBM,1327
70
73
  recce/data/_next/static/css/af2813f12c8a4fd8.css,sha256=sLhHpBJpH5zsmd5h13LU922e3dTlJWkPzZ5R67f8CzM,12260
71
- recce/data/_next/static/fCULljmCJ7BP8dZt7mK5Y/_buildManifest.js,sha256=oqgeL8q3W6T625zlDuGLFDWY3H4_3tlJCNWvj42kYcs,544
72
- recce/data/_next/static/fCULljmCJ7BP8dZt7mK5Y/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
73
74
  recce/data/_next/static/media/montserrat-cyrillic-800-normal.728ecd06.woff,sha256=XiXifLCW_CfSR4eNCIxDzi3Cg59f6L34ARTHrw-hoDA,10924
74
75
  recce/data/_next/static/media/montserrat-cyrillic-800-normal.ffc85bb1.woff2,sha256=6XlujeGVTucujV3iPSa7j1d9S5A6uuH-EduJBCeVFu8,11180
75
76
  recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.622429c0.woff2,sha256=9hQT0vu2HmOb21Pk5Ht7bydZU7QIsY7rCyAPtm1VxM8,12144
@@ -87,7 +88,7 @@ recce/data/imgs/feedback/thumbs-up.png,sha256=VF3BH8bmYEqcSsMDJO57xMqW4t6crCXUXa
87
88
  recce/data/logo/recce-logo-white.png,sha256=y3re8iEucJnMUkAkRS2CjWHTlOydyvgWdWjuQKcXDbk,46923
88
89
  recce/event/CONFIG,sha256=w8_AVcNu_JF-t8lNmjOqtsXbeOawMzpEhkISaMlm-iU,48
89
90
  recce/event/SENTRY_DNS,sha256=nWXZevLC4qDScvNtjs329X13oxqvtFI_Kiu6cfCDOBA,83
90
- recce/event/__init__.py,sha256=_pML9lfsPglECelccMzieVycTFo-NITtOceXXKl_VC4,8823
91
+ recce/event/__init__.py,sha256=UKo0U0OZTkWsr7QBnCwXOHu7XtY0dhYPRTXuCsX8uIw,8847
91
92
  recce/event/collector.py,sha256=6Vvcz9zejNNMytZ_K0HFV_kN6-S5B-SUOglBrlSPBo8,5884
92
93
  recce/event/track.py,sha256=3wBzpB8l2gtnDzjh-wJatmLeBbtnP0q-BLQZQVQ23sM,5310
93
94
  recce/models/__init__.py,sha256=F7cgALtdWnwv37R0eEgKZ_yBsMwxWnUfo3jAZ3u6qyU,209
@@ -96,10 +97,10 @@ recce/models/run.py,sha256=QK2gvOWvko9YYhd2NLs3BPt5l4MSCZGwpzTAiqx9zJw,1161
96
97
  recce/models/types.py,sha256=9ReOyIv3rUD3_cIQfB9Rplb0L2YQuZr4kS9Zai30nB8,5234
97
98
  recce/state/__init__.py,sha256=V1FRPTQJUz-uwI3Cn8wDa5Z9bueVs86MR_1Ti4RGfPc,650
98
99
  recce/state/cloud.py,sha256=hCLvRgMwQwRxlcQhU33L8gXq6yoytBdAIwgWIWPTeLc,25489
99
- recce/state/const.py,sha256=Me3uVQHi_uZysl3tfpmghPInuKF236X6LfMKVvJlCgA,827
100
+ recce/state/const.py,sha256=5XwGWEv4RhGucgl1RHWg_67gVY7fV0-jsbZPpfnOCTs,828
100
101
  recce/state/local.py,sha256=bZIkl7cAfyYaGgYEr3uD_JLrtwlHBnu8_o1Qz2djQzw,1920
101
102
  recce/state/state.py,sha256=D3wBd8VLtrYEqAb0ueNxzjF84OB4vFDNJL3a1fPhziQ,4177
102
- recce/state/state_loader.py,sha256=_4vzZ3XiIMdC7G2m2WUhGi_IIoJLzsE7KwTVUeebMc0,5979
103
+ recce/state/state_loader.py,sha256=IiGaFDm2y_0NMsv_YUZhSuDTsvWmdMrf9SXIv9w5sKE,5966
103
104
  recce/tasks/__init__.py,sha256=b553AtDHjYROgmMePv_Hv_X3fjh4iEn11gzzpUJz6_o,610
104
105
  recce/tasks/core.py,sha256=JFYa1CfgOiRPQ7KVTwMuxJjhMB-pvCwB-xezVt-h3RU,4080
105
106
  recce/tasks/dataframe.py,sha256=03UBWwt0DFTXlaEOtnV5i_mxdRKD7UbRayewEL2Ub48,3650
@@ -125,14 +126,16 @@ recce/util/pydantic_model.py,sha256=KumKuyCjbTzEMsKLE4-b-eZfp0gLhYDdmVtw1-hxiJw,
125
126
  recce/util/recce_cloud.py,sha256=fM4At7-90dS_TXIAxYNjWdEOW1OUbjTOqYmwWt89eEw,17171
126
127
  recce/util/singleton.py,sha256=1cU99I0f9tjuMQLMJyLsK1oK3fZJMsO5-TbRHAMXqds,627
127
128
  recce/yaml/__init__.py,sha256=PAym5akbtL24Ag7VR7EW8SS2VINNaJU06esbfe-ek-U,1328
128
- recce_nightly-1.23.0.20251029.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
129
+ recce_nightly-1.23.0.20251031a0.dist-info/licenses/LICENSE,sha256=CQjjMy9aYPhfe8xG_bcpIfKtNkdxLZ5IOb8oPygtUhY,11343
129
130
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
130
- tests/test_cli.py,sha256=CFtP4eJrtcHwpzoEgJAyveQcqxAmDQuWXPrLc_Iny7Q,6626
131
+ tests/test_cli.py,sha256=hT-KxF5PcVtNZ05HGsTrjec6WTDaD6vInCijJcCYiU8,9923
132
+ tests/test_cli_mcp_optional.py,sha256=w6Iwr22fheaOxw6vGXVZyvi0pw5uh8jDJKcdnd64-_o,1629
131
133
  tests/test_cloud_listing_cli.py,sha256=77As9n0Sngdh53dfHkShmqDETMTOohlqw1xXO9cIago,14208
132
134
  tests/test_config.py,sha256=ODDFe_XF6gphmSmmc422dGLBaCCmG-IjDzTkD5SJsJE,1557
133
135
  tests/test_connect_to_cloud.py,sha256=b2fgV8L1iQBdEwh6RumMsIIyYg7GtMOMRz1dvE3WRPg,3059
134
136
  tests/test_core.py,sha256=WgCFm8Au3YQI-V5UbZ6LA8irMNHRc7NWutIq2Mny0LE,6242
135
137
  tests/test_dbt.py,sha256=VzXvdoJNwwEmKNhJJDNB1N_qZYwp1aoJQ1sLcoyRBmk,1316
138
+ tests/test_mcp_server.py,sha256=k7FxIgHNQF-DWrFJ6eLVi3uyhlDz3AO5VHCEwY56v1s,7709
136
139
  tests/test_pull_request.py,sha256=HmZo5MoDaoKSgPwbLxJ3Ur3ajZ7IxhkzJxaOmhg6bwE,3562
137
140
  tests/test_server.py,sha256=sq03awzG-kvLNJwHaZGDvVjd2uAs502XUKZ2nLNwvRQ,3391
138
141
  tests/test_summary.py,sha256=D0WvAkdO-vzGcvholH2rfS1wTxUXjVHwWm59fWy45eA,2876
@@ -154,8 +157,8 @@ tests/tasks/test_row_count.py,sha256=21PaP2aq-x8-pqwzWHRT1sixhQ8g3CQNRWOZTTmbK0s
154
157
  tests/tasks/test_schema.py,sha256=7ds4Vx8ixaiIWDR49Lvjem4xlPkRP1cXazDRY3roUak,3121
155
158
  tests/tasks/test_top_k.py,sha256=YR_GS__DJsbDlQVaEEdJvNQ3fh1VmV5Nb3G7lb0r6YM,1779
156
159
  tests/tasks/test_valuediff.py,sha256=_xQJGgxsXoy2NYk_Z6Hsw2FlVh6zk2nN_iUueyRN1e8,2046
157
- recce_nightly-1.23.0.20251029.dist-info/METADATA,sha256=Ff3-DNP7eKwIViok1VaavWqYid0PiCqvvbEL1p4P9v0,9471
158
- recce_nightly-1.23.0.20251029.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
159
- recce_nightly-1.23.0.20251029.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
160
- recce_nightly-1.23.0.20251029.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
161
- recce_nightly-1.23.0.20251029.dist-info/RECORD,,
160
+ recce_nightly-1.23.0.20251031a0.dist-info/METADATA,sha256=2IJFCGLp0u13Z5c2UNvRzBcNmXuvDcMLlpW-IybdZGE,9581
161
+ recce_nightly-1.23.0.20251031a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
162
+ recce_nightly-1.23.0.20251031a0.dist-info/entry_points.txt,sha256=oqoY_IiwIqXbgrIsPnlqUqao2eiIeP2dprowkOlmeyg,40
163
+ recce_nightly-1.23.0.20251031a0.dist-info/top_level.txt,sha256=6PKGVpf75idP0C6KEaldDzzZUauIxNu1ZDstau1pI4I,12
164
+ recce_nightly-1.23.0.20251031a0.dist-info/RECORD,,
tests/test_cli.py CHANGED
@@ -67,6 +67,82 @@ class TestCommandServer(TestCase):
67
67
  mock_state_loader_class.assert_called_once()
68
68
  mock_run.assert_called_once()
69
69
 
70
+ @patch.object(RecceContext, "verify_required_artifacts")
71
+ @patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
72
+ @patch("recce.cli.uvicorn.run")
73
+ @patch("recce.cli.CloudStateLoader")
74
+ @patch("recce.cli.prepare_api_token", return_value="test_api_token")
75
+ def test_cmd_server_with_session_id(
76
+ self,
77
+ mock_prepare_api_token,
78
+ mock_state_loader_class,
79
+ mock_run,
80
+ mock_get_recce_cloud_onboarding_state,
81
+ mock_verify_required_artifacts,
82
+ ):
83
+ """Test that --session-id automatically enables cloud and review mode"""
84
+ mock_state_loader = MagicMock(spec=CloudStateLoader)
85
+ mock_state_loader.verify.return_value = True
86
+ mock_state_loader.review_mode = True
87
+ mock_get_recce_cloud_onboarding_state.return_value = "completed"
88
+ mock_verify_required_artifacts.return_value = True, None
89
+
90
+ mock_state_loader_class.return_value = mock_state_loader
91
+
92
+ # Test with --session-id (should automatically enable cloud and review)
93
+ result = self.runner.invoke(cli_command_server, ["--session-id", "test-session-123", "--single-env"])
94
+
95
+ # Should succeed
96
+ assert result.exit_code == 0
97
+
98
+ # Should create CloudStateLoader with session_id in cloud_options
99
+ mock_state_loader_class.assert_called_once()
100
+ call_args = mock_state_loader_class.call_args
101
+ assert call_args.kwargs["review_mode"] is True
102
+ assert "session_id" in call_args.kwargs["cloud_options"]
103
+ assert call_args.kwargs["cloud_options"]["session_id"] == "test-session-123"
104
+
105
+ mock_run.assert_called_once()
106
+
107
+ @patch.object(RecceContext, "verify_required_artifacts")
108
+ @patch("recce.util.recce_cloud.get_recce_cloud_onboarding_state")
109
+ @patch("recce.cli.uvicorn.run")
110
+ @patch("recce.cli.CloudStateLoader")
111
+ @patch("recce.cli.prepare_api_token", return_value="test_api_token")
112
+ def test_cmd_server_with_share_url(
113
+ self,
114
+ mock_prepare_api_token,
115
+ mock_state_loader_class,
116
+ mock_run,
117
+ mock_get_recce_cloud_onboarding_state,
118
+ mock_verify_required_artifacts,
119
+ ):
120
+ """Test that --share-url automatically enables cloud and review mode"""
121
+ mock_state_loader = MagicMock(spec=CloudStateLoader)
122
+ mock_state_loader.verify.return_value = True
123
+ mock_state_loader.review_mode = True
124
+ mock_get_recce_cloud_onboarding_state.return_value = "completed"
125
+ mock_verify_required_artifacts.return_value = True, None
126
+
127
+ mock_state_loader_class.return_value = mock_state_loader
128
+
129
+ # Test with --share-url (should automatically enable cloud and review)
130
+ result = self.runner.invoke(
131
+ cli_command_server, ["--share-url", "https://cloud.recce.io/share/abc123", "--single-env"]
132
+ )
133
+
134
+ # Should succeed
135
+ assert result.exit_code == 0
136
+
137
+ # Should create CloudStateLoader with share_id in cloud_options
138
+ mock_state_loader_class.assert_called_once()
139
+ call_args = mock_state_loader_class.call_args
140
+ assert call_args.kwargs["review_mode"] is True
141
+ assert "share_id" in call_args.kwargs["cloud_options"]
142
+ assert call_args.kwargs["cloud_options"]["share_id"] == "abc123"
143
+
144
+ mock_run.assert_called_once()
145
+
70
146
  @patch.object(RecceContext, "verify_required_artifacts")
71
147
  @patch("os.path.isdir", side_effect=lambda path: True if path == "existed_folder" else False)
72
148
  @patch("recce.cli.uvicorn.run")
@@ -0,0 +1,45 @@
1
+ """
2
+ Test that CLI can be imported even when mcp is not available.
3
+ """
4
+
5
+ import sys
6
+ from unittest.mock import patch
7
+
8
+
9
+ def test_cli_can_be_imported_without_mcp():
10
+ """Test that recce.cli can be imported even if mcp package is not available"""
11
+ # This test verifies that the CLI module doesn't fail to import
12
+ # when mcp is not installed, since mcp is an optional dependency
13
+ from recce import cli
14
+
15
+ assert cli is not None
16
+ assert hasattr(cli, "cli")
17
+ assert hasattr(cli, "mcp_server")
18
+
19
+
20
+ def test_mcp_server_command_fails_gracefully_without_mcp():
21
+ """Test that mcp-server command shows helpful error when mcp is not available"""
22
+ # Mock sys.modules to simulate mcp not being installed
23
+ with patch.dict(sys.modules, {"mcp": None, "mcp.server": None, "mcp.server.stdio": None, "mcp.types": None}):
24
+ # Remove mcp_server from modules to force reimport
25
+ if "recce.mcp_server" in sys.modules:
26
+ del sys.modules["recce.mcp_server"]
27
+
28
+ from recce.cli import mcp_server
29
+
30
+ # The function should exist
31
+ assert mcp_server is not None
32
+
33
+ # When called, it should handle ImportError gracefully
34
+ # (We can't easily test the actual execution without more mocking,
35
+ # but we've verified the function exists and can be imported)
36
+
37
+
38
+ def test_cli_command_exists():
39
+ """Test that both server and mcp-server commands are registered"""
40
+ from recce.cli import cli
41
+
42
+ # Check that both commands exist
43
+ commands = {cmd.name for cmd in cli.commands.values()}
44
+ assert "server" in commands
45
+ assert "mcp_server" in commands or "mcp-server" in commands