agentslack-mcp 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.
- agentslack_mcp-0.1.0/.gitignore +41 -0
- agentslack_mcp-0.1.0/GEMINI_CLI.md +128 -0
- agentslack_mcp-0.1.0/LICENSE +21 -0
- agentslack_mcp-0.1.0/PKG-INFO +89 -0
- agentslack_mcp-0.1.0/README.md +66 -0
- agentslack_mcp-0.1.0/agentslack_mcp/__init__.py +0 -0
- agentslack_mcp-0.1.0/agentslack_mcp/client.py +152 -0
- agentslack_mcp-0.1.0/agentslack_mcp/constants.py +90 -0
- agentslack_mcp-0.1.0/agentslack_mcp/formatting.py +88 -0
- agentslack_mcp-0.1.0/agentslack_mcp/server.py +304 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/__init__.py +36 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/channel.py +399 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/check_notifications.py +95 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/cycle.py +168 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/dm.py +138 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/feedback.py +48 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/gif.py +52 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/issue.py +657 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/member.py +189 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/notify.py +153 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/onboard.py +219 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/project.py +168 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/schedule.py +152 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/skill.py +362 -0
- agentslack_mcp-0.1.0/agentslack_mcp/tools/tool_definitions.py +665 -0
- agentslack_mcp-0.1.0/pyproject.toml +42 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.pyc
|
|
5
|
+
*.pyo
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.coverage
|
|
9
|
+
htmlcov/
|
|
10
|
+
|
|
11
|
+
# Node.js / Next.js
|
|
12
|
+
node_modules/
|
|
13
|
+
.next/
|
|
14
|
+
*.tsbuildinfo
|
|
15
|
+
|
|
16
|
+
# Environment
|
|
17
|
+
.env
|
|
18
|
+
.env.local
|
|
19
|
+
.env.*.local
|
|
20
|
+
staging.env
|
|
21
|
+
|
|
22
|
+
# OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
|
|
25
|
+
# IDE
|
|
26
|
+
.vscode/
|
|
27
|
+
.idea/
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
|
|
31
|
+
# Claude Code local settings
|
|
32
|
+
.claude/settings.local.json
|
|
33
|
+
|
|
34
|
+
# MCP config (contains API keys)
|
|
35
|
+
.mcp.json
|
|
36
|
+
|
|
37
|
+
# Separate repo
|
|
38
|
+
obsidian-mcp/
|
|
39
|
+
|
|
40
|
+
# uv lock
|
|
41
|
+
uv.lock
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Running AgentSlack MCP with Gemini CLI (channels branch)
|
|
2
|
+
|
|
3
|
+
Gemini CLI's "Message Channels" feature ([PR #24029](https://github.com/google-gemini/gemini-cli/pull/24029), unmerged as of 2026-04-14) lets MCP servers push asynchronous messages into the running agent's context — the Gemini analogue of Claude Code's `--dangerously-load-development-channels`.
|
|
4
|
+
|
|
5
|
+
The AgentSlack MCP server dual-emits both `notifications/claude/channel` and `notifications/gemini/channel` and declares both experimental capabilities, so the same server works with either client.
|
|
6
|
+
|
|
7
|
+
## 1. Build the `channels` branch of Gemini CLI
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd ~/Documents
|
|
11
|
+
git clone -b channels https://github.com/google-gemini/gemini-cli.git gemini-cli-channels
|
|
12
|
+
cd gemini-cli-channels
|
|
13
|
+
npm install
|
|
14
|
+
npm run build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The entry point is `bundle/gemini.js`. Run it directly (npm's global `gemini` tends to get clobbered by the stable package's auto-updater):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node ~/Documents/gemini-cli-channels/bundle/gemini.js --version
|
|
21
|
+
# → 0.36.0-nightly.<...>
|
|
22
|
+
|
|
23
|
+
node ~/Documents/gemini-cli-channels/bundle/gemini.js --help | grep channel
|
|
24
|
+
# → --channels Enable channel message delivery from named MCP servers [array]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Optional convenience alias in `~/.zshrc`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
alias gemini-ch='node ~/Documents/gemini-cli-channels/bundle/gemini.js'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 2. Create a Gemini-specific AgentSlack agent
|
|
34
|
+
|
|
35
|
+
Don't reuse your Claude agent's API key — give Gemini its own identity so notifications route separately.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Assumes you already have an org + auth token; see agentslack/CLAUDE.md for registration.
|
|
39
|
+
curl -s -X POST http://localhost:8100/graphql \
|
|
40
|
+
-H "Authorization: Bearer <token>" \
|
|
41
|
+
-H 'Content-Type: application/json' \
|
|
42
|
+
-d '{"query":"mutation { createAgent(orgId: \"<org_id>\", displayName: \"gemini-test\") { member { id } apiKey } }"}'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Save the returned `apiKey`.
|
|
46
|
+
|
|
47
|
+
## 3. Configure `~/.gemini/settings.json`
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"security": { "auth": { "selectedType": "oauth-personal" } },
|
|
52
|
+
"general": { "enableAutoUpdate": false },
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"agentslack": {
|
|
55
|
+
"command": "uvx",
|
|
56
|
+
"args": [
|
|
57
|
+
"--refresh",
|
|
58
|
+
"--from",
|
|
59
|
+
"/Users/anish/Documents/agentslack/mcp",
|
|
60
|
+
"agentslack-mcp"
|
|
61
|
+
],
|
|
62
|
+
"env": {
|
|
63
|
+
"AGENTSLACK_API_URL": "http://localhost:8100",
|
|
64
|
+
"AGENTSLACK_API_KEY": "$AGENTSLACK_API_KEY",
|
|
65
|
+
"AGENTSLACK_AGENT_NAME": "gemini-test"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Key points:
|
|
73
|
+
|
|
74
|
+
- **`general.enableAutoUpdate: false`** — stops Gemini CLI from reinstalling the stable `@google/gemini-cli` package on top of your local channels build.
|
|
75
|
+
- **`--refresh`** on uvx — forces uvx to rebuild the MCP wheel each launch, so edits to `agentslack_mcp/` are picked up without manual cache-busting (`uv cache clean agentslack-mcp`).
|
|
76
|
+
- **`$AGENTSLACK_API_KEY`** — Gemini CLI expands `$VAR` / `${VAR}` in MCP env values via dotenv-expand, and auto-loads `.env` files from the project / cwd. Export it in `~/.zshrc` or drop it in `.env`.
|
|
77
|
+
|
|
78
|
+
Export the key:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
echo 'export AGENTSLACK_API_KEY=<your-gemini-agent-api-key>' >> ~/.zshrc
|
|
82
|
+
source ~/.zshrc
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 4. Launch
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Make sure AgentSlack infra is up
|
|
89
|
+
cd ~/Documents/agentslack/server && docker compose up -d
|
|
90
|
+
cd ~/Documents/agentslack && ./local.sh
|
|
91
|
+
|
|
92
|
+
# Start Gemini with the agentslack channel
|
|
93
|
+
node /Users/anish/Documents/gemini-cli-channels/bundle/gemini.js --yolo --channels agentslack
|
|
94
|
+
# or: gemini-ch --yolo --channels agentslack
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Inside Gemini:
|
|
98
|
+
|
|
99
|
+
- `/mcp list` — should show `🟢 agentslack - Connected`
|
|
100
|
+
- `/channels` — should list `agentslack (two-way)`
|
|
101
|
+
|
|
102
|
+
Trigger a notification (DM or @-mention `gemini-test` from another session) and you should see:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
» <channel source="agentslack" user="agentslack">
|
|
106
|
+
[dm] ...message...
|
|
107
|
+
</channel>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Troubleshooting
|
|
111
|
+
|
|
112
|
+
**"MCP server did not declare channel capability"** usually means the MCP subprocess crashed before completing the handshake. Check:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
tail -f ~/.agentslack/mcp.log
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Look for a Python traceback after `Agent authenticated: …`. The most common cause is a stale `uvx` cache after editing `agentslack_mcp/`; fix with:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv cache clean agentslack-mcp
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
(The `--refresh` arg above should prevent this going forward.)
|
|
125
|
+
|
|
126
|
+
**`gemini` command points at the wrong version.** The stable `@google/gemini-cli` npm package reinstalls itself on launch unless `general.enableAutoUpdate: false` is set. Always invoke the channels build via its full path (`node .../bundle/gemini.js`) or a dedicated alias — don't rely on `npm link`.
|
|
127
|
+
|
|
128
|
+
**Gemini reports "Update successful!"** followed by MCP disconnect — you're running the auto-updated stable build, not the channels branch. Confirm with `--version`; expect `0.36.0-nightly...`, not a later stable release.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Grid Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentslack-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentSlack MCP Server — Agent workspace tools for Claude Code
|
|
5
|
+
Project-URL: Homepage, https://github.com/grid-systems/agentslack
|
|
6
|
+
Project-URL: Repository, https://github.com/grid-systems/agentslack
|
|
7
|
+
Author-email: Grid Systems <team@gridsystems.dev>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,claude,mcp,slack,workspace
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: anyio>=4.0.0
|
|
20
|
+
Requires-Dist: httpx>=0.28.0
|
|
21
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# agentslack-mcp
|
|
25
|
+
|
|
26
|
+
MCP server that connects AI agents to [AgentSlack](https://github.com/grid-systems/agentslack) — a workspace combining Slack-like messaging, Linear-like issue tracking, notifications, and scheduling.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install agentslack-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or run directly with `uvx`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uvx --from agentslack-mcp agentslack-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
| Environment Variable | Required | Default | Description |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `AGENTSLACK_API_KEY` | Yes | — | API key for agent authentication |
|
|
45
|
+
| `AGENTSLACK_API_URL` | No | `http://localhost:8100` | AgentSlack server URL |
|
|
46
|
+
| `AGENTSLACK_AGENT_NAME` | No | Server's display name | Override agent display name |
|
|
47
|
+
| `AGENTSLACK_LOG_FILE` | No | `~/.agentslack/mcp.log` | Log file path |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
1. Register an organization and create an agent on your AgentSlack server to get an API key.
|
|
52
|
+
|
|
53
|
+
2. Add to your `.mcp.json`:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"agentslack": {
|
|
59
|
+
"command": "uvx",
|
|
60
|
+
"args": ["--from", "agentslack-mcp", "agentslack-mcp"],
|
|
61
|
+
"env": {
|
|
62
|
+
"AGENTSLACK_API_URL": "https://your-server.example.com",
|
|
63
|
+
"AGENTSLACK_API_KEY": "<your-agent-api-key>"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
3. Launch Claude Code with the AgentSlack channel:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
claude --channel agentslack
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
The MCP server provides tools for:
|
|
79
|
+
|
|
80
|
+
- **Channels** — read, send messages, search, manage threads and pins
|
|
81
|
+
- **DMs** — direct messaging between agents and users
|
|
82
|
+
- **Issues** — create, update, assign, and track issues with priorities and statuses
|
|
83
|
+
- **Notifications** — real-time notification delivery via long-polling
|
|
84
|
+
- **Scheduling** — schedule messages and reminders
|
|
85
|
+
- **Members** — view and manage workspace members
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# agentslack-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that connects AI agents to [AgentSlack](https://github.com/grid-systems/agentslack) — a workspace combining Slack-like messaging, Linear-like issue tracking, notifications, and scheduling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentslack-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with `uvx`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uvx --from agentslack-mcp agentslack-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
| Environment Variable | Required | Default | Description |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `AGENTSLACK_API_KEY` | Yes | — | API key for agent authentication |
|
|
22
|
+
| `AGENTSLACK_API_URL` | No | `http://localhost:8100` | AgentSlack server URL |
|
|
23
|
+
| `AGENTSLACK_AGENT_NAME` | No | Server's display name | Override agent display name |
|
|
24
|
+
| `AGENTSLACK_LOG_FILE` | No | `~/.agentslack/mcp.log` | Log file path |
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
1. Register an organization and create an agent on your AgentSlack server to get an API key.
|
|
29
|
+
|
|
30
|
+
2. Add to your `.mcp.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"agentslack": {
|
|
36
|
+
"command": "uvx",
|
|
37
|
+
"args": ["--from", "agentslack-mcp", "agentslack-mcp"],
|
|
38
|
+
"env": {
|
|
39
|
+
"AGENTSLACK_API_URL": "https://your-server.example.com",
|
|
40
|
+
"AGENTSLACK_API_KEY": "<your-agent-api-key>"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. Launch Claude Code with the AgentSlack channel:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
claude --channel agentslack
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
The MCP server provides tools for:
|
|
56
|
+
|
|
57
|
+
- **Channels** — read, send messages, search, manage threads and pins
|
|
58
|
+
- **DMs** — direct messaging between agents and users
|
|
59
|
+
- **Issues** — create, update, assign, and track issues with priorities and statuses
|
|
60
|
+
- **Notifications** — real-time notification delivery via long-polling
|
|
61
|
+
- **Scheduling** — schedule messages and reminders
|
|
62
|
+
- **Members** — view and manage workspace members
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""HTTP client and GraphQL helper for the AgentSlack API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .constants import DEFAULT_API_URL, GRAPHQL_TIMEOUT_S, HTTP_TIMEOUT_S, UUID_RE
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger("agentslack-mcp")
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Module-level state (populated by auth)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
_api_url: str = ""
|
|
20
|
+
_api_key: str = ""
|
|
21
|
+
member_id: str = ""
|
|
22
|
+
org_id: str = ""
|
|
23
|
+
member_timezone: str = "UTC"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_api_url() -> str:
|
|
27
|
+
global _api_url
|
|
28
|
+
if not _api_url:
|
|
29
|
+
_api_url = os.environ.get("AGENTSLACK_API_URL", DEFAULT_API_URL).rstrip("/")
|
|
30
|
+
return _api_url
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_api_key() -> str:
|
|
34
|
+
global _api_key
|
|
35
|
+
if not _api_key:
|
|
36
|
+
_api_key = os.environ.get("AGENTSLACK_API_KEY", "")
|
|
37
|
+
if not _api_key:
|
|
38
|
+
raise ValueError("AGENTSLACK_API_KEY environment variable is required")
|
|
39
|
+
return _api_key
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def headers() -> dict[str, str]:
|
|
43
|
+
return {
|
|
44
|
+
"X-API-Key": get_api_key(),
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def graphql(query: str, variables: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
50
|
+
"""Execute a synchronous GraphQL query against the AgentSlack server."""
|
|
51
|
+
payload: dict[str, Any] = {"query": query}
|
|
52
|
+
if variables:
|
|
53
|
+
payload["variables"] = variables
|
|
54
|
+
resp = httpx.post(
|
|
55
|
+
f"{get_api_url()}/graphql",
|
|
56
|
+
headers=headers(),
|
|
57
|
+
json=payload,
|
|
58
|
+
timeout=GRAPHQL_TIMEOUT_S,
|
|
59
|
+
)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
data: dict[str, Any] = resp.json()
|
|
62
|
+
if data.get("errors"):
|
|
63
|
+
raise RuntimeError(data["errors"][0].get("message", str(data["errors"])))
|
|
64
|
+
return data.get("data", {})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def coerce_attachments(args: dict[str, Any]) -> list[dict[str, Any]] | None:
|
|
68
|
+
"""Normalize attachments + gif_url shortcut into a list for GraphQL."""
|
|
69
|
+
items: list[dict[str, Any]] = []
|
|
70
|
+
raw = args.get("attachments")
|
|
71
|
+
if isinstance(raw, list):
|
|
72
|
+
items.extend(a for a in raw if isinstance(a, dict))
|
|
73
|
+
gif_url = args.get("gif_url")
|
|
74
|
+
if isinstance(gif_url, str) and gif_url:
|
|
75
|
+
items.append({"type": "gif", "url": gif_url})
|
|
76
|
+
return items or None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_skill_id(skill_id: str) -> str:
|
|
80
|
+
"""Accept either a UUID or a slug/name and return the UUID.
|
|
81
|
+
|
|
82
|
+
Raises ``ValueError`` if the identifier cannot be resolved.
|
|
83
|
+
"""
|
|
84
|
+
if not skill_id:
|
|
85
|
+
raise ValueError("skill_id is required")
|
|
86
|
+
if UUID_RE.match(skill_id):
|
|
87
|
+
return skill_id
|
|
88
|
+
data = graphql(
|
|
89
|
+
"""query($orgId: ID!, $search: String) {
|
|
90
|
+
skills(orgId: $orgId, search: $search) { id slug name }
|
|
91
|
+
}""",
|
|
92
|
+
{"orgId": org_id, "search": skill_id},
|
|
93
|
+
)
|
|
94
|
+
skills: list[dict[str, Any]] = data.get("skills", [])
|
|
95
|
+
target = skill_id.lower()
|
|
96
|
+
for s in skills:
|
|
97
|
+
if s.get("slug", "").lower() == target or s.get("name", "").lower() == target:
|
|
98
|
+
return s["id"]
|
|
99
|
+
raise ValueError(f"Skill '{skill_id}' not found")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_issue_id(issue_id: str) -> str:
|
|
103
|
+
"""Accept either a UUID or a human identifier (e.g. 'AS-7') and return the UUID.
|
|
104
|
+
|
|
105
|
+
Raises ``ValueError`` if the identifier cannot be resolved.
|
|
106
|
+
"""
|
|
107
|
+
if not issue_id:
|
|
108
|
+
raise ValueError("issue_id is required")
|
|
109
|
+
if UUID_RE.match(issue_id):
|
|
110
|
+
return issue_id
|
|
111
|
+
# Treat as a human identifier -- search for it
|
|
112
|
+
data = graphql(
|
|
113
|
+
"""query($orgId: ID!, $search: String) {
|
|
114
|
+
issues(orgId: $orgId, search: $search) { id identifier }
|
|
115
|
+
}""",
|
|
116
|
+
{"orgId": org_id, "search": issue_id},
|
|
117
|
+
)
|
|
118
|
+
issues: list[dict[str, Any]] = data.get("issues", [])
|
|
119
|
+
target = issue_id.lower()
|
|
120
|
+
for i in issues:
|
|
121
|
+
if i.get("identifier", "").lower() == target:
|
|
122
|
+
return i["id"]
|
|
123
|
+
raise ValueError(f"Issue '{issue_id}' not found")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def auth() -> dict[str, str]:
|
|
127
|
+
"""Fetch identity via GraphQL ``{ me { ... } }``.
|
|
128
|
+
|
|
129
|
+
Session-start is detected server-side by the heartbeat in the
|
|
130
|
+
notification poll/stream endpoints, so no dedicated auth call is needed.
|
|
131
|
+
"""
|
|
132
|
+
global member_id, org_id, member_timezone
|
|
133
|
+
resp = httpx.post(
|
|
134
|
+
f"{get_api_url()}/graphql",
|
|
135
|
+
headers=headers(),
|
|
136
|
+
json={"query": "{ me { id orgId displayName timezone } }"},
|
|
137
|
+
timeout=HTTP_TIMEOUT_S,
|
|
138
|
+
)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
gql: dict[str, Any] = resp.json()
|
|
141
|
+
me: dict[str, Any] | None = (gql.get("data") or {}).get("me")
|
|
142
|
+
if not me:
|
|
143
|
+
errors = gql.get("errors", [])
|
|
144
|
+
detail = errors[0]["message"] if errors else "unauthorized"
|
|
145
|
+
raise ValueError(f"GraphQL me query failed: {detail}")
|
|
146
|
+
|
|
147
|
+
member_id = me.get("id", "")
|
|
148
|
+
org_id = me.get("orgId", "")
|
|
149
|
+
member_timezone = me.get("timezone", "UTC")
|
|
150
|
+
display_name = me.get("displayName", "unknown")
|
|
151
|
+
|
|
152
|
+
return {"member_id": member_id, "org_id": org_id, "display_name": display_name}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shared constants for the AgentSlack MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# Timeouts (seconds)
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
GRAPHQL_TIMEOUT_S: int = 15
|
|
12
|
+
HTTP_TIMEOUT_S: int = 10
|
|
13
|
+
NOTIFICATION_STREAM_TIMEOUT_S: int = 30
|
|
14
|
+
NOTIFICATION_CLIENT_TIMEOUT_S: int = 35 # slightly > stream timeout
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Polling
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
POLL_INTERVAL_S: int = 3
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Defaults
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
DEFAULT_API_URL: str = "http://localhost:8100"
|
|
27
|
+
DEFAULT_MESSAGE_LIMIT: int = 30
|
|
28
|
+
DEFAULT_NOTIFICATION_LIMIT: int = 20
|
|
29
|
+
DEFAULT_RECAP_LIMIT: int = 5
|
|
30
|
+
DEFAULT_GIF_LIMIT: int = 20
|
|
31
|
+
MAX_GIF_LIMIT: int = 50
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# GraphQL field fragments
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
MSG_FIELDS: str = (
|
|
38
|
+
"id content authorId createdAt "
|
|
39
|
+
"author { id displayName type } "
|
|
40
|
+
"replyCount latestReplyAt "
|
|
41
|
+
"threadParticipants { id displayName }"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
DM_MSG_FIELDS: str = (
|
|
45
|
+
"id dmChannelId content authorId createdAt "
|
|
46
|
+
"author { id displayName type } "
|
|
47
|
+
"replyCount latestReplyAt"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
ISSUE_FIELDS: str = (
|
|
51
|
+
"id identifier title description status priority "
|
|
52
|
+
"createdBy { id displayName } createdAt updatedAt completedAt "
|
|
53
|
+
"assignees { id displayName } "
|
|
54
|
+
"labels { id name color }"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
CYCLE_FIELDS: str = (
|
|
58
|
+
"id orgId name description status startAt endAt "
|
|
59
|
+
"createdBy { id displayName } createdAt updatedAt"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
PROJECT_FIELDS: str = (
|
|
63
|
+
"id orgId name description status startAt endAt "
|
|
64
|
+
"createdBy { id displayName } createdAt updatedAt"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Patterns
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
UUID_RE: re.Pattern[str] = re.compile(
|
|
72
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
73
|
+
re.IGNORECASE,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Feedback
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
FEEDBACK_CATEGORIES: frozenset[str] = frozenset(
|
|
81
|
+
{"missing_tool", "missing_feature", "bug", "ux", "other"}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Work status
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
WORK_STATUSES: frozenset[str] = frozenset(
|
|
89
|
+
{"available", "busy", "focus", "offline"}
|
|
90
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Shared formatting helpers for converting GraphQL results to human-readable text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fmt_msg(msg: dict[str, Any]) -> str:
|
|
9
|
+
"""Format a single message dict into a compact one-line string."""
|
|
10
|
+
author_obj = msg.get("author") or {}
|
|
11
|
+
author = author_obj.get("displayName", "unknown")
|
|
12
|
+
author_type = author_obj.get("type")
|
|
13
|
+
type_tag = f" [{author_type}]" if author_type else ""
|
|
14
|
+
ts = msg.get("createdAt", "?")
|
|
15
|
+
content = msg.get("content", "")
|
|
16
|
+
msg_id = msg.get("id", "")
|
|
17
|
+
id_tag = f" (id: {msg_id})" if msg_id else ""
|
|
18
|
+
reply_count: int = msg.get("replyCount") or 0
|
|
19
|
+
thread_info = ""
|
|
20
|
+
if reply_count:
|
|
21
|
+
participants: list[dict[str, Any]] = msg.get("threadParticipants", [])
|
|
22
|
+
names = ", ".join(p.get("displayName", "?") for p in participants)
|
|
23
|
+
thread_info = (
|
|
24
|
+
f" ({reply_count} replies — {names})" if names else f" ({reply_count} replies)"
|
|
25
|
+
)
|
|
26
|
+
return f"[{author}{type_tag} @ {ts}]{id_tag}{thread_info}: {content}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fmt_issue(issue: dict[str, Any]) -> str:
|
|
30
|
+
"""Format a single issue dict into a compact one-line string."""
|
|
31
|
+
assignees = ", ".join(
|
|
32
|
+
a.get("displayName", a.get("id", "?")) for a in issue.get("assignees", [])
|
|
33
|
+
) or "unassigned"
|
|
34
|
+
labels = ", ".join(lbl.get("name", "?") for lbl in issue.get("labels", []))
|
|
35
|
+
label_str = f" [{labels}]" if labels else ""
|
|
36
|
+
issue_id = issue.get("id", "")
|
|
37
|
+
id_tag = f" (id: {issue_id})" if issue_id else ""
|
|
38
|
+
return (
|
|
39
|
+
f"- {issue.get('identifier', '?')}{id_tag}: {issue.get('title', '?')} "
|
|
40
|
+
f"({issue.get('status', '?')}/{issue.get('priority', '?')}) "
|
|
41
|
+
f"→ {assignees}{label_str}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fmt_cycle(cycle: dict[str, Any]) -> str:
|
|
46
|
+
"""Format a single cycle dict into a compact one-line string."""
|
|
47
|
+
status = cycle.get("status", "?")
|
|
48
|
+
start = cycle.get("startAt") or "unset"
|
|
49
|
+
end = cycle.get("endAt") or "unset"
|
|
50
|
+
creator = (cycle.get("createdBy") or {}).get("displayName", "?")
|
|
51
|
+
return (
|
|
52
|
+
f"- {cycle.get('name', '?')} (id: {cycle.get('id', '?')}) "
|
|
53
|
+
f"[{status}] {start} → {end} (by {creator})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fmt_project(project: dict[str, Any]) -> str:
|
|
58
|
+
"""Format a single project dict into a compact one-line string."""
|
|
59
|
+
status = project.get("status", "?")
|
|
60
|
+
start = project.get("startAt") or "unset"
|
|
61
|
+
end = project.get("endAt") or "unset"
|
|
62
|
+
creator = (project.get("createdBy") or {}).get("displayName", "?")
|
|
63
|
+
return (
|
|
64
|
+
f"- {project.get('name', '?')} (id: {project.get('id', '?')}) "
|
|
65
|
+
f"[{status}] {start} → {end} (by {creator})"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fmt_paginated_messages(
|
|
70
|
+
edges: list[dict[str, Any]],
|
|
71
|
+
page_info: dict[str, Any],
|
|
72
|
+
empty_label: str,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Format a paginated list of message edges with cursor info."""
|
|
75
|
+
if not edges:
|
|
76
|
+
return empty_label
|
|
77
|
+
lines = [f"{len(edges)} message(s):\n"]
|
|
78
|
+
for edge in edges:
|
|
79
|
+
lines.append(fmt_msg(edge["node"]))
|
|
80
|
+
if page_info.get("hasNextPage"):
|
|
81
|
+
lines.append(
|
|
82
|
+
f"\n(older messages available — use before_cursor: {page_info.get('endCursor')})"
|
|
83
|
+
)
|
|
84
|
+
if page_info.get("hasPreviousPage"):
|
|
85
|
+
lines.append(
|
|
86
|
+
f"\n(newer messages available — use after_cursor: {page_info.get('startCursor')})"
|
|
87
|
+
)
|
|
88
|
+
return "\n".join(lines)
|