#
tokens: 33788/50000 37/37 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .github
│   ├── CODE_OF_CONDUCT.md
│   ├── ISSUE_TEMPLATE.md
│   └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── azure.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── infra
│   ├── abbreviations.json
│   ├── app
│   │   ├── api.bicep
│   │   ├── apim-mcp
│   │   │   ├── mcp-api.bicep
│   │   │   └── mcp-api.policy.xml
│   │   ├── apim-oauth
│   │   │   ├── authorize.policy.xml
│   │   │   ├── consent.policy.xml
│   │   │   ├── diagrams
│   │   │   │   ├── diagrams.md
│   │   │   │   ├── images
│   │   │   │   │   └── mcp-client-auth.png
│   │   │   │   └── mcp_client_auth.mmd
│   │   │   ├── entra-app.bicep
│   │   │   ├── oauth-callback.policy.xml
│   │   │   ├── oauth.bicep
│   │   │   ├── oauthmetadata-get.policy.xml
│   │   │   ├── oauthmetadata-options.policy.xml
│   │   │   ├── register-options.policy.xml
│   │   │   ├── register.policy.xml
│   │   │   └── token.policy.xml
│   │   ├── storage-Access.bicep
│   │   ├── storage-PrivateEndpoint.bicep
│   │   └── vnet.bicep
│   ├── bicepconfig.json
│   ├── core
│   │   ├── apim
│   │   │   └── apim.bicep
│   │   ├── database
│   │   │   ├── cosmosdb-rbac.bicep
│   │   │   └── cosmosdb.bicep
│   │   ├── host
│   │   │   ├── appserviceplan.bicep
│   │   │   └── functions-flexconsumption.bicep
│   │   ├── identity
│   │   │   └── userAssignedIdentity.bicep
│   │   ├── monitor
│   │   │   ├── appinsights-access.bicep
│   │   │   ├── applicationinsights.bicep
│   │   │   ├── loganalytics.bicep
│   │   │   └── monitoring.bicep
│   │   └── storage
│   │       └── storage-account.bicep
│   ├── main.bicep
│   └── main.parameters.json
├── LICENSE.md
├── mcp-client-authorization.gif
├── overview.png
├── pyproject.toml
├── README.md
└── src
    ├── .funcignore
    ├── .gitignore
    ├── .vscode
    │   ├── extensions.json
    │   ├── launch.json
    │   ├── settings.json
    │   └── tasks.json
    ├── function_app.py
    ├── host.json
    ├── local.settings.json
    └── requirements.txt
```

# Files

--------------------------------------------------------------------------------
/src/.funcignore:
--------------------------------------------------------------------------------

```
.venv
```

--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------

```
bin
obj
csx
.vs
edge
Publish

*.user
*.suo
*.cscfg
*.Cache
project.lock.json

/packages
/TestResults

/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json

node_modules
dist

# Local python packages
.python_packages/

# Python Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# UV
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#uv.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Ruff stuff:
.ruff_cache/

# PyPI configuration file
.pypirc

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
<!--
---
name: Remote MCP  using Azure API Management
description: Use Azure API Management as the AI Gateway for MCP Servers using Azure Functions  
page_type: sample
languages:
- python
- bicep
- azdeveloper
products:
- azure-api-management
- azure-functions
- azure
urlFragment: remote-mcp-apim-functions-python
---
-->

# Secure Remote MCP Servers using Azure API Management (Experimental)

![Diagram](mcp-client-authorization.gif)

Azure API Management acts as the [AI Gateway](https://github.com/Azure-Samples/AI-Gateway) for MCP servers. 

This sample implements the latest [MCP Authorization specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-10-third-party-authorization-flow)

This is a [sequence diagram](infra/app/apim-oauth/diagrams/diagrams.md) to understand the flow.

## Deploy Remote MCP Server to Azure

1. Register `Microsoft.App` resource provider.
    * If you are using Azure CLI, run `az provider register --namespace Microsoft.App --wait`.
    * If you are using Azure PowerShell, run `Register-AzResourceProvider -ProviderNamespace Microsoft.App`. Then run `(Get-AzResourceProvider -ProviderNamespace Microsoft.App).RegistrationState` after some time to check if the registration is complete.

2. Run this [azd](https://aka.ms/azd) command to provision the api management service, function app(with code) and all other required Azure resources

    ```shell
    azd up
    ```

### Test with MCP Inspector

1. In a **new terminal window**, install and run MCP Inspector

    ```shell
    npx @modelcontextprotocol/inspector
    ```

1. CTRL click to load the MCP Inspector web app from the URL displayed by the app (e.g. http://127.0.0.1:6274/#resources)
1. Set the transport type to `SSE`
1. Set the URL to your running API Management SSE endpoint displayed after `azd up` and **Connect**:

    ```shell
    https://<apim-servicename-from-azd-output>.azure-api.net/mcp/sse
    ```

5. **List Tools**.  Click on a tool and **Run Tool**.  


## Technical Architecture Overview

This solution deploys a secure MCP (Model Context Protocol) server infrastructure on Azure. The architecture implements a multi-layered security model with Azure API Management serving as an intelligent gateway that handles authentication, authorization, and request routing.

![overview diagram](overview.png)

### Deployed Azure Resources

The infrastructure provisions the following Azure resources:

#### Core Gateway Infrastructure
- **Azure API Management (APIM)** - The central security gateway that exposes both OAuth and MCP APIs
  - **SKU**: BasicV2 (configurable)
  - **Identity**: System-assigned and user-assigned managed identities
  - **Purpose**: Handles authentication flows, request validation, and secure proxying to backend services

#### Backend Compute
- **Azure Function App** - Hosts the MCP server implementation
  - **Runtime**: Python 3.11 on Flex Consumption plan
  - **Authentication**: Function-level authentication with managed identity integration
  - **Purpose**: Executes MCP tools and operations (snippet management in this example)

#### Storage and Data
- **Azure Storage Account** - Provides multiple storage functions
  - **Function hosting**: Stores function app deployment packages
  - **Application data**: Blob container for snippet storage
  - **Security**: Configured with managed identity access and optional private endpoints

#### Security and Identity
- **User-Assigned Managed Identity** - Enables secure service-to-service authentication
  - **Purpose**: Allows Function App to access Storage and Application Insights without secrets
  - **Permissions**: Storage Blob Data Owner, Storage Queue Data Contributor, Monitoring Metrics Publisher

- **Entra ID Application Registration** - OAuth2/OpenID Connect client for authentication
  - **Purpose**: Enables third-party authorization flow per MCP specification
  - **Configuration**: PKCE-enabled public client with custom redirect URIs

#### Monitoring and Observability
- **Application Insights** - Provides telemetry and monitoring
- **Log Analytics Workspace** - Centralized logging and analytics

#### Optional Network Security
- **Virtual Network (VNet)** - When `vnetEnabled` is true
  - **Private Endpoints**: Secure connectivity to Storage Account
  - **Network Isolation**: Functions and storage communicate over private network

### Why These Resources?

**Azure API Management** serves as the security perimeter, implementing:
- OAuth 2.0/PKCE authentication flows per MCP specification
- Session key encryption/decryption for secure API access  
- Request validation and header injection
- Rate limiting and throttling capabilities
- Centralized policy management

**Azure Functions** provides:
- Serverless, pay-per-use compute model
- Native integration with Azure services
- Automatic scaling based on demand
- Built-in monitoring and diagnostics

**Managed Identities** eliminate the need for:
- Service credentials management
- Secret rotation processes
- Credential exposure risks

## Azure API Management Configuration Details

The APIM instance is configured with two primary APIs that work together to implement the MCP authorization specification:

### OAuth API (`/oauth/*`)

This API implements the complete OAuth 2.0 authorization server functionality required by the MCP specification:

#### Endpoints and Operations

**Authorization Endpoint** (`GET /authorize`)
- **Purpose**: Initiates the OAuth 2.0/PKCE flow
- **Policy Logic**:
  1. Extracts PKCE parameters from MCP client request
  2. Checks for existing user consent (via cookies)
  3. Redirects to consent page if consent not granted
  4. Generates new PKCE parameters for Entra ID communication
  5. Stores authentication state in APIM cache
  6. Redirects user to Entra ID for authentication

**Consent Management** (`GET/POST /consent`)
- **Purpose**: Handles user consent for MCP client access
- **Features**: Consent persistence via secure cookies

**OAuth Metadata Endpoint** (`GET /.well-known/oauth-authorization-server`)
- **Purpose**: Publishes OAuth server configuration per RFC 8414
- **Returns**: JSON metadata about supported endpoints, flows, and capabilities
  
**Client Registration** (`POST /register`)
- **Purpose**: Supports dynamic client registration per MCP specification

**Token Endpoint** (`POST /token`)
- **Purpose**: Exchanges authorization codes for access tokens
- **Policy Logic**:
  1. Validates authorization code and PKCE verifier from MCP client
  2. Exchanges Entra ID authorization code for access tokens
  3. Generates encrypted session key for MCP API access
  4. Caches the access token with session key mapping
  5. Returns encrypted session key to MCP client

#### Named Values and Configuration

The OAuth API uses several APIM Named Values for configuration:
- `McpClientId` - The registered Entra ID application client ID
- `EntraIDFicClientId` - Service identity client ID for token exchange
- `APIMGatewayURL` - Base URL for callback and metadata endpoints
- `OAuthScopes` - Requested OAuth scopes (`openid` + Microsoft Graph)
- `EncryptionKey` / `EncryptionIV` - For session key encryption

### MCP API (`/mcp/*`)

This API provides the actual MCP protocol endpoints with security enforcement:

#### Endpoints and Operations

**Server-Sent Events Endpoint** (`GET /sse`)
- **Purpose**: Establishes real-time communication channel for MCP protocol
- **Security**: Requires valid encrypted session token

**Message Endpoint** (`POST /message`)
- **Purpose**: Handles MCP protocol messages and tool invocations
- **Security**: Requires valid encrypted session token

#### Security Policy Implementation

The MCP API applies a comprehensive security policy to all operations:

1. **Authorization Header Validation**
   ```xml
   <check-header name="Authorization" failed-check-httpcode="401" 
                 failed-check-error-message="Not authorized" ignore-case="false" />
   ```

2. **Session Key Decryption**
   - Extracts encrypted session key from Authorization header
   - Decrypts using AES with stored key and IV
   - Validates token format and structure

3. **Token Cache Lookup**
   ```xml
   <cache-lookup-value key="@($"EntraToken-{context.Variables.GetValueOrDefault("decryptedSessionKey")}")" 
                       variable-name="accessToken" />
   ```

4. **Access Token Validation**
   - Verifies cached access token exists and is valid
   - Returns 401 with proper WWW-Authenticate header if invalid

5. **Backend Authentication**
   ```xml
   <set-header name="x-functions-key" exists-action="override">
       <value>{{function-host-key}}</value>
   </set-header>
   ```

### Security Model

The solution implements a sophisticated multi-layer security model:

**Layer 1: OAuth 2.0/PKCE Authentication**
- MCP clients must complete full OAuth flow with Entra ID
- PKCE prevents authorization code interception attacks
- User consent management with persistent preferences

**Layer 2: Session Key Encryption**
- Access tokens are never exposed to MCP clients
- Encrypted session keys provide time-bounded access
- AES encryption with secure key management in APIM

**Layer 3: Function-Level Security**
- Function host keys protect direct access to Azure Functions
- Managed identity ensures secure service-to-service communication
- Network isolation available via VNet integration

**Layer 4: Azure Platform Security**
- All traffic encrypted in transit (TLS)
- Storage access via managed identities
- Audit logging through Application Insights

This layered approach ensures that even if one security boundary is compromised, multiple additional protections remain in place.




```

--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
# Microsoft Open Source Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).

Resources:

- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [[email protected]](mailto:[email protected]) with questions or concerns

```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
    MIT License

    Copyright (c) Microsoft Corporation.

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE
```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to [project-title]

This project welcomes contributions and suggestions.  Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [[email protected]](mailto:[email protected]) with any additional questions or comments.

 - [Code of Conduct](#coc)
 - [Issues and Bugs](#issue)
 - [Feature Requests](#feature)
 - [Submission Guidelines](#submit)

## <a name="coc"></a> Code of Conduct
Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/).

## <a name="issue"></a> Found an Issue?
If you find a bug in the source code or a mistake in the documentation, you can help us by
[submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can
[submit a Pull Request](#submit-pr) with a fix.

## <a name="feature"></a> Want a Feature?
You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub
Repository. If you would like to *implement* a new feature, please submit an issue with
a proposal for your work first, to be sure that we can use it.

* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).

## <a name="submit"></a> Submission Guidelines

### <a name="submit-issue"></a> Submitting an Issue
Before you submit an issue, search the archive, maybe your question was already answered.

If your issue appears to be a bug, and hasn't been reported, open a new issue.
Help us to maximize the effort we can spend fixing issues and adding new
features, by not reporting duplicate issues.  Providing the following information will increase the
chances of your issue being dealt with quickly:

* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
* **Version** - what version is affected (e.g. 0.1.2)
* **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
* **Browsers and Operating System** - is this a problem with all browsers?
* **Reproduce the Error** - provide a live example or a unambiguous set of steps
* **Related Issues** - has a similar issue been reported before?
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
  causing the problem (line of code or commit)

You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new].

### <a name="submit-pr"></a> Submitting a Pull Request (PR)
Before you submit your Pull Request (PR) consider the following guidelines:

* Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR
  that relates to your submission. You don't want to duplicate effort.

* Make your changes in a new git fork:

* Commit your changes using a descriptive commit message
* Push your fork to GitHub:
* In GitHub, create a pull request
* If we suggest changes then:
  * Make the required updates.
  * Rebase your fork and force push to your GitHub repository (this will update your Pull Request):

    ```shell
    git rebase master -i
    git push -f
    ```

That's it! Thank you for your contribution!

```

--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------

```json
{
  "recommendations": [
    "ms-azuretools.vscode-azurefunctions",
    "ms-python.python"
  ]
}
```

--------------------------------------------------------------------------------
/src/.vscode/extensions.json:
--------------------------------------------------------------------------------

```json
{
  "recommendations": [
    "ms-azuretools.vscode-azurefunctions",
    "ms-python.python"
  ]
}
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/diagrams/diagrams.md:
--------------------------------------------------------------------------------

```markdown
# Sequence Diagrams

## MCP Client Auth Flow

![MCP Client Authorization Flow](images/mcp-client-auth.png)

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[tool.ruff]
line-length = 120
target-version = "py311"
lint.select = ["E", "F", "I", "UP", "A"]
lint.ignore = ["D203"]

```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
## [project-title] Changelog

<a name="x.y.z"></a>
# x.y.z (yyyy-mm-dd)

*Features*
* ...

*Bug Fixes*
* ...

*Breaking Changes*
* ...

```

--------------------------------------------------------------------------------
/src/local.settings.json:
--------------------------------------------------------------------------------

```json
{
    "IsEncrypted": false,
    "Values": {
      "FUNCTIONS_WORKER_RUNTIME": "python",
      "AzureWebJobsStorage": "UseDevelopmentStorage=true"
    }
  }
```

--------------------------------------------------------------------------------
/src/requirements.txt:
--------------------------------------------------------------------------------

```
# Do not include azure-functions-worker in this file
# The Python Worker is managed by the Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions

```

--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------

```json
{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "func",
			"label": "func: host start",
			"command": "host start",
			"problemMatcher": "$func-python-watch",
			"isBackground": true,
			"options": {
				"cwd": "${workspaceFolder}/src"
			}
		}
	]
}
```

--------------------------------------------------------------------------------
/src/.vscode/launch.json:
--------------------------------------------------------------------------------

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to Python Functions",
      "type": "debugpy",
      "request": "attach",
      "connect": {
        "host": "localhost",
        "port": 9091
      },
      "preLaunchTask": "func: host start"
    }
  ]
}
```

--------------------------------------------------------------------------------
/infra/bicepconfig.json:
--------------------------------------------------------------------------------

```json
{
  "experimentalFeaturesEnabled": {
      "extensibility": true
  },
  // specify an alias for the version of the v1.0 dynamic types package you want to use
  "extensions": {
    "microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.2.0-preview"
  }
}

