Skip to content

Commit 7890092

Browse files
authored
Mcp auth (#364)
* Add auth to mcp
1 parent 55c81ad commit 7890092

32 files changed

Lines changed: 1035 additions & 177 deletions

File tree

.github/workflows/pr-build.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,9 @@ jobs:
252252
pip3 install virtualenv
253253
virtualenv venv
254254
source venv/bin/activate
255-
pip3 install -r requirements.txt
256-
pip3 install -r dev-requirements.txt
255+
pip install --upgrade pip setuptools
256+
pip install -r requirements.txt
257+
pip install -r dev-requirements.txt
257258
mkdir -p test-results
258259
source .env
259260
IS_TESTING=True python3 manage.py test --no-input --testrunner xmlrunner.extra.djangotestrunner.XMLTestRunner

deploy/docker/docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ services:
154154
crapi-chatbot:
155155
container_name: crapi-chatbot
156156
image: crapi/crapi-chatbot:${VERSION:-latest}
157+
ports:
158+
- "${LISTEN_IP:-127.0.0.1}:5500:5500" # MCP server
157159
environment:
158160
- TLS_ENABLED=${TLS_ENABLED:-false}
159161
- SERVER_PORT=${CHATBOT_SERVER_PORT:-5002}

deploy/helm/templates/chatbot/deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ spec:
5151
imagePullPolicy: {{ .Values.imagePullPolicy }}
5252
ports:
5353
- containerPort: {{ .Values.chatbot.port }}
54+
- containerPort: {{ .Values.chatbot.mcpPort }}
5455
envFrom:
5556
- configMapRef:
5657
name: {{ .Values.chatbot.config.name }}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: {{ .Values.chatbot.service.name }}-mcp
5+
labels:
6+
release: {{ .Release.Name }}
7+
{{- with .Values.chatbot.service.labels }}
8+
{{- toYaml . | nindent 4 }}
9+
{{- end }}
10+
spec:
11+
ports:
12+
- name: mcp
13+
port: {{ .Values.chatbot.mcpPort }}
14+
nodePort: {{ .Values.chatbot.service.mcpNodePort }}
15+
protocol: TCP
16+
selector:
17+
{{- toYaml .Values.chatbot.serviceSelectorLabels | nindent 4 }}
18+
sessionAffinity: None
19+
type: LoadBalancer

deploy/helm/templates/chatbot/service.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ metadata:
1010
spec:
1111
ports:
1212
- port: {{ .Values.chatbot.port }}
13-
name: python
13+
name: chatbot
14+
- port: {{ .Values.chatbot.mcpPort }}
15+
name: mcp
1416
selector:
1517
{{- toYaml .Values.chatbot.serviceSelectorLabels | nindent 4 }}

deploy/helm/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,13 @@ chatbot:
188188
name: crapi-chatbot
189189
image: crapi/crapi-chatbot
190190
port: 5002
191+
mcpPort: 5500
191192
replicaCount: 1
192193
service:
193194
name: crapi-chatbot
194195
labels:
195196
app: crapi-chatbot
197+
mcpNodePort: 30500
196198
config:
197199
name: crapi-chatbot-configmap
198200
labels:

deploy/k8s/base/chatbot/deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ spec:
1818
imagePullPolicy: Always
1919
ports:
2020
- containerPort: 5002
21+
- containerPort: 5500
2122
envFrom:
2223
- configMapRef:
2324
name: crapi-chatbot-configmap
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: crapi-chatbot-mcp
5+
labels:
6+
app: crapi-chatbot
7+
spec:
8+
ports:
9+
- name: mcp
10+
port: 5500
11+
nodePort: 30500
12+
protocol: TCP
13+
selector:
14+
app: crapi-chatbot
15+
sessionAffinity: None
16+
type: LoadBalancer

deploy/k8s/base/chatbot/service.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ metadata:
77
spec:
88
ports:
99
- port: 5002
10-
name: go
10+
name: chatbot
11+
- port: 5500
12+
name: mcp
1113
selector:
1214
app: crapi-chatbot

services/chatbot/src/chatbot/aws_credentials.py

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ def _get_base_session():
4545
logger.info(
4646
"[BASE_SESSION] Creating boto3 session - region: %s, has_access_key: %s, "
4747
"has_secret_key: %s, has_session_token: %s, will_use_instance_profile: %s",
48-
region, has_access_key, has_secret_key, has_session_token,
49-
not (has_access_key and has_secret_key)
48+
region,
49+
has_access_key,
50+
has_secret_key,
51+
has_session_token,
52+
not (has_access_key and has_secret_key),
5053
)
5154
# Use None for empty strings so boto3 falls back to instance profile/IRSA
5255
session = boto3.Session(
@@ -60,8 +63,8 @@ def _get_base_session():
6063
if creds:
6164
logger.info(
6265
"[BASE_SESSION] Session created - credential_method: %s, access_key_prefix: %s",
63-
creds.method if hasattr(creds, 'method') else 'unknown',
64-
creds.access_key[:8] + "..." if creds and creds.access_key else "(none)"
66+
creds.method if hasattr(creds, "method") else "unknown",
67+
creds.access_key[:8] + "..." if creds and creds.access_key else "(none)",
6568
)
6669
else:
6770
logger.warning("[BASE_SESSION] Session created but NO credentials found!")
@@ -76,7 +79,9 @@ def _assume_role() -> dict:
7679

7780
logger.info(
7881
"[ASSUME_ROLE] Starting assume role - role_arn: %s, session_name: %s, has_external_id: %s",
79-
role_arn, session_name, bool(external_id)
82+
role_arn,
83+
session_name,
84+
bool(external_id),
8085
)
8186

8287
try:
@@ -85,7 +90,8 @@ def _assume_role() -> dict:
8590
except Exception as e:
8691
logger.error(
8792
"Failed to create base session for assume role - role_arn: %s, error: %s",
88-
role_arn, str(e)
93+
role_arn,
94+
str(e),
8995
)
9096
raise
9197

@@ -95,7 +101,8 @@ def _assume_role() -> dict:
95101
except Exception as e:
96102
logger.error(
97103
"Failed to create STS client for assume role - role_arn: %s, error: %s",
98-
role_arn, str(e)
104+
role_arn,
105+
str(e),
99106
)
100107
raise
101108

@@ -109,7 +116,10 @@ def _assume_role() -> dict:
109116
assume_role_kwargs["ExternalId"] = external_id
110117
logger.debug("External ID configured for assume role")
111118

112-
logger.debug("Calling STS assume_role with kwargs: %s", {k: v for k, v in assume_role_kwargs.items() if k != "ExternalId"})
119+
logger.debug(
120+
"Calling STS assume_role with kwargs: %s",
121+
{k: v for k, v in assume_role_kwargs.items() if k != "ExternalId"},
122+
)
113123

114124
try:
115125
logger.info("[ASSUME_ROLE] Calling sts:AssumeRole...")
@@ -123,7 +133,9 @@ def _assume_role() -> dict:
123133
session_name,
124134
credentials["Expiration"],
125135
response.get("AssumedRoleUser", {}).get("AssumedRoleId", "unknown"),
126-
credentials["AccessKeyId"][:8] + "..." if credentials.get("AccessKeyId") else "(none)",
136+
credentials["AccessKeyId"][:8] + "..."
137+
if credentials.get("AccessKeyId")
138+
else "(none)",
127139
)
128140

129141
return {
@@ -135,7 +147,10 @@ def _assume_role() -> dict:
135147
except Exception as e:
136148
logger.error(
137149
"[ASSUME_ROLE] FAILED - role_arn: %s, session_name: %s, error_type: %s, error: %s",
138-
role_arn, session_name, type(e).__name__, str(e)
150+
role_arn,
151+
session_name,
152+
type(e).__name__,
153+
str(e),
139154
)
140155
raise
141156

@@ -152,13 +167,14 @@ def _get_cached_credentials() -> Optional[dict]:
152167
if time_until_expiry <= CREDENTIALS_REFRESH_BUFFER_SECONDS:
153168
logger.info(
154169
"Cached credentials expiring soon - time_until_expiry: %.0f seconds, refresh_buffer: %d seconds",
155-
time_until_expiry, CREDENTIALS_REFRESH_BUFFER_SECONDS
170+
time_until_expiry,
171+
CREDENTIALS_REFRESH_BUFFER_SECONDS,
156172
)
157173
return None
158174

159175
logger.debug(
160176
"Using cached credentials - time_until_expiry: %.0f seconds",
161-
time_until_expiry
177+
time_until_expiry,
162178
)
163179
return _credentials_cache["credentials"]
164180

@@ -169,8 +185,7 @@ def _set_cached_credentials(credentials: dict) -> None:
169185
_credentials_cache["credentials"] = credentials
170186
_credentials_cache["expiration"] = credentials["expiry_time"]
171187
logger.debug(
172-
"Cached new credentials - expires_at: %s",
173-
credentials["expiry_time"]
188+
"Cached new credentials - expires_at: %s", credentials["expiry_time"]
174189
)
175190

176191

@@ -192,21 +207,22 @@ def get_aws_credentials() -> dict:
192207
Config.AWS_ASSUME_ROLE_ARN or "(not set)",
193208
bool(os.getenv("AWS_ACCESS_KEY_ID")),
194209
bool(os.getenv("AWS_SECRET_ACCESS_KEY")),
195-
bool(Config.AWS_BEARER_TOKEN_BEDROCK)
210+
bool(Config.AWS_BEARER_TOKEN_BEDROCK),
196211
)
197212

198213
# If assume role is configured, use it
199214
if Config.AWS_ASSUME_ROLE_ARN:
200215
logger.info(
201-
"[AWS_CREDS] Assume role path - role_arn: %s",
202-
Config.AWS_ASSUME_ROLE_ARN
216+
"[AWS_CREDS] Assume role path - role_arn: %s", Config.AWS_ASSUME_ROLE_ARN
203217
)
204218
# Try to use cached credentials
205219
cached = _get_cached_credentials()
206220
if cached:
207221
logger.info(
208222
"[AWS_CREDS] Using CACHED assume role credentials - access_key_prefix: %s",
209-
cached["access_key"][:8] + "..." if cached.get("access_key") else "(none)"
223+
cached["access_key"][:8] + "..."
224+
if cached.get("access_key")
225+
else "(none)",
210226
)
211227
return {
212228
"access_key": cached["access_key"],
@@ -221,7 +237,9 @@ def get_aws_credentials() -> dict:
221237
_set_cached_credentials(credentials)
222238
logger.info(
223239
"[AWS_CREDS] Assume role succeeded - access_key_prefix: %s",
224-
credentials["access_key"][:8] + "..." if credentials.get("access_key") else "(none)"
240+
credentials["access_key"][:8] + "..."
241+
if credentials.get("access_key")
242+
else "(none)",
225243
)
226244
return {
227245
"access_key": credentials["access_key"],
@@ -239,7 +257,7 @@ def get_aws_credentials() -> dict:
239257
logger.info(
240258
"[AWS_CREDS] Using STATIC credentials from env - access_key_prefix: %s, has_session_token: %s",
241259
access_key[:8] + "..." if access_key else "(none)",
242-
bool(session_token)
260+
bool(session_token),
243261
)
244262
result = {
245263
"access_key": access_key,
@@ -280,15 +298,23 @@ def get_bedrock_client():
280298
281299
This bypasses any token-based auth that botocore might pick up from env vars.
282300
"""
283-
logger.info("[BEDROCK_CLIENT] Creating bedrock-runtime client with explicit credentials")
301+
logger.info(
302+
"[BEDROCK_CLIENT] Creating bedrock-runtime client with explicit credentials"
303+
)
284304
credentials = get_aws_credentials()
285-
region = credentials.get("region") or os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION")
305+
region = (
306+
credentials.get("region")
307+
or os.getenv("AWS_REGION")
308+
or os.getenv("AWS_DEFAULT_REGION")
309+
)
286310

287311
logger.info(
288312
"[BEDROCK_CLIENT] Using credentials - access_key_prefix: %s, has_token: %s, region: %s",
289-
credentials["access_key"][:8] + "..." if credentials.get("access_key") else "(none)",
313+
credentials["access_key"][:8] + "..."
314+
if credentials.get("access_key")
315+
else "(none)",
290316
bool(credentials.get("token")),
291-
region
317+
region,
292318
)
293319

294320
# Create client with explicit credentials, bypassing any default chain or token discovery
@@ -304,7 +330,9 @@ def get_bedrock_client():
304330
retries={"max_attempts": 3},
305331
),
306332
)
307-
logger.info("[BEDROCK_CLIENT] Client created successfully with explicit credentials")
333+
logger.info(
334+
"[BEDROCK_CLIENT] Client created successfully with explicit credentials"
335+
)
308336
return client
309337

310338

@@ -323,11 +351,16 @@ def get_bedrock_credentials_kwargs() -> dict:
323351
except Exception as e:
324352
logger.error(
325353
"[BEDROCK_KWARGS] Failed to get credentials: %s - %s",
326-
type(e).__name__, str(e)
354+
type(e).__name__,
355+
str(e),
327356
)
328357
raise
329358

330-
region = credentials.get("region") or os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION")
359+
region = (
360+
credentials.get("region")
361+
or os.getenv("AWS_REGION")
362+
or os.getenv("AWS_DEFAULT_REGION")
363+
)
331364

332365
# Pass explicit credentials to ChatBedrock/BedrockEmbeddings
333366
# This ensures we use SigV4 signing, not any token-based auth
@@ -343,9 +376,11 @@ def get_bedrock_credentials_kwargs() -> dict:
343376

344377
logger.info(
345378
"[BEDROCK_KWARGS] Using explicit credentials - access_key_prefix: %s, has_token: %s, region: %s",
346-
credentials["access_key"][:8] + "..." if credentials.get("access_key") else "(none)",
379+
credentials["access_key"][:8] + "..."
380+
if credentials.get("access_key")
381+
else "(none)",
347382
bool(credentials.get("token")),
348-
region
383+
region,
349384
)
350385

351386
return kwargs

0 commit comments

Comments
 (0)