Skip to content

Commit 75f5f71

Browse files
committed
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.
1 parent abc940f commit 75f5f71

8 files changed

Lines changed: 101 additions & 51 deletions

File tree

pydantic-ai/agent/casey.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import logging
12
import os
23

3-
from pydantic_ai import Agent
4+
from pydantic_ai import Agent, RunContext
45
from pydantic_ai.mcp import MCPServerStreamableHTTP
56

67
from agent.deps import CaseyDeps
@@ -76,6 +77,8 @@
7677
- If unsure about a user's issue, ask clarifying questions before taking action
7778
"""
7879

80+
logger = logging.getLogger(__name__)
81+
7982
_cached_model: str | None = None
8083

8184

@@ -117,16 +120,43 @@ def get_model() -> str:
117120
)
118121

119122

123+
@casey_agent.system_prompt
124+
def slack_mcp_prompt(ctx: RunContext[CaseyDeps]) -> str | None:
125+
"""Append Slack MCP Server instructions when the user token is available."""
126+
if not ctx.deps.user_token:
127+
return None
128+
return """\
129+
130+
## SLACK MCP SERVER
131+
You have access to the Slack MCP Server, which gives you powerful Slack tools beyond \
132+
your built-in IT helpdesk tools. Use them whenever they would help the user.
133+
134+
Available capabilities:
135+
- **Search**: Search messages and files across public channels, search for channels by name
136+
- **Read**: Read channel message history, read thread replies, read canvas documents
137+
- **Write**: Send messages, create draft messages, schedule messages for later
138+
- **Canvases**: Create, read, and update Slack canvas documents
139+
140+
Use these tools proactively when they can help resolve an IT issue — for example, \
141+
searching for related reports from other users, checking a channel for outage updates, \
142+
or creating a canvas to document a solution. Also use them when the user explicitly \
143+
asks you to perform a Slack action like sending a message or creating a canvas.
144+
"""
145+
146+
120147
def run_casey(text, deps, message_history=None):
121148
"""Run the Casey agent, optionally connecting to the Slack MCP server."""
122149
toolsets = []
123150
if deps.user_token:
151+
logger.info("Slack MCP Server enabled (user_token present)")
124152
toolsets.append(
125153
MCPServerStreamableHTTP(
126154
SLACK_MCP_URL,
127155
headers={"Authorization": f"Bearer {deps.user_token}"},
128156
)
129157
)
158+
else:
159+
logger.info("Slack MCP Server disabled (no user_token)")
130160

131161
return casey_agent.run_sync(
132162
text,

pydantic-ai/listeners/events/app_home_opened.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from logging import Logger
3+
from urllib.parse import urljoin
34

45
from slack_bolt import BoltContext
56
from slack_sdk import WebClient
@@ -11,11 +12,11 @@ def handle_app_home_opened(client: WebClient, context: BoltContext, logger: Logg
1112
"""Publish the App Home view when a user opens the app's Home tab."""
1213
try:
1314
user_id = context.user_id
14-
authorize_url = None
15+
install_url = None
1516
is_connected = False
1617

1718
if os.environ.get("SLACK_CLIENT_ID"):
18-
from oauth import authorize_url_generator, installation_store, state_store
19+
from oauth import installation_store
1920