```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
{
    "azureFunctions.deploySubpath": "src",
    "azureFunctions.scmDoBuildDuringDeployment": true,
    "azureFunctions.projectLanguage": "Python",
    "azureFunctions.projectRuntime": "~4",
    "debug.internalConsoleOptions": "neverOpen",
    "azureFunctions.projectLanguageModel": 2
}
```

--------------------------------------------------------------------------------
/azure.yaml:
--------------------------------------------------------------------------------

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json

name: remote-mcp-apim-functions-python
metadata:
  template: [email protected]
services:
  api:
    project: ./src/
    language: python
    host: function

```

--------------------------------------------------------------------------------
/src/host.json:
--------------------------------------------------------------------------------

```json
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle.Experimental",
    "version": "[4.*, 5.0.0)"
  }
}
```

--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------

```json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach to Python Functions",
            "type": "debugpy",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 9091
            },
            "preLaunchTask": "func: host start"
        }
    ]
}
```

--------------------------------------------------------------------------------
/infra/main.parameters.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environmentName": {
      "value": "${AZURE_ENV_NAME}"
    },
    "location": {
      "value": "${AZURE_LOCATION}"
    },
    "vnetEnabled": {
      "value": "${VNET_ENABLED=true}"
    },
    "apimSku": {
      "value": "Basicv2" 
    }
  }
}
```

--------------------------------------------------------------------------------
/src/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
{
    "files.exclude": {
        "obj": true,
        "bin": true
    },
    "azureFunctions.deploySubpath": ".",
    "azureFunctions.scmDoBuildDuringDeployment": true,
    "azureFunctions.pythonVenv": ".venv",
    "azureFunctions.projectLanguage": "Python",
    "azureFunctions.projectRuntime": "~4",
    "debug.internalConsoleOptions": "neverOpen",
    "azureFunctions.projectLanguageModel": 2,
    "azureFunctions.preDeployTask": "func: extensions install"
}
```

--------------------------------------------------------------------------------
/src/.vscode/tasks.json:
--------------------------------------------------------------------------------

```json
{
	"version": "2.0.0",
	"tasks": [
		{
			"label": "pip install (functions)",
			"type": "shell",
			"osx": {
				"command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt"
			},
			"windows": {
				"command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt"
			},
			"linux": {
				"command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt"
			},
			"problemMatcher": []
		},
		{
			"type": "func",
			"label": "func: host start",
			"command": "host start",
			"problemMatcher": "$func-python-watch",
			"isBackground": true,
			"dependsOn": "func: extensions install"
		},
		{
			"type": "func",
			"command": "extensions install",
			"dependsOn": "pip install (functions)",
			"problemMatcher": []
		}
	]
}
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
<!--
IF SUFFICIENT INFORMATION IS NOT PROVIDED VIA THE FOLLOWING TEMPLATE THE ISSUE MIGHT BE CLOSED WITHOUT FURTHER CONSIDERATION OR INVESTIGATION
-->
> Please provide us with the following information:
> ---------------------------------------------------------------

### This issue is for a: (mark with an `x`)
```
- [ ] bug report -> please search issues before submitting
- [ ] feature request
- [ ] documentation issue or request
- [ ] regression (a behavior that used to work and stopped in a new release)
```

### Minimal steps to reproduce
>

### Any log messages given by the failure
>

### Expected/desired behavior
>

### OS and Version?
> Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?)

### Versions
>

### Mention any other details that might be useful

> ---------------------------------------------------------------
> Thanks! We'll be in touch soon.

```

--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
## Purpose
<!-- Describe the intention of the changes being proposed. What problem does it solve or functionality does it add? -->
* ...

## Does this introduce a breaking change?
<!-- Mark one with an "x". -->
```
[ ] Yes
[ ] No
```

## Pull Request Type
What kind of change does this Pull Request introduce?

<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Documentation content changes
[ ] Other... Please describe:
```

## How to Test
*  Get the code

```
git clone [repo-address]
cd [repo-name]
git checkout [branch-name]
npm install
```

* Test the code
<!-- Add steps to run the tests suite and/or manually test -->
```
```

## What to Check
Verify that the following are valid
* ...

## Other Information
<!-- Add any other helpful information that may be needed here. -->
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/oauthmetadata-options.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    OAUTH METADATA OPTIONS POLICY
    This policy handles OPTIONS requests to the OAuth metadata endpoint, implementing CORS support
    for cross-origin requests to the OAuth authorization server.
-->
<policies>
    <inbound>
        <!-- Return CORS headers for OPTIONS requests -->
        <return-response>
            <set-status code="200" reason="OK" />
            <set-header name="Access-Control-Allow-Origin" exists-action="override">
                <value>*</value>
            </set-header>
            <set-header name="Access-Control-Allow-Methods" exists-action="override">
                <value>GET, OPTIONS</value>
            </set-header>
            <set-header name="Access-Control-Allow-Headers" exists-action="override">
                <value>Content-Type, Authorization</value>
            </set-header>
            <set-header name="Access-Control-Max-Age" exists-action="override">
                <value>86400</value>
            </set-header>
            <set-body />
        </return-response>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/register-options.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    REGISTER OPTIONS POLICY
    This policy handles the OPTIONS pre-flight requests for the OAuth client registration endpoint.
    It returns the appropriate CORS headers to allow cross-origin requests.
-->
<policies>
    <inbound>
        <!-- Return a 200 OK response with appropriate CORS headers -->
        <return-response>
            <set-status code="200" reason="OK" />
            <set-header name="Access-Control-Allow-Origin" exists-action="override">
                <value>*</value>
            </set-header>
            <set-header name="Access-Control-Allow-Methods" exists-action="override">
                <value>GET, OPTIONS</value>
            </set-header>
            <set-header name="Access-Control-Allow-Headers" exists-action="override">
                <value>Content-Type, Authorization</value>
            </set-header>
            <set-header name="Access-Control-Max-Age" exists-action="override">
                <value>86400</value>
            </set-header>
            <set-body />
        </return-response>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/oauthmetadata-get.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    OAUTH METADATA POLICY
    This policy implements the OpenID Connect and OAuth 2.0 discovery endpoint (.well-known/oauth-authorization-server).
-->
<policies>
    <inbound>
        <!-- Return the OAuth metadata in JSON format -->
        <return-response>
            <set-status code="200" reason="OK" />
            <set-header name="Content-Type" exists-action="override">
                <value>application/json; charset=utf-8</value>
            </set-header>
            <set-header name="access-control-allow-origin" exists-action="override">
                <value>*</value>
            </set-header>                
            <set-body>
                {
                    "issuer": "{{APIMGatewayURL}}",
                    "service_documentation": "https://microsoft.com/",
                    "authorization_endpoint": "{{APIMGatewayURL}}/authorize",
                    "token_endpoint": "{{APIMGatewayURL}}/token",
                    "revocation_endpoint": "{{APIMGatewayURL}}/revoke",
                    "registration_endpoint": "{{APIMGatewayURL}}/register",
                    "response_types_supported": [
                        "code"
                    ],
                    "code_challenge_methods_supported": [
                        "S256"
                    ],
                    "token_endpoint_auth_methods_supported": [
                        "none"
                    ],
                    "grant_types_supported": [
                        "authorization_code",
                        "refresh_token"
                    ],
                    "revocation_endpoint_auth_methods_supported": [
                        "client_secret_post"
                    ]
                }
            </set-body>
        </return-response>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

--------------------------------------------------------------------------------
/infra/app/apim-mcp/mcp-api.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    MCP API POLICY
    This policy applies to all operations in the MCP API.
    It adds authorization header check for security.
-->
<policies>
    <inbound>
        <base />
        <check-header name="Authorization" failed-check-httpcode="401" failed-check-error-message="Not authorized" ignore-case="false" />
        <set-variable name="IV" value="{{EncryptionIV}}" />
        <set-variable name="key" value="{{EncryptionKey}}" />
        <set-variable name="decryptedSessionKey" value="@{
            // Retrieve the encrypted session key from the request header
            string authHeader = context.Request.Headers.GetValueOrDefault("Authorization");
        
            string encryptedSessionKey = authHeader.StartsWith("Bearer ") ? authHeader.Substring(7) : authHeader;
            
            // Decrypt the session key using AES
            byte[] IV = Convert.FromBase64String((string)context.Variables["IV"]);
            byte[] key = Convert.FromBase64String((string)context.Variables["key"]);
            
            byte[] encryptedBytes = Convert.FromBase64String(encryptedSessionKey);
            byte[] decryptedBytes = encryptedBytes.Decrypt("Aes", key, IV);
            
            return Encoding.UTF8.GetString(decryptedBytes);
        }" />
        <cache-lookup-value key="@($"EntraToken-{context.Variables.GetValueOrDefault("decryptedSessionKey")}")" variable-name="accessToken" />
        
        <choose>
            <when condition="@(context.Variables.GetValueOrDefault("accessToken") == null)">
                <return-response>
                    <set-status code="401" reason="Unauthorized" />
                    <set-header name="WWW-Authenticate" exists-action="override">
                        <value>Bearer error="invalid_token"</value>
                    </set-header>
                </return-response>
            </when>
        </choose>
        
        <set-header name="x-functions-key" exists-action="override">
            <value>{{function-host-key}}</value>
        </set-header>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

```

--------------------------------------------------------------------------------
/src/function_app.py:
--------------------------------------------------------------------------------

```python
from dataclasses import dataclass
import json
import logging

import azure.functions as func

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

# Constants for the Azure Blob Storage container, file, and blob path
_SNIPPET_NAME_PROPERTY_NAME = "snippetname"
_SNIPPET_PROPERTY_NAME = "snippet"
_BLOB_PATH = "snippets/{mcptoolargs." + _SNIPPET_NAME_PROPERTY_NAME + "}.json"


@dataclass
class ToolProperty:
    propertyName: str
    propertyType: str
    description: str


# Define the tool properties using the ToolProperty class
tool_properties_save_snippets_object = [
    ToolProperty(_SNIPPET_NAME_PROPERTY_NAME, "string", "The name of the snippet."),
    ToolProperty(_SNIPPET_PROPERTY_NAME, "string", "The content of the snippet."),
]

tool_properties_get_snippets_object = [ToolProperty(_SNIPPET_NAME_PROPERTY_NAME, "string", "The name of the snippet.")]

# Convert the tool properties to JSON
tool_properties_save_snippets_json = json.dumps([prop.__dict__ for prop in tool_properties_save_snippets_object])
tool_properties_get_snippets_json = json.dumps([prop.__dict__ for prop in tool_properties_get_snippets_object])


@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="hello_mcp",
    description="Hello world.",
    toolProperties="[]",
)
def hello_mcp(context) -> str:
    """
    A simple function that returns a greeting message.

    Args:
        context: The trigger context (not used in this function).

    Returns:
        str: A greeting message.
    """
    return "Hello I am MCPTool!"


@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="get_snippet",
    description="Retrieve a snippet by name.",
    toolProperties=tool_properties_get_snippets_json,
)
@app.generic_input_binding(arg_name="file", type="blob", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def get_snippet(file: func.InputStream, context) -> str:
    """
    Retrieves a snippet by name from Azure Blob Storage.

    Args:
        file (func.InputStream): The input binding to read the snippet from Azure Blob Storage.
        context: The trigger context containing the input arguments.

    Returns:
        str: The content of the snippet or an error message.
    """
    snippet_content = file.read().decode("utf-8")
    logging.info("Retrieved snippet: %s", snippet_content)
    return snippet_content


@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="save_snippet",
    description="Save a snippet with a name.",
    toolProperties=tool_properties_save_snippets_json,
)
@app.generic_output_binding(arg_name="file", type="blob", connection="AzureWebJobsStorage", path=_BLOB_PATH)
def save_snippet(file: func.Out[str], context) -> str:
    content = json.loads(context)
    if "arguments" not in content:
        return "No arguments provided"

    snippet_name_from_args = content["arguments"].get(_SNIPPET_NAME_PROPERTY_NAME)
    snippet_content_from_args = content["arguments"].get(_SNIPPET_PROPERTY_NAME)

    if not snippet_name_from_args:
        return "No snippet name provided"

    if not snippet_content_from_args:
        return "No snippet content provided"

    file.set(snippet_content_from_args)
    logging.info("Saved snippet: %s", snippet_content_from_args)
    return f"Snippet '{snippet_content_from_args}' saved successfully"

