quickcall-integrations 0.1.6__tar.gz → 0.1.7__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.6 → quickcall_integrations-0.1.7}/.gitignore +4 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/PKG-INFO +94 -9
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/README.md +92 -8
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/__init__.py +1 -1
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/api_clients/github_client.py +26 -4
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/api_clients/slack_client.py +189 -9
- quickcall_integrations-0.1.7/mcp_server/resources/__init__.py +1 -0
- quickcall_integrations-0.1.7/mcp_server/resources/slack_resources.py +50 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/server.py +4 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/tools/auth_tools.py +87 -3
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/tools/slack_tools.py +159 -3
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/plugins/quickcall/.claude-plugin/plugin.json +1 -1
- quickcall_integrations-0.1.7/plugins/quickcall/commands/connect.md +31 -0
- quickcall_integrations-0.1.7/plugins/quickcall/commands/slack-summary.md +56 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/pyproject.toml +2 -1
- quickcall_integrations-0.1.6/tests/test_auth_flow.py → quickcall_integrations-0.1.7/tests/test_integrations.py +139 -7
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/uv.lock +93 -1
- quickcall_integrations-0.1.6/plugins/quickcall/.mcp.json +0 -8
- quickcall_integrations-0.1.6/plugins/quickcall/commands/connect.md +0 -37
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/.claude-plugin/marketplace.json +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/.github/workflows/publish-pypi.yml +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/Dockerfile +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/assets/logo.png +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/api_clients/__init__.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/auth/__init__.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/auth/credentials.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/auth/device_flow.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/tools/__init__.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/tools/git_tools.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/tools/github_tools.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/tools/utility_tools.py +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/plugins/quickcall/commands/status.md +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/plugins/quickcall/commands/updates.md +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/requirements.txt +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/tests/README.md +0 -0
- {quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/tests/test_tools.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickcall-integrations
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: MCP server with developer integrations for Claude Code and Cursor
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: fastmcp>=2.13.0
|
|
7
7
|
Requires-Dist: httpx>=0.28.0
|
|
8
8
|
Requires-Dist: pydantic>=2.11.7
|
|
9
9
|
Requires-Dist: pygithub>=2.8.1
|
|
10
|
+
Requires-Dist: rapidfuzz>=3.0.0
|
|
10
11
|
Description-Content-Type: text/markdown
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
@@ -34,11 +35,67 @@ Description-Content-Type: text/markdown
|
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
38
|
+
## Capabilities
|
|
39
|
+
|
|
40
|
+
- **Get standup updates** from git history (commits, diffs, stats)
|
|
41
|
+
- **List PRs, commits, branches** from GitHub repos
|
|
42
|
+
- **Send messages to Slack** channels
|
|
43
|
+
- **Check connection status** for all integrations
|
|
44
|
+
|
|
37
45
|
## Integrations
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
| Integration | Features | Auth Required |
|
|
48
|
+
|-------------|----------|---------------|
|
|
49
|
+
| **Git** | Commits, diffs, standup summaries | No |
|
|
50
|
+
| **GitHub** | Repos, PRs, commits, branches | Yes |
|
|
51
|
+
| **Slack** | Read/send messages, threads, channels | Yes |
|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary><strong>Available Tools (22)</strong></summary>
|
|
55
|
+
|
|
56
|
+
### Git
|
|
57
|
+
| Tool | Description |
|
|
58
|
+
|------|-------------|
|
|
59
|
+
| `get_updates` | Get git commits, diff stats, and uncommitted changes |
|
|
60
|
+
|
|
61
|
+
### GitHub
|
|
62
|
+
| Tool | Description |
|
|
63
|
+
|------|-------------|
|
|
64
|
+
| `list_repos` | List accessible repositories |
|
|
65
|
+
| `list_prs` | List pull requests (open/closed/all) |
|
|
66
|
+
| `get_pr` | Get PR details (title, description, files changed) |
|
|
67
|
+
| `list_commits` | List commits with optional filters |
|
|
68
|
+
| `get_commit` | Get commit details (message, stats, files) |
|
|
69
|
+
| `list_branches` | List repository branches |
|
|
70
|
+
| `check_github_connection` | Verify GitHub connection |
|
|
71
|
+
|
|
72
|
+
### Slack
|
|
73
|
+
| Tool | Description |
|
|
74
|
+
|------|-------------|
|
|
75
|
+
| `list_slack_channels` | List channels bot has access to |
|
|
76
|
+
| `send_slack_message` | Send message to a channel |
|
|
77
|
+
| `read_slack_messages` | Read messages from a channel (with date filter) |
|
|
78
|
+
| `read_slack_thread` | Read replies in a thread |
|
|
79
|
+
| `list_slack_users` | List workspace users |
|
|
80
|
+
| `check_slack_connection` | Verify Slack connection |
|
|
81
|
+
|
|
82
|
+
### Auth
|
|
83
|
+
| Tool | Description |
|
|
84
|
+
|------|-------------|
|
|
85
|
+
| `connect_quickcall` | Start device flow authentication |
|
|
86
|
+
| `check_quickcall_status` | Check connection status |
|
|
87
|
+
| `disconnect_quickcall` | Remove local credentials |
|
|
88
|
+
| `connect_github` | Install GitHub App |
|
|
89
|
+
| `connect_slack` | Authorize Slack App |
|
|
90
|
+
|
|
91
|
+
### Utility
|
|
92
|
+
| Tool | Description |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| `get_current_datetime` | Get current UTC datetime |
|
|
95
|
+
| `calculate_date_range` | Calculate date range for queries |
|
|
96
|
+
| `calculate_date_offset` | Add/subtract time from a date |
|
|
97
|
+
|
|
98
|
+
</details>
|
|
42
99
|
|
|
43
100
|
## Install
|
|
44
101
|
|
|
@@ -116,11 +173,39 @@ Credentials are stored locally in `~/.quickcall/credentials.json`.
|
|
|
116
173
|
|
|
117
174
|
### Cursor / Other IDEs
|
|
118
175
|
|
|
119
|
-
Ask the AI naturally
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
176
|
+
Ask the AI naturally - see examples below.
|
|
177
|
+
|
|
178
|
+
## Example Prompts
|
|
179
|
+
|
|
180
|
+
### Git
|
|
181
|
+
```
|
|
182
|
+
What did I work on today?
|
|
183
|
+
Give me a standup summary for the last 3 days
|
|
184
|
+
What changes are uncommitted?
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### GitHub
|
|
188
|
+
```
|
|
189
|
+
List my repos
|
|
190
|
+
Show open PRs on [repo-name]
|
|
191
|
+
What commits were made this week?
|
|
192
|
+
Get details of PR #123
|
|
193
|
+
List branches on [repo-name]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Slack
|
|
197
|
+
```
|
|
198
|
+
Send "Build completed" to #deployments
|
|
199
|
+
What messages were posted in #general today?
|
|
200
|
+
Show me the thread replies for that message
|
|
201
|
+
List channels I have access to
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Combined
|
|
205
|
+
```
|
|
206
|
+
List open PRs on [repo] and send titles to #updates channel
|
|
207
|
+
What did I work on this week? Send summary to #standup
|
|
208
|
+
```
|
|
124
209
|
|
|
125
210
|
## Troubleshooting
|
|
126
211
|
|
|
@@ -23,11 +23,67 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
+
## Capabilities
|
|
27
|
+
|
|
28
|
+
- **Get standup updates** from git history (commits, diffs, stats)
|
|
29
|
+
- **List PRs, commits, branches** from GitHub repos
|
|
30
|
+
- **Send messages to Slack** channels
|
|
31
|
+
- **Check connection status** for all integrations
|
|
32
|
+
|
|
26
33
|
## Integrations
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
| Integration | Features | Auth Required |
|
|
36
|
+
|-------------|----------|---------------|
|
|
37
|
+
| **Git** | Commits, diffs, standup summaries | No |
|
|
38
|
+
| **GitHub** | Repos, PRs, commits, branches | Yes |
|
|
39
|
+
| **Slack** | Read/send messages, threads, channels | Yes |
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary><strong>Available Tools (22)</strong></summary>
|
|
43
|
+
|
|
44
|
+
### Git
|
|
45
|
+
| Tool | Description |
|
|
46
|
+
|------|-------------|
|
|
47
|
+
| `get_updates` | Get git commits, diff stats, and uncommitted changes |
|
|
48
|
+
|
|
49
|
+
### GitHub
|
|
50
|
+
| Tool | Description |
|
|
51
|
+
|------|-------------|
|
|
52
|
+
| `list_repos` | List accessible repositories |
|
|
53
|
+
| `list_prs` | List pull requests (open/closed/all) |
|
|
54
|
+
| `get_pr` | Get PR details (title, description, files changed) |
|
|
55
|
+
| `list_commits` | List commits with optional filters |
|
|
56
|
+
| `get_commit` | Get commit details (message, stats, files) |
|
|
57
|
+
| `list_branches` | List repository branches |
|
|
58
|
+
| `check_github_connection` | Verify GitHub connection |
|
|
59
|
+
|
|
60
|
+
### Slack
|
|
61
|
+
| Tool | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `list_slack_channels` | List channels bot has access to |
|
|
64
|
+
| `send_slack_message` | Send message to a channel |
|
|
65
|
+
| `read_slack_messages` | Read messages from a channel (with date filter) |
|
|
66
|
+
| `read_slack_thread` | Read replies in a thread |
|
|
67
|
+
| `list_slack_users` | List workspace users |
|
|
68
|
+
| `check_slack_connection` | Verify Slack connection |
|
|
69
|
+
|
|
70
|
+
### Auth
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| `connect_quickcall` | Start device flow authentication |
|
|
74
|
+
| `check_quickcall_status` | Check connection status |
|
|
75
|
+
| `disconnect_quickcall` | Remove local credentials |
|
|
76
|
+
| `connect_github` | Install GitHub App |
|
|
77
|
+
| `connect_slack` | Authorize Slack App |
|
|
78
|
+
|
|
79
|
+
### Utility
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| `get_current_datetime` | Get current UTC datetime |
|
|
83
|
+
| `calculate_date_range` | Calculate date range for queries |
|
|
84
|
+
| `calculate_date_offset` | Add/subtract time from a date |
|
|
85
|
+
|
|
86
|
+
</details>
|
|
31
87
|
|
|
32
88
|
## Install
|
|
33
89
|
|
|
@@ -105,11 +161,39 @@ Credentials are stored locally in `~/.quickcall/credentials.json`.
|
|
|
105
161
|
|
|
106
162
|
### Cursor / Other IDEs
|
|
107
163
|
|
|
108
|
-
Ask the AI naturally
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
Ask the AI naturally - see examples below.
|
|
165
|
+
|
|
166
|
+
## Example Prompts
|
|
167
|
+
|
|
168
|
+
### Git
|
|
169
|
+
```
|
|
170
|
+
What did I work on today?
|
|
171
|
+
Give me a standup summary for the last 3 days
|
|
172
|
+
What changes are uncommitted?
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### GitHub
|
|
176
|
+
```
|
|
177
|
+
List my repos
|
|
178
|
+
Show open PRs on [repo-name]
|
|
179
|
+
What commits were made this week?
|
|
180
|
+
Get details of PR #123
|
|
181
|
+
List branches on [repo-name]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Slack
|
|
185
|
+
```
|
|
186
|
+
Send "Build completed" to #deployments
|
|
187
|
+
What messages were posted in #general today?
|
|
188
|
+
Show me the thread replies for that message
|
|
189
|
+
List channels I have access to
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Combined
|
|
193
|
+
```
|
|
194
|
+
List open PRs on [repo] and send titles to #updates channel
|
|
195
|
+
What did I work on this week? Send summary to #standup
|
|
196
|
+
```
|
|
113
197
|
|
|
114
198
|
## Troubleshooting
|
|
115
199
|
|
|
@@ -127,14 +127,36 @@ class GitHubClient:
|
|
|
127
127
|
def health_check(self) -> bool:
|
|
128
128
|
"""Check if GitHub API is accessible with the token."""
|
|
129
129
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
# Use installation/repositories endpoint - works with GitHub App tokens
|
|
131
|
+
with httpx.Client() as client:
|
|
132
|
+
response = client.get(
|
|
133
|
+
"https://api.github.com/installation/repositories",
|
|
134
|
+
headers={
|
|
135
|
+
"Authorization": f"Bearer {self.token}",
|
|
136
|
+
"Accept": "application/vnd.github+json",
|
|
137
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
138
|
+
},
|
|
139
|
+
params={"per_page": 1},
|
|
140
|
+
)
|
|
141
|
+
return response.status_code == 200
|
|
132
142
|
except Exception:
|
|
133
143
|
return False
|
|
134
144
|
|
|
135
145
|
def get_authenticated_user(self) -> str:
|
|
136
|
-
"""
|
|
137
|
-
|
|
146
|
+
"""
|
|
147
|
+
Get the GitHub username associated with this installation.
|
|
148
|
+
|
|
149
|
+
Note: GitHub App installation tokens can't access /user endpoint.
|
|
150
|
+
We return the installation owner instead.
|
|
151
|
+
"""
|
|
152
|
+
# Try to get from first repo's owner
|
|
153
|
+
try:
|
|
154
|
+
repos = self.list_repos(limit=1)
|
|
155
|
+
if repos:
|
|
156
|
+
return repos[0].owner.login
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
return "GitHub App" # Fallback
|
|
138
160
|
|
|
139
161
|
def close(self):
|
|
140
162
|
"""Close GitHub API client."""
|
{quickcall_integrations-0.1.6 → quickcall_integrations-0.1.7}/mcp_server/api_clients/slack_client.py
RENAMED
|
@@ -10,6 +10,7 @@ from typing import List, Optional, Dict, Any
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
from pydantic import BaseModel
|
|
13
|
+
from rapidfuzz import fuzz, process
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -51,6 +52,18 @@ class SlackMessage(BaseModel):
|
|
|
51
52
|
message: Optional[Dict[str, Any]] = None
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
class SlackChannelMessage(BaseModel):
|
|
56
|
+
"""Represents a message from channel history."""
|
|
57
|
+
|
|
58
|
+
ts: str # Message timestamp (used as ID)
|
|
59
|
+
user: Optional[str] = None
|
|
60
|
+
user_name: Optional[str] = None
|
|
61
|
+
text: str
|
|
62
|
+
thread_ts: Optional[str] = None
|
|
63
|
+
reply_count: int = 0
|
|
64
|
+
has_thread: bool = False
|
|
65
|
+
|
|
66
|
+
|
|
54
67
|
# ============================================================================
|
|
55
68
|
# Slack Client
|
|
56
69
|
# ============================================================================
|
|
@@ -62,6 +75,16 @@ class SlackClient:
|
|
|
62
75
|
|
|
63
76
|
Provides simplified interface for Slack operations.
|
|
64
77
|
Uses bot token authentication.
|
|
78
|
+
|
|
79
|
+
Note on Caching:
|
|
80
|
+
This client caches channel list and user mappings to reduce API calls.
|
|
81
|
+
Cache is per-instance and does NOT expire automatically.
|
|
82
|
+
New channels/users won't appear until:
|
|
83
|
+
- MCP server restarts (new session)
|
|
84
|
+
- New SlackClient instance is created
|
|
85
|
+
|
|
86
|
+
TODO: Add TTL-based cache invalidation if this becomes an issue.
|
|
87
|
+
See internal-docs/issues/007-slack-api-caching.md
|
|
65
88
|
"""
|
|
66
89
|
|
|
67
90
|
BASE_URL = "https://slack.com/api"
|
|
@@ -80,6 +103,9 @@ class SlackClient:
|
|
|
80
103
|
"Authorization": f"Bearer {bot_token}",
|
|
81
104
|
"Content-Type": "application/json",
|
|
82
105
|
}
|
|
106
|
+
# Caches (per-instance, cleared on new client)
|
|
107
|
+
self._channel_cache: Optional[List["SlackChannel"]] = None
|
|
108
|
+
self._user_cache: Optional[Dict[str, str]] = None
|
|
83
109
|
|
|
84
110
|
async def _request(
|
|
85
111
|
self,
|
|
@@ -161,7 +187,7 @@ class SlackClient:
|
|
|
161
187
|
# ========================================================================
|
|
162
188
|
|
|
163
189
|
def list_channels(
|
|
164
|
-
self, include_private: bool = True, limit: int = 200
|
|
190
|
+
self, include_private: bool = True, limit: int = 200, use_cache: bool = True
|
|
165
191
|
) -> List[SlackChannel]:
|
|
166
192
|
"""
|
|
167
193
|
List Slack channels the bot has access to.
|
|
@@ -169,18 +195,30 @@ class SlackClient:
|
|
|
169
195
|
Args:
|
|
170
196
|
include_private: Whether to include private channels
|
|
171
197
|
limit: Maximum channels to return
|
|
198
|
+
use_cache: Use cached results if available (default: True)
|
|
172
199
|
|
|
173
200
|
Returns:
|
|
174
201
|
List of channels
|
|
175
202
|
"""
|
|
203
|
+
# Return cached if available
|
|
204
|
+
if use_cache and self._channel_cache is not None:
|
|
205
|
+
return (
|
|
206
|
+
self._channel_cache[:limit]
|
|
207
|
+
if limit < len(self._channel_cache)
|
|
208
|
+
else self._channel_cache
|
|
209
|
+
)
|
|
210
|
+
|
|
176
211
|
types = (
|
|
177
212
|
"public_channel,private_channel" if include_private else "public_channel"
|
|
178
213
|
)
|
|
179
214
|
|
|
215
|
+
# Always fetch 200 to ensure we get all channels for caching
|
|
216
|
+
fetch_limit = 200
|
|
217
|
+
|
|
180
218
|
data = self._request_sync(
|
|
181
219
|
"GET",
|
|
182
220
|
"conversations.list",
|
|
183
|
-
params={"types": types, "limit":
|
|
221
|
+
params={"types": types, "limit": fetch_limit, "exclude_archived": True},
|
|
184
222
|
)
|
|
185
223
|
|
|
186
224
|
channels = []
|
|
@@ -196,11 +234,13 @@ class SlackClient:
|
|
|
196
234
|
)
|
|
197
235
|
)
|
|
198
236
|
|
|
199
|
-
|
|
237
|
+
# Cache the full result
|
|
238
|
+
self._channel_cache = channels
|
|
239
|
+
return channels[:limit] if limit < len(channels) else channels
|
|
200
240
|
|
|
201
241
|
def _resolve_channel(self, channel: Optional[str] = None) -> str:
|
|
202
242
|
"""
|
|
203
|
-
Resolve channel name to channel ID.
|
|
243
|
+
Resolve channel name to channel ID with fuzzy matching.
|
|
204
244
|
|
|
205
245
|
Args:
|
|
206
246
|
channel: Channel name (with or without #) or channel ID
|
|
@@ -213,8 +253,8 @@ class SlackClient:
|
|
|
213
253
|
if not channel:
|
|
214
254
|
raise ValueError("No channel specified and no default channel configured")
|
|
215
255
|
|
|
216
|
-
# If it's already an ID (starts with C), return as-is
|
|
217
|
-
if channel.startswith("C"):
|
|
256
|
+
# If it's already an ID (starts with C or G for private), return as-is
|
|
257
|
+
if channel.startswith("C") or channel.startswith("G"):
|
|
218
258
|
return channel
|
|
219
259
|
|
|
220
260
|
# Strip # prefix if present
|
|
@@ -222,9 +262,27 @@ class SlackClient:
|
|
|
222
262
|
|
|
223
263
|
# Look up channel by name
|
|
224
264
|
channels = self.list_channels()
|
|
225
|
-
for ch in channels
|
|
226
|
-
|
|
227
|
-
|
|
265
|
+
channel_names = {ch.name.lower(): ch for ch in channels}
|
|
266
|
+
|
|
267
|
+
# First try exact match
|
|
268
|
+
if channel_name in channel_names:
|
|
269
|
+
return channel_names[channel_name].id
|
|
270
|
+
|
|
271
|
+
# Use rapidfuzz for fuzzy matching
|
|
272
|
+
# token_sort_ratio handles word reordering (e.g., "dev no sleep" = "no sleep dev")
|
|
273
|
+
match = process.extractOne(
|
|
274
|
+
channel_name,
|
|
275
|
+
list(channel_names.keys()),
|
|
276
|
+
scorer=fuzz.token_sort_ratio,
|
|
277
|
+
score_cutoff=70, # Minimum 70% match
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if match:
|
|
281
|
+
matched_name, score, _ = match
|
|
282
|
+
logger.info(
|
|
283
|
+
f"Fuzzy matched '{channel}' to '{matched_name}' (score: {score})"
|
|
284
|
+
)
|
|
285
|
+
return channel_names[matched_name].id
|
|
228
286
|
|
|
229
287
|
raise ValueError(f"Channel '{channel}' not found or bot is not a member")
|
|
230
288
|
|
|
@@ -304,6 +362,128 @@ class SlackClient:
|
|
|
304
362
|
message=data.get("message"),
|
|
305
363
|
)
|
|
306
364
|
|
|
365
|
+
# ========================================================================
|
|
366
|
+
# Message History
|
|
367
|
+
# ========================================================================
|
|
368
|
+
|
|
369
|
+
def get_channel_messages(
|
|
370
|
+
self,
|
|
371
|
+
channel: str,
|
|
372
|
+
oldest: Optional[str] = None,
|
|
373
|
+
latest: Optional[str] = None,
|
|
374
|
+
limit: int = 100,
|
|
375
|
+
) -> List[SlackChannelMessage]:
|
|
376
|
+
"""
|
|
377
|
+
Get messages from a channel.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
channel: Channel name or ID
|
|
381
|
+
oldest: Unix timestamp - only messages after this time
|
|
382
|
+
latest: Unix timestamp - only messages before this time
|
|
383
|
+
limit: Maximum messages to return (default 100)
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of messages (newest first)
|
|
387
|
+
"""
|
|
388
|
+
channel_id = self._resolve_channel(channel)
|
|
389
|
+
|
|
390
|
+
params = {
|
|
391
|
+
"channel": channel_id,
|
|
392
|
+
"limit": limit,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if oldest:
|
|
396
|
+
params["oldest"] = oldest
|
|
397
|
+
if latest:
|
|
398
|
+
params["latest"] = latest
|
|
399
|
+
|
|
400
|
+
data = self._request_sync("GET", "conversations.history", params=params)
|
|
401
|
+
|
|
402
|
+
# Get user info for resolving names
|
|
403
|
+
user_map = self._get_user_map()
|
|
404
|
+
|
|
405
|
+
messages = []
|
|
406
|
+
for msg in data.get("messages", []):
|
|
407
|
+
# Skip non-message types (joins, leaves, etc.)
|
|
408
|
+
if msg.get("subtype") in ["channel_join", "channel_leave", "bot_add"]:
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
user_id = msg.get("user")
|
|
412
|
+
messages.append(
|
|
413
|
+
SlackChannelMessage(
|
|
414
|
+
ts=msg.get("ts", ""),
|
|
415
|
+
user=user_id,
|
|
416
|
+
user_name=user_map.get(user_id, user_id),
|
|
417
|
+
text=msg.get("text", ""),
|
|
418
|
+
thread_ts=msg.get("thread_ts"),
|
|
419
|
+
reply_count=msg.get("reply_count", 0),
|
|
420
|
+
has_thread=msg.get("reply_count", 0) > 0,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return messages
|
|
425
|
+
|
|
426
|
+
def get_thread_replies(
|
|
427
|
+
self,
|
|
428
|
+
channel: str,
|
|
429
|
+
thread_ts: str,
|
|
430
|
+
limit: int = 100,
|
|
431
|
+
) -> List[SlackChannelMessage]:
|
|
432
|
+
"""
|
|
433
|
+
Get replies in a thread.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
channel: Channel name or ID
|
|
437
|
+
thread_ts: Thread parent message timestamp
|
|
438
|
+
limit: Maximum replies to return
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of replies (includes parent message first)
|
|
442
|
+
"""
|
|
443
|
+
channel_id = self._resolve_channel(channel)
|
|
444
|
+
|
|
445
|
+
params = {
|
|
446
|
+
"channel": channel_id,
|
|
447
|
+
"ts": thread_ts,
|
|
448
|
+
"limit": limit,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
data = self._request_sync("GET", "conversations.replies", params=params)
|
|
452
|
+
|
|
453
|
+
user_map = self._get_user_map()
|
|
454
|
+
|
|
455
|
+
messages = []
|
|
456
|
+
for msg in data.get("messages", []):
|
|
457
|
+
user_id = msg.get("user")
|
|
458
|
+
messages.append(
|
|
459
|
+
SlackChannelMessage(
|
|
460
|
+
ts=msg.get("ts", ""),
|
|
461
|
+
user=user_id,
|
|
462
|
+
user_name=user_map.get(user_id, user_id),
|
|
463
|
+
text=msg.get("text", ""),
|
|
464
|
+
thread_ts=msg.get("thread_ts"),
|
|
465
|
+
reply_count=msg.get("reply_count", 0),
|
|
466
|
+
has_thread=False,
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return messages
|
|
471
|
+
|
|
472
|
+
def _get_user_map(self) -> Dict[str, str]:
|
|
473
|
+
"""Get a mapping of user IDs to display names (cached)."""
|
|
474
|
+
# Return cached if available
|
|
475
|
+
if self._user_cache is not None:
|
|
476
|
+
return self._user_cache
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
users = self.list_users(limit=500, include_bots=True)
|
|
480
|
+
self._user_cache = {
|
|
481
|
+
u.id: u.display_name or u.real_name or u.name for u in users
|
|
482
|
+
}
|
|
483
|
+
return self._user_cache
|
|
484
|
+
except Exception:
|
|
485
|
+
return {}
|
|
486
|
+
|
|
307
487
|
# ========================================================================
|
|
308
488
|
# User Operations
|
|
309
489
|
# ========================================================================
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP Resources for QuickCall."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slack MCP Resources - Exposes Slack data for Claude's context.
|
|
3
|
+
|
|
4
|
+
Resources are automatically available in Claude's context when connected.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from mcp_server.tools.slack_tools import _get_client
|
|
11
|
+
from mcp_server.auth import get_credential_store
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_slack_resources(mcp: FastMCP) -> None:
|
|
17
|
+
"""Add Slack resources to the MCP server."""
|
|
18
|
+
|
|
19
|
+
@mcp.resource("slack://channels")
|
|
20
|
+
def get_slack_channels() -> str:
|
|
21
|
+
"""
|
|
22
|
+
List of Slack channels the bot has access to.
|
|
23
|
+
|
|
24
|
+
Use these channel names when reading/sending messages.
|
|
25
|
+
"""
|
|
26
|
+
store = get_credential_store()
|
|
27
|
+
|
|
28
|
+
if not store.is_authenticated():
|
|
29
|
+
return "Slack not connected. Run connect_quickcall first."
|
|
30
|
+
|
|
31
|
+
creds = store.get_api_credentials()
|
|
32
|
+
if not creds or not creds.slack_connected or not creds.slack_bot_token:
|
|
33
|
+
return "Slack not connected. Connect at quickcall.dev/assistant."
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Use shared cached client
|
|
37
|
+
client = _get_client()
|
|
38
|
+
channels = client.list_channels(include_private=True, limit=200)
|
|
39
|
+
|
|
40
|
+
# Format as readable list
|
|
41
|
+
lines = ["Available Slack Channels:", ""]
|
|
42
|
+
for ch in channels:
|
|
43
|
+
status = "member" if ch.is_member else "not member"
|
|
44
|
+
privacy = "private" if ch.is_private else "public"
|
|
45
|
+
lines.append(f"- #{ch.name} ({privacy}, {status})")
|
|
46
|
+
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"Failed to fetch Slack channels: {e}")
|
|
50
|
+
return f"Error fetching channels: {str(e)}"
|
|
@@ -23,6 +23,7 @@ from mcp_server.tools.utility_tools import create_utility_tools
|
|
|
23
23
|
from mcp_server.tools.github_tools import create_github_tools
|
|
24
24
|
from mcp_server.tools.slack_tools import create_slack_tools
|
|
25
25
|
from mcp_server.tools.auth_tools import create_auth_tools
|
|
26
|
+
from mcp_server.resources.slack_resources import create_slack_resources
|
|
26
27
|
|
|
27
28
|
# Configure logging
|
|
28
29
|
logging.basicConfig(
|
|
@@ -54,6 +55,9 @@ def create_server() -> FastMCP:
|
|
|
54
55
|
create_github_tools(mcp)
|
|
55
56
|
create_slack_tools(mcp)
|
|
56
57
|
|
|
58
|
+
# Register resources (available in Claude's context)
|
|
59
|
+
create_slack_resources(mcp)
|
|
60
|
+
|
|
57
61
|
# Log current status
|
|
58
62
|
if is_authenticated:
|
|
59
63
|
logger.info("QuickCall: authenticated")
|