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 ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ MCP Server for QuickCall
3
+ GitHub integration tools for AI assistant
4
+ """
5
+
6
+ __version__ = "0.3.4"
mcp_server/config.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Configuration for QuickCall Integrations MCP Server.
3
+ """
4
+
5
+
6
+ class Config:
7
+ """Configuration class for MCP server."""
8
+
9
+ SERVER_NAME: str = "QuickCall Integrations (OSS)"
10
+ SERVER_VERSION: str = "1.0.0"
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ quickcall-integrations = mcp_server.server:main