```

--------------------------------------------------------------------------------
/infra/abbreviations.json:
--------------------------------------------------------------------------------

```json
{
    "analysisServicesServers": "as",
    "apiManagementService": "apim-",
    "appConfigurationConfigurationStores": "appcs-",
    "applications": "app-",
    "appManagedEnvironments": "cae-",
    "appContainerApps": "ca-",
    "authorizationPolicyDefinitions": "policy-",
    "automationAutomationAccounts": "aa-",
    "blueprintBlueprints": "bp-",
    "blueprintBlueprintsArtifacts": "bpa-",
    "cacheRedis": "redis-",
    "cdnProfiles": "cdnp-",
    "cdnProfilesEndpoints": "cdne-",
    "cognitiveServicesAccounts": "cog-",
    "cognitiveServicesFormRecognizer": "cog-fr-",
    "cognitiveServicesTextAnalytics": "cog-ta-",
    "computeAvailabilitySets": "avail-",
    "computeCloudServices": "cld-",
    "computeDiskEncryptionSets": "des",
    "computeDisks": "disk",
    "computeDisksOs": "osdisk",
    "computeGalleries": "gal",
    "computeSnapshots": "snap-",
    "computeVirtualMachines": "vm",
    "computeVirtualMachineScaleSets": "vmss-",
    "containerInstanceContainerGroups": "ci",
    "containerRegistryRegistries": "cr",
    "containerServiceManagedClusters": "aks-",
    "databricksWorkspaces": "dbw-",
    "dataFactoryFactories": "adf-",
    "dataLakeAnalyticsAccounts": "dla",
    "dataLakeStoreAccounts": "dls",
    "dataMigrationServices": "dms-",
    "dBforMySQLServers": "mysql-",
    "dBforPostgreSQLServers": "psql-",
    "devicesIotHubs": "iot-",
    "devicesProvisioningServices": "provs-",
    "devicesProvisioningServicesCertificates": "pcert-",
    "documentDBDatabaseAccounts": "cosmos-",
    "eventGridDomains": "evgd-",
    "eventGridDomainsTopics": "evgt-",
    "eventGridEventSubscriptions": "evgs-",
    "eventHubNamespaces": "evhns-",
    "eventHubNamespacesEventHubs": "evh-",
    "hdInsightClustersHadoop": "hadoop-",
    "hdInsightClustersHbase": "hbase-",
    "hdInsightClustersKafka": "kafka-",
    "hdInsightClustersMl": "mls-",
    "hdInsightClustersSpark": "spark-",
    "hdInsightClustersStorm": "storm-",
    "hybridComputeMachines": "arcs-",
    "insightsActionGroups": "ag-",
    "insightsComponents": "appi-",
    "keyVaultVaults": "kv-",
    "kubernetesConnectedClusters": "arck",
    "kustoClusters": "dec",
    "kustoClustersDatabases": "dedb",
    "logicIntegrationAccounts": "ia-",
    "logicWorkflows": "logic-",
    "machineLearningServicesWorkspaces": "mlw-",
    "managedIdentityUserAssignedIdentities": "id-",
    "managementManagementGroups": "mg-",
    "migrateAssessmentProjects": "migr-",
    "networkApplicationGateways": "agw-",
    "networkApplicationSecurityGroups": "asg-",
    "networkAzureFirewalls": "afw-",
    "networkBastionHosts": "bas-",
    "networkConnections": "con-",
    "networkDnsZones": "dnsz-",
    "networkExpressRouteCircuits": "erc-",
    "networkFirewallPolicies": "afwp-",
    "networkFirewallPoliciesWebApplication": "waf",
    "networkFirewallPoliciesRuleGroups": "wafrg",
    "networkFrontDoors": "fd-",
    "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-",
    "networkLoadBalancersExternal": "lbe-",
    "networkLoadBalancersInternal": "lbi-",
    "networkLoadBalancersInboundNatRules": "rule-",
    "networkLocalNetworkGateways": "lgw-",
    "networkNatGateways": "ng-",
    "networkNetworkInterfaces": "nic-",
    "networkNetworkSecurityGroups": "nsg-",
    "networkNetworkSecurityGroupsSecurityRules": "nsgsr-",
    "networkNetworkWatchers": "nw-",
    "networkPrivateDnsZones": "pdnsz-",
    "networkPrivateLinkServices": "pl-",
    "networkPublicIPAddresses": "pip-",
    "networkPublicIPPrefixes": "ippre-",
    "networkRouteFilters": "rf-",
    "networkRouteTables": "rt-",
    "networkRouteTablesRoutes": "udr-",
    "networkTrafficManagerProfiles": "traf-",
    "networkVirtualNetworkGateways": "vgw-",
    "networkVirtualNetworks": "vnet-",
    "networkVirtualNetworksSubnets": "snet-",
    "networkVirtualNetworksVirtualNetworkPeerings": "peer-",
    "networkVirtualWans": "vwan-",
    "networkVpnGateways": "vpng-",
    "networkVpnGatewaysVpnConnections": "vcn-",
    "networkVpnGatewaysVpnSites": "vst-",
    "notificationHubsNamespaces": "ntfns-",
    "notificationHubsNamespacesNotificationHubs": "ntf-",
    "operationalInsightsWorkspaces": "log-",
    "portalDashboards": "dash-",
    "powerBIDedicatedCapacities": "pbi-",
    "purviewAccounts": "pview-",
    "recoveryServicesVaults": "rsv-",
    "resourcesResourceGroups": "rg-",
    "searchSearchServices": "srch-",
    "serviceBusNamespaces": "sb-",
    "serviceBusNamespacesQueues": "sbq-",
    "serviceBusNamespacesTopics": "sbt-",
    "serviceEndPointPolicies": "se-",
    "serviceFabricClusters": "sf-",
    "signalRServiceSignalR": "sigr",
    "sqlManagedInstances": "sqlmi-",
    "sqlServers": "sql-",
    "sqlServersDataWarehouse": "sqldw-",
    "sqlServersDatabases": "sqldb-",
    "sqlServersDatabasesStretch": "sqlstrdb-",
    "storageStorageAccounts": "st",
    "storageStorageAccountsVm": "stvm",
    "storSimpleManagers": "ssimp",
    "streamAnalyticsCluster": "asa-",
    "synapseWorkspaces": "syn",
    "synapseWorkspacesAnalyticsWorkspaces": "synw",
    "synapseWorkspacesSqlPoolsDedicated": "syndp",
    "synapseWorkspacesSqlPoolsSpark": "synsp",
    "timeSeriesInsightsEnvironments": "tsi-",
    "webServerFarms": "plan-",
    "webSitesAppService": "app-",
    "webSitesAppServiceEnvironment": "ase-",
    "webSitesFunctions": "func-",
    "webStaticSites": "stapp-"
}
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/register.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    REGISTER POLICY
    This policy implements the dynamic client registration endpoint for OAuth2 flow.
    
    Flow:
    1. MCP client sends a registration request with redirect URIs
    2. We store the registration information in CosmosDB for persistence
    3. We generate and return client credentials with the provided redirect URIs
-->
<policies>
    <inbound>
        <base />
        <!-- STEP 1: Extract client registration data from request -->
        <set-variable name="requestBody" value="@(context.Request.Body.As<JObject>(preserveContent: true))" />
        
        <!-- STEP 2: Generate a unique client ID (GUID) -->
        <set-variable name="uniqueClientId" value="@(Guid.NewGuid().ToString())" />
        
        <!-- STEP 3: Prepare client info document for CosmosDB -->
        <set-variable name="clientDocument" value="@{
            var requestBody = context.Variables.GetValueOrDefault<JObject>("requestBody");
            var uniqueClientId = context.Variables.GetValueOrDefault<string>("uniqueClientId");
            
            var document = new JObject();
            document["id"] = uniqueClientId;
            document["clientId"] = uniqueClientId;
            document["client_name"] = requestBody["client_name"]?.ToString() ?? "Unknown Application";
            document["client_uri"] = requestBody["client_uri"]?.ToString() ?? "";
            document["redirect_uris"] = requestBody["redirect_uris"];
            document["created_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            
            return document.ToString();
        }" />
        
        <!-- STEP 4: Get CosmosDB access token using managed identity -->
        <authentication-managed-identity resource="https://cosmos.azure.com" output-token-variable-name="cosmosAccessToken" />
        
        <!-- STEP 5: Store client registration in CosmosDB using AAD token -->
        <send-request mode="new" response-variable-name="cosmosResponse" timeout="30" ignore-error="false">
            <set-url>@($"{{CosmosDbEndpoint}}/dbs/{{CosmosDbDatabase}}/colls/{{CosmosDbContainer}}/docs")</set-url>
            <set-method>POST</set-method>
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-header name="x-ms-version" exists-action="override">
                <value>2018-12-31</value>
            </set-header>
            <set-header name="x-ms-documentdb-partitionkey" exists-action="override">
                <value>@($"[\"{context.Variables.GetValueOrDefault<string>("uniqueClientId")}\"]")</value>
            </set-header>
            <set-header name="Authorization" exists-action="override">
                <value>@($"type=aad&ver=1.0&sig={context.Variables.GetValueOrDefault<string>("cosmosAccessToken")}")</value>
            </set-header>
            <set-body>@(context.Variables.GetValueOrDefault<string>("clientDocument"))</set-body>
        </send-request>
        
        <!-- STEP 6: Check if CosmosDB operation was successful -->
        <choose>
            <when condition="@(((IResponse)context.Variables["cosmosResponse"]).StatusCode >= 400)">
                <return-response>
                    <set-status code="500" reason="Internal Server Error" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        return new JObject
                        {
                            ["error"] = "server_error",
                            ["error_description"] = "Failed to store client registration"
                        }.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- STEP 7: Cache the redirect URI for backward compatibility with other policies -->
        <cache-store-value duration="3600" 
            key="ClientRedirectUri" 
            value="@(context.Variables.GetValueOrDefault<JObject>("requestBody")["redirect_uris"][0].ToString())" />
        
        <!-- Store client info by client ID for easy lookup during consent -->
        <cache-store-value duration="3600" 
            key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("uniqueClientId")}")" 
            value="@{
                var requestBody = context.Variables.GetValueOrDefault<JObject>("requestBody");
                var clientInfo = new JObject();
                clientInfo["client_name"] = requestBody["client_name"]?.ToString() ?? "Unknown Application";
                clientInfo["client_uri"] = requestBody["client_uri"]?.ToString() ?? "";
                clientInfo["redirect_uris"] = requestBody["redirect_uris"];
                return clientInfo.ToString();
            }" />
        
        <!-- STEP 8: Set response content type -->
        <set-header name="Content-Type" exists-action="override">
            <value>application/json</value>
        </set-header>
        
        <!-- STEP 9: Return client credentials response -->
        <return-response>
            <set-status code="200" reason="OK" />
            <set-header name="access-control-allow-origin" exists-action="override">
                <value>*</value>
            </set-header>
            <set-body template="none">@{
                var requestBody = context.Variables.GetValueOrDefault<JObject>("requestBody");
                
                // Generate timestamps dynamically
                // Current time in seconds since epoch (Unix timestamp)
                long currentTimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
                
                // Client ID issued at current time
                long clientIdIssuedAt = currentTimeSeconds;
                
                // Client secret expires in 1 year (31536000 seconds = 365 days)
                long clientSecretExpiresAt = currentTimeSeconds + 31536000;

                // Use the generated client ID from earlier
                string uniqueClientId = context.Variables.GetValueOrDefault<string>("uniqueClientId", Guid.NewGuid().ToString());
                
                return new JObject
                {
                    ["client_id"] = uniqueClientId,
                    ["client_id_issued_at"] = clientIdIssuedAt,
                    ["client_secret_expires_at"] = clientSecretExpiresAt,
                    ["redirect_uris"] = requestBody["redirect_uris"]?.ToObject<JArray>(),
                    ["client_name"] = requestBody["client_name"]?.ToString() ?? "Unknown Application",
                    ["client_uri"] = requestBody["client_uri"]?.ToString() ?? ""
                }.ToString();
            }</set-body>
        </return-response>
    </inbound>
    <backend />
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/authorize.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    AUTHORIZE POLICY
    OAuth 2.0 PKCE authorization endpoint with Entra ID integration.
    
    Flow: Client → Consent (if needed) → Entra ID → Callback → Client
-->
<policies>
    <inbound>
        <base />
        <!-- Extract all OAuth parameters -->
        <set-variable name="clientId" value="@((string)context.Request.Url.Query.GetValueOrDefault("client_id", ""))" />
        <set-variable name="redirect_uri" value="@((string)context.Request.Url.Query.GetValueOrDefault("redirect_uri", ""))" />
        <set-variable name="currentState" value="@((string)context.Request.Url.Query.GetValueOrDefault("state", ""))" />
        <set-variable name="mcpScope" value="@((string)context.Request.Url.Query.GetValueOrDefault("scope", ""))" />
        <set-variable name="mcpClientCodeChallenge" value="@((string)context.Request.Url.Query.GetValueOrDefault("code_challenge", ""))" />
        <set-variable name="mcpClientCodeChallengeMethod" value="@((string)context.Request.Url.Query.GetValueOrDefault("code_challenge_method", ""))" />
        
        <!-- Validate required OAuth parameters -->
        <choose>
            <when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientId")) || 
                              string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("redirect_uri")) || 
                              string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("currentState")))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-header name="Cache-Control" exists-action="override">
                        <value>no-store, no-cache</value>
                    </set-header>
                    <set-body>@{
                        return new JObject {
                            ["error"] = "invalid_request",
                            ["error_description"] = "Missing required parameters: client_id, redirect_uri, and state are all required for OAuth authorization"
                        }.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- Validate required PKCE parameters -->
        <choose>
            <when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("mcpClientCodeChallenge")) || 
                              string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("mcpClientCodeChallengeMethod")))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-header name="Cache-Control" exists-action="override">
                        <value>no-store, no-cache</value>
                    </set-header>
                    <set-body>@{
                        return new JObject {
                            ["error"] = "invalid_request",
                            ["error_description"] = "Missing required PKCE parameters: code_challenge and code_challenge_method are required for secure authorization"
                        }.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- Normalize redirect URI -->
        <set-variable name="normalized_redirect_uri" value="@{
            string redirectUri = context.Variables.GetValueOrDefault<string>("redirect_uri", "");
            if (string.IsNullOrEmpty(redirectUri)) {
                return "";
            }
            
            try {
                string decodedUri = System.Net.WebUtility.UrlDecode(redirectUri);
                return decodedUri;
            } catch (Exception) {
                return redirectUri;
            }
        }" />
        
        <!-- Check for existing approval cookie -->
        <set-variable name="has_approval_cookie" value="@{
            try {
                if (string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientId", "")) || 
                    string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""))) {
                    return false;
                }
                  
                string clientId = context.Variables.GetValueOrDefault<string>("clientId", "");
                string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                
                string APPROVAL_COOKIE_NAME = "__Host-MCP_APPROVED_CLIENTS";
                
                var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
                if (string.IsNullOrEmpty(cookieHeader)) {
                    return false;
                }
                
                string[] cookies = cookieHeader.Split(';');
                foreach (string cookie in cookies) {
                    string trimmedCookie = cookie.Trim();
                    if (trimmedCookie.StartsWith(APPROVAL_COOKIE_NAME + "=")) {
                        try {
                            string cookieValue = trimmedCookie.Substring(APPROVAL_COOKIE_NAME.Length + 1);
                            string decodedValue = System.Text.Encoding.UTF8.GetString(
                                System.Convert.FromBase64String(cookieValue));
                            JArray approvedClients = JArray.Parse(decodedValue);
                            
                            string clientKey = $"{clientId}:{redirectUri}";
                            foreach (var item in approvedClients) {
                                if (item.ToString() == clientKey) {
                                    return true;
                                }
                            }
                        } catch (Exception ex) {
                            // Error parsing approval cookie - ignore and continue
                        }
                        break;
                    }
                }
                
                return false;
            } catch (Exception ex) {
                // Error checking approval cookie - return false
                return false;
            }
        }" />
        
        <!-- Check if the client has been approved via secure cookie -->
        <choose>
            <when condition="@(context.Variables.GetValueOrDefault<bool>("has_approval_cookie"))">
                <!-- Continue with normal flow - client is authorized via secure cookie -->
            </when>
            <otherwise>
                <!-- Redirect to consent page for user approval -->
                <return-response>
                    <set-status code="302" reason="Found" />
                    <set-header name="Location" exists-action="override">
                        <value>@{
                            string basePath = context.Request.OriginalUrl.Scheme + "://" + context.Request.OriginalUrl.Host + (context.Request.OriginalUrl.Port == 80 || context.Request.OriginalUrl.Port == 443 ? "" : ":" + context.Request.OriginalUrl.Port);
                            string clientId = context.Variables.GetValueOrDefault<string>("clientId");
                            // Use the normalized (already decoded) redirect_uri to avoid double-encoding
                            string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri");
                            string state = context.Variables.GetValueOrDefault<string>("currentState");
                            string codeChallenge = context.Variables.GetValueOrDefault<string>("mcpClientCodeChallenge");
                            string codeChallengeMethod = context.Variables.GetValueOrDefault<string>("mcpClientCodeChallengeMethod");
                            
                            // URL encode parameters for the consent redirect URL
                            string encodedClientId = System.Net.WebUtility.UrlEncode(clientId);
                            string encodedRedirectUri = System.Net.WebUtility.UrlEncode(redirectUri);
                            
                            // State parameter: use as-is without additional encoding
                            // context.Request.Url.Query.GetValueOrDefault() preserves the original encoding
                            string encodedState = state;
                            
                            // Code challenge parameters: use as-is since they typically don't need encoding
                            string encodedCodeChallenge = codeChallenge;
                            string encodedCodeChallengeMethod = codeChallengeMethod;
                            
                            return $"{basePath}/consent?client_id={encodedClientId}&redirect_uri={encodedRedirectUri}&state={encodedState}&code_challenge={encodedCodeChallenge}&code_challenge_method={encodedCodeChallengeMethod}";
                        }</value>
                    </set-header>
                </return-response>
            </otherwise>
        </choose>
        
        <set-variable name="codeVerifier" value="@((string)Guid.NewGuid().ToString().Replace("-", ""))" />
        <set-variable name="codeChallenge" value="@{
            using (var sha256 = System.Security.Cryptography.SHA256.Create())
            {
                var bytes = System.Text.Encoding.UTF8.GetBytes((string)context.Variables.GetValueOrDefault("codeVerifier", ""));
                var hash = sha256.ComputeHash(bytes);
                return System.Convert.ToBase64String(hash).TrimEnd('=').Replace('+', '-').Replace('/', '_');
            }
        }" />
        
        <!-- Build the complete Entra ID URL using client's original state -->
        <set-variable name="authUrl" value="@{
            string baseUrl = "https://login.microsoftonline.com/{{EntraIDTenantId}}/oauth2/v2.0/authorize";
            string codeChallenge = context.Variables.GetValueOrDefault("codeChallenge", "");
            string clientState = context.Variables.GetValueOrDefault("currentState", "");
            
            return $"{baseUrl}?response_type=code&client_id={{EntraIDClientId}}&redirect_uri={{OAuthCallbackUri}}&scope={{OAuthScopes}}&code_challenge={codeChallenge}&code_challenge_method=S256&state={System.Net.WebUtility.UrlEncode(clientState)}";
        }" />
        
        <!-- STEP 5: Store authentication data in cache for use in callback -->
        <!-- Generate a confirmation code to return to the MCP client -->
        <set-variable name="mcpConfirmConsentCode" value="@((string)Guid.NewGuid().ToString())" />
        
        <!-- Store code verifier for token exchange using client state -->
        <cache-store-value duration="3600" 
            key="@("CodeVerifier-"+context.Variables.GetValueOrDefault("currentState", ""))" 
            value="@(context.Variables.GetValueOrDefault("codeVerifier", ""))" />
        
        <!-- Map client state to MCP confirmation code for callback -->
        <cache-store-value duration="3600" 
            key="@((string)context.Variables.GetValueOrDefault("currentState"))" 
            value="@(context.Variables.GetValueOrDefault("mcpConfirmConsentCode", ""))" />

        <!-- Store MCP client data -->
        <cache-store-value duration="3600" 
            key="@($"McpClientAuthData-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" 
            value="@{
                return new JObject{
                    ["mcpClientCodeChallenge"] = (string)context.Variables["mcpClientCodeChallenge"],
                    ["mcpClientCodeChallengeMethod"] = (string)context.Variables["mcpClientCodeChallengeMethod"],
                    ["mcpClientState"] = (string)context.Variables["currentState"],
                    ["mcpClientScope"] = (string)context.Variables["mcpScope"],
                    ["mcpCallbackRedirectUri"] = (string)context.Variables["normalized_redirect_uri"]
                }.ToString();
            }" />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />        
        <!-- Return the response with a 302 status code for redirect -->
        <return-response>
            <set-status code="302" reason="Found" />
            <set-header name="Location" exists-action="override">
                <value>@(context.Variables.GetValueOrDefault("authUrl", ""))</value>
            </set-header>
            <!-- Add cache control headers to ensure browser follows redirect -->
            <set-header name="Cache-Control" exists-action="override">
                <value>no-store, no-cache, must-revalidate</value>
            </set-header>
            <set-header name="Pragma" exists-action="override">
                <value>no-cache</value>
            </set-header>
            <!-- Remove any content-type that might interfere -->
            <set-header name="Content-Type" exists-action="delete" />
        </return-response>
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/oauth-callback.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    OAUTH CALLBACK POLICY
    This policy implements the callback endpoint for PKCE OAuth2 flow with Entra ID.
