quickcall-integrations 0.1.2__py3-none-any.whl → 0.1.4__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.
- mcp_server/api_clients/__init__.py +6 -0
- mcp_server/api_clients/github_client.py +440 -0
- mcp_server/api_clients/slack_client.py +359 -0
- mcp_server/auth/__init__.py +24 -0
- mcp_server/auth/credentials.py +278 -0
- mcp_server/auth/device_flow.py +253 -0
- mcp_server/server.py +54 -2
- mcp_server/tools/__init__.py +12 -0
- mcp_server/tools/auth_tools.py +411 -0
- mcp_server/tools/git_tools.py +58 -20
- mcp_server/tools/github_tools.py +338 -0
- mcp_server/tools/slack_tools.py +203 -0
- quickcall_integrations-0.1.4.dist-info/METADATA +138 -0
- quickcall_integrations-0.1.4.dist-info/RECORD +18 -0
- mcp_server/config.py +0 -10
- quickcall_integrations-0.1.2.dist-info/METADATA +0 -81
- quickcall_integrations-0.1.2.dist-info/RECORD +0 -10
- {quickcall_integrations-0.1.2.dist-info → quickcall_integrations-0.1.4.dist-info}/WHEEL +0 -0
- {quickcall_integrations-0.1.2.dist-info → quickcall_integrations-0.1.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication tools for QuickCall MCP.
|
|
3
|
+
|
|
4
|
+
Provides tools for users to connect, check status, and disconnect
|
|
5
|
+
their QuickCall account from the CLI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
import webbrowser
|
|
11
|
+
from typing import Dict, Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
from mcp_server.auth import (
|
|
17
|
+
get_credential_store,
|
|
18
|
+
is_authenticated,
|
|
19
|
+
DeviceFlowAuth,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# QuickCall API URL - configurable for local dev
|
|
25
|
+
QUICKCALL_API_URL = os.getenv("QUICKCALL_API_URL", "https://api.quickcall.dev")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_auth_tools(mcp: FastMCP):
|
|
29
|
+
"""Register authentication tools with the MCP server."""
|
|
30
|
+
|
|
31
|
+
@mcp.tool(tags={"auth", "quickcall"})
|
|
32
|
+
def connect_quickcall() -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Connect your CLI to QuickCall.
|
|
35
|
+
|
|
36
|
+
This starts the OAuth device flow authentication:
|
|
37
|
+
1. Opens your browser to quickcall.dev
|
|
38
|
+
2. You sign in with Google
|
|
39
|
+
3. Your CLI is linked to your account
|
|
40
|
+
|
|
41
|
+
After connecting, you can use GitHub and Slack tools
|
|
42
|
+
with your configured integrations from quickcall.dev.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Authentication result with status and instructions
|
|
46
|
+
"""
|
|
47
|
+
store = get_credential_store()
|
|
48
|
+
|
|
49
|
+
# Check if already authenticated
|
|
50
|
+
if store.is_authenticated():
|
|
51
|
+
status = store.get_status()
|
|
52
|
+
return {
|
|
53
|
+
"status": "already_connected",
|
|
54
|
+
"message": "You're already connected to QuickCall!",
|
|
55
|
+
"user": status.get("username") or status.get("email"),
|
|
56
|
+
"github_connected": status.get("github", {}).get("connected", False),
|
|
57
|
+
"slack_connected": status.get("slack", {}).get("connected", False),
|
|
58
|
+
"hint": "Use check_quickcall_status for details or disconnect_quickcall to logout.",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Start device flow
|
|
62
|
+
auth = DeviceFlowAuth()
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# Initialize flow
|
|
66
|
+
device_code, user_code, verification_url, expires_in, interval = (
|
|
67
|
+
auth.init_flow()
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Build URL with code
|
|
71
|
+
auth_url = f"{verification_url}?code={user_code}"
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"status": "pending",
|
|
75
|
+
"message": "Authentication started! Complete the following steps:",
|
|
76
|
+
"code": user_code,
|
|
77
|
+
"url": auth_url,
|
|
78
|
+
"instructions": [
|
|
79
|
+
f"1. Open this URL in your browser: {auth_url}",
|
|
80
|
+
"2. Sign in with Google",
|
|
81
|
+
"3. Your CLI will be connected automatically",
|
|
82
|
+
],
|
|
83
|
+
"expires_in_minutes": expires_in // 60,
|
|
84
|
+
"hint": "The browser should open automatically. If not, copy the URL above.",
|
|
85
|
+
"_device_code": device_code,
|
|
86
|
+
"_interval": interval,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to start authentication: {e}")
|
|
91
|
+
return {
|
|
92
|
+
"status": "error",
|
|
93
|
+
"message": f"Failed to start authentication: {e}",
|
|
94
|
+
"hint": "Check your internet connection and try again.",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@mcp.tool(tags={"auth", "quickcall"})
|
|
98
|
+
def check_quickcall_status() -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Check your QuickCall connection status.
|
|
101
|
+
|
|
102
|
+
Shows:
|
|
103
|
+
- Whether you're connected
|
|
104
|
+
- Your account info
|
|
105
|
+
- GitHub connection status
|
|
106
|
+
- Slack connection status
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Current authentication and integration status
|
|
110
|
+
"""
|
|
111
|
+
store = get_credential_store()
|
|
112
|
+
|
|
113
|
+
if not store.is_authenticated():
|
|
114
|
+
return {
|
|
115
|
+
"connected": False,
|
|
116
|
+
"message": "Not connected to QuickCall",
|
|
117
|
+
"hint": "Use connect_quickcall to authenticate.",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
status = store.get_status()
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"connected": True,
|
|
124
|
+
"user": {
|
|
125
|
+
"id": status.get("user_id"),
|
|
126
|
+
"email": status.get("email"),
|
|
127
|
+
"username": status.get("username"),
|
|
128
|
+
},
|
|
129
|
+
"authenticated_at": status.get("authenticated_at"),
|
|
130
|
+
"integrations": {
|
|
131
|
+
"github": {
|
|
132
|
+
"connected": status.get("github", {}).get("connected", False),
|
|
133
|
+
"username": status.get("github", {}).get("username"),
|
|
134
|
+
},
|
|
135
|
+
"slack": {
|
|
136
|
+
"connected": status.get("slack", {}).get("connected", False),
|
|
137
|
+
"team_name": status.get("slack", {}).get("team_name"),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
"credentials_file": status.get("credentials_file"),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@mcp.tool(tags={"auth", "quickcall"})
|
|
144
|
+
def disconnect_quickcall() -> Dict[str, Any]:
|
|
145
|
+
"""
|
|
146
|
+
Disconnect your CLI from QuickCall.
|
|
147
|
+
|
|
148
|
+
This removes your local credentials. You'll need to
|
|
149
|
+
run connect_quickcall again to use GitHub and Slack tools.
|
|
150
|
+
|
|
151
|
+
Note: This doesn't revoke the device from your QuickCall
|
|
152
|
+
account. To fully revoke access, visit quickcall.dev/settings.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Disconnection result
|
|
156
|
+
"""
|
|
157
|
+
store = get_credential_store()
|
|
158
|
+
|
|
159
|
+
if not store.is_authenticated():
|
|
160
|
+
return {
|
|
161
|
+
"status": "not_connected",
|
|
162
|
+
"message": "You're not connected to QuickCall.",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Get user info before clearing
|
|
167
|
+
status = store.get_status()
|
|
168
|
+
user = (
|
|
169
|
+
status.get("username") or status.get("email") or status.get("user_id")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Clear credentials
|
|
173
|
+
store.clear()
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"status": "disconnected",
|
|
177
|
+
"message": f"Disconnected from QuickCall ({user})",
|
|
178
|
+
"hint": "To fully revoke access, visit quickcall.dev/settings. Use connect_quickcall to reconnect.",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"Failed to disconnect: {e}")
|
|
183
|
+
return {
|
|
184
|
+
"status": "error",
|
|
185
|
+
"message": f"Failed to disconnect: {e}",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@mcp.tool(tags={"auth", "quickcall"})
|
|
189
|
+
def complete_quickcall_auth(
|
|
190
|
+
device_code: str, timeout_seconds: int = 300
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Complete QuickCall authentication after browser sign-in.
|
|
194
|
+
|
|
195
|
+
This polls for the authentication result after you've signed
|
|
196
|
+
in via the browser. Usually called automatically after connect_quickcall.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
device_code: The device code from connect_quickcall
|
|
200
|
+
timeout_seconds: How long to wait for authentication (default: 5 minutes)
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Authentication result
|
|
204
|
+
"""
|
|
205
|
+
auth = DeviceFlowAuth()
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
credentials = auth.poll_for_completion(
|
|
209
|
+
device_code=device_code,
|
|
210
|
+
timeout=timeout_seconds,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if credentials:
|
|
214
|
+
return {
|
|
215
|
+
"status": "success",
|
|
216
|
+
"message": "Successfully connected to QuickCall!",
|
|
217
|
+
"user_id": credentials.user_id,
|
|
218
|
+
"email": credentials.email,
|
|
219
|
+
"hint": "You can now use GitHub and Slack tools. Run check_quickcall_status to see your integrations.",
|
|
220
|
+
}
|
|
221
|
+
else:
|
|
222
|
+
return {
|
|
223
|
+
"status": "failed",
|
|
224
|
+
"message": "Authentication failed or timed out.",
|
|
225
|
+
"hint": "Try running connect_quickcall again.",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Authentication error: {e}")
|
|
230
|
+
return {
|
|
231
|
+
"status": "error",
|
|
232
|
+
"message": f"Authentication error: {e}",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@mcp.tool(tags={"auth", "github", "quickcall"})
|
|
236
|
+
def connect_github(open_browser: bool = True) -> Dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
Connect GitHub to your QuickCall account.
|
|
239
|
+
|
|
240
|
+
This opens your browser to install the QuickCall GitHub App.
|
|
241
|
+
After installation, you'll be able to use GitHub tools like
|
|
242
|
+
list_repos, create_issue, etc.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
open_browser: Automatically open the install URL in browser (default: True)
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Install URL and instructions
|
|
249
|
+
"""
|
|
250
|
+
store = get_credential_store()
|
|
251
|
+
|
|
252
|
+
if not store.is_authenticated():
|
|
253
|
+
return {
|
|
254
|
+
"status": "error",
|
|
255
|
+
"message": "Not connected to QuickCall",
|
|
256
|
+
"hint": "Run connect_quickcall first to authenticate.",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
stored = store.get_stored_credentials()
|
|
260
|
+
if not stored:
|
|
261
|
+
return {
|
|
262
|
+
"status": "error",
|
|
263
|
+
"message": "No stored credentials found",
|
|
264
|
+
"hint": "Run connect_quickcall to authenticate.",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
with httpx.Client(timeout=30.0) as client:
|
|
269
|
+
response = client.get(
|
|
270
|
+
f"{QUICKCALL_API_URL}/api/cli/github/install-url",
|
|
271
|
+
headers={"Authorization": f"Bearer {stored.device_token}"},
|
|
272
|
+
)
|
|
273
|
+
response.raise_for_status()
|
|
274
|
+
data = response.json()
|
|
275
|
+
|
|
276
|
+
if data.get("already_connected"):
|
|
277
|
+
return {
|
|
278
|
+
"status": "already_connected",
|
|
279
|
+
"message": f"GitHub is already connected (username: {data.get('username')})",
|
|
280
|
+
"hint": "You can use GitHub tools like list_repos, create_issue, etc.",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
install_url = data.get("install_url")
|
|
284
|
+
|
|
285
|
+
if open_browser and install_url:
|
|
286
|
+
try:
|
|
287
|
+
webbrowser.open(install_url)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.warning(f"Failed to open browser: {e}")
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"status": "pending",
|
|
293
|
+
"message": "Please complete GitHub App installation in your browser.",
|
|
294
|
+
"install_url": install_url,
|
|
295
|
+
"instructions": [
|
|
296
|
+
f"1. Open this URL: {install_url}",
|
|
297
|
+
"2. Select the organization/account to install",
|
|
298
|
+
"3. Choose which repositories to grant access",
|
|
299
|
+
"4. Click 'Install'",
|
|
300
|
+
],
|
|
301
|
+
"hint": "After installation, run check_quickcall_status to verify.",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
except httpx.HTTPStatusError as e:
|
|
305
|
+
if e.response.status_code == 401:
|
|
306
|
+
return {
|
|
307
|
+
"status": "error",
|
|
308
|
+
"message": "Session expired. Please reconnect.",
|
|
309
|
+
"hint": "Run disconnect_quickcall then connect_quickcall again.",
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
"status": "error",
|
|
313
|
+
"message": f"API error: {e.response.status_code}",
|
|
314
|
+
}
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"Failed to get GitHub install URL: {e}")
|
|
317
|
+
return {
|
|
318
|
+
"status": "error",
|
|
319
|
+
"message": f"Failed to connect GitHub: {e}",
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@mcp.tool(tags={"auth", "slack", "quickcall"})
|
|
323
|
+
def connect_slack(open_browser: bool = True) -> Dict[str, Any]:
|
|
324
|
+
"""
|
|
325
|
+
Connect Slack to your QuickCall account.
|
|
326
|
+
|
|
327
|
+
This opens your browser to authorize the QuickCall Slack App.
|
|
328
|
+
After authorization, you'll be able to use Slack tools like
|
|
329
|
+
list_channels, send_message, etc.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
open_browser: Automatically open the install URL in browser (default: True)
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Install URL and instructions
|
|
336
|
+
"""
|
|
337
|
+
store = get_credential_store()
|
|
338
|
+
|
|
339
|
+
if not store.is_authenticated():
|
|
340
|
+
return {
|
|
341
|
+
"status": "error",
|
|
342
|
+
"message": "Not connected to QuickCall",
|
|
343
|
+
"hint": "Run connect_quickcall first to authenticate.",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
stored = store.get_stored_credentials()
|
|
347
|
+
if not stored:
|
|
348
|
+
return {
|
|
349
|
+
"status": "error",
|
|
350
|
+
"message": "No stored credentials found",
|
|
351
|
+
"hint": "Run connect_quickcall to authenticate.",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
with httpx.Client(timeout=30.0) as client:
|
|
356
|
+
response = client.get(
|
|
357
|
+
f"{QUICKCALL_API_URL}/api/cli/slack/install-url",
|
|
358
|
+
headers={"Authorization": f"Bearer {stored.device_token}"},
|
|
359
|
+
)
|
|
360
|
+
response.raise_for_status()
|
|
361
|
+
data = response.json()
|
|
362
|
+
|
|
363
|
+
if data.get("already_connected"):
|
|
364
|
+
return {
|
|
365
|
+
"status": "already_connected",
|
|
366
|
+
"message": f"Slack is already connected (workspace: {data.get('team_name')})",
|
|
367
|
+
"hint": "You can use Slack tools like list_channels, send_message, etc.",
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
install_url = data.get("install_url")
|
|
371
|
+
|
|
372
|
+
if open_browser and install_url:
|
|
373
|
+
try:
|
|
374
|
+
webbrowser.open(install_url)
|
|
375
|
+
except Exception as e:
|
|
376
|
+
logger.warning(f"Failed to open browser: {e}")
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
"status": "pending",
|
|
380
|
+
"message": "Please complete Slack authorization in your browser.",
|
|
381
|
+
"install_url": install_url,
|
|
382
|
+
"instructions": [
|
|
383
|
+
f"1. Open this URL: {install_url}",
|
|
384
|
+
"2. Select the Slack workspace",
|
|
385
|
+
"3. Review permissions and click 'Allow'",
|
|
386
|
+
],
|
|
387
|
+
"hint": "After authorization, run check_quickcall_status to verify.",
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
except httpx.HTTPStatusError as e:
|
|
391
|
+
if e.response.status_code == 401:
|
|
392
|
+
return {
|
|
393
|
+
"status": "error",
|
|
394
|
+
"message": "Session expired. Please reconnect.",
|
|
395
|
+
"hint": "Run disconnect_quickcall then connect_quickcall again.",
|
|
396
|
+
}
|
|
397
|
+
if e.response.status_code == 503:
|
|
398
|
+
return {
|
|
399
|
+
"status": "error",
|
|
400
|
+
"message": "Slack integration is not configured on the server.",
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
"status": "error",
|
|
404
|
+
"message": f"API error: {e.response.status_code}",
|
|
405
|
+
}
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.error(f"Failed to get Slack install URL: {e}")
|
|
408
|
+
return {
|
|
409
|
+
"status": "error",
|
|
410
|
+
"message": f"Failed to connect Slack: {e}",
|
|
411
|
+
}
|
mcp_server/tools/git_tools.py
CHANGED
|
@@ -80,17 +80,35 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
80
80
|
),
|
|
81
81
|
) -> dict:
|
|
82
82
|
"""
|
|
83
|
-
Get updates from a git repository.
|
|
83
|
+
Get updates from a git repository. Returns commits, diff stats, and uncommitted changes.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
FORMAT OUTPUT AS A STANDUP SUMMARY:
|
|
86
|
+
|
|
87
|
+
**Summary:** One sentence of what was accomplished.
|
|
88
|
+
|
|
89
|
+
**What I worked on:**
|
|
90
|
+
- Bullet points of key changes (group related commits)
|
|
91
|
+
- Focus on features/fixes, not individual commits
|
|
92
|
+
- Use past tense action verbs
|
|
93
|
+
|
|
94
|
+
**In Progress:**
|
|
95
|
+
- Any uncommitted changes (what's being worked on now)
|
|
96
|
+
|
|
97
|
+
**Blockers:** Only mention if there are merge conflicts or issues visible.
|
|
98
|
+
|
|
99
|
+
Never display raw JSON to the user.
|
|
86
100
|
"""
|
|
87
101
|
try:
|
|
88
102
|
repo_info = _get_repo_info(path)
|
|
89
|
-
repo_name =
|
|
103
|
+
repo_name = (
|
|
104
|
+
f"{repo_info['owner']}/{repo_info['repo']}"
|
|
105
|
+
if repo_info["owner"]
|
|
106
|
+
else repo_info["root"]
|
|
107
|
+
)
|
|
90
108
|
|
|
91
109
|
result = {
|
|
92
110
|
"repository": repo_name,
|
|
93
|
-
"branch": repo_info[
|
|
111
|
+
"branch": repo_info["branch"],
|
|
94
112
|
"period": f"Last {days} days",
|
|
95
113
|
}
|
|
96
114
|
|
|
@@ -109,18 +127,25 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
109
127
|
continue
|
|
110
128
|
parts = line.split("|", 3)
|
|
111
129
|
if len(parts) >= 4:
|
|
112
|
-
commits.append(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
commits.append(
|
|
131
|
+
{
|
|
132
|
+
"sha": parts[0][:7],
|
|
133
|
+
"author": parts[1],
|
|
134
|
+
"date": parts[2],
|
|
135
|
+
"message": parts[3],
|
|
136
|
+
}
|
|
137
|
+
)
|
|
118
138
|
|
|
119
139
|
result["commits"] = commits
|
|
120
140
|
result["commit_count"] = len(commits)
|
|
121
141
|
|
|
122
142
|
if not commits:
|
|
123
|
-
result["diff"] = {
|
|
143
|
+
result["diff"] = {
|
|
144
|
+
"files_changed": 0,
|
|
145
|
+
"additions": 0,
|
|
146
|
+
"deletions": 0,
|
|
147
|
+
"patch": "",
|
|
148
|
+
}
|
|
124
149
|
return result
|
|
125
150
|
|
|
126
151
|
# Get total diff between oldest and newest commit
|
|
@@ -129,7 +154,9 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
129
154
|
|
|
130
155
|
try:
|
|
131
156
|
# Get stats
|
|
132
|
-
numstat = _run_git(
|
|
157
|
+
numstat = _run_git(
|
|
158
|
+
["diff", "--numstat", f"{oldest_sha}^", newest_sha], path
|
|
159
|
+
)
|
|
133
160
|
|
|
134
161
|
files = []
|
|
135
162
|
total_add = 0
|
|
@@ -142,11 +169,13 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
142
169
|
if len(parts) >= 3:
|
|
143
170
|
adds = int(parts[0]) if parts[0] != "-" else 0
|
|
144
171
|
dels = int(parts[1]) if parts[1] != "-" else 0
|
|
145
|
-
files.append(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
files.append(
|
|
173
|
+
{
|
|
174
|
+
"file": parts[2],
|
|
175
|
+
"additions": adds,
|
|
176
|
+
"deletions": dels,
|
|
177
|
+
}
|
|
178
|
+
)
|
|
150
179
|
total_add += adds
|
|
151
180
|
total_del += dels
|
|
152
181
|
|
|
@@ -155,7 +184,9 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
155
184
|
|
|
156
185
|
# Truncate if too large
|
|
157
186
|
if len(diff_patch) > 50000:
|
|
158
|
-
diff_patch =
|
|
187
|
+
diff_patch = (
|
|
188
|
+
diff_patch[:50000] + "\n\n... (truncated, diff too large)"
|
|
189
|
+
)
|
|
159
190
|
|
|
160
191
|
result["diff"] = {
|
|
161
192
|
"files_changed": len(files),
|
|
@@ -165,7 +196,12 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
165
196
|
"patch": diff_patch,
|
|
166
197
|
}
|
|
167
198
|
except ToolError:
|
|
168
|
-
result["diff"] = {
|
|
199
|
+
result["diff"] = {
|
|
200
|
+
"files_changed": 0,
|
|
201
|
+
"additions": 0,
|
|
202
|
+
"deletions": 0,
|
|
203
|
+
"patch": "",
|
|
204
|
+
}
|
|
169
205
|
|
|
170
206
|
# Uncommitted changes
|
|
171
207
|
staged = _run_git(["diff", "--cached", "--name-only"], path)
|
|
@@ -178,7 +214,9 @@ def create_git_tools(mcp: FastMCP) -> None:
|
|
|
178
214
|
# Get uncommitted diff patch too
|
|
179
215
|
uncommitted_patch = _run_git(["diff", "HEAD"], path)
|
|
180
216
|
if len(uncommitted_patch) > 20000:
|
|
181
|
-
uncommitted_patch =
|
|
217
|
+
uncommitted_patch = (
|
|
218
|
+
uncommitted_patch[:20000] + "\n\n... (truncated)"
|
|
219
|
+
)
|
|
182
220
|
|
|
183
221
|
result["uncommitted"] = {
|
|
184
222
|
"staged": staged_list,
|