Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bbe2866
feat: add HTTP server entry point for OAuth support
mwbrooks Apr 1, 2026
d0e7a52
feat: add Connect/Disconnect button to App Home
mwbrooks Apr 1, 2026
79efc88
docs: add OAuth install step to README setup instructions
mwbrooks Apr 2, 2026
479c406
feat: add Slack MCP Server support when user token is available
mwbrooks Apr 2, 2026
9fe578d
Make it work
WilliamBergamin Apr 2, 2026
cbaed00
Revert "Make it work"
mwbrooks Apr 6, 2026
657d88e
feat: replace the connect/disconnect buttons with status text
mwbrooks Apr 6, 2026
abc940f
feat: fallback on bot token when oauth creds are missing
mwbrooks Apr 6, 2026
75f5f71
refactor: use /slack/install URL and remove MCP fallback logic
mwbrooks Apr 6, 2026
1a5d616
chore: remove unused connect_account action handler
mwbrooks Apr 6, 2026
1a64be3
refactor: merge Slack MCP prompt into main system prompt
mwbrooks Apr 6, 2026
0bf9caa
chore: remove install URL print on server startup
mwbrooks Apr 6, 2026
6f6dde1
docs: add MCP toggle step to OAuth setup instructions
mwbrooks Apr 6, 2026
37962fd
chore: move SLACK_SIGNING_SECRET under OAuth credentials in .env.sample
mwbrooks Apr 7, 2026
6f73709
chore: standardize ngrok placeholder to YOUR_NGROK_SUBDOMAIN
mwbrooks Apr 7, 2026
35e44fb
docs: improve readability of OAuth env setup step in README
mwbrooks Apr 7, 2026
5b59b59
fix: remove divider before @Casey context block in App Home
mwbrooks Apr 7, 2026
1d4aa94
feat: instruct Casey to look up email via Slack MCP before password r…
mwbrooks Apr 7, 2026
3704b8f
chore: use context.user_token instead of manual query
mwbrooks Apr 7, 2026
caaca55
feat: port Slack MCP Server changes to claude-agent-sdk and openai-ag…
mwbrooks Apr 7, 2026
9db55bb
bug: fix displaying the email address for password resets
mwbrooks Apr 7, 2026
c64ac1c
bug: fix context access in openai-agents-sdk emoji and resolve tools
mwbrooks Apr 7, 2026
a93888a
chore: ignore .slack/apps.json files in monorepo gitignore
mwbrooks Apr 7, 2026
5ec69b3
bug: read OAuth user scopes from manifest.json instead of hardcoding
mwbrooks Apr 7, 2026
71a44cc
fix: add pytest asyncio_mode and fix ruff formatting
mwbrooks Apr 7, 2026
f3bc921
Merge branch 'main' into slack-mcp-server
mwbrooks Apr 7, 2026
413fc75
fix: improve password reset prompt to look up email from Slack profile
mwbrooks Apr 7, 2026
bf7f95d
refactor: consolidate manifest.json and manifest_oauth.json into sing…
mwbrooks Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions claude-agent-sdk/.env.sample
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Required, set your Anthropic API key.
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY

# Optional, uncomment and set when running without the Slack CLI (python3 app.py).
# SLACK_APP_TOKEN=YOUR_SLACK_APP_TOKEN
# SLACK_BOT_TOKEN=YOUR_SLACK_BOT_TOKEN

# Optional, uncomment and set when running app_oauth.py without the Slack CLI.
# SLACK_SIGNING_SECRET=YOUR_SLACK_SIGNING_SECRET

# Required for OAuth (app_oauth.py). Set your app's OAuth credentials.
# SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
# SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
# SLACK_REDIRECT_URI=https://YOUR_NGROK_URL.ngrok-free.app/slack/oauth_redirect

# Optional, uncomment and set when using a custom Slack instance.
# SLACK_API_URL=YOUR_SLACK_API_URL

# Required, set your Anthropic API key.
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
3 changes: 3 additions & 0 deletions claude-agent-sdk/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ logs/
.pytype/
.idea/

# oauth data
data/