-->
<policies>
    <inbound>
        <base />
        <!-- STEP 1: Extract the authorization code and state from Entra ID callback -->
        <set-variable name="authCode" value="@((string)context.Request.Url.Query.GetValueOrDefault("code", ""))" />
        <set-variable name="clientState" value="@{
            string stateValue = (string)context.Request.Url.Query.GetValueOrDefault("state", "");
            return !string.IsNullOrEmpty(stateValue) ? System.Net.WebUtility.UrlDecode(stateValue) : "";
        }" />
        <set-variable name="sessionState" value="@((string)context.Request.Url.Query.GetValueOrDefault("session_state", ""))" />
        
        <!-- Validate required OAuth parameters -->
        <choose>
            <when condition="@(string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("authCode", "")) || string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("clientState", "")))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errorResponse = new JObject();
                        errorResponse["error"] = "invalid_request";
                        errorResponse["error_description"] = "Missing required OAuth callback parameters";
                        return errorResponse.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- STEP 1.5: Validate that the state matches what the user consented to -->
        <set-variable name="consent_state_valid" value="@{
            try {
                string returnedState = context.Variables.GetValueOrDefault<string>("clientState", "");
                if (string.IsNullOrEmpty(returnedState)) {
                    return false;
                }
                
                // Extract consent state from cookie
                var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
                if (string.IsNullOrEmpty(cookieHeader)) {
                    return false;
                }
                
                string cookieName = "__Host-MCP_CONSENT_STATE";
                string[] cookies = cookieHeader.Split(';');
                foreach (string cookie in cookies) {
                    string trimmedCookie = cookie.Trim();
                    if (trimmedCookie.StartsWith(cookieName + "=")) {
                        string cookieValue = trimmedCookie.Substring(cookieName.Length + 1);
                        string decodedValue = System.Text.Encoding.UTF8.GetString(
                            System.Convert.FromBase64String(cookieValue));
                        JObject consentData = JObject.Parse(decodedValue);
                        
                        string consentedState = consentData["state"]?.ToString();
                        
                        // Constant-time comparison to prevent timing attacks
                        if (string.IsNullOrEmpty(consentedState) || returnedState.Length != consentedState.Length) {
                            return false;
                        }
                        
                        int result = 0;
                        for (int i = 0; i < returnedState.Length; i++) {
                            result |= returnedState[i] ^ consentedState[i];
                        }
                        
                        return (result == 0);
                    }
                }
                
                return false;
            } catch (Exception ex) {
                return false;
            }
        }" />
        
        <!-- Validate consent state cookie -->
        <choose>
            <when condition="@(!context.Variables.GetValueOrDefault<bool>("consent_state_valid"))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errorResponse = new JObject();
                        errorResponse["error"] = "invalid_state";
                        errorResponse["error_description"] = "State parameter does not match consented state.";
                        return errorResponse.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- Clear the consent state cookie since it's been validated -->
        <set-variable name="clear_consent_cookie" value="__Host-MCP_CONSENT_STATE=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=Lax" />
        
        <!-- STEP 2: Retrieve stored PKCE code verifier using the client state parameter -->
        <cache-lookup-value key="@("CodeVerifier-"+context.Variables.GetValueOrDefault("clientState", ""))" variable-name="codeVerifier" />
        
        <!-- Validate that code verifier was found in cache -->
        <choose>
            <when condition="@(string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("codeVerifier", "")))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errorResponse = new JObject();
                        errorResponse["error"] = "invalid_request";
                        errorResponse["error_description"] = "Authorization session expired or invalid state parameter";
                        return errorResponse.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- STEP 3: Set token request parameters -->
        <set-variable name="codeChallengeMethod" value="S256" />
        <set-variable name="redirectUri" value="{{OAuthCallbackUri}}" />
        <set-variable name="clientId" value="{{EntraIDClientId}}" />
        <set-variable name="clientAssertionType" value="@(System.Net.WebUtility.UrlEncode("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"))" />
        <authentication-managed-identity resource="api://AzureADTokenExchange" client-id="{{EntraIDFicClientId}}" output-token-variable-name="ficToken"/>
         
        <!-- STEP 4: Configure token request to Entra ID -->
        <set-method>POST</set-method>
        <set-header name="Content-Type" exists-action="override">
            <value>application/x-www-form-urlencoded</value>
        </set-header>
        <set-body>@{
            return $"client_id={context.Variables.GetValueOrDefault("clientId")}&grant_type=authorization_code&code={context.Variables.GetValueOrDefault("authCode")}&redirect_uri={context.Variables.GetValueOrDefault("redirectUri")}&scope=User.Read&code_verifier={context.Variables.GetValueOrDefault("codeVerifier")}&client_assertion_type={context.Variables.GetValueOrDefault("clientAssertionType")}&client_assertion={context.Variables.GetValueOrDefault("ficToken")}";
        }</set-body>
        <rewrite-uri template="/token" />
    </inbound>

    <backend>
        <base />
    </backend>    <outbound>
        <base />
        <!-- STEP 5: Process the token response from Entra ID -->
        <trace source="apim-policy">
            <message>@("Token response received: " + context.Response.Body.As<string>(preserveContent: true))</message>
        </trace>
        <!-- Check if the response is successful (200 OK) and contains a token -->
        <choose>
            <when condition="@(context.Response.StatusCode != 200 || string.IsNullOrEmpty(context.Response.Body.As<JObject>(preserveContent: true)["access_token"]?.ToString()))">
                <return-response>
                    <set-status code="@(context.Response.StatusCode)" reason="@(context.Response.StatusReason)" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errorResponse = new JObject();
                        errorResponse["error"] = "token_error";
                        errorResponse["error_description"] = "Failed to retrieve access token from Entra ID.";
                        return errorResponse.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- STEP 6: Generate secure session token for MCP client -->
        <set-variable name="IV" value="{{EncryptionIV}}" />
        <set-variable name="key" value="{{EncryptionKey}}" />
        <set-variable name="sessionId" value="@((string)Guid.NewGuid().ToString().Replace("-", ""))" />
        <set-variable name="encryptedSessionKey" value="@{
            // Generate a unique session ID
            string sessionId = (string)context.Variables.GetValueOrDefault("sessionId");
            byte[] sessionIdBytes = Encoding.UTF8.GetBytes(sessionId);
            
            // Encrypt the session ID using AES
            byte[] IV = Convert.FromBase64String((string)context.Variables["IV"]);
            byte[] key = Convert.FromBase64String((string)context.Variables["key"]);
            
            byte[] encryptedBytes = sessionIdBytes.Encrypt("Aes", key, IV);
            
            return Convert.ToBase64String(encryptedBytes);
        }" />

        <!-- STEP 6: Lookup MCP client redirect URI stored during authorization -->
        <cache-lookup-value key="@((string)context.Variables.GetValueOrDefault("clientState"))" variable-name="mcpConfirmConsentCode" />
        <cache-lookup-value key="@($"McpClientAuthData-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" variable-name="mcpClientData" />
        
        <!-- Validate that MCP client data was found in cache -->
        <choose>
            <when condition="@(string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("mcpConfirmConsentCode", "")) || string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("mcpClientData", "")))">
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errorResponse = new JObject();
                        errorResponse["error"] = "invalid_request";
                        errorResponse["error_description"] = "MCP client authorization session expired or invalid";
                        return errorResponse.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>

        <!-- STEP 8: Use the client's original state parameter directly -->
        <set-variable name="mcpState" value="@(context.Variables.GetValueOrDefault<string>("clientState"))" />
        <!-- STEP 9: Extract the stored mcp client callback redirect uri from cache -->
        <set-variable name="callbackRedirectUri" value="@{
            var mcpAuthDataAsJObject = JObject.Parse((string)context.Variables["mcpClientData"]);
            return mcpAuthDataAsJObject["mcpCallbackRedirectUri"];
        }" />
        <!-- STEP 10: Store the encrypted session key and Entra token in cache -->
        <!-- Store the encrypted session key with the MCP confirmation code as key -->
        <cache-store-value duration="3600" 
            key="@($"AccessToken-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" 
            value="@($"{context.Variables.GetValueOrDefault("encryptedSessionKey")}")" />
        
        <!-- Store the Entra token for later use -->
        <cache-store-value duration="3600" 
            key="@($"EntraToken-{context.Variables.GetValueOrDefault("sessionId")}")"
            value="@(context.Response.Body.As<JObject>(preserveContent: true).ToString())" />
        
        <!-- STEP 11: Redirect back to MCP client with confirmation code -->
        <return-response>
            <set-status code="302" reason="Found" />
            <set-header name="Location" exists-action="override">
                <value>@($"{context.Variables.GetValueOrDefault("callbackRedirectUri")}?code={context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}&state={System.Net.WebUtility.UrlEncode((string)context.Variables.GetValueOrDefault("mcpState"))}")</value>
            </set-header>
            <!-- Clear the consent state cookie -->
            <set-header name="Set-Cookie" exists-action="append">
                <value>@(context.Variables.GetValueOrDefault<string>("clear_consent_cookie"))</value>
            </set-header>
            <set-body />
        </return-response>
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/token.policy.xml:
--------------------------------------------------------------------------------

