quickcall-integrations 0.1.0__tar.gz
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.
- quickcall_integrations-0.1.0/.claude/settings.local.json +7 -0
- quickcall_integrations-0.1.0/.github/workflows/deploy-mcp.yml +96 -0
- quickcall_integrations-0.1.0/.gitignore +101 -0
- quickcall_integrations-0.1.0/Dockerfile +23 -0
- quickcall_integrations-0.1.0/PKG-INFO +63 -0
- quickcall_integrations-0.1.0/README.md +54 -0
- quickcall_integrations-0.1.0/mcp_server/__init__.py +6 -0
- quickcall_integrations-0.1.0/mcp_server/config.py +10 -0
- quickcall_integrations-0.1.0/mcp_server/server.py +42 -0
- quickcall_integrations-0.1.0/mcp_server/tools/__init__.py +1 -0
- quickcall_integrations-0.1.0/mcp_server/tools/git_tools.py +194 -0
- quickcall_integrations-0.1.0/mcp_server/tools/utility_tools.py +115 -0
- quickcall_integrations-0.1.0/plugins/claude/.claude-plugin/plugin.json +8 -0
- quickcall_integrations-0.1.0/plugins/claude/.mcp.json +8 -0
- quickcall_integrations-0.1.0/plugins/claude/commands/daily-updates.md +25 -0
- quickcall_integrations-0.1.0/pyproject.toml +20 -0
- quickcall_integrations-0.1.0/requirements.txt +3 -0
- quickcall_integrations-0.1.0/tests/README.md +80 -0
- quickcall_integrations-0.1.0/tests/test_tools.py +144 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
name: Deploy MCP Server
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
MCP_IMAGE: quickcall-mcp-oss
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-deploy-mcp:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
packages: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout code
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Log in to Azure Container Registry
|
|
24
|
+
uses: docker/login-action@v3
|
|
25
|
+
with:
|
|
26
|
+
registry: ${{ secrets.AZURE_CONTAINER_REGISTRY }}
|
|
27
|
+
username: ${{ secrets.AZURE_ACR_USERNAME }}
|
|
28
|
+
password: ${{ secrets.AZURE_ACR_PASSWORD }}
|
|
29
|
+
|
|
30
|
+
- name: Set up Docker Buildx
|
|
31
|
+
uses: docker/setup-buildx-action@v3
|
|
32
|
+
|
|
33
|
+
- name: Build and push MCP image
|
|
34
|
+
uses: docker/build-push-action@v5
|
|
35
|
+
with:
|
|
36
|
+
context: .
|
|
37
|
+
file: ./Dockerfile
|
|
38
|
+
push: true
|
|
39
|
+
tags: |
|
|
40
|
+
${{ secrets.AZURE_CONTAINER_REGISTRY }}/${{ env.MCP_IMAGE }}:latest
|
|
41
|
+
${{ secrets.AZURE_CONTAINER_REGISTRY }}/${{ env.MCP_IMAGE }}:${{ github.sha }}
|
|
42
|
+
cache-from: type=registry,ref=${{ secrets.AZURE_CONTAINER_REGISTRY }}/${{ env.MCP_IMAGE }}:buildcache
|
|
43
|
+
cache-to: type=registry,ref=${{ secrets.AZURE_CONTAINER_REGISTRY }}/${{ env.MCP_IMAGE }}:buildcache,mode=max
|
|
44
|
+
|
|
45
|
+
- name: Azure Login
|
|
46
|
+
uses: azure/login@v1
|
|
47
|
+
with:
|
|
48
|
+
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
|
49
|
+
|
|
50
|
+
- name: Configure Container Registry
|
|
51
|
+
run: |
|
|
52
|
+
az containerapp registry set \
|
|
53
|
+
--name quickcall-mcp-oss \
|
|
54
|
+
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
|
55
|
+
--server ${{ secrets.AZURE_CONTAINER_REGISTRY }} \
|
|
56
|
+
--username ${{ secrets.AZURE_ACR_USERNAME }} \
|
|
57
|
+
--password ${{ secrets.AZURE_ACR_PASSWORD }}
|
|
58
|
+
|
|
59
|
+
- name: Deploy MCP to Azure Container Apps
|
|
60
|
+
env:
|
|
61
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
62
|
+
run: |
|
|
63
|
+
az containerapp update \
|
|
64
|
+
--name quickcall-mcp-oss \
|
|
65
|
+
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
|
66
|
+
--image ${{ secrets.AZURE_CONTAINER_REGISTRY }}/${{ env.MCP_IMAGE }}:${{ github.sha }} \
|
|
67
|
+
--min-replicas 1 \
|
|
68
|
+
--max-replicas 5 \
|
|
69
|
+
--set-env-vars \
|
|
70
|
+
MCP_TRANSPORT=streamable-http \
|
|
71
|
+
MCP_HOST=0.0.0.0 \
|
|
72
|
+
MCP_PORT=8001 \
|
|
73
|
+
OPENAI_API_KEY="$OPENAI_API_KEY"
|
|
74
|
+
|
|
75
|
+
- name: Configure External Ingress
|
|
76
|
+
run: |
|
|
77
|
+
az containerapp ingress enable \
|
|
78
|
+
--name quickcall-mcp-oss \
|
|
79
|
+
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
|
80
|
+
--type external \
|
|
81
|
+
--target-port 8001 \
|
|
82
|
+
--transport http
|
|
83
|
+
|
|
84
|
+
- name: Get App URL
|
|
85
|
+
run: |
|
|
86
|
+
APP_URL=$(az containerapp show \
|
|
87
|
+
--name quickcall-mcp-oss \
|
|
88
|
+
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
|
89
|
+
--query properties.configuration.ingress.fqdn -o tsv)
|
|
90
|
+
|
|
91
|
+
echo "🚀 MCP Server deployed at: https://$APP_URL"
|
|
92
|
+
echo "📊 Health check: https://$APP_URL/health"
|
|
93
|
+
echo "🔧 MCP endpoint: https://$APP_URL/mcp"
|
|
94
|
+
|
|
95
|
+
- name: Azure Logout
|
|
96
|
+
run: az logout
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Secrets and environment
|
|
2
|
+
secrets/
|
|
3
|
+
*.env
|
|
4
|
+
*.env.*
|
|
5
|
+
*.pem
|
|
6
|
+
mcp-inspector-config.json
|
|
7
|
+
|
|
8
|
+
# Byte-compiled / optimized / DLL files
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[codz]
|
|
11
|
+
*$py.class
|
|
12
|
+
|
|
13
|
+
# C extensions
|
|
14
|
+
*.so
|
|
15
|
+
|
|
16
|
+
# Distribution / packaging
|
|
17
|
+
.Python
|
|
18
|
+
build/
|
|
19
|
+
develop-eggs/
|
|
20
|
+
dist/
|
|
21
|
+
downloads/
|
|
22
|
+
eggs/
|
|
23
|
+
.eggs/
|
|
24
|
+
lib/
|
|
25
|
+
lib64/
|
|
26
|
+
parts/
|
|
27
|
+
sdist/
|
|
28
|
+
var/
|
|
29
|
+
wheels/
|
|
30
|
+
share/python-wheels/
|
|
31
|
+
*.egg-info/
|
|
32
|
+
.installed.cfg
|
|
33
|
+
*.egg
|
|
34
|
+
MANIFEST
|
|
35
|
+
|
|
36
|
+
# PyInstaller
|
|
37
|
+
*.manifest
|
|
38
|
+
*.spec
|
|
39
|
+
|
|
40
|
+
# Installer logs
|
|
41
|
+
pip-log.txt
|
|
42
|
+
pip-delete-this-directory.txt
|
|
43
|
+
|
|
44
|
+
# Unit test / coverage reports
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.nox/
|
|
48
|
+
.coverage
|
|
49
|
+
.coverage.*
|
|
50
|
+
.cache
|
|
51
|
+
nosetests.xml
|
|
52
|
+
coverage.xml
|
|
53
|
+
*.cover
|
|
54
|
+
*.py.cover
|
|
55
|
+
.hypothesis/
|
|
56
|
+
.pytest_cache/
|
|
57
|
+
cover/
|
|
58
|
+
|
|
59
|
+
# Translations
|
|
60
|
+
*.mo
|
|
61
|
+
*.pot
|
|
62
|
+
|
|
63
|
+
# Logs
|
|
64
|
+
*.log
|
|
65
|
+
|
|
66
|
+
# Environments
|
|
67
|
+
.env
|
|
68
|
+
.envrc
|
|
69
|
+
.venv
|
|
70
|
+
env/
|
|
71
|
+
venv/
|
|
72
|
+
ENV/
|
|
73
|
+
env.bak/
|
|
74
|
+
venv.bak/
|
|
75
|
+
|
|
76
|
+
# UV
|
|
77
|
+
.uv/
|
|
78
|
+
|
|
79
|
+
# mypy
|
|
80
|
+
.mypy_cache/
|
|
81
|
+
.dmypy.json
|
|
82
|
+
dmypy.json
|
|
83
|
+
|
|
84
|
+
# Pyre type checker
|
|
85
|
+
.pyre/
|
|
86
|
+
|
|
87
|
+
# pytype static type analyzer
|
|
88
|
+
.pytype/
|
|
89
|
+
|
|
90
|
+
# Ruff
|
|
91
|
+
.ruff_cache/
|
|
92
|
+
|
|
93
|
+
# IDE
|
|
94
|
+
.idea/
|
|
95
|
+
.vscode/
|
|
96
|
+
*.swp
|
|
97
|
+
*.swo
|
|
98
|
+
|
|
99
|
+
# OS
|
|
100
|
+
.DS_Store
|
|
101
|
+
Thumbs.db
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install uv
|
|
6
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
7
|
+
|
|
8
|
+
# Copy requirements and install dependencies
|
|
9
|
+
COPY requirements.txt .
|
|
10
|
+
RUN uv pip install --system -r requirements.txt
|
|
11
|
+
|
|
12
|
+
# Copy application code
|
|
13
|
+
COPY mcp_server/ ./mcp_server/
|
|
14
|
+
|
|
15
|
+
# Set Python path
|
|
16
|
+
ENV PYTHONPATH=/app/mcp_server
|
|
17
|
+
|
|
18
|
+
# Expose MCP port
|
|
19
|
+
EXPOSE 8001
|
|
20
|
+
|
|
21
|
+
# Run MCP server
|
|
22
|
+
WORKDIR /app/mcp_server
|
|
23
|
+
CMD ["python", "server.py"]
|
|
@@ -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,54 @@
|
|
|
1
|
+
# QuickCall Integrations
|
|
2
|
+
|
|
3
|
+
Developer integrations for Claude Code and Cursor.
|
|
4
|
+
|
|
5
|
+
**Current integrations:**
|
|
6
|
+
- Git - commits, diffs, code changes
|
|
7
|
+
|
|
8
|
+
**Coming soon:**
|
|
9
|
+
- Calendar
|
|
10
|
+
- Slack
|
|
11
|
+
- GitHub PRs & Issues
|
|
12
|
+
|
|
13
|
+
## Quick Install
|
|
14
|
+
|
|
15
|
+
Add to Claude Code:
|
|
16
|
+
```bash
|
|
17
|
+
claude mcp add quickcall -- uvx quickcall-integrations
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or add to your `.mcp.json`:
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"quickcall": {
|
|
25
|
+
"command": "uvx",
|
|
26
|
+
"args": ["quickcall-integrations"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Just ask Claude:
|
|
35
|
+
- "What did I work on today?"
|
|
36
|
+
- "Show me recent commits"
|
|
37
|
+
- "What's changed in the last week?"
|
|
38
|
+
|
|
39
|
+
Or use the plugin command: `/quickcall:daily-updates`
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Clone and install
|
|
45
|
+
git clone https://github.com/quickcall-dev/quickcall-integrations
|
|
46
|
+
cd quickcall-integrations
|
|
47
|
+
uv pip install -e .
|
|
48
|
+
|
|
49
|
+
# Run locally
|
|
50
|
+
quickcall-integrations
|
|
51
|
+
|
|
52
|
+
# Run with SSE (for remote deployment)
|
|
53
|
+
MCP_TRANSPORT=sse quickcall-integrations
|
|
54
|
+
```
|
|
@@ -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,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Get git updates for current repository (commits, diffs, changes)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Daily Updates
|
|
6
|
+
|
|
7
|
+
Get the recent git updates for the user's current working directory.
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
1. Use the `get_updates` tool from the quickcall MCP server
|
|
12
|
+
2. Pass the current working directory as the `path` parameter
|
|
13
|
+
3. Default to 1 day of history (or use the number the user specifies)
|
|
14
|
+
4. Summarize the changes in a clear format:
|
|
15
|
+
- Number of commits
|
|
16
|
+
- Authors who contributed
|
|
17
|
+
- Key changes made (based on commit messages and diff)
|
|
18
|
+
- Any uncommitted changes
|
|
19
|
+
|
|
20
|
+
## Output Format
|
|
21
|
+
|
|
22
|
+
Provide a concise summary like:
|
|
23
|
+
- "3 commits today by Alice and Bob"
|
|
24
|
+
- "Main changes: Added auth flow, fixed login bug"
|
|
25
|
+
- "You have 2 uncommitted files"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "quickcall-integrations"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server with developer integrations for Claude Code and Cursor"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastmcp>=2.13.0",
|
|
9
|
+
"pydantic>=2.11.7",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
quickcall-integrations = "mcp_server.server:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["mcp_server"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Testing QuickCall Integrations MCP Server
|
|
2
|
+
|
|
3
|
+
## Quick Test
|
|
4
|
+
|
|
5
|
+
### 1. Start the server
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd src && python server.py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### 2. Run the test
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python tests/test_tools.py
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Using with Claude Code
|
|
18
|
+
|
|
19
|
+
### Add to Claude Code config
|
|
20
|
+
|
|
21
|
+
Add to `~/.claude/settings.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"quickcall": {
|
|
27
|
+
"command": "python",
|
|
28
|
+
"args": ["/path/to/quickcall-mcp-server/src/server.py"],
|
|
29
|
+
"env": {
|
|
30
|
+
"MCP_TRANSPORT": "stdio"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or for HTTP mode (run server separately):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"quickcall": {
|
|
43
|
+
"url": "http://localhost:8001/mcp"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Test in Claude Code
|
|
50
|
+
|
|
51
|
+
Once configured, try these prompts:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
"What have I been working on today?"
|
|
55
|
+
"Show me my uncommitted changes"
|
|
56
|
+
"Summarize my work this week"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Using MCP Inspector
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx @modelcontextprotocol/inspector
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then connect to `http://localhost:8001/mcp` and test the tools:
|
|
66
|
+
- `get_updates` - Shows uncommitted + committed changes
|
|
67
|
+
- `get_diff` - Shows detailed diffs
|
|
68
|
+
- `summarize_updates` - AI summary (needs OPENAI_API_KEY)
|
|
69
|
+
|
|
70
|
+
## Environment Variables
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Optional - for AI summaries
|
|
74
|
+
export OPENAI_API_KEY=your_key
|
|
75
|
+
|
|
76
|
+
# Server config (optional)
|
|
77
|
+
export MCP_HOST=0.0.0.0
|
|
78
|
+
export MCP_PORT=8001
|
|
79
|
+
export MCP_TRANSPORT=streamable-http # or "stdio" for Claude Code
|
|
80
|
+
```
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test QuickCall Integrations MCP Server.
|
|
4
|
+
|
|
5
|
+
Tests all git tools by connecting to the running server.
|
|
6
|
+
No authentication required - just uses local git commands.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
1. Start the server: cd mcp_server && python server.py
|
|
10
|
+
2. Run this test: python tests/test_tools.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import httpx
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
# MCP server endpoint
|
|
22
|
+
MCP_URL = "http://localhost:8001"
|
|
23
|
+
|
|
24
|
+
# Test repository path (this repo)
|
|
25
|
+
TEST_REPO_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_server():
|
|
29
|
+
"""Check if server is running."""
|
|
30
|
+
try:
|
|
31
|
+
response = httpx.get(f"{MCP_URL}/mcp", timeout=2)
|
|
32
|
+
# SSE endpoint returns 200 even without proper SSE headers
|
|
33
|
+
console.print(f"[green]✅ Server is running at {MCP_URL}[/green]")
|
|
34
|
+
return True
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
console.print("[red]❌ Server is not running![/red]")
|
|
39
|
+
console.print(" Start it with: cd mcp_server && python server.py")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def test_with_fastmcp():
|
|
44
|
+
"""Test using FastMCP client."""
|
|
45
|
+
from fastmcp import Client
|
|
46
|
+
|
|
47
|
+
console.print("\n[bold]Testing with FastMCP Client[/bold]\n")
|
|
48
|
+
|
|
49
|
+
client = Client(f"{MCP_URL}/mcp")
|
|
50
|
+
|
|
51
|
+
async with client:
|
|
52
|
+
console.print("[green]✅ Connected to MCP server[/green]\n")
|
|
53
|
+
|
|
54
|
+
# List available tools
|
|
55
|
+
console.print("[bold]Available Tools:[/bold]")
|
|
56
|
+
tools = await client.list_tools()
|
|
57
|
+
for tool in tools:
|
|
58
|
+
console.print(f" - {tool.name}: {tool.description[:60]}...")
|
|
59
|
+
|
|
60
|
+
console.print("\n" + "=" * 60 + "\n")
|
|
61
|
+
|
|
62
|
+
# Test 1: get_updates
|
|
63
|
+
console.print("[bold cyan]Test 1: get_updates[/bold cyan]")
|
|
64
|
+
console.print(f"Testing on repo: {TEST_REPO_PATH}\n")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
result = await client.call_tool("get_updates", {
|
|
68
|
+
"path": TEST_REPO_PATH,
|
|
69
|
+
"days": 7
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if isinstance(result, list) and len(result) > 0:
|
|
73
|
+
data = json.loads(result[0].text)
|
|
74
|
+
console.print(f"[green]✅ Repository: {data['repository']}[/green]")
|
|
75
|
+
console.print(f" Branch: {data['branch']}")
|
|
76
|
+
console.print(f" Period: {data['period']}")
|
|
77
|
+
console.print(f" Commits: {data['commit_count']}")
|
|
78
|
+
|
|
79
|
+
if data.get('diff'):
|
|
80
|
+
diff = data['diff']
|
|
81
|
+
console.print(f" Files changed: {diff['files_changed']}")
|
|
82
|
+
console.print(f" Additions: +{diff['additions']}")
|
|
83
|
+
console.print(f" Deletions: -{diff['deletions']}")
|
|
84
|
+
|
|
85
|
+
if diff.get('patch'):
|
|
86
|
+
lines = diff['patch'].split('\n')[:10]
|
|
87
|
+
console.print("\n Diff preview:")
|
|
88
|
+
for line in lines:
|
|
89
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
90
|
+
console.print(f" [green]{line[:80]}[/green]")
|
|
91
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
92
|
+
console.print(f" [red]{line[:80]}[/red]")
|
|
93
|
+
else:
|
|
94
|
+
console.print(f" {line[:80]}")
|
|
95
|
+
|
|
96
|
+
if data.get('uncommitted'):
|
|
97
|
+
uncommitted = data['uncommitted']
|
|
98
|
+
console.print(f"\n Uncommitted - Staged: {len(uncommitted.get('staged', []))}")
|
|
99
|
+
console.print(f" Uncommitted - Unstaged: {len(uncommitted.get('unstaged', []))}")
|
|
100
|
+
|
|
101
|
+
if data.get('commits'):
|
|
102
|
+
console.print("\n Recent commits:")
|
|
103
|
+
for commit in data['commits'][:3]:
|
|
104
|
+
msg = commit['message'][:50] + "..." if len(commit['message']) > 50 else commit['message']
|
|
105
|
+
console.print(f" - {commit['sha']} {msg}")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
109
|
+
|
|
110
|
+
console.print("\n" + "=" * 60 + "\n")
|
|
111
|
+
|
|
112
|
+
# Test 2: Utility tools
|
|
113
|
+
console.print("[bold cyan]Test 2: Utility Tools[/bold cyan]")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
result = await client.call_tool("get_current_datetime", {})
|
|
117
|
+
if isinstance(result, list) and len(result) > 0:
|
|
118
|
+
data = json.loads(result[0].text)
|
|
119
|
+
console.print(f"[green]✅ Current time: {data['datetime']}[/green]")
|
|
120
|
+
|
|
121
|
+
result = await client.call_tool("calculate_date_range", {"days_ago": 7})
|
|
122
|
+
if isinstance(result, list) and len(result) > 0:
|
|
123
|
+
data = json.loads(result[0].text)
|
|
124
|
+
console.print(f"[green]✅ Last 7 days: since {data['since']}[/green]")
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
128
|
+
|
|
129
|
+
console.print("\n" + "=" * 60 + "\n")
|
|
130
|
+
console.print("[green]✅ All tests completed![/green]")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def main():
|
|
134
|
+
"""Run tests."""
|
|
135
|
+
console.print("\n[bold]QuickCall Integrations - MCP Server Test[/bold]\n")
|
|
136
|
+
|
|
137
|
+
if not check_server():
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
await test_with_fastmcp()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
asyncio.run(main())
|