Skip to content

Commit 0008a5d

Browse files
feat: add Slack MCP Server support as HTTP Server (#47)
* feat: add HTTP server entry point for OAuth support Add app_oauth.py as an alternative to app.py that starts an HTTP server instead of using Socket Mode. Include manifest_oauth.json with request URLs configured for ngrok and update READMEs with setup instructions. * feat: add Connect/Disconnect button to App Home Add a toggle button to the App Home that tracks account connection state using an in-memory store. This is a placeholder for the real OAuth flow. * docs: add OAuth install step to README setup instructions * feat: add Slack MCP Server support when user token is available * Make it work * Revert "Make it work" This reverts commit 9fe578d. * feat: replace the connect/disconnect buttons with status text * feat: fallback on bot token when oauth creds are missing * refactor: use /slack/install URL and remove MCP fallback logic Replace the deep-link authorize URL with the standard /slack/install path for the App Home connect link. Remove the MCP server try/except fallback from run_casey() to avoid silently dropping all MCP tools on connection errors. Clean up unused AuthorizeUrlGenerator. * chore: remove unused connect_account action handler * refactor: merge Slack MCP prompt into main system prompt * chore: remove install URL print on server startup * docs: add MCP toggle step to OAuth setup instructions * chore: move SLACK_SIGNING_SECRET under OAuth credentials in .env.sample * chore: standardize ngrok placeholder to YOUR_NGROK_SUBDOMAIN * docs: improve readability of OAuth env setup step in README * fix: remove divider before @casey context block in App Home * feat: instruct Casey to look up email via Slack MCP before password reset * chore: use context.user_token instead of manual query * feat: port Slack MCP Server changes to claude-agent-sdk and openai-agents-sdk Port all pydantic-ai branch changes to the other two implementations: - Add Slack MCP Server integration with OAuth user token - Replace button-based App Home UI with text-based MCP status - Use AsyncOAuthSettings for claude-agent-sdk's AsyncApp - Add MCP setup steps to README OAuth sections - Expand user scopes for MCP capabilities - Update system prompt with Slack MCP Server section - Simplify app_home_opened to use context.user_token * bug: fix displaying the email address for password resets * bug: fix context access in openai-agents-sdk emoji and resolve tools RunContextWrapper.context IS the CaseyDeps instance directly — unlike Pydantic AI's RunContext which has a .deps property. The incorrect .deps access raised AttributeError silently caught by the SDK. * chore: ignore .slack/apps.json files in monorepo gitignore Prevents accidentally committing App IDs from maintainer environments. This top-level gitignore is not part of the app templates. * bug: read OAuth user scopes from manifest.json instead of hardcoding * fix: add pytest asyncio_mode and fix ruff formatting * fix: improve password reset prompt to look up email from Slack profile Update the agent system prompt and tool descriptions so the agent tries to look up the user's email from their Slack profile before triggering a password reset, falling back to asking the user if the lookup fails. * refactor: consolidate manifest.json and manifest_oauth.json into single file Merge both manifests into a single manifest.json per directory. The superset manifest includes user scopes and placeholder URLs using ngrok-free.app, which validates against Slack's schema. For HTTP/OAuth mode, developers just set socket_mode_enabled to false and replace ngrok-free.app with their ngrok domain. --------- Co-authored-by: William Bergamin <wbergamin@slack-corp.com>
1 parent c117ee4 commit 0008a5d

48 files changed

Lines changed: 1257 additions & 65 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@
66

77
# Git
88
.worktrees/
9+
10+
# Slack App Templates
11+
*/.slack/apps.json
12+
*/.slack/apps.dev.json

claude-agent-sdk/.env.sample

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
# Required, set your Anthropic API key.
2+
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
3+
14
# Optional, uncomment and set when running without the Slack CLI (python3 app.py).
25
# SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
36
# SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN
47

8+
# Required for OAuth (app_oauth.py). Set your app's OAuth credentials.
9+
# SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
10+
# SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
11+
# SLACK_REDIRECT_URI=https://YOUR_NGROK_SUBDOMAIN.ngrok-free.app/slack/oauth_redirect
12+
# SLACK_SIGNING_SECRET=YOUR_SLACK_SIGNING_SECRET
13+
514
# Optional, uncomment and set when using a custom Slack instance.
615
# SLACK_API_URL=YOUR_SLACK_API_URL
7-
8-
# Required, set your Anthropic API key.
9-
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY

claude-agent-sdk/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,8 @@ logs/
3939
.pytype/
4040
.idea/
4141

42+
# oauth data
43+
data/
44+
4245
# claude
4346
.claude/*.local.json

claude-agent-sdk/README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,101 @@ python3 app.py
140140

141141
</details>
142142

143+
<details><summary><strong>Using OAuth HTTP Server (with ngrok)</strong></summary>
144+
145+
#### OAuth HTTP Server
146+
147+
This mode uses an HTTP server instead of Socket Mode, which is required for OAuth-based distribution.
148+
149+
1. Install [ngrok](https://ngrok.com/download) and start a tunnel:
150+
151+
```sh
152+
ngrok http 3000
153+
```
154+
155+
2. Copy the `https://*.ngrok-free.app` URL from the ngrok output.
156+
157+
<details><summary><strong>Using Slack CLI</strong></summary>
158+
159+
#### Slack CLI
160+
161+
3. Update `manifest.json` for HTTP mode:
162+
- Set `socket_mode_enabled` to `false`
163+
- Replace `ngrok-free.app` with your ngrok domain (e.g. `YOUR_NGROK_SUBDOMAIN.ngrok-free.app`)
164+
165+
4. Create a new local dev app:
166+
167+
```sh
168+
slack install -E local
169+
```
170+
171+
5. Enable MCP for your app:
172+
- Run `slack app settings` to open your app's settings
173+
- Navigate to **Agents & AI Apps** in the left-side navigation
174+
- Toggle **Model Context Protocol** on
175+
176+
6. Update your `.env` OAuth environment variables:
177+
- Run `slack app settings` to open App Settings
178+
- Copy **Client ID**, **Client Secret**, and **Signing Secret**
179+
- Update `SLACK_REDIRECT_URI` in `.env` with your ngrok domain
180+
181+
```sh
182+
SLACK_CLIENT_ID=YOUR_CLIENT_ID
183+
SLACK_CLIENT_SECRET=YOUR_CLIENT_SECRET
184+
SLACK_REDIRECT_URI=https://YOUR_NGROK_SUBDOMAIN.ngrok-free.app/slack/oauth_redirect
185+
SLACK_SIGNING_SECRET=YOUR_SIGNING_SECRET
186+
```
187+
188+
7. Start the app:
189+
190+
```sh
191+
slack run app_oauth.py
192+
```
193+
194+
8. Click the install URL printed in the terminal to install the app to your workspace via OAuth.
195+
196+
</details>
197+
198+
<details><summary><strong>Using the Terminal</strong></summary>
199+
200+
#### Terminal
201+
202+
3. Create your Slack app at [api.slack.com/apps/new](https://api.slack.com/apps/new) using [`manifest.json`](./manifest.json). Before pasting the manifest, set `socket_mode_enabled` to `false` and replace `ngrok-free.app` with your ngrok domain.
203+
204+
4. Install the app to your workspace and copy the following values into your `.env`:
205+
- **Signing Secret** — from _Basic Information_
206+
- **Bot User OAuth Token** — from _OAuth & Permissions_
207+
- **Client ID** and **Client Secret** — from _Basic Information_
208+
209+
```sh
210+
SLACK_SIGNING_SECRET=YOUR_SIGNING_SECRET
211+
SLACK_BOT_TOKEN=xoxb-YOUR_BOT_TOKEN
212+
SLACK_CLIENT_ID=YOUR_CLIENT_ID
213+
SLACK_CLIENT_SECRET=YOUR_CLIENT_SECRET
214+
SLACK_REDIRECT_URI=https://YOUR_NGROK_SUBDOMAIN.ngrok-free.app/slack/oauth_redirect
215+
```
216+
217+
Replace `your-subdomain` in `SLACK_REDIRECT_URI` with your ngrok subdomain.
218+
219+
5. Enable MCP for your app:
220+
- Open your app at [api.slack.com/apps](https://api.slack.com/apps)
221+
- Navigate to **Agents & AI Apps** in the left-side navigation
222+
- Toggle **Model Context Protocol** on
223+
224+
6. Start the app:
225+
226+
```sh
227+
python3 app_oauth.py
228+
```
229+
230+
7. Click the install URL printed in the terminal to install the app to your workspace via OAuth.
231+
232+
</details>
233+
234+
> **Note:** Each time ngrok restarts, it generates a new URL. You'll need to update the ngrok domain in `manifest.json`, `SLACK_REDIRECT_URI` in your `.env`, and re-install the app.
235+
236+
</details>
237+
143238
### Using the App
144239

145240
Once Casey is running, there are three ways to interact:
@@ -172,6 +267,14 @@ ruff format
172267

173268
`app.py` is the entry point for the application and is the file you'll run to start the server. This project uses `AsyncApp` from Bolt for Python, with all handlers running asynchronously.
174269

270+
### `app_oauth.py`
271+
272+
`app_oauth.py` is an alternative entry point that runs the app in HTTP mode instead of Socket Mode. This is intended for deployments that use OAuth for app distribution. See the HTTP Mode section under Development for setup instructions.
273+
274+
### `manifest_oauth.json`
275+
276+
`manifest_oauth.json` is the app manifest configured for HTTP mode (Socket Mode disabled, with request URLs for event subscriptions and interactivity). Use this when setting up the app for HTTP mode instead of `manifest.json`.
277+
175278
### `/listeners`
176279

177280
Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used.

claude-agent-sdk/agent/casey.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
TextBlock,
77
create_sdk_mcp_server,
88
)
9+
from claude_agent_sdk.types import McpHttpServerConfig
910

1011
from agent.context import casey_deps_var
1112
from agent.deps import CaseyDeps
@@ -56,6 +57,7 @@
5657
2. Search the knowledge base for relevant articles
5758
3. If the KB has a solution, walk the user through it step by step
5859
4. If the issue requires action (password reset, ticket creation), use the appropriate tool
60+
- For password resets: follow the instructions in the `trigger_password_reset` tool description to obtain the user's email before calling it
5961
5. After taking action, confirm what was done and what the user should expect next
6062
6. If you cannot resolve the issue, create a support ticket and let the user know
6163
@@ -74,6 +76,21 @@
7476
Call this once when the issue is fully resolved (password reset done, ticket created, problem fixed).
7577
- Do not use `eyes` — it is added automatically
7678
79+
## SLACK MCP SERVER
80+
You may have access to the Slack MCP Server, which gives you powerful Slack tools beyond \
81+
your built-in IT helpdesk tools. Use them whenever they would help the user.
82+
83+
Available capabilities:
84+
- **Search**: Search messages and files across public channels, search for channels by name
85+
- **Read**: Read channel message history, read thread replies, read canvas documents
86+
- **Write**: Send messages, create draft messages, schedule messages for later
87+
- **Canvases**: Create, read, and update Slack canvas documents
88+
89+
Use these tools proactively when they can help resolve an IT issue — for example, \
90+
searching for related reports from other users, checking a channel for outage updates, \
91+
or creating a canvas to document a solution. Also use them when the user explicitly \
92+
asks you to perform a Slack action like sending a message or creating a canvas.
93+
7794
## BOUNDARIES
7895
- You are an IT helpdesk agent only — politely redirect non-IT questions
7996
- Do not make up system statuses or ticket numbers — always use the provided tools
@@ -95,7 +112,9 @@
95112
],
96113
)
97114

98-
ALLOWED_TOOLS = [
115+
SLACK_MCP_URL = "https://mcp.slack.com/mcp"
116+
117+
CASEY_TOOLS = [
99118
"add_emoji_reaction",
100119
"check_system_status",
101120
"create_support_ticket",
@@ -124,10 +143,21 @@ async def run_casey_agent(
124143
if deps:
125144
casey_deps_var.set(deps)
126145

146+
mcp_servers: dict = {"casey-tools": casey_tools_server}
147+
allowed_tools = list(CASEY_TOOLS)
148+
149+
if deps and deps.user_token:
150+
mcp_servers["slack-mcp"] = McpHttpServerConfig(
151+
type="http",
152+
url=SLACK_MCP_URL,
153+
headers={"Authorization": f"Bearer {deps.user_token}"},
154+
)
155+
allowed_tools.append("mcp__slack-mcp__*")
156+
127157
options = ClaudeAgentOptions(
128158
system_prompt=CASEY_SYSTEM_PROMPT,
129-
mcp_servers={"casey-tools": casey_tools_server},
130-
allowed_tools=ALLOWED_TOOLS,
159+
mcp_servers=mcp_servers,
160+
allowed_tools=allowed_tools,
131161
permission_mode="bypassPermissions",
132162
)
133163

claude-agent-sdk/agent/deps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ class CaseyDeps:
1010
channel_id: str
1111
thread_ts: str
1212
message_ts: str
13+
user_token: str | None = None

claude-agent-sdk/agent/tools/password_reset.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
"Trigger a password reset for a specified user account. "
88
"Use this tool when a user requests a password reset for their own account "
99
"or reports being locked out. The reset link will be sent to their registered "
10-
"email address."
10+
"email address. "
11+
"IMPORTANT: You need the user's email address for target_user. "
12+
"First, try to look up the user's Slack profile to get their email address. "
13+
"If the lookup fails or does not return an email, ask the user for their email address. "
14+
"Never guess or assume — you must either look it up or ask for it."
1115
),
1216
input_schema={"target_user": str},
1317
)
@@ -17,7 +21,7 @@ async def trigger_password_reset_tool(args):
1721

1822
text = (
1923
f"Password reset initiated for **{target_user}**.\n\n"
20-
f"A reset link has been sent to the email address on file. "
24+
f"A reset link has been emailed to **{target_user}**. "
2125
f"The link will expire in 30 minutes.\n\n"
2226
f"_If the user doesn't receive the email within 5 minutes, "
2327
f"ask them to check their spam folder or verify their registered email address._"

claude-agent-sdk/app_oauth.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import logging
2+
import os
3+
4+
from dotenv import load_dotenv
5+
from slack_bolt.async_app import AsyncApp
6+
from slack_sdk.web.async_client import AsyncWebClient
7+
8+
from listeners import register_listeners
9+
from oauth import oauth_settings
10+
11+
load_dotenv(dotenv_path=".env", override=False)
12+
13+
logging.basicConfig(level=logging.DEBUG)
14+
15+
app = AsyncApp(
16+
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
17+
token=os.environ.get("SLACK_BOT_TOKEN"),
18+
client=AsyncWebClient(
19+
base_url=os.environ.get("SLACK_API_URL", "https://slack.com/api"),
20+
token=os.environ.get("SLACK_BOT_TOKEN"),
21+
),
22+
# Allow bot-posted messages (e.g. issue modal submissions with metadata)
23+
# to reach the message handler instead of being silently dropped
24+
ignoring_self_events_enabled=False,
25+
oauth_settings=oauth_settings,
26+
)
27+
28+
register_listeners(app)
29+
30+
if __name__ == "__main__":
31+
port = int(os.environ.get("PORT", 3000))
32+
app.start(port=port)

claude-agent-sdk/listeners/events/app_home_opened.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import os
12
from logging import Logger
3+
from urllib.parse import urljoin
24

35
from slack_bolt.context.async_context import AsyncBoltContext
46
from slack_sdk.web.async_client import AsyncWebClient
@@ -12,7 +14,17 @@ async def handle_app_home_opened(
1214
"""Publish the App Home view when a user opens the app's Home tab."""
1315
try:
1416
user_id = context.user_id
15-
view = build_app_home_view()
17+
install_url = None
18+
is_connected = False
19+
20+
if os.environ.get("SLACK_CLIENT_ID"):
21+
if context.user_token:
22+
is_connected = True
23+
else:
24+
redirect_uri = os.environ.get("SLACK_REDIRECT_URI", "")
25+
install_url = urljoin(redirect_uri, "/slack/install")
26+
27+
view = build_app_home_view(install_url=install_url, is_connected=is_connected)
1628
await client.views_publish(user_id=user_id, view=view)
1729
except Exception as e:
1830
logger.exception(f"Failed to publish App Home: {e}")

claude-agent-sdk/listeners/events/app_mentioned.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ async def handle_app_mentioned(
6767
channel_id=channel_id,
6868
thread_ts=thread_ts,
6969
message_ts=event["ts"],
70+
user_token=context.user_token,
7071
)
7172
response_text, new_session_id = await run_casey_agent(
7273
cleaned_text, session_id=existing_session_id, deps=deps

0 commit comments

Comments
 (0)