```
<!--
    TOKEN POLICY
    This policy implements the token endpoint for PKCE OAuth2 flow.
    
    Flow:
    1. MCP client sends token request with code and code_verifier
    2. We validate the code_verifier against the stored code_challenge
    3. We retrieve the cached access token and return it to the client
-->
<policies>
    <inbound>
        <base />
        <!-- STEP 1: Extract parameters from token request -->
        <!-- Read the request body as a string while preserving it for later processing -->
        <set-variable name="tokenRequestBody" value="@((string)context.Request.Body.As<string>(preserveContent: true))" />
        
        <!-- Extract the confirmation code from the request -->
        <set-variable name="mcpConfirmConsentCode" value="@{
            // Retrieve the raw body string
            var body = context.Variables.GetValueOrDefault<string>("tokenRequestBody");
            if (!string.IsNullOrEmpty(body))
            {
                // Split the body into name/value pairs
                var pairs = body.Split('&');
                foreach (var pair in pairs)
                {
                    var keyValue = pair.Split('=');
                    if (keyValue.Length == 2)
                    {
                        if(keyValue[0] == "code")
                        {
                            return keyValue[1];
                        }
                    }
                }
            }
            return "";
        }" />              
        <!-- Extract the code_verifier from the request and URL-decode it -->        
        <set-variable name="mcpClientCodeVerifier" value="@{
            // Retrieve the raw body string
            var body = context.Variables.GetValueOrDefault<string>("tokenRequestBody");
            if (!string.IsNullOrEmpty(body))
            {
                // Split the body into name/value pairs
                var pairs = body.Split('&');
                foreach (var pair in pairs)
                {
                    var keyValue = pair.Split('=');
                    if (keyValue.Length == 2)
                    {
                        if(keyValue[0] == "code_verifier")
                        {
                            // URL-decode the code_verifier if needed
                            return System.Net.WebUtility.UrlDecode(keyValue[1]);
                        }
                    }
                }
            }
            return "";
        }" />
            
        <!-- STEP 2: Extract state parameters -->
        <set-variable name="mcpState" value="@((string)context.Request.Url.Query.GetValueOrDefault("state", ""))" />
        <set-variable name="stateSession" value="@((string)context.Request.Url.Query.GetValueOrDefault("state_session", ""))" />        
    </inbound>
    <backend />
    <outbound>
        <base />
        <!-- STEP 3: Retrieve stored MCP client data -->
        <!-- Lookup the stored MCP client code challenge and challenge method from the cache -->
        <cache-lookup-value key="@($"McpClientAuthData-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" variable-name="mcpClientAuthData" />
        
        <!-- Extract the stored code challenge from the cached data -->
        <set-variable name="storedMcpClientCodeChallenge" value="@{
            var mcpAuthDataAsJObject = JObject.Parse((string)context.Variables["mcpClientAuthData"]);
            return (string)mcpAuthDataAsJObject["mcpClientCodeChallenge"];
        }" />            
        <!-- STEP 4: Compute and validate the code challenge -->
        <!-- Generate a challenge from the incoming code_verifier using the stored challenge method -->
        <set-variable name="mcpServerComputedCodeChallenge" value="@{
            var mcpAuthDataAsJObject = JObject.Parse((string)context.Variables["mcpClientAuthData"]);
            string codeVerifier = (string)context.Variables.GetValueOrDefault("mcpClientCodeVerifier", "");
            string codeChallengeMethod = ((string)mcpAuthDataAsJObject["mcpClientCodeChallengeMethod"]).ToLower();
            
            if(string.IsNullOrEmpty(codeVerifier)){
                return string.Empty;
            }
            
            if(codeChallengeMethod == "plain"){
                // For "plain", no transformation is applied
                return codeVerifier;
            } else if(codeChallengeMethod == "s256"){
                // For S256, compute the SHA256 hash, Base64 encode it, and convert to URL-safe format
                using (var sha256 = System.Security.Cryptography.SHA256.Create())
                {
                    var bytes = System.Text.Encoding.UTF8.GetBytes(codeVerifier);
                    var hash = sha256.ComputeHash(bytes);
                    
                    // Convert the hash to a Base64 string
                    string base64 = Convert.ToBase64String(hash);

                    // Convert Base64 string into a URL-safe variant
                    // Replace '+' with '-', '/' with '_', and remove any '=' padding
                    return base64.Replace("+", "-").Replace("/", "_").Replace("=", "");
                }
            } else {
                // Unsupported method
                return string.Empty;
            }
        }" />            
        <!-- STEP 5: Verify code challenge matches -->
        <choose>
            <when condition="@(string.Compare((string)context.Variables.GetValueOrDefault("mcpServerComputedCodeChallenge", ""), (string)context.Variables.GetValueOrDefault("storedMcpClientCodeChallenge", "")) != 0)">
                <!-- If they don't match, return an error -->
                <return-response>
                    <set-status code="400" reason="Bad Request" />
                    <set-body>@("{\"error\": \"code_verifier does not match.\"}")</set-body>
                </return-response>
            </when>
        </choose>
        
        <!-- STEP 5.5: Verify client registration -->
        <!-- Extract client ID and redirect URI from the token request -->
        <set-variable name="client_id" value="@{
            // Retrieve the raw body string
            var body = context.Variables.GetValueOrDefault<string>("tokenRequestBody");
            if (!string.IsNullOrEmpty(body))
            {
                // Split the body into name/value pairs
                var pairs = body.Split('&');
                foreach (var pair in pairs)
                {
                    var keyValue = pair.Split('=');
                    if (keyValue.Length == 2)
                    {
                        if(keyValue[0] == "client_id")
                        {
                            return System.Net.WebUtility.UrlDecode(keyValue[1]);
                        }
                    }
                }
            }
            return "";
        }" />
        
        <set-variable name="redirect_uri" value="@{
            // Retrieve the raw body string
            var body = context.Variables.GetValueOrDefault<string>("tokenRequestBody");
            if (!string.IsNullOrEmpty(body))
            {
                // Split the body into name/value pairs
                var pairs = body.Split('&');
                foreach (var pair in pairs)
                {
                    var keyValue = pair.Split('=');
                    if (keyValue.Length == 2)
                    {
                        if(keyValue[0] == "redirect_uri")
                        {
                            return System.Net.WebUtility.UrlDecode(keyValue[1]);
                        }
                    }
                }
            }
            return "";
        }" />
        
        <!-- Normalize the redirect URI -->
        <set-variable name="normalized_redirect_uri" value="@{
            string redirectUri = context.Variables.GetValueOrDefault<string>("redirect_uri", "");
            return System.Net.WebUtility.UrlDecode(redirectUri);
        }" />
        
        <!-- Look up client information from cache -->
        <cache-lookup-value key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("client_id")}")" variable-name="clientInfoJson" />
        
        <!-- If cache lookup failed, try to retrieve from CosmosDB -->
        <choose>
            <when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientInfoJson")))">
                <!-- Get CosmosDB access token using managed identity -->
                <authentication-managed-identity resource="https://cosmos.azure.com" output-token-variable-name="cosmosAccessToken" />
                
                <send-request mode="new" response-variable-name="cosmosClientResponse" timeout="30" ignore-error="true">
                    <set-url>@($"{{CosmosDbEndpoint}}/dbs/{{CosmosDbDatabase}}/colls/{{CosmosDbContainer}}/docs/{context.Variables.GetValueOrDefault<string>("client_id")}")</set-url>
                    <set-method>GET</set-method>
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-header name="x-ms-version" exists-action="override">
                        <value>2018-12-31</value>
                    </set-header>
                    <set-header name="x-ms-partitionkey" exists-action="override">
                        <value>@($"[\"{context.Variables.GetValueOrDefault<string>("client_id")}\"]")</value>
                    </set-header>
                    <set-header name="Authorization" exists-action="override">
                        <value>@($"type=aad&ver=1.0&sig={context.Variables.GetValueOrDefault<string>("cosmosAccessToken")}")</value>
                    </set-header>
                </send-request>
                
                <!-- If CosmosDB request was successful, extract client info -->
                <choose>
                    <when condition="@(((IResponse)context.Variables["cosmosClientResponse"]).StatusCode == 200)">
                        <set-variable name="clientInfoJson" value="@{
                            var cosmosResponse = (IResponse)context.Variables["cosmosClientResponse"];
                            var cosmosDocument = cosmosResponse.Body.As<JObject>();
                            
                            // Extract the client info fields we need
                            var clientInfo = new JObject();
                            clientInfo["client_name"] = cosmosDocument["client_name"];
                            clientInfo["client_uri"] = cosmosDocument["client_uri"];
                            clientInfo["redirect_uris"] = cosmosDocument["redirect_uris"];
                            
                            return clientInfo.ToString();
                        }" />
                        
                        <!-- Store in cache for future requests -->
                        <cache-store-value duration="3600" 
                            key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("client_id")}")" 
                            value="@(context.Variables.GetValueOrDefault<string>("clientInfoJson"))" />
                    </when>
                </choose>
            </when>
        </choose>
        
        <!-- Verify that the client exists and the redirect URI is valid -->
        <set-variable name="is_client_registered" value="@{
            try {
                string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                
                if (string.IsNullOrEmpty(clientId)) {
                    return false;
                }
                
                // Get the client info from the variable set by cache-lookup-value
                string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson");
                  if (string.IsNullOrEmpty(clientInfoJson)) {
                    context.Trace($"Client info not found in cache for client_id: {clientId}");
                    return false;
                }
                
                // Parse client info
                JObject clientInfo = JObject.Parse(clientInfoJson);
                JArray redirectUris = clientInfo["redirect_uris"]?.ToObject<JArray>();
                
                // Check if the redirect URI is in the registered URIs
                if (redirectUris != null) {
                    foreach (var uri in redirectUris) {
                        // Normalize the URI from the cache for comparison
                        string registeredUri = System.Net.WebUtility.UrlDecode(uri.ToString());
                        if (registeredUri == redirectUri) {
                            return true;
                        }
                    }
                }
                
                context.Trace($"Redirect URI mismatch - URI: {redirectUri} not found in registered URIs");
                return false;
            }
            catch (Exception ex) {
                context.Trace($"Error checking client registration: {ex.Message}");
                return false;
            }
        }" />
        
        <!-- Check if client is properly registered -->
        <choose>
            <when condition="@(!context.Variables.GetValueOrDefault<bool>("is_client_registered"))">
                <!-- Client is not properly registered, return error -->
                <return-response>
                    <set-status code="401" reason="Unauthorized" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-body>@{
                        var errorResponse = new JObject();
                        errorResponse["error"] = "invalid_client";
                        errorResponse["error_description"] = "Client not found or redirect URI is invalid.";
                        return errorResponse.ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>

        <!-- STEP 6: Retrieve cached tokens -->
        <!-- Get the access token stored during the authorization process -->
        <cache-lookup-value key="@($"AccessToken-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" variable-name="cachedSessionToken" />
        
        <!-- STEP 7: Generate token response -->
        <set-variable name="jsonPayload" value="@{
            var accessToken = context.Variables.GetValueOrDefault<string>("cachedSessionToken");
            var payloadObject = new
            {
                access_token = accessToken,
                token_type = "Bearer",
                expires_in = 3600,
                refresh_token = "",
                scope = "openid profile email"
            };

            // Serialize the object to a JSON string.
            return Newtonsoft.Json.JsonConvert.SerializeObject(payloadObject);
        }" />
        <set-body template="none">@{
            return (string)context.Variables.GetValueOrDefault("jsonPayload", "");
        }</set-body>
        <set-header name="access-control-allow-origin" exists-action="override">
            <value>*</value>
        </set-header>
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
```

--------------------------------------------------------------------------------
/infra/app/apim-oauth/consent.policy.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
    Consent Policy - Handles user consent for OAuth client applications
-->
<policies>
    <inbound>        
        <base />        
        <!-- Extract form body once -->
        <set-variable name="form_body" value="@{
            if (context.Request.Method == "POST") {
                string contentType = context.Request.Headers.GetValueOrDefault("Content-Type", "");
                if (contentType.Contains("application/x-www-form-urlencoded")) {
                    return context.Request.Body.As<string>(preserveContent: true);
                }
            }
            return "";
        }" />
        
        <!-- Extract individual parameters with consistent decoding -->
        <set-variable name="client_id" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "client_id") {
                        return System.Net.WebUtility.UrlDecode(keyValue[1]);
                    }
                }
            }
            
            // Fallback to query string (GET)
            string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("client_id", "");
            return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : "";
        }" />
        
        <set-variable name="redirect_uri" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "redirect_uri") {
                        return keyValue[1];
                    }
                }
            }
            
            // Fallback to query string (GET)
            return (string)context.Request.Url.Query.GetValueOrDefault("redirect_uri", "");
        }" />
        
        <set-variable name="state" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "state") {
                        return System.Net.WebUtility.UrlDecode(keyValue[1]);
                    }
                }
            }
            
            // Fallback to query string (GET)
            string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("state", "");
            return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : "";
        }" />
        
        <set-variable name="code_challenge" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "code_challenge") {
                        return keyValue[1];
                    }
                }
            }
            
            // Fallback to query string (GET)
            return (string)context.Request.Url.Query.GetValueOrDefault("code_challenge", "");
        }" />
        
        <set-variable name="code_challenge_method" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "code_challenge_method") {
                        return keyValue[1];
                    }
                }
            }
            
            // Fallback to query string (GET)
            return (string)context.Request.Url.Query.GetValueOrDefault("code_challenge_method", "");
        }" />
      
         <set-variable name="access_denied_template" value="@{
            return @"<html lang='en'>
<head>    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Access Denied</title>
    <style>
__COMMON_STYLES__
        .error-details {
            background-color: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 8px;
            padding: 20px;
            margin: 20px 0;
            font-family: 'Courier New', Consolas, monospace;
            font-size: 14px;
            line-height: 1.6;
            white-space: pre-wrap;
            overflow-x: auto;
        }
        
        .error-title {
            color: #dc3545;
            font-weight: bold;
            margin-bottom: 10px;
        }
        
        .debug-section {
            margin-top: 15px;
            padding-top: 15px;
            border-top: 1px solid #dee2e6;
        }
        
        .debug-label {
            font-weight: bold;
            color: #495057;
        }
    </style>
</head>
<body>
    <div class='consent-container'>
        <h1 class='denial-heading'>Access Denied</h1>
        <div class='error-details'>
            <div class='error-title'>Error Details:</div>
            __DENIAL_MESSAGE__
        </div>
        <p>The application will not be able to access your data.</p>
        <p>You can close this window safely.</p>
    </div>
</body>
</html>";
        }" />
        
        <!-- Reusable function to generate 403 error response -->
        <set-variable name="generate_403_response" value="@{
            string errorTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template");
            string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles");
            string message = "Access denied.";
            
            // Replace placeholders with actual content
            errorTemplate = errorTemplate.Replace("__COMMON_STYLES__", commonStyles);
            errorTemplate = errorTemplate.Replace("__DENIAL_MESSAGE__", message);
            
            return errorTemplate;
        }" />
            <!-- Error page template -->
        <set-variable name="client_not_found_template" value="@{
            return @"<html lang='en'>
<head>    
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Client Not Found</title>
    <style>
__COMMON_STYLES__
    </style>
</head>
<body>
    <div class='consent-container'>        
        <h1 class='denial-heading'>Client Not Found</h1>
        <p>The client registration for the specified client was not found.</p>
        <div class='client-info'>
            <p><strong>Client ID:</strong> <code>__CLIENT_ID_DISPLAY__</code></p>
            <p><strong>Redirect URI:</strong> <code>__REDIRECT_URI__</code></p>
        </div>
        <p>Please ensure that you are using a properly registered client application.</p>
        <p>You can close this window safely.</p>
    </div>