# claude
.claude/*.local.json
97 changes: 97 additions & 0 deletions claude-agent-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,95 @@ python3 app.py

</details>

<details><summary><strong>Using OAuth HTTP Server (with ngrok)</strong></summary>

#### OAuth HTTP Server

This mode uses an HTTP server instead of Socket Mode, which is required for OAuth-based distribution.

1. Install [ngrok](https://ngrok.com/download) and start a tunnel:

```sh
ngrok http 3000
```

2. Copy the `https://*.ngrok-free.app` URL from the ngrok output.

<details><summary><strong>Using Slack CLI</strong></summary>

#### Slack CLI

3. Swap the manifest files and update the request URL placeholders:

```sh
mv manifest.json manifest_socket_mode.json
mv manifest_oauth.json manifest.json
```

Replace all instances of `https://PLACEHOLDER.ngrok-free.app` in `manifest.json` with your ngrok URL.

4. Create a new local dev app:

```sh
slack install -E local
```

5. Copy the following values into your `.env`. Run `slack app settings` and copy the **Signing Secret**, **Client ID**, and **Client Secret**:

```sh
SLACK_SIGNING_SECRET=YOUR_SIGNING_SECRET
SLACK_CLIENT_ID=YOUR_CLIENT_ID
SLACK_CLIENT_SECRET=YOUR_CLIENT_SECRET
SLACK_REDIRECT_URI=https://YOUR_NGROK_URL.ngrok-free.app/slack/oauth_redirect
```

Replace `YOUR_NGROK_URL` in `SLACK_REDIRECT_URI` with your ngrok subdomain.

6. Start the app:

```sh
slack run app_oauth.py
```

7. Click the install URL printed in the terminal to install the app to your workspace via OAuth.

</details>

<details><summary><strong>Using the Terminal</strong></summary>

#### Terminal

3. Create your Slack app at [api.slack.com/apps/new](https://api.slack.com/apps/new) using [`manifest_oauth.json`](./manifest_oauth.json). Before pasting the manifest, replace all instances of `https://PLACEHOLDER.ngrok-free.app` with your ngrok URL.

4. Install the app to your workspace and copy the following values into your `.env`:
- **Signing Secret** — from _Basic Information_
- **Bot User OAuth Token** — from _OAuth & Permissions_
- **Client ID** and **Client Secret** — from _Basic Information_

```sh
SLACK_SIGNING_SECRET=YOUR_SIGNING_SECRET
SLACK_BOT_TOKEN=xoxb-YOUR_BOT_TOKEN
SLACK_CLIENT_ID=YOUR_CLIENT_ID
SLACK_CLIENT_SECRET=YOUR_CLIENT_SECRET
SLACK_REDIRECT_URI=https://YOUR_NGROK_URL.ngrok-free.app/slack/oauth_redirect
```

Replace `YOUR_NGROK_URL` in `SLACK_REDIRECT_URI` with your ngrok subdomain.

5. Start the app:

```sh
python3 app_oauth.py
```

6. Click the install URL printed in the terminal to install the app to your workspace via OAuth.

</details>

> **Note:** Each time ngrok restarts, it generates a new URL. You'll need to update the Request URL in your app's [Event Subscriptions](https://api.slack.com/apps) and [Interactivity](https://api.slack.com/apps) settings, or repeat the manifest setup steps above.

</details>

### Using the App

Once Casey is running, there are three ways to interact:
Expand Down Expand Up @@ -172,6 +261,14 @@ ruff format

`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.

### `app_oauth.py`

`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.

### `manifest_oauth.json`

`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`.

### `/listeners`

Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used.
Expand Down
20 changes: 17 additions & 3 deletions claude-agent-sdk/agent/casey.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
TextBlock,
create_sdk_mcp_server,
)
from claude_agent_sdk.types import McpHttpServerConfig

from agent.context import casey_deps_var
from agent.deps import CaseyDeps
Expand Down Expand Up @@ -95,7 +96,9 @@
],
)

ALLOWED_TOOLS = [
SLACK_MCP_URL = "https://mcp.slack.com/mcp"

CASEY_TOOLS = [
"add_emoji_reaction",
"check_system_status",
"create_support_ticket",
Expand Down Expand Up @@ -124,10 +127,21 @@ async def run_casey_agent(
if deps:
casey_deps_var.set(deps)

mcp_servers: dict = {"casey-tools": casey_tools_server}
allowed_tools = list(CASEY_TOOLS)

if deps and deps.user_token:
mcp_servers["slack-mcp"] = McpHttpServerConfig(
type="http",
url=SLACK_MCP_URL,
headers={"Authorization": f"Bearer {deps.user_token}"},
)
allowed_tools.append("mcp__slack-mcp__*")

options = ClaudeAgentOptions(
system_prompt=CASEY_SYSTEM_PROMPT,
mcp_servers={"casey-tools": casey_tools_server},
allowed_tools=ALLOWED_TOOLS,
mcp_servers=mcp_servers,
allowed_tools=allowed_tools,
permission_mode="bypassPermissions",
)

Expand Down
1 change: 1 addition & 0 deletions claude-agent-sdk/agent/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ class CaseyDeps:
channel_id: str
thread_ts: str
message_ts: str
user_token: str | None = None
41 changes: 41 additions & 0 deletions claude-agent-sdk/app_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging
import os

from dotenv import load_dotenv
from slack_bolt.async_app import AsyncApp
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.web.async_client import AsyncWebClient

from listeners import register_listeners
from oauth import BOT_SCOPES, USER_SCOPES, installation_store, state_store

load_dotenv(dotenv_path=".env", override=False)

logging.basicConfig(level=logging.DEBUG)

app = AsyncApp(
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
token=os.environ.get("SLACK_BOT_TOKEN"),
client=AsyncWebClient(
base_url=os.environ.get("SLACK_API_URL", "https://slack.com/api"),
token=os.environ.get("SLACK_BOT_TOKEN"),
),
# Allow bot-posted messages (e.g. issue modal submissions with metadata)
# to reach the message handler instead of being silently dropped
ignoring_self_events_enabled=False,
oauth_settings=OAuthSettings(
client_id=os.environ.get("SLACK_CLIENT_ID"),
client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
scopes=BOT_SCOPES,
user_scopes=USER_SCOPES,
installation_store=installation_store,
state_store=state_store,
),
)

register_listeners(app)

if __name__ == "__main__":
port = int(os.environ.get("PORT", 3000))
print(f"To install the app, navigate to http://localhost:{port}/slack/install")
app.start(port=port)
3 changes: 3 additions & 0 deletions claude-agent-sdk/listeners/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from slack_bolt.async_app import AsyncApp

from .account_connection import handle_connect_account, handle_disconnect_account
from .issue_buttons import handle_issue_button
from .feedback_buttons import handle_feedback_button


def register(app: AsyncApp):
app.action(re.compile(r"^category_"))(handle_issue_button)
app.action("feedback")(handle_feedback_button)
app.action("connect_account")(handle_connect_account)
app.action("disconnect_account")(handle_disconnect_account)
37 changes: 37 additions & 0 deletions claude-agent-sdk/listeners/actions/account_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from logging import Logger

from slack_bolt.context.async_context import AsyncBoltContext
from slack_sdk.web.async_client import AsyncWebClient

from listeners.views.app_home_builder import build_app_home_view


async def handle_connect_account(ack, logger: Logger):
"""Handle the Connect button click on App Home.

The Connect button is a URL button that opens the OAuth page in the
browser, so we only need to acknowledge the action.
"""
await ack()


async def handle_disconnect_account(
ack, client: AsyncWebClient, context: AsyncBoltContext, logger: Logger
):
"""Handle the Disconnect button click on App Home."""
await ack()
try:
from oauth import authorize_url_generator, installation_store, state_store

user_id = context.user_id
installation_store.delete_installation(
enterprise_id=context.enterprise_id or "",
team_id=context.team_id or "",
user_id=user_id,
)
state = state_store.issue()
authorize_url = authorize_url_generator.generate(state)
view = build_app_home_view(authorize_url=authorize_url)
await client.views_publish(user_id=user_id, view=view)
except Exception as e:
logger.exception(f"Failed to handle disconnect: {e}")
22 changes: 21 additions & 1 deletion claude-agent-sdk/listeners/events/app_home_opened.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from logging import Logger

from slack_bolt.context.async_context import AsyncBoltContext
Expand All @@ -12,7 +13,26 @@ async def handle_app_home_opened(
"""Publish the App Home view when a user opens the app's Home tab."""
try:
user_id = context.user_id
view = build_app_home_view()
authorize_url = None
is_connected = False

if os.environ.get("SLACK_CLIENT_ID"):
from oauth import authorize_url_generator, installation_store, state_store

installation = installation_store.find_installation(
enterprise_id=context.enterprise_id or "",
team_id=context.team_id or "",
user_id=user_id,
)
if installation and installation.user_token:
is_connected = True
else:
state = state_store.issue()
authorize_url = authorize_url_generator.generate(state)

view = build_app_home_view(
authorize_url=authorize_url, is_connected=is_connected
)
await client.views_publish(user_id=user_id, view=view)
except Exception as e:
logger.exception(f"Failed to publish App Home: {e}")
1 change: 1 addition & 0 deletions claude-agent-sdk/listeners/events/app_mentioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ async def handle_app_mentioned(
channel_id=channel_id,
thread_ts=thread_ts,
message_ts=event["ts"],
user_token=context.user_token,
)
response_text, new_session_id = await run_casey_agent(
cleaned_text, session_id=existing_session_id, deps=deps
Expand Down
1 change: 1 addition & 0 deletions claude-agent-sdk/listeners/events/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ async def handle_message(
channel_id=channel_id,
thread_ts=thread_ts,
message_ts=event["ts"],
user_token=context.user_token,
)
response_text, new_session_id = await run_casey_agent(
text, session_id=existing_session_id, deps=deps
Expand Down
Loading
Loading