2021
installation = installation_store.find_installation(
2122
enterprise_id=context.enterprise_id or "",
@@ -25,11 +26,11 @@ def handle_app_home_opened(client: WebClient, context: BoltContext, logger: Logg
2526
if installation and installation.user_token:
2627
is_connected = True
2728
else:
28-
state = state_store.issue()
29-
authorize_url = authorize_url_generator.generate(state)
29+
redirect_uri = os.environ.get("SLACK_REDIRECT_URI", "")
30+
install_url = urljoin(redirect_uri, "/slack/install")
3031

3132
view = build_app_home_view(
32-
authorize_url=authorize_url, is_connected=is_connected
33+
install_url=install_url, is_connected=is_connected
3334
)
3435
client.views_publish(user_id=user_id, view=view)
3536
except Exception as e:

pydantic-ai/listeners/events/app_mentioned.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from agent import CaseyDeps, run_casey
88
from thread_context import conversation_store
99
from listeners.views.feedback_builder import build_feedback_blocks
10+
from oauth import installation_store
1011

1112

1213
def handle_app_mentioned(
@@ -58,14 +59,25 @@ def handle_app_mentioned(
5859
# Get conversation history
5960
history = conversation_store.get_history(channel_id, thread_ts)
6061

62+
# Look up the user token directly from the installation store rather
63+
# than context.user_token. Bolt's InstallationStoreAuthorize may fail
64+
# to resolve the user token when installer-latest is missing or the
65+
# requesting user differs from the original installer.
66+
installation = installation_store.find_installation(
67+
enterprise_id=context.enterprise_id,
68+
team_id=context.team_id,
69+
user_id=user_id,
70+
)
71+
user_token = installation.user_token if installation else None
72+
6173
# Run the agent
6274
deps = CaseyDeps(
6375
client=client,
6476
user_id=user_id,
6577
channel_id=channel_id,
6678
thread_ts=thread_ts,
6779
message_ts=event["ts"],
68-
user_token=context.user_token,
80+
user_token=user_token,
6981
)
7082
result = run_casey(cleaned_text, deps, message_history=history)
7183

pydantic-ai/listeners/events/message.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from agent import CaseyDeps, run_casey
77
from thread_context import conversation_store
88
from listeners.views.feedback_builder import build_feedback_blocks
9+
from oauth import installation_store
910

1011

1112
def handle_message(
@@ -81,14 +82,25 @@ def handle_message(
8182
],
8283
)
8384

85+
# Look up the user token directly from the installation store rather
86+
# than context.user_token. Bolt's InstallationStoreAuthorize may fail
87+
# to resolve the user token when installer-latest is missing or the
88+
# requesting user differs from the original installer.
89+
installation = installation_store.find_installation(
90+
enterprise_id=context.enterprise_id,
91+
team_id=context.team_id,
92+
user_id=user_id,
93+
)
94+
user_token = installation.user_token if installation else None
95+
8496
# Run the agent
8597
deps = CaseyDeps(
8698
client=client,
8799
user_id=user_id,
88100
channel_id=channel_id,
89101
thread_ts=thread_ts,
90102
message_ts=event["ts"],
91-
user_token=context.user_token,
103+
user_token=user_token,
92104
)
93105
result = run_casey(text, deps, message_history=history)
94106

pydantic-ai/listeners/views/app_home_builder.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,15 @@
2828

2929

3030
def build_app_home_view(
31-
authorize_url: str | None = None, is_connected: bool = False
31+
install_url: str | None = None, is_connected: bool = False
3232
) -> dict:
3333
"""Build the App Home Block Kit view with category buttons.
3434
3535
Args:
36-
authorize_url: OAuth authorize URL. When provided, the user has not
37-
connected and will see a "Connect" URL button.
38-
is_connected: When ``True``, the user is connected and will see a
39-
"Disconnect" action button. When both params are default, the
40-
OAuth section is hidden (app.py mode).
36+
install_url: OAuth install URL. When provided, the user has not
37+
connected and will see a link to install.
38+
is_connected: When ``True``, the user is connected and the MCP
39+
status section shows as connected.
4140
"""
4241
blocks = [
4342
{
@@ -109,13 +108,13 @@ def build_app_home_view(
109108
],
110109
}
111110
)
112-
elif authorize_url:
111+
elif install_url:
113112
blocks.append(
114113
{
115114
"type": "section",
116115
"text": {
117116
"type": "mrkdwn",
118-
"text": f"🔴 *Slack MCP Server is disconnected.* <{authorize_url}|Connect the Slack MCP Server.>",
117+
"text": f"🔴 *Slack MCP Server is disconnected.* <{install_url}|Connect the Slack MCP Server.>",
119118
},
120119
}
121120
)

pydantic-ai/manifest_oauth.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@
3030
"groups:read",
3131
"im:history",
3232
"mpim:history",
33-
"users:read"
33+
"users:read",
34+
"search:read.public",
35+
"search:read.private",
36+
"search:read.mpim",
37+
"search:read.im",
38+
"search:read.files",
39+
"search:read.users",
40+
"chat:write",
41+
"canvases:read",
42+
"canvases:write",
43+
"users:read.email"
3444
],
3545
"bot": [
3646
"app_mentions:read",

pydantic-ai/oauth.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from slack_bolt.authorization.authorize_result import AuthorizeResult
77
from slack_bolt.oauth.oauth_settings import OAuthSettings
88
from slack_sdk import WebClient
9-
from slack_sdk.oauth import AuthorizeUrlGenerator
109
from slack_sdk.oauth.installation_store import FileInstallationStore
1110
from slack_sdk.oauth.state_store import FileOAuthStateStore
1211

@@ -28,21 +27,13 @@
2827

2928
installation_store = FileInstallationStore(
3029
base_dir="./data/installations",
31-
historical_data_enabled=False,
3230
)
3331

3432
state_store = FileOAuthStateStore(
3533
expiration_seconds=600,
3634
base_dir="./data/states",
3735
)
3836

39-
authorize_url_generator = AuthorizeUrlGenerator(
40-
client_id=os.environ.get("SLACK_CLIENT_ID", ""),
41-
redirect_uri=os.environ.get("SLACK_REDIRECT_URI", ""),
42-
scopes=BOT_SCOPES,
43-
user_scopes=USER_SCOPES,
44-
)
45-
4637
oauth_settings = OAuthSettings(
4738
client_id=os.environ.get("SLACK_CLIENT_ID"),
4839
client_secret=os.environ.get("SLACK_CLIENT_SECRET"),

pydantic-ai/tests/test_view_builders.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_build_feedback_blocks():
2626

2727

2828
def test_build_app_home_view_default():
29-
"""Default args (app.py mode) — no OAuth section."""
29+
"""Default args (app.py mode) — no MCP status section."""
3030
view = build_app_home_view()
3131

3232
assert view["type"] == "home"
@@ -35,36 +35,31 @@ def test_build_app_home_view_default():
3535
actions_block = next(b for b in view["blocks"] if b["type"] == "actions")
3636
assert len(actions_block["elements"]) == len(CATEGORIES)
3737

38-
# No connect or disconnect buttons
39-
section_blocks = [b for b in view["blocks"] if b["type"] == "section"]
40-
accessory_actions = [
41-
b["accessory"]["action_id"] for b in section_blocks if "accessory" in b
38+
# No MCP status section
39+
section_texts = [
40+
b["text"]["text"] for b in view["blocks"] if b["type"] == "section"
4241
]
43-
assert "connect_account" not in accessory_actions
44-
assert "disconnect_account" not in accessory_actions
42+
assert not any("Slack MCP Server" in t for t in section_texts)
4543

4644

4745
def test_build_app_home_view_connect():
48-
"""authorize_url provided — shows Connect URL button."""
49-
view = build_app_home_view(authorize_url="https://example.com/oauth")
46+
"""install_url provided — shows disconnected status with install link."""
47+
view = build_app_home_view(install_url="https://example.com/slack/install")
5048

51-
section_blocks = [b for b in view["blocks"] if b["type"] == "section"]
52-
connect_section = next(
53-
b
54-
for b in section_blocks
55-
if b.get("accessory", {}).get("action_id") == "connect_account"
56-
)
57-
assert connect_section["accessory"]["url"] == "https://example.com/oauth"
49+
section_texts = [
50+
b["text"]["text"] for b in view["blocks"] if b["type"] == "section"
51+
]
52+
mcp_section = next(t for t in section_texts if "Slack MCP Server" in t)
53+
assert "disconnected" in mcp_section
54+
assert "https://example.com/slack/install" in mcp_section
5855

5956

60-
def test_build_app_home_view_disconnect():
61-
"""is_connected=True — shows Disconnect button."""
57+
def test_build_app_home_view_connected():
58+
"""is_connected=True — shows connected status."""
6259
view = build_app_home_view(is_connected=True)
6360

64-
section_blocks = [b for b in view["blocks"] if b["type"] == "section"]
65-
disconnect_section = next(
66-
b
67-
for b in section_blocks
68-
if b.get("accessory", {}).get("action_id") == "disconnect_account"
69-
)
70-
assert disconnect_section["accessory"]["style"] == "danger"
61+
section_texts = [
62+
b["text"]["text"] for b in view["blocks"] if b["type"] == "section"
63+
]
64+
mcp_section = next(t for t in section_texts if "Slack MCP Server" in t)
65+
assert "connected" in mcp_section

0 commit comments

Comments
 (0)