</body>
</html>";
        }" />
        <!-- Normalize redirect URI by handling potential double-encoding -->
        <set-variable name="normalized_redirect_uri" value="@{
            string redirectUri = context.Variables.GetValueOrDefault<string>("redirect_uri", "");
            
            if (string.IsNullOrEmpty(redirectUri)) {
                return "";
            }
            
            try {
                string firstDecode = System.Net.WebUtility.UrlDecode(redirectUri);
                
                // Check if still encoded (contains % followed by hex digits)
                if (firstDecode.Contains("%") && System.Text.RegularExpressions.Regex.IsMatch(firstDecode, @"%[0-9A-Fa-f]{2}")) {
                    // Double-encoded, decode again
                    string secondDecode = System.Net.WebUtility.UrlDecode(firstDecode);
                    return secondDecode;
                } else {
                    // Single encoding, first decode is sufficient
                    return firstDecode;
                }
            } catch (Exception) {
                // If decoding fails, return original value
                return redirectUri;
            }
        }" />
        
        <!-- Cache client information lookup -->
        <cache-lookup-value key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("client_id")}")" variable-name="clientInfoJson" />
        
        <!-- If cache lookup failed, try to retrieve from CosmosDB -->
        <choose>
            <when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientInfoJson")))">
                <!-- Get CosmosDB access token using managed identity -->
                <authentication-managed-identity resource="https://cosmos.azure.com" output-token-variable-name="cosmosAccessToken" />
                
                <send-request mode="new" response-variable-name="cosmosClientResponse" timeout="30" ignore-error="true">
                    <set-url>@($"{{CosmosDbEndpoint}}/dbs/{{CosmosDbDatabase}}/colls/{{CosmosDbContainer}}/docs/{context.Variables.GetValueOrDefault<string>("client_id")}")</set-url>
                    <set-method>GET</set-method>
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json</value>
                    </set-header>
                    <set-header name="x-ms-version" exists-action="override">
                        <value>2018-12-31</value>
                    </set-header>
                    <set-header name="x-ms-partitionkey" exists-action="override">
                        <value>@($"[\"{context.Variables.GetValueOrDefault<string>("client_id")}\"]")</value>
                    </set-header>
                    <set-header name="Authorization" exists-action="override">
                        <value>@($"type=aad&ver=1.0&sig={context.Variables.GetValueOrDefault<string>("cosmosAccessToken")}")</value>
                    </set-header>
                </send-request>
                
                <!-- If CosmosDB request was successful, extract client info -->
                <choose>
                    <when condition="@(((IResponse)context.Variables["cosmosClientResponse"]).StatusCode == 200)">
                        <set-variable name="clientInfoJson" value="@{
                            var cosmosResponse = (IResponse)context.Variables["cosmosClientResponse"];
                            var cosmosDocument = cosmosResponse.Body.As<JObject>();
                            
                            // Extract the client info fields we need
                            var clientInfo = new JObject();
                            clientInfo["client_name"] = cosmosDocument["client_name"];
                            clientInfo["client_uri"] = cosmosDocument["client_uri"];
                            clientInfo["redirect_uris"] = cosmosDocument["redirect_uris"];
                            
                            return clientInfo.ToString();
                        }" />
                        
                        <!-- Store in cache for future requests -->
                        <cache-store-value duration="3600" 
                            key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("client_id")}")" 
                            value="@(context.Variables.GetValueOrDefault<string>("clientInfoJson"))" />
                    </when>
                </choose>
            </when>
        </choose>
        
        <!-- Get OAuth scopes from configuration -->
        <set-variable name="oauth_scopes" value="{{OAuthScopes}}" />
        
        <!-- Generate CSRF token for form protection (GET requests only) -->
        <set-variable name="csrf_token" value="@{
            // Only generate tokens for GET requests (showing consent form)
            // POST requests validate existing tokens, not generate new ones
            if (context.Request.Method != "GET") {
                return "";
            }
            
            // Generate random CSRF token using Guid and timestamp
            string guidPart = Guid.NewGuid().ToString("N");
            string timestampPart = DateTime.UtcNow.Ticks.ToString();
            string combinedString = guidPart + timestampPart;
            
            // Create URL-safe token by encoding combined string
            string token = System.Convert.ToBase64String(
                System.Text.Encoding.UTF8.GetBytes(combinedString)
            ).Replace("+", "-").Replace("/", "_").Replace("=", "").Substring(0, 32);
            
            return token;
        }" />
        
        <!-- Cache CSRF token for validation (GET requests only) -->
        <choose>
            <when condition="@(context.Request.Method == "GET" && !string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("csrf_token")))">
                <cache-store-value key="@($"CSRF-{context.Variables.GetValueOrDefault<string>("csrf_token")}")" 
                                  value="@{
                                      string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                                      string normalizedRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                                      string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
                                      string tokenData = $"{clientId}:{normalizedRedirectUri}:{timestamp}";
                                      
                                      // Add debugging metadata
                                      string debugInfo = $"CACHED_AT:{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}";
                                      return $"{tokenData}|{debugInfo}";
                                  }" 
                                  duration="900" />
                <!-- Track token caching for debugging -->
                <set-variable name="csrf_token_cached" value="true" />
            </when>
            <otherwise>
                <set-variable name="csrf_token_cached" value="false" />
            </otherwise>
        </choose>
          <!-- Validate client registration -->
        <set-variable name="is_client_registered" value="@{
            try {
                string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                
                if (string.IsNullOrEmpty(clientId)) {
                    return false;
                }
                
                // Get client info from cache lookup
                string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson");
                  if (string.IsNullOrEmpty(clientInfoJson)) {
                    return false;
                }
                
                // Parse client configuration
                JObject clientInfo = JObject.Parse(clientInfoJson);
                JArray redirectUris = clientInfo["redirect_uris"]?.ToObject<JArray>();
                
                // Validate redirect URI is registered
                if (redirectUris != null) {
                    foreach (var uri in redirectUris) {
                        // Normalize registered URI for comparison
                        string registeredUri = System.Net.WebUtility.UrlDecode(uri.ToString());
                        if (registeredUri == redirectUri) {
                            return true;
                        }
                    }
                }
                
                return false;
            }
            catch (Exception ex) {
                return false;
            }
        }" />
        
        <!-- Extract client name from cache -->
        <set-variable name="client_name" value="@{
            try {
                string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                
                if (string.IsNullOrEmpty(clientId)) {
                    return "Unknown Application";
                }
                
                // Get client info from cache lookup
                string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson");
                
                if (string.IsNullOrEmpty(clientInfoJson)) {
                    return clientId;
                }
                
                // Parse client configuration
                JObject clientInfo = JObject.Parse(clientInfoJson);
                string clientName = clientInfo["client_name"]?.ToString();
                
                return string.IsNullOrEmpty(clientName) ? clientId : clientName;
            }
            catch (Exception ex) {
                return context.Variables.GetValueOrDefault<string>("client_id", "Unknown Application");
            }
        }" />
        
        <!-- Extract client URI from cache -->
        <set-variable name="client_uri" value="@{
            try {
                // Get client info from cache lookup
                string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson");
                
                if (string.IsNullOrEmpty(clientInfoJson)) {
                    return "N/A";
                }
                
                // Parse client configuration
                JObject clientInfo = JObject.Parse(clientInfoJson);
                string clientUri = clientInfo["client_uri"]?.ToString();
                
                return string.IsNullOrEmpty(clientUri) ? "N/A" : clientUri;
            }
            catch (Exception ex) {
                return "N/A";
            }
        }" />
        
        <!-- Define common styles for consent and error pages -->
        <set-variable name="common_styles" value="@{
            return @"        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 100%;
            margin: 0;            padding: 0;
            line-height: 1.6;
            min-height: 100vh;
            background: linear-gradient(135deg, #1f1f1f, #333344, #3f4066); /* Modern dark gradient */
            color: #333333;
            display: flex;
            justify-content: center;
            align-items: center;
        }.container, .consent-container {
            background-color: #ffffff;
            border-radius: 4px; /* Adding some subtle rounding */
            padding: 30px;
            max-width: 600px;            width: 90%;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
            border: none;
        }
        
        h1 {
            margin-bottom: 20px;
            border-bottom: 1px solid #EDEBE9;
            padding-bottom: 10px;
            font-weight: 500;
        }
        .consent-heading {
            color: #0078D4; /* Microsoft Blue */
        }
        .denial-heading {
            color: #D83B01; /* Microsoft Attention color */
        }
        
        p {
            margin: 15px 0;
            line-height: 1.7;
            color: #323130; /* Microsoft text color */
        }          .client-info {
            background-color: #F5F5F5; /* Light gray background for info boxes */
            padding: 15px;
            border-radius: 4px; /* Adding some subtle rounding */
            margin: 15px 0;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
            border: 1px solid #EDEBE9;
        }
          .client-info p {
            display: flex;
            align-items: flex-start;
            margin: 8px 0;
        }
        
        .client-info strong {
            min-width: 160px;
            flex-shrink: 0;
            text-align: left;
            padding-right: 15px;
            color: #0078D4; /* Microsoft Blue */
        }
          .client-info code {
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            background-color: rgba(240, 240, 250, 0.5);
            padding: 2px 6px;
            border-radius: 4px; /* Adding some subtle rounding */
            color: #0078D4; /* Microsoft Blue */
            word-break: break-all;
        }
          .btn {
            display: inline-block;
            padding: 8px 16px;
            margin: 10px 0;
            border-radius: 4px; /* Adding some subtle rounding */
            text-decoration: none;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
        }
        
        .btn-primary {
            background-color: #0078D4; /* Microsoft Blue */
            color: white;
            border: none;
        }
        .btn-primary:hover {
            background-color: #106EBE; /* Microsoft Blue hover */
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        }
        
        .btn-secondary {
            background-color: #D83B01; /* Microsoft Red */
            color: white; /* White text */
            border: none;
        }
        .btn-secondary:hover {
            background-color: #A80000; /* Darker red on hover */
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        }
          .buttons {
            margin-top: 20px;
            display: flex;
            gap: 10px;
            justify-content: flex-start;
        }
        
        a {
            color: #0078D4; /* Microsoft Blue */
            text-decoration: none;
            font-weight: 600;
        }
        a:hover {
            text-decoration: underline;
        }
        strong {
            color: #0078D4; /* Microsoft Blue */
            font-weight: 600;
        }          .error-message {
            background-color: #FDE7E9; /* Light red background */
            padding: 15px;
            margin: 15px 0;
            border-radius: 4px; /* Adding some subtle rounding */
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
            border-left: 3px solid #D83B01; /* Microsoft Attention color */
        }
        
        .error-message p {
            margin: 8px 0;
        }
        
        .error-message p:first-child {
            font-weight: 500;
            color: #D83B01; /* Microsoft Attention color */
        }";
        }" />
        
        <!-- Consent page HTML template -->
        <set-variable name="consent_page_template" value="@{
            return @"<html lang='en'>
<head>    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Application Consent</title>
    <style>
__COMMON_STYLES__    /* Additional styles for scopes list */
    .scopes-list {
        margin: 0;
        padding-left: 0;
    }
    .scopes-list li {
        list-style-type: none;
        padding: 4px 0;
        display: flex;
    }
    </style>
</head>
<body>
    <div class='consent-container'>
        <h1 class='consent-heading'>Application Access Request</h1>

        <p>The following application is requesting access to <strong>{{MCPServerName}}</strong>, which might include access to everything <strong>{{MCPServerName}}</strong> has been and will be granted access to.</p>
        
        <div class='client-info'>
            <p><strong>Application Name:</strong> <code>__CLIENT_NAME__</code></p>
            <p><strong>Application Website:</strong> <code>__CLIENT_URI__</code></p>
            <p><strong>Application ID:</strong> <code>__CLIENT_ID_DISPLAY__</code></p>
            <p><strong>Redirect URI:</strong> <code>__REDIRECT_URI__</code></p>
        </div>        
        <p>The application will have access to the following scopes, used by <strong>{{MCPServerName}}</strong>:</p>
        <div class='client-info'>
            <ul class='scopes-list'>
                <li>__OAUTH_SCOPES__</li>
            </ul>
        </div>        <div class='buttons'>
            <form method='post' action='__CONSENT_ACTION_URL__' style='display: inline-block;'>
                <input type='hidden' name='client_id' value='__CLIENT_ID_FORM__'>
                <input type='hidden' name='redirect_uri' value='__REDIRECT_URI__'>
                <input type='hidden' name='state' value='__STATE__'>
                <input type='hidden' name='code_challenge' value='__CODE_CHALLENGE__'>
                <input type='hidden' name='code_challenge_method' value='__CODE_CHALLENGE_METHOD__'>
                <input type='hidden' name='csrf_token' value='__CSRF_TOKEN__'>
                <input type='hidden' name='consent_action' value='allow'>
                <button type='submit' class='btn btn-primary'>Allow</button>
            </form>
            
            <form method='post' action='__CONSENT_ACTION_URL__' style='display: inline-block;'>                <input type='hidden' name='client_id' value='__CLIENT_ID_FORM__'>
                <input type='hidden' name='redirect_uri' value='__REDIRECT_URI__'>
                <input type='hidden' name='state' value='__STATE__'>
                <input type='hidden' name='code_challenge' value='__CODE_CHALLENGE__'>
                <input type='hidden' name='code_challenge_method' value='__CODE_CHALLENGE_METHOD__'>
                <input type='hidden' name='csrf_token' value='__CSRF_TOKEN__'>
                <input type='hidden' name='consent_action' value='deny'>
                <button type='submit' class='btn btn-secondary'>Deny</button>
            </form>
        </div>
    </div>
