Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2b29744
feat: implement am-mcp-gateway service with LLM routing, JWT authenti…
sahim99 Jun 13, 2026
e1c328a
ci: add automated and manual build/deploy workflows for am-mcp-gateway
sahim99 Jun 13, 2026
73fe794
feat: add npm workspaces configuration and scripts for am-mcp-gateway
sahim99 Jun 13, 2026
887dbe6
feat: integrate am-mcp-gateway API endpoints and environment variable…
sahim99 Jun 13, 2026
afd8711
fix(mcp-gateway): align Dockerfile to multi-module build context to f…
sahim99 Jun 13, 2026
3867c58
fix(mcp-gateway): template REDIS_URL from host, port, and password fi…
sahim99 Jun 13, 2026
3762115
fix(mcp-gateway): bypass strict expected_audience check to allow user…
sahim99 Jun 13, 2026
6321a4c
fix(mcp-gateway): update LLM model, fix prod vault paths, and sync OI…
sahim99 Jun 13, 2026
27f6ee0
feat(mcp-gateway): add dev:preprod scripts and env file support
sahim99 Jun 13, 2026
0bd9674
refactor(mcp-gateway): simplify package.json script names to preprod …
sahim99 Jun 13, 2026
59a831b
feat(mcp-gateway): support module-specific and environment-specific e…
sahim99 Jun 13, 2026
935ab35
feat(mcp-gateway): enhance LLM provider integration with new scripts,…
sahim99 Jun 14, 2026
a54ab40
Merge branch 'feature/am-mcp-gateway' of https://github.com/AM-Portfo…
sahim99 Jun 14, 2026
d6dfd8a
feat(observability): refactor MLflow logging to support async operations
sahim99 Jun 14, 2026
4844f6d
fix(ai-gateway): update model configuration for Qwen to use Qwen3-VL-…
sahim99 Jun 14, 2026
55ebf75
feat(user-identity): update user claim extraction to prioritize userI…
sahim99 Jul 12, 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
40 changes: 40 additions & 0 deletions .github/workflows/am-mcp-gateway.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: AM MCP Gateway Publish

on:
push:
branches: ["main", "develop", "test/**", "feature/**", "hotfix/**", "fix/**"]
paths:
- "am-mcp-gateway/**"
- "libraries/**"
- "requirements.txt"
- "requirements-dev.txt"
- ".dockerignore"
- "docker/**"
pull_request:
branches: ["main", "develop", "test/**", "feature/**", "hotfix/**", "fix/**"]
paths:
- "am-mcp-gateway/**"
- "libraries/**"
- "requirements.txt"
- "requirements-dev.txt"
- ".dockerignore"
- "docker/**"
workflow_dispatch:

jobs:
publish:
name: Publish AM MCP Gateway
permissions:
contents: read
packages: write
uses: AM-Portfolio/am-pipelines/.github/workflows/central-build-publish.yml@main
with:
language: "python"
working_directory: "am-mcp-gateway"
image_name: "am-mcp-gateway"
build_context: ".."
preprod_namespace: "am-apps-preprod"
prod_namespace: "am-apps-prod"
deploy_dev: true
deploy_prod: true
secrets: inherit
38 changes: 38 additions & 0 deletions .github/workflows/deploy-am-mcp-gateway.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Manual Deploy (AM MCP Gateway)

on:
workflow_dispatch:
inputs:
image_tag:
description: "Select Image Tag"
required: true
default: "latest"
type: choice
options:
- latest
- custom
custom_tag:
description: "If 'custom' selected, enter tag here"
required: false
type: string
environment:
description: "Target Environment"
required: true
default: "preprod"
type: choice
options:
- preprod
- prod

