Skip to content

Commit 3a68555

Browse files
authored
Merge pull request #58 from maia-iyer/slack_tool_jwks_validation
⚠️ Slack tool jwks validation
2 parents 3e86c06 + 1c1aab1 commit 3a68555

4 files changed

Lines changed: 37 additions & 155 deletions

File tree

mcp/slack_tool/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,13 @@ You can configure the server with the following environment variables:
1212
| `SLACK_BOT_TOKEN` | Yes | - | Bot token for the Slack server. Required for any functionality |
1313
| `LOG_LEVEL` | No | `DEBUG` | Application log level |
1414
| `MCP_TRANSPORT` | No | `streamable-http` | Passed into mcp.run to determine mcp transport |
15-
| `ISSUER` | No | - | If populated, will publish that it is OAuth-secured by this issuer (but no actual verification). Must be URI format |
16-
| `INTROSPECTION_ENDPOINT` | No | - | If populated with `CLIENT_ID` and `CLIENT_SECRET`, will introspect access tokens at this endpoint |
17-
| `CLIENT_ID` | No | - | If populated with `INTROSPECTION_ENDPOINT` and `CLIENT_SECRET`, will introspect access tokens using this as the client id to authenticate |
18-
| `CLIENT_SECRET` | No | - | If populated with `INTROSPECTION_ENDPOINT` and `CLIENT_ID`, will introspect access tokens using this as the client secret to authenticate |
19-
| `AUDIENCE ` | No | - | If populated with `INTROSPECTION_ENDPOINT` will perform audience validation |
15+
| `JWKS_URI` | No | - | If populated, will perform token validation using the JWKS endpoint |
16+
| `ISSUER` | No | - | If populated with `JWKS_URI`, will additionally check the `iss` claim during token validation |
17+
| `AUDIENCE` | No | - | If populated with `JWKS_URI`, will additionally check the `aud` claim during token validation |
2018
| `ADMIN_SLACK_BOT_TOKEN` | No | - | Bot token for Slack server with Admin privileges. Required for fine grained authz |
2119
| `ADMIN_SCOPE_NAME` | No | - | Scope that triggers `ADMIN_SLACK_BOT_TOKEN` to be used |
2220

23-
Note: `ISSUER` only affects the published authorization endpoint. All three of `INTROSPECTION_ENDPOINT`, `CLIENT_ID`, and `CLIENT_SECRET` are required for token validation to occur. `AUDIENCE` enables the additional audience check.
21+
Note: `JWKS_URI` triggers token validation at runtime. `ISSUER` and `AUDIENCE` will not affect behavior if `JWKS_URI` is not implemented.
2422

2523
Note: Fine-grained authz is enabled with `ADMIN_SLACK_BOT_TOKEN` and `ADMIN_SCOPE_NAME`. If a received access token includes the `ADMIN_SCOPE_NAME` as a scope, it will use the `ADMIN_SLACK_BOT_TOKEN`
2624

mcp/slack_tool/auth.py

Lines changed: 0 additions & 126 deletions
This file was deleted.

mcp/slack_tool/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.10"
77
dependencies = [
88
"httpx>=0.28.1",
9-
"mcp[cli]>=1.12.3",
9+
"fastmcp>=2.12.1",
1010
"requests>=2.32.3",
1111
"slack_sdk>=3.36.0",
1212
]

mcp/slack_tool/slack_tool.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import sys
33
import logging
44
from typing import List, Dict, Any
5-
from mcp.server.fastmcp import FastMCP
6-
from mcp.server.auth.middleware.auth_context import get_access_token
5+
from fastmcp import FastMCP
6+
from fastmcp.server.dependencies import get_access_token, AccessToken
7+
from fastmcp.server.auth.providers.jwt import JWTVerifier
78
from slack_sdk import WebClient
89
from slack_sdk.errors import SlackApiError
9-
from auth import get_token_verifier, get_auth
1010

1111
logger = logging.getLogger(__name__)
1212
logging.basicConfig(level=os.getenv("LOG_LEVEL", "DEBUG"), stream=sys.stdout, format='%(levelname)s: %(message)s')
@@ -29,15 +29,17 @@ def slack_client_from_bot_token(bot_token):
2929
logger.exception(f"An unexpected error occurred during Slack client initialization: {e}")
3030
return None
3131

32-
def get_slack_client(access_token = None):
32+
def get_slack_client(access_token=None):
3333
if ADMIN_SLACK_BOT_TOKEN is None:
3434
logger.debug(f"No ADMIN_SLACK_BOT_TOKEN configured - automatically configuring based on SLACK_BOT_TOKEN. ")
3535
return slack_client_from_bot_token(SLACK_BOT_TOKEN)
3636
# we do fine-grained authz
3737
if access_token is None:
3838
logger.error(f"ADMIN_SLACK_BOT_TOKEN configured, but no access token passed. ")
3939
return None
40-
access_token_scopes = access_token.scopes
40+
41+
# Access token is now claims dict from FastMCP AccessToken
42+
access_token_scopes = access_token.get("scope", "").split() if access_token.get("scope") else []
4143
logger.debug(f"Received scopes: {access_token_scopes}")
4244
admin_scope = os.getenv("ADMIN_SCOPE_NAME")
4345
is_admin = admin_scope in access_token_scopes
@@ -47,10 +49,19 @@ def get_slack_client(access_token = None):
4749
return slack_client_from_bot_token(SLACK_BOT_TOKEN)
4850