</body>
</html>";
        }" />

        <!-- Check for existing client denial cookie -->
        <set-variable name="has_denial_cookie" value="@{
            try {
                string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                
                if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri)) {
                    return false;
                }
                
                var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
                if (string.IsNullOrEmpty(cookieHeader)) {
                    return false;
                }
                
                string cookieName = "__Host-MCP_DENIED_CLIENTS";
                string[] cookies = cookieHeader.Split(';');
                foreach (string cookie in cookies) {
                    string trimmedCookie = cookie.Trim();
                    if (trimmedCookie.StartsWith(cookieName + "=")) {
                        string cookieValue = trimmedCookie.Substring(cookieName.Length + 1);
                        try {
                            string decodedValue = System.Text.Encoding.UTF8.GetString(
                                System.Convert.FromBase64String(cookieValue.Split('.')[0]));
                            JArray clients = JArray.Parse(decodedValue);
                            
                            string clientKey = $"{clientId}:{redirectUri}";
                            
                            foreach (var item in clients) {
                                string itemString = item.ToString();
                                
                                if (itemString == clientKey) {
                                    return true;
                                }
                                
                                // Handle URL-encoded redirect URI in stored cookie
                                try {
                                    if (itemString.Contains(':')) {
                                        string[] parts = itemString.Split(new char[] {':'}, 2);
                                        if (parts.Length == 2) {
                                            string storedClientId = parts[0];
                                            string storedRedirectUri = System.Net.WebUtility.UrlDecode(parts[1]);
                                            
                                            if (storedClientId == clientId && storedRedirectUri == redirectUri) {
                                                return true;
                                            }
                                        }
                                    }
                                } catch (Exception ex) {
                                    // Ignore comparison errors and continue
                                }
                            }
                        } catch (Exception ex) {
                            // Ignore cookie parsing errors and continue
                        }
                    }
                }
                
                return false;
            } catch (Exception ex) {
                return false;
            }
        }" />
        
        <!-- Check for existing client approval cookie -->
        <set-variable name="has_approval_cookie" value="@{
            try {
                string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                
                if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri)) {
                    return false;
                }
                
                var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
                if (string.IsNullOrEmpty(cookieHeader)) {
                    return false;
                }
                
                string cookieName = "__Host-MCP_APPROVED_CLIENTS";
                string[] cookies = cookieHeader.Split(';');
                foreach (string cookie in cookies) {
                    string trimmedCookie = cookie.Trim();
                    if (trimmedCookie.StartsWith(cookieName + "=")) {
                        string cookieValue = trimmedCookie.Substring(cookieName.Length + 1);
                        try {
                            string decodedValue = System.Text.Encoding.UTF8.GetString(
                                System.Convert.FromBase64String(cookieValue.Split('.')[0]));
                            JArray clients = JArray.Parse(decodedValue);
                            
                            string clientKey = $"{clientId}:{redirectUri}";
                            
                            foreach (var item in clients) {
                                string itemString = item.ToString();
                                
                                if (itemString == clientKey) {
                                    return true;
                                }
                                
                                // Handle URL-encoded redirect URI in stored cookie
                                try {
                                    if (itemString.Contains(':')) {
                                        string[] parts = itemString.Split(new char[] {':'}, 2);
                                        if (parts.Length == 2) {
                                            string storedClientId = parts[0];
                                            string storedRedirectUri = System.Net.WebUtility.UrlDecode(parts[1]);
                                            
                                            if (storedClientId == clientId && storedRedirectUri == redirectUri) {
                                                return true;
                                            }
                                        }
                                    }
                                } catch (Exception ex) {
                                    // Ignore comparison errors and continue
                                }
                            }
                        } catch (Exception ex) {
                            // Ignore cookie parsing errors and continue
                        }
                    }
                }
                
                return false;
            } catch (Exception ex) {
                return false;
            }
        }" />
        
        <set-variable name="consent_action" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "consent_action") {
                        return System.Net.WebUtility.UrlDecode(keyValue[1]);
                    }
                }
            }
            
            // Fallback to query string (GET)
            string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("consent_action", "");
            return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : "";
        }" />
        
        <!-- Extract CSRF token from form data -->
        <set-variable name="csrf_token_from_form" value="@{
            string formBody = context.Variables.GetValueOrDefault<string>("form_body", "");
            
            // Check form data first (POST)
            if (!string.IsNullOrEmpty(formBody)) {
                string[] pairs = formBody.Split('&');
                foreach (string pair in pairs) {
                    string[] keyValue = pair.Split(new char[] {'='}, 2);
                    if (keyValue.Length == 2 && keyValue[0] == "csrf_token") {
                        return System.Net.WebUtility.UrlDecode(keyValue[1]);
                    }
                }
            }
            
            // Fallback to query string (GET)
            string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("csrf_token", "");
            return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : "";
        }" />
        
        <!-- Validate CSRF token for POST requests -->
        <set-variable name="csrf_valid" value="@{
            if (context.Request.Method != "POST") {
                return true; // Only validate POST requests
            }
            
            string submittedToken = context.Variables.GetValueOrDefault<string>("csrf_token_from_form", "");
            if (string.IsNullOrEmpty(submittedToken)) {
                return false;
            }
            
            // Token cache lookup validation happens next
            string cacheKey = $"CSRF-{submittedToken}";
            
            return true; // Initial validation passes, detailed validation follows
        }" />
        
        <!-- Validate Origin/Referer headers for CSRF protection -->
        <set-variable name="origin_referer_valid" value="@{
            if (context.Request.Method != "POST") {
                return true; // Only validate state-changing operations
            }
            
            // Get the target origin (expected origin)
            string targetOrigin = "{{APIMGatewayURL}}";
            
            // Remove protocol and trailing slash for comparison
            if (targetOrigin.StartsWith("https://")) {
                targetOrigin = targetOrigin.Substring(8);
            } else if (targetOrigin.StartsWith("http://")) {
                targetOrigin = targetOrigin.Substring(7);
            }
            if (targetOrigin.EndsWith("/")) {
                targetOrigin = targetOrigin.TrimEnd('/');
            }
            
            // First check Origin header (preferred)
            string originHeader = context.Request.Headers.GetValueOrDefault("Origin", "");
            if (!string.IsNullOrEmpty(originHeader)) {
                try {
                    Uri originUri = new Uri(originHeader);
                    string sourceOrigin = originUri.Host;
                    if (originUri.Port != 80 && originUri.Port != 443) {
                        sourceOrigin += ":" + originUri.Port;
                    }
                    
                    if (sourceOrigin.Equals(targetOrigin, StringComparison.OrdinalIgnoreCase)) {
                        return true;
                    } else {
                        return false;
                    }
                } catch (Exception ex) {
                    return false;
                }
            }
            
            // Fallback to Referer header if Origin is not present
            string refererHeader = context.Request.Headers.GetValueOrDefault("Referer", "");
            if (!string.IsNullOrEmpty(refererHeader)) {
                try {
                    Uri refererUri = new Uri(refererHeader);
                    string sourceOrigin = refererUri.Host;
                    if (refererUri.Port != 80 && refererUri.Port != 443) {
                        sourceOrigin += ":" + refererUri.Port;
                    }
                    
                    if (sourceOrigin.Equals(targetOrigin, StringComparison.OrdinalIgnoreCase)) {
                        return true;
                    } else {
                        return false;
                    }
                } catch (Exception ex) {
                    return false;
                }
            }
            
            // Neither Origin nor Referer header present - this is suspicious for POST requests
            // OWASP recommends blocking such requests for better security
            return false; // Block requests without proper origin validation
        }" />
        
        <!-- Validate Fetch Metadata headers for CSRF protection -->
        <set-variable name="fetch_metadata_valid" value="@{
            // Check Sec-Fetch-Site header for cross-site request detection
            string secFetchSite = context.Request.Headers.GetValueOrDefault("Sec-Fetch-Site", "");
            
            // Allow same-origin, same-site, and direct navigation
            if (string.IsNullOrEmpty(secFetchSite) || 
                secFetchSite == "same-origin" || 
                secFetchSite == "same-site" || 
                secFetchSite == "none") {
                return true;
            }
            
            // Block cross-site POST requests
            if (context.Request.Method == "POST" && secFetchSite == "cross-site") {
                return false;
            }
            
            // Allow other values for compatibility
            return true;
        }" />
        
        <!-- Lookup CSRF token from cache -->
        <cache-lookup-value key="@($"CSRF-{context.Variables.GetValueOrDefault<string>("csrf_token_from_form")}")" variable-name="csrf_token_data" />
        
        <!-- Validate CSRF token details -->
        <set-variable name="csrf_validation_result" value="@{
            if (context.Request.Method != "POST") {
                return "valid"; // No validation needed for GET requests
            }
            
            string submittedToken = context.Variables.GetValueOrDefault<string>("csrf_token_from_form", "");
            if (string.IsNullOrEmpty(submittedToken)) {
                return "missing_token";
            }
            
            string tokenData = context.Variables.GetValueOrDefault<string>("csrf_token_data");
            if (string.IsNullOrEmpty(tokenData)) {
                return "invalid_token";
            }
            
            try {
                // Extract token data (before debug info separator)
                string actualTokenData = tokenData;
                if (tokenData.Contains("|")) {
                    actualTokenData = tokenData.Split('|')[0];
                }
                
                // Parse token data: client_id:redirect_uri:timestamp
                // Since both redirect_uri and timestamp can contain colons, we need to be very careful
                // The timestamp format is: YYYY-MM-DDTHH:mm:ssZ
                // So we look for the last occurrence of a timestamp pattern
                
                // Find the last occurrence of a timestamp pattern (YYYY-MM-DDTHH:mm:ssZ)
                var timestampPattern = @":\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$";
                var timestampMatch = System.Text.RegularExpressions.Regex.Match(actualTokenData, timestampPattern);
                
                if (!timestampMatch.Success) {
                    return "malformed_token";
                }
                
                // Extract the timestamp (without the leading colon)
                string timestampStr = timestampMatch.Value.Substring(1);
                
                // Extract everything before the timestamp match as the client_id:redirect_uri part
                string clientAndRedirect = actualTokenData.Substring(0, timestampMatch.Index);
                
                // Split client_id:redirect_uri on the first colon only
                int firstColonIndex = clientAndRedirect.IndexOf(':');
                if (firstColonIndex == -1) {
                    return "malformed_token";
                }
                
                string tokenClientId = clientAndRedirect.Substring(0, firstColonIndex);
                string tokenRedirectUri = clientAndRedirect.Substring(firstColonIndex + 1);
                
                // Validate client_id and redirect_uri match using constant-time comparison
                string currentClientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                string currentRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                
                // Constant-time string comparison for client_id to prevent timing attacks
                bool clientIdMatches = true;
                if (tokenClientId == null || currentClientId == null) {
                    clientIdMatches = (tokenClientId == currentClientId);
                } else if (tokenClientId.Length != currentClientId.Length) {
                    clientIdMatches = false;
                } else {
                    int result = 0;
                    for (int i = 0; i < tokenClientId.Length; i++) {
                        result |= tokenClientId[i] ^ currentClientId[i];
                    }
                    clientIdMatches = (result == 0);
                }
                
                if (!clientIdMatches) {
                    return "client_mismatch";
                }
                
                // Constant-time string comparison for redirect_uri to prevent timing attacks
                bool redirectUriMatches = true;
                if (tokenRedirectUri == null || currentRedirectUri == null) {
                    redirectUriMatches = (tokenRedirectUri == currentRedirectUri);
                } else if (tokenRedirectUri.Length != currentRedirectUri.Length) {
                    redirectUriMatches = false;
                } else {
                    int result = 0;
                    for (int i = 0; i < tokenRedirectUri.Length; i++) {
                        result |= tokenRedirectUri[i] ^ currentRedirectUri[i];
                    }
                    redirectUriMatches = (result == 0);
                }
                
                if (!redirectUriMatches) {
                    return "redirect_mismatch";
                }
                
                // Validate timestamp (token should not be older than 15 minutes)
                DateTime tokenTime;
                try {
                    tokenTime = DateTime.Parse(timestampStr);
                } catch (Exception) {
                    return "invalid_timestamp";
                }
                
                TimeSpan age = DateTime.UtcNow - tokenTime;
                if (age.TotalMinutes > 15) {
                    return "expired_token";
                }
                
                return "valid";
                
            } catch (Exception ex) {
                return "validation_error";
            }
        }" />
        
        <!-- If this is a form submission, process the consent choice -->
        <choose>
            <when condition="@(context.Request.Method == "POST")">
                <!-- Validate Origin/Referer headers -->
                <choose>
                    <when condition="@(!context.Variables.GetValueOrDefault<bool>("origin_referer_valid"))">
                        <!-- Origin/Referer validation failed -->
                        <return-response>
                            <set-status code="403" reason="Forbidden" />
                            <set-header name="Content-Type" exists-action="override">
                                <value>text/html</value>
                            </set-header>
                            <set-header name="Cache-Control" exists-action="override">
                                <value>no-store, no-cache</value>
                            </set-header>
                            <set-header name="Pragma" exists-action="override">
                                <value>no-cache</value>
                            </set-header>
                            <set-body>@(context.Variables.GetValueOrDefault<string>("generate_403_response"))</set-body>
                        </return-response>
                    </when>
                    <otherwise>
                        <!-- Origin/Referer validation passed -->
                        <!-- Validate Fetch Metadata headers -->
                        <choose>
                            <when condition="@(!context.Variables.GetValueOrDefault<bool>("fetch_metadata_valid"))">
                                <!-- Fetch metadata validation failed -->
                                <return-response>
                                    <set-status code="403" reason="Forbidden" />
                                    <set-header name="Content-Type" exists-action="override">
                                        <value>text/html</value>
                                    </set-header>
                                    <set-header name="Cache-Control" exists-action="override">
                                        <value>no-store, no-cache</value>
                                    </set-header>
                                    <set-header name="Pragma" exists-action="override">
                                        <value>no-cache</value>
                                    </set-header>
                                    <set-body>@(context.Variables.GetValueOrDefault<string>("generate_403_response"))</set-body>
                                </return-response>
                            </when>
                            <otherwise>
                                <!-- Fetch metadata validation passed -->
                                <!-- Validate CSRF token -->
                                <choose>
                                    <when condition="@(context.Variables.GetValueOrDefault<string>("csrf_validation_result") != "valid")">
                                        <!-- CSRF validation failed -->
                                        <return-response>
                                            <set-status code="403" reason="Forbidden" />
                                            <set-header name="Content-Type" exists-action="override">
                                                <value>text/html</value>
                                            </set-header>
                                            <set-header name="Cache-Control" exists-action="override">
                                                <value>no-store, no-cache</value>
                                            </set-header>
                                            <set-header name="Pragma" exists-action="override">
                                                <value>no-cache</value>
                                            </set-header>
                                            <set-body>@(context.Variables.GetValueOrDefault<string>("generate_403_response"))</set-body>
                                        </return-response>
                                    </when>
                                    <otherwise>
                                        <!-- CSRF validation passed -->
                                        <!-- Delete CSRF token from cache to prevent reuse -->
                                        <cache-remove-value key="@($"CSRF-{context.Variables.GetValueOrDefault<string>("csrf_token_from_form")}")" />
                                        
                                        <choose>
                                            <when condition="@(context.Variables.GetValueOrDefault<string>("consent_action") == "allow")">
                        <!-- Process consent approval -->
                        <set-variable name="response_status_code" value="302" />
                        
                        <set-variable name="response_redirect_location" value="@{
                            string baseUrl = "{{APIMGatewayURL}}";

                            string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                            string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                            string originalState = context.Variables.GetValueOrDefault<string>("state", "");
                            
                            string encodedClientId = System.Net.WebUtility.UrlEncode(clientId);
                            string encodedRedirectUri = System.Net.WebUtility.UrlEncode(redirectUri);
                            // State should be used as-is since it's already properly formatted from the original request
                            string encodedState = originalState;
                            
                            // Add PKCE parameters if they exist
                            string codeChallenge = context.Variables.GetValueOrDefault<string>("code_challenge", "");
                            string codeChallengeMethod = context.Variables.GetValueOrDefault<string>("code_challenge_method", "");
                            
                            string url = $"{baseUrl}/authorize?client_id={encodedClientId}&redirect_uri={encodedRedirectUri}&state={encodedState}";
                            
                            if (!string.IsNullOrEmpty(codeChallenge)) {
                                url += $"&code_challenge={System.Net.WebUtility.UrlEncode(codeChallenge)}";
                            }
                            
                            if (!string.IsNullOrEmpty(codeChallengeMethod)) {
                                url += $"&code_challenge_method={System.Net.WebUtility.UrlEncode(codeChallengeMethod)}";
                            }
                            
                            return url;
                        }" />
                          <!-- Calculate approval cookie value -->
                        <set-variable name="approval_cookie" value="@{
                            string cookieName = "__Host-MCP_APPROVED_CLIENTS";
                            
                            // Use already extracted parameters instead of re-parsing form data
                            string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                            string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                            
                            // Create a unique identifier for this client/redirect combination
                            string clientKey = $"{clientId}:{redirectUri}";
                            
                            // Check for existing cookie
                            var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
                            JArray approvedClients = new JArray();
                            
                            if (!string.IsNullOrEmpty(cookieHeader)) {
                                // Parse cookies to find our approval cookie
                                string[] cookies = cookieHeader.Split(';');
                                foreach (string cookie in cookies) {
                                    string trimmedCookie = cookie.Trim();
                                    if (trimmedCookie.StartsWith(cookieName + "=")) {
                                        try {
                                            // Extract and parse the cookie value
                                            string cookieValue = trimmedCookie.Substring(cookieName.Length + 1);
                                            // Get the payload part (before the first dot if cookie is signed)
                                            string payload = cookieValue.Contains('.') ? 
                                                cookieValue.Split('.')[0] : cookieValue;
                                            string decodedValue = System.Text.Encoding.UTF8.GetString(
                                                System.Convert.FromBase64String(payload));
                                            approvedClients = JArray.Parse(decodedValue);
                                        } catch (Exception) {
                                            // If parsing fails, we'll just create a new cookie
                                            approvedClients = new JArray();
                                        }
                                        break;
                                    }
                                }
                            }
                            
                            // Add the current client if not already in the list
                            bool clientExists = false;
                            foreach (var item in approvedClients) {
                                if (item.ToString() == clientKey) {
                                    clientExists = true;
                                    break;
                                }
                            }
                            
                            if (!clientExists) {
                                approvedClients.Add(clientKey);
                            }
                            
                            // Base64 encode the client list
                            string jsonClients = approvedClients.ToString(Newtonsoft.Json.Formatting.None);
                            string encodedClients = System.Convert.ToBase64String(
                                System.Text.Encoding.UTF8.GetBytes(jsonClients));
                            
                            // Return the full cookie string with appropriate settings
                            return $"{cookieName}={encodedClients}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax";
                        }" />
                        
                        <!-- Set variables for outbound policy awareness -->
                        <set-variable name="consent_approved" value="true" />
                        <set-variable name="cookie_name" value="__Host-MCP_APPROVED_CLIENTS" />
                        
                        <!-- Return the response with the cookie already set -->
                        <return-response>
                            <set-status code="302" reason="Found" />
                            <set-header name="Location" exists-action="override">
                                <value>@(context.Variables.GetValueOrDefault<string>("response_redirect_location", ""))</value>
                            </set-header>
                            <set-header name="Set-Cookie" exists-action="append">
                                <value>@(context.Variables.GetValueOrDefault<string>("approval_cookie"))</value>
                            </set-header>
                        </return-response>
                    </when>
                    <when condition="@(context.Variables.GetValueOrDefault<string>("consent_action") == "deny")">
                        <!-- Process consent denial -->
                        <set-variable name="response_status_code" value="403" />
                        <set-variable name="response_content_type" value="text/html" />
                        <set-variable name="response_cache_control" value="no-store, no-cache" />
                        <set-variable name="response_pragma" value="no-cache" />
                        
                        <!-- Calculate the cookie value right here in inbound before returning response -->
                        <set-variable name="denial_cookie" value="@{
                            string cookieName = "__Host-MCP_DENIED_CLIENTS";
                            
                            // Use already extracted parameters instead of re-parsing form data
                            string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                            string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                            
                            // Create a unique identifier for this client/redirect combination
                            string clientKey = $"{clientId}:{redirectUri}";
                            
                            // Check for existing cookie
                            var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
                            JArray deniedClients = new JArray();
                            
                            if (!string.IsNullOrEmpty(cookieHeader)) {
                                // Parse cookies to find our denial cookie
                                string[] cookies = cookieHeader.Split(';');
                                foreach (string cookie in cookies) {
                                    string trimmedCookie = cookie.Trim();
                                    if (trimmedCookie.StartsWith(cookieName + "=")) {
                                        try {
                                            // Extract and parse the cookie value
                                            string cookieValue = trimmedCookie.Substring(cookieName.Length + 1);
                                            // Get the payload part (before the first dot if cookie is signed)
                                            string payload = cookieValue.Contains('.') ? 
                                                cookieValue.Split('.')[0] : cookieValue;
                                            string decodedValue = System.Text.Encoding.UTF8.GetString(
                                                System.Convert.FromBase64String(payload));
                                            deniedClients = JArray.Parse(decodedValue);
                                        } catch (Exception) {
                                            // If parsing fails, we'll just create a new cookie
                                            deniedClients = new JArray();
                                        }
                                        break;
                                    }
                                }
                            }
                            
                            // Add the current client if not already in the list
                            bool clientExists = false;
                            foreach (var item in deniedClients) {
                                if (item.ToString() == clientKey) {
                                    clientExists = true;
                                    break;
                                }
                            }
                            
                            if (!clientExists) {
                                deniedClients.Add(clientKey);
                            }
                            
                            // Base64 encode the client list
                            string jsonClients = deniedClients.ToString(Newtonsoft.Json.Formatting.None);
                            string encodedClients = System.Convert.ToBase64String(
                                System.Text.Encoding.UTF8.GetBytes(jsonClients));
                            
                            // Return the full cookie string with appropriate settings
                            return $"{cookieName}={encodedClients}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax";
                        }" />                        <!-- Store the HTML content for the access denied page -->
                        <set-variable name="response_body" value="@{
                            string denialTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template");
                            string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles");
                            
                            // Replace placeholders with actual content
                            denialTemplate = denialTemplate.Replace("__COMMON_STYLES__", commonStyles);
                            denialTemplate = denialTemplate.Replace("__DENIAL_MESSAGE__", 
                                "You have denied authorization for this application against the MCP server.");
                            
                            return denialTemplate;
                        }" />
                        
                        <!-- Set variables for outbound policy awareness -->
                        <set-variable name="consent_denied" value="true" />
                        <set-variable name="cookie_name" value="__Host-MCP_DENIED_CLIENTS" />
                        
                        <!-- Return the response with the cookie already set -->
                        <return-response>
                            <set-status code="403" reason="Forbidden" />
                            <set-header name="Content-Type" exists-action="override">
                                <value>text/html</value>
                            </set-header>
                            <set-header name="Cache-Control" exists-action="override">
                                <value>no-store, no-cache</value>
                            </set-header>
                            <set-header name="Pragma" exists-action="override">
                                <value>no-cache</value>
                            </set-header>
                            <set-header name="Set-Cookie" exists-action="append">
                                <value>@(context.Variables.GetValueOrDefault<string>("denial_cookie"))</value>
                            </set-header>
                            <set-body>@(context.Variables.GetValueOrDefault<string>("response_body", ""))</set-body>
                        </return-response>
                    </when>
                    <otherwise>
                        <!-- Invalid consent action - return error -->
                        <return-response>
                            <set-status code="403" reason="Forbidden" />
                            <set-header name="Content-Type" exists-action="override">
                                <value>text/html</value>
                            </set-header>
                            <!-- Explicitly disable any redirects -->
                            <set-header name="Cache-Control" exists-action="override">
                                <value>no-store, no-cache</value>
                            </set-header>
                            <set-header name="Pragma" exists-action="override">
                                <value>no-cache</value>
                            </set-header>
                            <set-body>@{
                                string denialTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template");
                                string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles");
                                string consentAction = context.Variables.GetValueOrDefault<string>("consent_action", "");
                                
                                string detailedMessage = $"Invalid consent action '{consentAction}' received. Expected 'allow' or 'deny'. This may indicate a form tampering attempt or a browser compatibility issue.";
                                
                                // Replace placeholders with actual content
                                denialTemplate = denialTemplate.Replace("__COMMON_STYLES__", commonStyles);
                                denialTemplate = denialTemplate.Replace("__DENIAL_MESSAGE__", detailedMessage);
                                
                                return denialTemplate;
                            }</set-body>
                        </return-response>
                                            </otherwise>
                                        </choose>
                                    </otherwise>
                                </choose>
                            </otherwise>
                        </choose>
                    </otherwise>
                </choose>
            </when>
            <!-- For GET requests, check for cookies first, then display consent page if no cookie found -->
            <otherwise>
                <choose>
                    <!-- If there's an approval cookie, skip consent and redirect to authorization endpoint -->
                    <when condition="@(context.Variables.GetValueOrDefault<bool>("has_approval_cookie"))">
                        <!-- Set redirect location to authorization endpoint -->
                        <set-variable name="response_redirect_location" value="@{
                            string baseUrl = "{{APIMGatewayURL}}";
                            string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                            string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                            string state = context.Variables.GetValueOrDefault<string>("state", "");
                            
                            // URL encode parameters to prevent injection attacks
                            string encodedClientId = System.Net.WebUtility.UrlEncode(clientId);
                            string encodedRedirectUri = System.Net.WebUtility.UrlEncode(redirectUri);
                            // State is already properly encoded, don't double-encode
                            string encodedState = state;
                            
                            // Add PKCE parameters if they exist
                            string codeChallenge = context.Variables.GetValueOrDefault<string>("code_challenge", "");
                            string codeChallengeMethod = context.Variables.GetValueOrDefault<string>("code_challenge_method", "");
                            
                            string url = $"{baseUrl}/authorize?client_id={encodedClientId}&redirect_uri={encodedRedirectUri}&state={encodedState}";
                            
                            if (!string.IsNullOrEmpty(codeChallenge)) {
                                url += $"&code_challenge={System.Net.WebUtility.UrlEncode(codeChallenge)}";
                            }
                            
                            if (!string.IsNullOrEmpty(codeChallengeMethod)) {
                                url += $"&code_challenge_method={System.Net.WebUtility.UrlEncode(codeChallengeMethod)}";
                            }
                            
                            return url;
                        }" />
                        
                        <!-- Redirect to authorization endpoint -->
                        <return-response>
                            <set-status code="302" reason="Found" />
                            <set-header name="Location" exists-action="override">
                                <value>@(context.Variables.GetValueOrDefault<string>("response_redirect_location", ""))</value>
                            </set-header>
                        </return-response>
                    </when>
                    
                    <!-- If there's a denial cookie, return access denied page immediately -->
                    <when condition="@(context.Variables.GetValueOrDefault<bool>("has_denial_cookie"))">
                        <return-response>
                            <set-status code="403" reason="Forbidden" />
                            <set-header name="Content-Type" exists-action="override">
                                <value>text/html</value>
                            </set-header>
                            <!-- Explicitly disable any redirects -->
                            <set-header name="Cache-Control" exists-action="override">
                                <value>no-store, no-cache</value>
                            </set-header>
                            <set-header name="Pragma" exists-action="override">
                                <value>no-cache</value>
                            </set-header>
                            <set-body>@{
                                string denialTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template");
                                string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles");
                                
                                // Replace placeholders with actual content
                                denialTemplate = denialTemplate.Replace("__COMMON_STYLES__", commonStyles);
                                denialTemplate = denialTemplate.Replace("__DENIAL_MESSAGE__", 
                                    "You have previously denied access to this application.");
                                
                                return denialTemplate;
                            }</set-body>
                        </return-response>
                    </when>
                      <!-- If no cookies found, show the consent screen -->
                    <otherwise>
                        <!-- Check if client is registered first -->
                        <choose>
                            <when condition="@(!context.Variables.GetValueOrDefault<bool>("is_client_registered"))">
                                <!-- Client is not registered, show error page -->
                                <return-response>
                                    <set-status code="403" reason="Forbidden" />
                                    <set-header name="Content-Type" exists-action="override">
                                        <value>text/html</value>
                                    </set-header>
                                    <set-header name="Cache-Control" exists-action="override">
                                        <value>no-store, no-cache</value>
                                    </set-header>
                                    <set-header name="Pragma" exists-action="override">
                                        <value>no-cache</value>
                                    </set-header>
                                    <set-body>@{
                                        string template = context.Variables.GetValueOrDefault<string>("client_not_found_template");
                                        string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles");
                                        string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                                        string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                                        
                                        // Replace placeholders with HTML-encoded content to prevent XSS
                                        template = template.Replace("__COMMON_STYLES__", commonStyles);
                                        template = template.Replace("__CLIENT_ID_DISPLAY__", System.Net.WebUtility.HtmlEncode(clientId));
                                        template = template.Replace("__REDIRECT_URI__", System.Net.WebUtility.HtmlEncode(redirectUri));
                                        
                                        return template;
                                    }</set-body>
                                </return-response>
                            </when>
                            <otherwise>                                <!-- Client is registered, get client name from the cache -->
                                <!-- Build consent page using the standardized template -->
                                <set-variable name="consent_page" value="@{
                                    string template = context.Variables.GetValueOrDefault<string>("consent_page_template");
                                    string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles");
                                    
                                    // Use the service URL from APIM configuration
                                    string basePath = "{{APIMGatewayURL}}";
                                    
                                    string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                                    string clientName = context.Variables.GetValueOrDefault<string>("client_name", "Unknown Application");
                                    string clientUri = context.Variables.GetValueOrDefault<string>("client_uri", "N/A");
                                    string oauthScopes = context.Variables.GetValueOrDefault<string>("oauth_scopes", "");
                                    
                                    // Get the normalized (human-readable) redirect URI for display
                                    string normalizedRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                                    
                                    // Use the normalized redirect URI for form submission to ensure consistency
                                    string formRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                                    string htmlEncodedFormUri = System.Net.WebUtility.HtmlEncode(formRedirectUri);
                                    
                                    string state = context.Variables.GetValueOrDefault<string>("state", "");
                                    string csrfToken = context.Variables.GetValueOrDefault<string>("csrf_token", "");
                                    
                                    // Create a temporary placeholder for the form fields
                                    string FORM_FIELD_PLACEHOLDER = "___ENCODED_REDIRECT_URI___";
                                    
                                    // Replace the styles first
                                    template = template.Replace("__COMMON_STYLES__", commonStyles);
                                    
                                    // First, create a temporary placeholder for the form fields
                                    template = template.Replace("value='__REDIRECT_URI__'", "value='" + FORM_FIELD_PLACEHOLDER + "'");
                                      
                                    // Replace template placeholders with properly encoded values
                                    template = template.Replace("__CLIENT_NAME__", System.Net.WebUtility.HtmlEncode(clientName));
                                    template = template.Replace("__CLIENT_URI__", System.Net.WebUtility.HtmlEncode(clientUri));
                                    // For display purposes, use HtmlEncode for safety
                                    template = template.Replace("__CLIENT_ID_DISPLAY__", System.Net.WebUtility.HtmlEncode(clientId));
                                    template = template.Replace("__REDIRECT_URI__", System.Net.WebUtility.HtmlEncode(normalizedRedirectUri));
                                    // For form field values, use HtmlEncode for XSS protection
                                    template = template.Replace("__CLIENT_ID_FORM__", System.Net.WebUtility.HtmlEncode(clientId));
                                    // State should be HTML-encoded for form safety (don't URL-decode first as it may already be in correct format)
                                    template = template.Replace("__STATE__", System.Net.WebUtility.HtmlEncode(state));
                                    template = template.Replace("__CODE_CHALLENGE__", System.Net.WebUtility.HtmlEncode(context.Variables.GetValueOrDefault<string>("code_challenge", "")));
                                    template = template.Replace("__CODE_CHALLENGE_METHOD__", System.Net.WebUtility.HtmlEncode(context.Variables.GetValueOrDefault<string>("code_challenge_method", "")));
                                    template = template.Replace("__CSRF_TOKEN__", System.Net.WebUtility.HtmlEncode(csrfToken));
                                    template = template.Replace("__CONSENT_ACTION_URL__", basePath + "/consent");                                    
                                    // Handle space-separated OAuth scopes and create individual list items with HTML encoding
                                    string[] scopeArray = oauthScopes.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries);
                                    StringBuilder scopeList = new StringBuilder();
                                    
                                    foreach (string scope in scopeArray) {
                                        scopeList.AppendLine($"<li><code>{System.Net.WebUtility.HtmlEncode(scope)}</code></li>");
                                    }
                                    
                                    template = template.Replace("__OAUTH_SCOPES__", scopeList.ToString());
                                    
                                // Replace form field placeholder with encoded URI
                                template = template.Replace(FORM_FIELD_PLACEHOLDER, htmlEncodedFormUri);                                    return template;
                                }" />
                                
                                <!-- Return consent page -->
                                <return-response>
                                    <set-status code="200" reason="OK" />
                                    <set-header name="Content-Type" exists-action="override">
                                        <value>text/html</value>
                                    </set-header>
                                    <!-- Security headers -->
                                    <set-header name="X-Frame-Options" exists-action="override">
                                        <value>DENY</value>
                                    </set-header>
                                    <set-header name="X-Content-Type-Options" exists-action="override">
                                        <value>nosniff</value>
                                    </set-header>
                                    <set-header name="X-XSS-Protection" exists-action="override">
                                        <value>1; mode=block</value>
                                    </set-header>
                                    <set-header name="Referrer-Policy" exists-action="override">
                                        <value>strict-origin-when-cross-origin</value>
                                    </set-header>
                                    <set-header name="Content-Security-Policy" exists-action="override">
                                        <value>default-src 'self'; style-src 'unsafe-inline'; script-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self' https:</value>
                                    </set-header>
                                    <set-header name="Cache-Control" exists-action="override">
                                        <value>no-store, no-cache, must-revalidate</value>
                                    </set-header>
                                    <set-header name="Pragma" exists-action="override">
                                        <value>no-cache</value>
                                    </set-header>
                                    <!-- Store the state parameter in a secure cookie for validation -->
                                    <set-header name="Set-Cookie" exists-action="append">
                                        <value>@{
                                            string state = context.Variables.GetValueOrDefault<string>("state", "");
                                            string clientId = context.Variables.GetValueOrDefault<string>("client_id", "");
                                            string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
                                            
                                            // Create consent context data
                                            var consentData = new JObject {
                                                ["state"] = state,
                                                ["clientId"] = clientId,
                                                ["redirectUri"] = redirectUri,
                                                ["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
                                            };
                                            
                                            // Base64 encode the consent data
                                            string consentDataJson = consentData.ToString(Newtonsoft.Json.Formatting.None);
                                            string encodedConsentData = System.Convert.ToBase64String(
                                                System.Text.Encoding.UTF8.GetBytes(consentDataJson));
                                            
                                            return $"__Host-MCP_CONSENT_STATE={encodedConsentData}; Max-Age=900; Path=/; Secure; HttpOnly; SameSite=Lax";
                                        }</value>
                                    </set-header>
                                    <set-body>@{
                                        return context.Variables.GetValueOrDefault<string>("consent_page", "");
                                    }</set-body>
                                </return-response>
                            </otherwise>
                        </choose>
                    </otherwise>
                </choose>
            </otherwise>
        </choose>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

```