jobs:
deploy:
name: Deploy AM MCP Gateway to ${{ github.event.inputs.environment }}
uses: AM-Portfolio/am-pipelines/.github/workflows/central-deploy.yml@main
with:
service_name: "am-mcp-gateway"
working_directory: "am-mcp-gateway"
language: "python"
environment: ${{ github.event.inputs.environment }}
image_tag: ${{ github.event.inputs.image_tag == 'custom' && github.event.inputs.custom_tag || github.event.inputs.image_tag }}
namespace: ${{ github.event.inputs.environment == 'prod' && 'am-apps-prod' || 'am-apps-preprod' }}
secrets: inherit
2 changes: 1 addition & 1 deletion am-identity/am_identity/api/user_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
def _profile_from_claims(claims: dict[str, Any]) -> dict[str, Any]:
"""Build profile from validated JWT when Keycloak userinfo is unavailable."""
return {
"sub": claims.get("sub", ""),
"sub": claims.get("userId") or claims.get("sub", ""),
"email": claims.get("email"),
"preferred_username": claims.get("preferred_username"),
"given_name": claims.get("given_name"),
Expand Down
31 changes: 31 additions & 0 deletions am-mcp-gateway/.env.preprod
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ── App Environment ────────────────────────────────────────────────────────
APP_ENV=preprod
LOG_LEVEL=DEBUG
LOG_FORMAT=text

# ── OIDC / Keycloak (am-preprod-realm) ────────────────────────────────────
OIDC_JWKS_URL=http://auth.munish.org/auth/realms/am-preprod-realm/protocol/openid-connect/certs
OIDC_ISSUER=http://auth.munish.org/auth/realms/am-preprod-realm

# ── MCP Service Client Credentials ────────────────────────────────────────
AM_MCP_CLIENT_ID=am-mcp-service
AM_MCP_CLIENT_SECRET=hkk4698D7xZ8m2VpPL3zNfepAoTwRN8r

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove committed credentials and rotate them immediately.

Line 12, Line 16, Line 24, and Line 25 contain plaintext secrets in a tracked file. This is an immediate credential-leak risk and should be treated as compromised material.

Suggested remediation
-AM_MCP_CLIENT_SECRET=hkk4698D7xZ8m2VpPL3zNfepAoTwRN8r
+AM_MCP_CLIENT_SECRET=<set-via-vault-or-ci-secret>

-LITELLM_MASTER_KEY=sk-27ad0c81915a946bfcf010e9b28a777c1ddc1a42f6640a6d
+LITELLM_MASTER_KEY=<set-via-vault-or-ci-secret>

-LANGFUSE_PUBLIC_KEY=pk-lf-cc35cb35-f20e-463d-90df-b41caec0a962
-LANGFUSE_SECRET_KEY=sk-lf-f64795da-863f-4a83-8f47-5b43a1bd0472
+LANGFUSE_PUBLIC_KEY=<set-via-vault-or-ci-secret>
+LANGFUSE_SECRET_KEY=<set-via-vault-or-ci-secret>

Also applies to: 16-17, 24-25

🧰 Tools
🪛 Betterleaks (1.3.1)

[high] 12-12: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@am-mcp-gateway/.env.preprod` at line 12, Remove all plaintext secrets from
the .env.preprod file immediately. Specifically, delete the exposed credentials
on lines 12 (AM_MCP_CLIENT_SECRET), lines 16-17, and lines 24-25. Replace these
lines with placeholder values or environment variable references that point to
secure secret management systems. After removing the credentials from the file,
ensure the credentials are rotated immediately through your credential
management system as they have been exposed in the tracked repository.

Source: Linters/SAST tools


# ── LiteLLM (local port-forward to cluster) ───────────────────────────────
LITELLM_BASE_URL=http://localhost:4000
LITELLM_MASTER_KEY=sk-27ad0c81915a946bfcf010e9b28a777c1ddc1a42f6640a6d
LLM_MODEL=together_ai/meta-llama/Meta-Llama-3-8B-Instruct-Lite
LLM_FALLBACK_CHAIN=litellm
LLM_CB_ENABLED=false

# ── Langfuse Observability ────────────────────────────────────────────────
LANGFUSE_ENABLED=true
LANGFUSE_HOST=https://langfuse.munish.org
LANGFUSE_PUBLIC_KEY=pk-lf-cc35cb35-f20e-463d-90df-b41caec0a962
LANGFUSE_SECRET_KEY=sk-lf-f64795da-863f-4a83-8f47-5b43a1bd0472

# ── UI test agent (MCP tool proxy) ────────────────────────────
UI_TEST_AGENT_BASE_URL=http://localhost:8130
UI_TEST_MANIFEST_PATH=../../am-modern-ui/testing/manifest.json
MCP_GATEWAY_PUBLIC_URL=http://localhost:8120
LITELLM_MCP_SERVER_ALIAS=am_mcp_gateway
7 changes: 7 additions & 0 deletions am-mcp-gateway/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.env
.env.*
!.env.example
.pytest_cache/
__pycache__/
*.pyc
.venv/
44 changes: 44 additions & 0 deletions am-mcp-gateway/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# AM MCP Gateway — build context: am-platform root (build_context: ..)
FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1

RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
curl \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy root requirements first to cache base platform dependencies
COPY requirements.txt ./requirements-platform.txt
RUN pip install --no-cache-dir -r requirements-platform.txt

# Copy gateway-specific requirements and install them
COPY am-mcp-gateway/requirements.txt ./am-mcp-gateway/requirements.txt
RUN pip install --no-cache-dir -r am-mcp-gateway/requirements.txt

# Copy shared libraries
COPY libraries/am-platform-common/ libraries/am-platform-common/
COPY libraries/am-platform-security/ libraries/am-platform-security/
RUN pip install --no-cache-dir ./libraries/am-platform-common ./libraries/am-platform-security

# Copy service code
COPY am-mcp-gateway/ am-mcp-gateway/

# Set environment
ENV PYTHONPATH=/app/libraries/am-platform-common:/app/libraries/am-platform-security:/app/am-mcp-gateway
ENV APP_ENV=production
ENV TZ=Asia/Kolkata
ENV APP_PORT=8120

EXPOSE 8120

HEALTHCHECK --interval=30s --timeout=3s --start-period=45s --retries=3 \
CMD curl -f http://localhost:8120/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8120"]

19 changes: 19 additions & 0 deletions am-mcp-gateway/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.PHONY: run test lint format clean

run:
uvicorn app.main:app --host 0.0.0.0 --port 8120 --reload

test:
pytest tests/

lint:
black --check app/ tests/
isort --check-only app/ tests/

format:
black app/ tests/
isort app/ tests/

clean:
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
31 changes: 31 additions & 0 deletions am-mcp-gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# AM MCP Gateway

The Gateway routes MCP and LLM queries through local and remote models, ensuring secure, fast, and structured execution.

## 🔒 Secret Management & Security Guidelines

To prevent sensitive credentials and API keys from leaking, the gateway strictly decouples credentials from code and configuration:

### 1. In-Cluster (Preprod / Production)
In cluster environments, secrets are injected dynamically using **HashiCorp Vault Agent Sidecars**.
- Secrets are mapped via [vault-mappings.yaml](file:///a:/InfraCode/AM-Portfolio-grp/am-platform/am-mcp-gateway/helm/vault-mappings.yaml).
- The Vault agent writes secrets to memory-mounted files under `/vault/secrets/`.
- Sourced files automatically set environment variables (such as `TOGETHER_API_KEY`, `LITELLM_MASTER_KEY`, etc.) inside the shell before running the Gateway process:
```bash
. /vault/secrets/identity-oidc 2>/dev/null || true
. /vault/secrets/llm-api-keys 2>/dev/null || true
. /vault/secrets/observability 2>/dev/null || true
. /vault/secrets/redis 2>/dev/null || true
exec uvicorn app.main:app
```

### 2. Local Development
For local testing:
- Create a `.env` file in the root of `am-mcp-gateway` (or use the global `am-platform/.secrets.env`).
- Secrets will be parsed automatically at startup by Pydantic settings.
- **Never commit `.env` or `.secrets.env` files.** The global and local `.gitignore` rules are configured to prevent these files from being tracked by Git.

### 3. Golden Rule
> [!IMPORTANT]
> **Never hardcode or place real API keys/credentials inside values files (like `values.yaml` or `values.preprod.yaml`).**
> All credential integrations must be mapped via Vault paths and key bindings.
122 changes: 122 additions & 0 deletions am-mcp-gateway/app/api/agent_llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Service-to-service LLM endpoint for internal agents (e.g. ui-test-agent).

Routes through LiteLLM so all agent LLM/vision calls appear in LiteLLM logs + Langfuse.
"""
from __future__ import annotations

import logging
import time
import uuid
from typing import Any, Optional

from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field

from am_platform_security.dependencies import require_auth_context
from am_platform_security.models import AuthContext

from app.api.chat import _litellm_metadata, _log_chat_trace
from app.llm.router import llm_router

logger = logging.getLogger(__name__)

router = APIRouter(include_in_schema=False)


class AgentLLMRequest(BaseModel):
messages: list[dict[str, Any]] = Field(..., description="OpenAI-style messages (text or multimodal)")
model: str = Field(..., description="LiteLLM model name")
temperature: float = Field(default=0.2, ge=0.0, le=2.0)
max_tokens: int = Field(default=4096, ge=1, le=8192)
sessionId: Optional[str] = None
testId: Optional[str] = Field(default=None, description="UI test run id for trace correlation")
source: str = Field(default="ui-test-agent", description="Caller service name")


class AgentLLMResponse(BaseModel):
content: str
model: str
sessionId: str
traceId: str
usage: dict[str, int] | None = None


def _prompt_summary(messages: list[dict[str, Any]]) -> str:
parts: list[str] = []
for msg in messages:
content = msg.get("content")
if isinstance(content, str):
parts.append(content[:500])
elif isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
parts.append(str(block.get("text", ""))[:500])
return "\n".join(parts)[:2000] or "(multimodal request)"


@router.post("/agent/llm/completions", response_model=AgentLLMResponse)
async def agent_llm_completions(
request: AgentLLMRequest,
auth_context: AuthContext = Depends(require_auth_context()),
):
"""Proxy agent LLM/vision calls to LiteLLM with gateway + Langfuse tracing."""
user_id = auth_context.subject
session_id = request.sessionId or request.testId or str(uuid.uuid4())
trace_id = str(uuid.uuid4())
start_time = time.time()

metadata = _litellm_metadata(user_id=user_id, session_id=session_id, trace_id=trace_id)
metadata["source"] = request.source
if request.testId:
metadata["test_id"] = request.testId

response_text, provider, usage = await llm_router.generate_chat_messages(
request.messages,
model=request.model,
temperature=request.temperature,
metadata=metadata,
max_tokens=request.max_tokens,
)
latency = time.time() - start_time

await _log_chat_trace(
user_id=user_id,
session_id=session_id,
trace_id=trace_id,
response=response_text,
model=request.model,
latency=latency,
provider=provider,
usage=usage,
request=_AgentTraceRequest(
message=_prompt_summary(request.messages),
model=request.model,
temperature=request.temperature,
stream=False,
),
)

logger.info(
"Agent LLM completion source=%s test_id=%s model=%s latency=%.2fs",
request.source,
request.testId,
request.model,
latency,
)
return AgentLLMResponse(
content=response_text,
model=request.model,
sessionId=session_id,
traceId=trace_id,
usage=usage,
)


class _AgentTraceRequest:
"""Minimal adapter so agent calls reuse chat trace logging."""

def __init__(self, *, message: str, model: str, temperature: float, stream: bool):
self.message = message
self.model = model
self.temperature = temperature
self.stream = stream
Loading
Loading