4951

50-
mcp = FastMCP("Slack", host="0.0.0.0", port=8000,
51-
token_verifier=get_token_verifier(),
52-
auth=get_auth(),
53-
)
52+
# Create FastMCP app
53+
# Temporary environment variables to manually create verifier
54+
verifier = None
55+
JWKS_URI = os.getenv("JWKS_URI")
56+
ISSUER = os.getenv("ISSUER")
57+
AUDIENCE = os.getenv("AUDIENCE")
58+
if not JWKS_URI is None:
59+
verifier = JWTVerifier(
60+
jwks_uri = JWKS_URI,
61+
issuer = ISSUER,
62+
audience = AUDIENCE
63+
)
64+
mcp = FastMCP("Slack", auth=verifier)
5465

5566
@mcp.tool()
5667
def get_channels() -> List[Dict[str, Any]]:
@@ -59,8 +70,9 @@ def get_channels() -> List[Dict[str, Any]]:
5970
"""
6071
logger.debug(f"Called get_channels tool")
6172

62-
63-
slack_client = get_slack_client(access_token=get_access_token())
73+
# Get the current authenticated user's token using FastMCP 2.0 dependency
74+
access_token: AccessToken | None = get_access_token()
75+
slack_client = get_slack_client(access_token=access_token.claims if access_token else None)
6476
if slack_client is None:
6577
return [{"error": f"Could not start slack client. Check the configured bot token"}]
6678

@@ -93,7 +105,8 @@ def get_channel_history(channel_id: str, limit: int = 20) -> List:
93105
"""
94106
logger.debug(f"Called get_channel_history tool: {channel_id}")
95107

96-
slack_client = get_slack_client(access_token=get_access_token())
108+
access_token: AccessToken | None = get_access_token()
109+
slack_client = get_slack_client(access_token=access_token.claims if access_token else None)
97110
if slack_client is None:
98111
return [{"error": f"Could not start slack client. Check the configured bot token"}]
99112

@@ -115,25 +128,22 @@ def get_channel_history(channel_id: str, limit: int = 20) -> List:
115128
# transport can be specified with MCP_TRANSPORT env variable (defaults to streamable-http)
116129
def run_server():
117130
transport = os.getenv("MCP_TRANSPORT", "streamable-http")
118-
mcp.run(transport=transport)
131+
host = os.getenv("HOST", "0.0.0.0")
132+
port = int(os.getenv("PORT", "8000"))
133+
mcp.run(transport=transport, host=host, port=port)
119134

120135
if __name__ == "__main__":
121136
if SLACK_BOT_TOKEN is None: # default slack token
122137
logger.warning("Please configure the SLACK_BOT_TOKEN environment variable before running the server")
123138
elif ADMIN_SLACK_BOT_TOKEN is None: # one token set -> we just validate the JWT
124-
logger.info("Configured SLACK_BOT_TOKEN environment variable but not ADMIN_SLACK_BOT_TOKEN; will validate token signature")
139+
logger.info("Configured SLACK_BOT_TOKEN environment variable but not ADMIN_SLACK_BOT_TOKEN; all requests will use the `SLACK_BOT_TOKEN` to reach the Slack API")
125140
logger.info("Starting Slack MCP Server")
126141
run_server()
127142
else: # two tokens set -> we validate the JWT and connect to slack based on access token scope
128143
# check if other required Auth variables are set
129-
introspection_endpoint = os.getenv("INTROSPECTION_ENDPOINT")
130-
client_id = os.getenv("CLIENT_ID")
131-
client_secret = os.getenv("CLIENT_SECRET")
132-
expected_audience = os.getenv("AUDIENCE")
133-
admin_scope = os.getenv("ADMIN_SCOPE_NAME")
134-
issuer = os.getenv("ISSUER")
135-
if None in [introspection_endpoint, client_id, client_secret, expected_audience, issuer, admin_scope]:
136-
logger.error("Configured ADMIN_SLACK_BOT_TOKEN but not one or more of INTROSPECTION_ENDPOINT, CLIENT_ID, CLIENT_SECRET, AUDIENCE, ISSUER. ")
144+
auth = os.getenv("JWKS_URI")
145+
if auth is None:
146+
logger.error("Configured ADMIN_SLACK_BOT_TOKEN but auth is not configured - fine grained authz requires token validation")
137147
else:
138148
logger.info("Configured two slack tokens; finer-grained authz enabled")
139149
logger.info("Starting Slack MCP Server")

0 commit comments

Comments
 (0)