quickcall-integrations 0.1.0__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/__init__.py +6 -0
- mcp_server/config.py +10 -0
- mcp_server/server.py +42 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/git_tools.py +194 -0
- mcp_server/tools/utility_tools.py +115 -0
- quickcall_integrations-0.1.0.dist-info/METADATA +63 -0
- quickcall_integrations-0.1.0.dist-info/RECORD +10 -0
- quickcall_integrations-0.1.0.dist-info/WHEEL +4 -0
- quickcall_integrations-0.1.0.dist-info/entry_points.txt +2 -0
mcp_server/__init__.py
ADDED
mcp_server/config.py
ADDED
mcp_server/server.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuickCall Integrations MCP Server
|
|
3
|
+
|
|
4
|
+
Git tools for developers - view commits, diffs, and changes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
from mcp_server.tools.git_tools import create_git_tools
|
|
12
|
+
from mcp_server.tools.utility_tools import create_utility_tools
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_server() -> FastMCP:
|
|
16
|
+
"""Create and configure the MCP server."""
|
|
17
|
+
mcp = FastMCP("quickcall-integrations")
|
|
18
|
+
|
|
19
|
+
create_git_tools(mcp)
|
|
20
|
+
create_utility_tools(mcp)
|
|
21
|
+
|
|
22
|
+
return mcp
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
mcp = create_server()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
"""Entry point for the CLI."""
|
|
30
|
+
transport = os.getenv("MCP_TRANSPORT", "stdio")
|
|
31
|
+
|
|
32
|
+
if transport == "stdio":
|
|
33
|
+
mcp.run(transport="stdio")
|
|
34
|
+
else:
|
|
35
|
+
host = os.getenv("MCP_HOST", "0.0.0.0")
|
|
36
|
+
port = int(os.getenv("MCP_PORT", "8001"))
|
|
37
|
+
print(f"Starting server: http://{host}:{port}/mcp (transport: {transport})")
|
|
38
|
+
mcp.run(transport=transport, host=host, port=port)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tools for external integrations"""
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Tools - Simple tools for viewing repository changes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.exceptions import ToolError
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_git(args: List[str], cwd: Optional[str] = None) -> str:
|
|
14
|
+
"""Run a git command and return output."""
|
|
15
|
+
try:
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
["git"] + args,
|
|
18
|
+
cwd=cwd,
|
|
19
|
+
capture_output=True,
|
|
20
|
+
text=True,
|
|
21
|
+
timeout=30,
|
|
22
|
+
)
|
|
23
|
+
if result.returncode != 0:
|
|
24
|
+
raise ToolError(f"Git error: {result.stderr.strip()}")
|
|
25
|
+
return result.stdout.strip()
|
|
26
|
+
except subprocess.TimeoutExpired:
|
|
27
|
+
raise ToolError("Git command timed out")
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
raise ToolError("Git not found")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_repo_info(cwd: Optional[str] = None) -> dict:
|
|
33
|
+
"""Get repository info."""
|
|
34
|
+
try:
|
|
35
|
+
_run_git(["rev-parse", "--git-dir"], cwd)
|
|
36
|
+
except ToolError:
|
|
37
|
+
raise ToolError(f"Not a git repository: {cwd or 'current directory'}")
|
|
38
|
+
|
|
39
|
+
repo_root = _run_git(["rev-parse", "--show-toplevel"], cwd)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
remote_url = _run_git(["remote", "get-url", "origin"], cwd)
|
|
43
|
+
if "github.com" in remote_url:
|
|
44
|
+
if remote_url.startswith("git@"):
|
|
45
|
+
path = remote_url.split(":")[-1]
|
|
46
|
+
else:
|
|
47
|
+
path = remote_url.split("github.com/")[-1]
|
|
48
|
+
path = path.rstrip(".git")
|
|
49
|
+
parts = path.split("/")
|
|
50
|
+
owner, repo = (parts[0], parts[1]) if len(parts) >= 2 else (None, None)
|
|
51
|
+
else:
|
|
52
|
+
owner, repo = None, None
|
|
53
|
+
except ToolError:
|
|
54
|
+
owner, repo = None, None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd)
|
|
58
|
+
except ToolError:
|
|
59
|
+
branch = "unknown"
|
|
60
|
+
|
|
61
|
+
return {"root": repo_root, "owner": owner, "repo": repo, "branch": branch}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_git_tools(mcp: FastMCP) -> None:
|
|
65
|
+
"""Add git tools to the MCP server."""
|
|
66
|
+
|
|
67
|
+
@mcp.tool(tags={"git", "updates"})
|
|
68
|
+
def get_updates(
|
|
69
|
+
path: str = Field(
|
|
70
|
+
...,
|
|
71
|
+
description="Path to git repository. Use the user's current working directory.",
|
|
72
|
+
),
|
|
73
|
+
days: int = Field(
|
|
74
|
+
default=7,
|
|
75
|
+
description="Number of days to look back (default: 7)",
|
|
76
|
+
),
|
|
77
|
+
author: Optional[str] = Field(
|
|
78
|
+
default=None,
|
|
79
|
+
description="Filter by author name/email",
|
|
80
|
+
),
|
|
81
|
+
) -> dict:
|
|
82
|
+
"""
|
|
83
|
+
Get updates from a git repository.
|
|
84
|
+
|
|
85
|
+
Returns commits, diff stats, file changes, and actual code diff for the given period.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
repo_info = _get_repo_info(path)
|
|
89
|
+
repo_name = f"{repo_info['owner']}/{repo_info['repo']}" if repo_info['owner'] else repo_info['root']
|
|
90
|
+
|
|
91
|
+
result = {
|
|
92
|
+
"repository": repo_name,
|
|
93
|
+
"branch": repo_info['branch'],
|
|
94
|
+
"period": f"Last {days} days",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Get commits
|
|
98
|
+
since_date = f"{days} days ago"
|
|
99
|
+
log_format = "--pretty=format:%H|%an|%ad|%s"
|
|
100
|
+
log_args = ["log", log_format, "--date=short", f"--since={since_date}"]
|
|
101
|
+
if author:
|
|
102
|
+
log_args.extend(["--author", author])
|
|
103
|
+
|
|
104
|
+
log_output = _run_git(log_args, path)
|
|
105
|
+
|
|
106
|
+
commits = []
|
|
107
|
+
for line in log_output.split("\n"):
|
|
108
|
+
if not line:
|
|
109
|
+
continue
|
|
110
|
+
parts = line.split("|", 3)
|
|
111
|
+
if len(parts) >= 4:
|
|
112
|
+
commits.append({
|
|
113
|
+
"sha": parts[0][:7],
|
|
114
|
+
"author": parts[1],
|
|
115
|
+
"date": parts[2],
|
|
116
|
+
"message": parts[3],
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
result["commits"] = commits
|
|
120
|
+
result["commit_count"] = len(commits)
|
|
121
|
+
|
|
122
|
+
if not commits:
|
|
123
|
+
result["diff"] = {"files_changed": 0, "additions": 0, "deletions": 0, "patch": ""}
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
# Get total diff between oldest and newest commit
|
|
127
|
+
oldest_sha = commits[-1]["sha"]
|
|
128
|
+
newest_sha = commits[0]["sha"]
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Get stats
|
|
132
|
+
numstat = _run_git(["diff", "--numstat", f"{oldest_sha}^", newest_sha], path)
|
|
133
|
+
|
|
134
|
+
files = []
|
|
135
|
+
total_add = 0
|
|
136
|
+
total_del = 0
|
|
137
|
+
|
|
138
|
+
for line in numstat.split("\n"):
|
|
139
|
+
if not line:
|
|
140
|
+
continue
|
|
141
|
+
parts = line.split("\t")
|
|
142
|
+
if len(parts) >= 3:
|
|
143
|
+
adds = int(parts[0]) if parts[0] != "-" else 0
|
|
144
|
+
dels = int(parts[1]) if parts[1] != "-" else 0
|
|
145
|
+
files.append({
|
|
146
|
+
"file": parts[2],
|
|
147
|
+
"additions": adds,
|
|
148
|
+
"deletions": dels,
|
|
149
|
+
})
|
|
150
|
+
total_add += adds
|
|
151
|
+
total_del += dels
|
|
152
|
+
|
|
153
|
+
# Get actual diff patch
|
|
154
|
+
diff_patch = _run_git(["diff", f"{oldest_sha}^", newest_sha], path)
|
|
155
|
+
|
|
156
|
+
# Truncate if too large
|
|
157
|
+
if len(diff_patch) > 50000:
|
|
158
|
+
diff_patch = diff_patch[:50000] + "\n\n... (truncated, diff too large)"
|
|
159
|
+
|
|
160
|
+
result["diff"] = {
|
|
161
|
+
"files_changed": len(files),
|
|
162
|
+
"additions": total_add,
|
|
163
|
+
"deletions": total_del,
|
|
164
|
+
"files": files[:30],
|
|
165
|
+
"patch": diff_patch,
|
|
166
|
+
}
|
|
167
|
+
except ToolError:
|
|
168
|
+
result["diff"] = {"files_changed": 0, "additions": 0, "deletions": 0, "patch": ""}
|
|
169
|
+
|
|
170
|
+
# Uncommitted changes
|
|
171
|
+
staged = _run_git(["diff", "--cached", "--name-only"], path)
|
|
172
|
+
unstaged = _run_git(["diff", "--name-only"], path)
|
|
173
|
+
|
|
174
|
+
staged_list = [f for f in staged.split("\n") if f]
|
|
175
|
+
unstaged_list = [f for f in unstaged.split("\n") if f]
|
|
176
|
+
|
|
177
|
+
if staged_list or unstaged_list:
|
|
178
|
+
# Get uncommitted diff patch too
|
|
179
|
+
uncommitted_patch = _run_git(["diff", "HEAD"], path)
|
|
180
|
+
if len(uncommitted_patch) > 20000:
|
|
181
|
+
uncommitted_patch = uncommitted_patch[:20000] + "\n\n... (truncated)"
|
|
182
|
+
|
|
183
|
+
result["uncommitted"] = {
|
|
184
|
+
"staged": staged_list,
|
|
185
|
+
"unstaged": unstaged_list,
|
|
186
|
+
"patch": uncommitted_patch,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
except ToolError:
|
|
192
|
+
raise
|
|
193
|
+
except Exception as e:
|
|
194
|
+
raise ToolError(f"Failed to get updates: {str(e)}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility tools for common operations.
|
|
3
|
+
|
|
4
|
+
Provides datetime helpers useful for constructing queries:
|
|
5
|
+
- Get current datetime
|
|
6
|
+
- Calculate date ranges (e.g., "last 7 days")
|
|
7
|
+
- Add/subtract time from dates
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timezone, timedelta
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_utility_tools(mcp: FastMCP) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Add utility tools to the MCP server.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
mcp: FastMCP instance to add tools to
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@mcp.tool(tags={"utility", "datetime"})
|
|
26
|
+
def get_current_datetime(
|
|
27
|
+
format: str = Field(
|
|
28
|
+
default="iso",
|
|
29
|
+
description="Output format: 'iso' for ISO 8601, 'unix' for Unix timestamp",
|
|
30
|
+
),
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Get the current date and time in UTC.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Current datetime in the specified format
|
|
37
|
+
"""
|
|
38
|
+
now = datetime.now(timezone.utc)
|
|
39
|
+
|
|
40
|
+
if format == "unix":
|
|
41
|
+
return {
|
|
42
|
+
"datetime": int(now.timestamp()),
|
|
43
|
+
"format": "Unix timestamp",
|
|
44
|
+
}
|
|
45
|
+
else:
|
|
46
|
+
return {
|
|
47
|
+
"datetime": now.isoformat().replace("+00:00", "Z"),
|
|
48
|
+
"format": "ISO 8601",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@mcp.tool(tags={"utility", "datetime"})
|
|
52
|
+
def calculate_date_range(
|
|
53
|
+
days_ago: int = Field(
|
|
54
|
+
...,
|
|
55
|
+
description="Days ago to start. Use 7 for 'last week', 1 for 'yesterday', 0 for 'today'.",
|
|
56
|
+
),
|
|
57
|
+
) -> dict:
|
|
58
|
+
"""
|
|
59
|
+
Calculate a date range from N days ago until now.
|
|
60
|
+
|
|
61
|
+
Use this to get the 'since' parameter for list_commits.
|
|
62
|
+
|
|
63
|
+
Common mappings:
|
|
64
|
+
- "last week" = days_ago=7
|
|
65
|
+
- "yesterday" = days_ago=1
|
|
66
|
+
- "today" = days_ago=0
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary with 'since' ISO datetime string
|
|
70
|
+
"""
|
|
71
|
+
now = datetime.now(timezone.utc)
|
|
72
|
+
|
|
73
|
+
# Calculate start date (N days ago at midnight UTC)
|
|
74
|
+
start = now - timedelta(days=days_ago)
|
|
75
|
+
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"since": start.isoformat().replace("+00:00", "Z"),
|
|
79
|
+
"now": now.isoformat().replace("+00:00", "Z"),
|
|
80
|
+
"days_ago": days_ago,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@mcp.tool(tags={"utility", "datetime"})
|
|
84
|
+
def calculate_date_offset(
|
|
85
|
+
days: int = Field(
|
|
86
|
+
default=0,
|
|
87
|
+
description="Number of days to add (negative to subtract)",
|
|
88
|
+
),
|
|
89
|
+
hours: int = Field(
|
|
90
|
+
default=0,
|
|
91
|
+
description="Number of hours to add (negative to subtract)",
|
|
92
|
+
),
|
|
93
|
+
base_date: Optional[str] = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="Base date in ISO format. If not provided, uses current time.",
|
|
96
|
+
),
|
|
97
|
+
) -> dict:
|
|
98
|
+
"""
|
|
99
|
+
Calculate a new date by adding/subtracting time.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
New datetime after applying the offset
|
|
103
|
+
"""
|
|
104
|
+
if base_date:
|
|
105
|
+
base = datetime.fromisoformat(base_date.replace("Z", "+00:00"))
|
|
106
|
+
else:
|
|
107
|
+
base = datetime.now(timezone.utc)
|
|
108
|
+
|
|
109
|
+
result = base + timedelta(days=days, hours=hours)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"datetime": result.isoformat().replace("+00:00", "Z"),
|
|
113
|
+
"base_date": base.isoformat().replace("+00:00", "Z"),
|
|
114
|
+
"offset": f"{days} days, {hours} hours",
|
|
115
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quickcall-integrations
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server with developer integrations for Claude Code and Cursor
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: fastmcp>=2.13.0
|
|
7
|
+
Requires-Dist: pydantic>=2.11.7
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# QuickCall Integrations
|
|
11
|
+
|
|
12
|
+
Developer integrations for Claude Code and Cursor.
|
|
13
|
+
|
|
14
|
+
**Current integrations:**
|
|
15
|
+
- Git - commits, diffs, code changes
|
|
16
|
+
|
|
17
|
+
**Coming soon:**
|
|
18
|
+
- Calendar
|
|
19
|
+
- Slack
|
|
20
|
+
- GitHub PRs & Issues
|
|
21
|
+
|
|
22
|
+
## Quick Install
|
|
23
|
+
|
|
24
|
+
Add to Claude Code:
|
|
25
|
+
```bash
|
|
26
|
+
claude mcp add quickcall -- uvx quickcall-integrations
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or add to your `.mcp.json`:
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"quickcall": {
|
|
34
|
+
"command": "uvx",
|
|
35
|
+
"args": ["quickcall-integrations"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Just ask Claude:
|
|
44
|
+
- "What did I work on today?"
|
|
45
|
+
- "Show me recent commits"
|
|
46
|
+
- "What's changed in the last week?"
|
|
47
|
+
|
|
48
|
+
Or use the plugin command: `/quickcall:daily-updates`
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Clone and install
|
|
54
|
+
git clone https://github.com/quickcall-dev/quickcall-integrations
|
|
55
|
+
cd quickcall-integrations
|
|
56
|
+
uv pip install -e .
|
|
57
|
+
|
|
58
|
+
# Run locally
|
|
59
|
+
quickcall-integrations
|
|
60
|
+
|
|
61
|
+
# Run with SSE (for remote deployment)
|
|
62
|
+
MCP_TRANSPORT=sse quickcall-integrations
|
|
63
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mcp_server/__init__.py,sha256=wAVZ0eHQoGovs-66UH9-kRkcv37bprVEUeinyUFS_KI,98
|
|
2
|
+
mcp_server/config.py,sha256=ZvvON2pHa7eYvGjP6-SMdm-pkr3-umvtLCtZcV4WnVQ,212
|
|
3
|
+
mcp_server/server.py,sha256=EWrhqcAKrbauscgnaa7kwh7l6ReflLl2Quv0GDiWtgI,945
|
|
4
|
+
mcp_server/tools/__init__.py,sha256=Vqy1qzTxdt90tWFN9ZLEhTCX8aIHFB8TmMR_z70qLtE,42
|
|
5
|
+
mcp_server/tools/git_tools.py,sha256=cj7Y70c0PTIEFGW_XqBAsoA4V2x12IykmPyEq3cceR8,6686
|
|
6
|
+
mcp_server/tools/utility_tools.py,sha256=1WiOpJivu6Ug9OLajm77lzsmFfBPgWHs8e1hNCEX_Aw,3359
|
|
7
|
+
quickcall_integrations-0.1.0.dist-info/METADATA,sha256=Yal1egxxsPVR0A4xekzOeHMsPSCBpR_2fg4V0AKc5Dw,1189
|
|
8
|
+
quickcall_integrations-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
quickcall_integrations-0.1.0.dist-info/entry_points.txt,sha256=kkcunmJUzncYvQ1rOR35V2LPm2HcFTKzdI2l3n7NwiM,66
|
|
10
|
+
quickcall_integrations-0.1.0.dist-info/RECORD,,
|